diff --git a/CHANGELOG.md b/CHANGELOG.md index 597735da2..ff15b1af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Features +- dbaas: clickhouse subcommands + +### Improvements +- deps: bump egoscale/v3 to v3.1.44 + ## 1.98.0 ### Features diff --git a/cmd/dbaas/dbaas_acl.go b/cmd/dbaas/dbaas_acl.go new file mode 100644 index 000000000..98fb17447 --- /dev/null +++ b/cmd/dbaas/dbaas_acl.go @@ -0,0 +1,14 @@ +package dbaas + +import ( + "github.com/spf13/cobra" +) + +var dbaasAclCmd = &cobra.Command{ + Use: "acl", + Short: "Manage DBaaS ACL configuration", +} + +func init() { + dbaasCmd.AddCommand(dbaasAclCmd) +} diff --git a/cmd/dbaas/dbaas_acl_show.go b/cmd/dbaas/dbaas_acl_show.go new file mode 100644 index 000000000..68133831d --- /dev/null +++ b/cmd/dbaas/dbaas_acl_show.go @@ -0,0 +1,151 @@ +package dbaas + +import ( + "bytes" + "fmt" + + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" +) + +type dbaasAclShowOutput struct { + Users []dbaasAclUserOutput `json:"users"` +} + +func (o *dbaasAclShowOutput) ToJSON() { output.JSON(o) } +func (o *dbaasAclShowOutput) ToText() { output.Text(o) } + +type dbaasAclUserOutput struct { + Username string `json:"username"` + Roles []dbaasAclRoleOutput `json:"roles"` + Privileges []dbaasPrivOutput `json:"privileges"` +} + +type dbaasAclRoleOutput struct { + Name string `json:"name"` + Default bool `json:"default,omitempty"` + WithAdminOption bool `json:"with-admin-option,omitempty"` +} + +type dbaasPrivOutput struct { + AccessType string `json:"access-type"` + Database string `json:"database,omitempty"` + Table string `json:"table,omitempty"` + Column string `json:"column,omitempty"` + GrantOption bool `json:"grant-option,omitempty"` + PartialRevoke bool `json:"partial-revoke,omitempty"` +} + +type dbaasAclShowCmd struct { + exocmd.CliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"show"` + Name string `cli-arg:"#" cli-usage:"NAME"` + Zone string `cli-short:"z" cli-usage:"Database Service zone"` +} + +func (c *dbaasAclShowCmd) CmdAliases() []string { return nil } +func (c *dbaasAclShowCmd) CmdShort() string { return "Show ClickHouse ACL configuration" } +func (c *dbaasAclShowCmd) CmdLong() string { return "Show the current ClickHouse ACL configuration for a DBaaS service." } + +func (c *dbaasAclShowCmd) CmdPreRun(cmd *cobra.Command, args []string) error { + exocmd.CmdSetZoneFlagFromDefault(cmd) + return exocmd.CliCommandDefaultPreRun(c, cmd, args) +} + +func (c *dbaasAclShowCmd) CmdRun(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return err + } + + acl, err := client.GetDBAASClickhouseAclConfig(ctx, c.Name) + if err != nil { + return err + } + + out := &dbaasAclShowOutput{} + for _, u := range acl.Users { + userOut := dbaasAclUserOutput{ + Username: string(u.Username), + } + for _, r := range u.Roles { + userOut.Roles = append(userOut.Roles, dbaasAclRoleOutput{ + Name: r.Name, + Default: utils.DefaultBool(r.Default, false), + WithAdminOption: utils.DefaultBool(r.WithAdminOption, false), + }) + } + for _, p := range u.Privileges { + userOut.Privileges = append(userOut.Privileges, dbaasPrivOutput{ + AccessType: p.AccessType, + Database: p.Database, + Table: p.Table, + Column: p.Column, + GrantOption: utils.DefaultBool(p.GrantOption, false), + PartialRevoke: utils.DefaultBool(p.PartialRevoke, false), + }) + } + out.Users = append(out.Users, userOut) + } + + return c.OutputFunc(out, nil) +} + +func (o *dbaasAclShowOutput) ToTable() { + t := table.NewTable(nil) + defer t.Render() + + if len(o.Users) == 0 { + t.Append([]string{"No ACL configuration found", ""}) + return + } + + for _, u := range o.Users { + t.Append([]string{"User", u.Username}) + + buf := bytes.NewBuffer(nil) + rolesTable := table.NewEmbeddedTable(buf) + rolesTable.SetHeader([]string{"Role", "Default", "Admin"}) + for _, r := range u.Roles { + rolesTable.Append([]string{ + r.Name, + fmt.Sprintf("%v", r.Default), + fmt.Sprintf("%v", r.WithAdminOption), + }) + } + rolesTable.Render() + t.Append([]string{"Roles", buf.String()}) + + buf.Reset() + privsTable := table.NewEmbeddedTable(buf) + privsTable.SetHeader([]string{"Access", "Database", "Table", "Column", "Grant", "Partial"}) + for _, p := range u.Privileges { + privsTable.Append([]string{ + p.AccessType, + p.Database, + p.Table, + p.Column, + fmt.Sprintf("%v", p.GrantOption), + fmt.Sprintf("%v", p.PartialRevoke), + }) + } + privsTable.Render() + t.Append([]string{"Privileges", buf.String()}) + t.Append([]string{"", ""}) + } +} + +func init() { + cobra.CheckErr(exocmd.RegisterCLICommand(dbaasAclCmd, &dbaasAclShowCmd{ + CliCommandSettings: exocmd.DefaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas/dbaas_create.go b/cmd/dbaas/dbaas_create.go index 7abc4c1a0..a8bf977f5 100644 --- a/cmd/dbaas/dbaas_create.go +++ b/cmd/dbaas/dbaas_create.go @@ -25,9 +25,10 @@ type dbaasServiceCreateCmd struct { HelpOpensearch bool `cli-usage:"show usage for flags specific to the opensearch type"` HelpMysql bool `cli-usage:"show usage for flags specific to the mysql type"` HelpPg bool `cli-usage:"show usage for flags specific to the pg type"` - HelpValkey bool `cli-usage:"show usage for flags specific to the valkey type"` - HelpGrafana bool `cli-usage:"show usage for flags specific to the grafana type"` - HelpThanos bool `cli-usage:"show usage for flags specific to the thanos type"` + HelpValkey bool `cli-usage:"show usage for flags specific to the valkey type"` + HelpGrafana bool `cli-usage:"show usage for flags specific to the grafana type"` + HelpThanos bool `cli-usage:"show usage for flags specific to the thanos type"` + HelpClickhouse bool `cli-usage:"show usage for flags specific to the clickhouse type"` MaintenanceDOW string `cli-flag:"maintenance-dow" cli-usage:"automated Database Service maintenance day-of-week"` MaintenanceTime string `cli-usage:"automated Database Service maintenance time (format HH:MM:SS)"` @@ -128,6 +129,13 @@ type dbaasServiceCreateCmd struct { // "thanos" type specific flags ThanosIPFilter []string `cli-flag:"thanos-ip-filter" cli-usage:"allow incoming connections from CIDR address block" cli-hidden:""` ThanosSettings string `cli-flag:"thanos-settings" cli-usage:"Thanos configuration settings (JSON format)" cli-hidden:""` + + // "clickhouse" type specific flags + ClickhouseForkFrom string `cli-flag:"clickhouse-fork-from" cli-usage:"name of a Database Service to fork from" cli-hidden:""` + ClickhouseIPFilter []string `cli-flag:"clickhouse-ip-filter" cli-usage:"allow incoming connections from CIDR address block" cli-hidden:""` + ClickhouseRecoveryBackupName string `cli-flag:"clickhouse-recovery-backup-name" cli-usage:"the name of the backup to restore when forking from a Database Service" cli-hidden:""` + ClickhouseSettings string `cli-flag:"clickhouse-settings" cli-usage:"ClickHouse configuration settings (JSON format)" cli-hidden:""` + ClickhouseVersion string `cli-flag:"clickhouse-version" cli-usage:"ClickHouse major version" cli-hidden:""` } func (c *dbaasServiceCreateCmd) CmdAliases() []string { return exocmd.GCreateAlias } @@ -167,6 +175,9 @@ func (c *dbaasServiceCreateCmd) CmdPreRun(cmd *cobra.Command, args []string) err case cmd.Flags().Changed("help-thanos"): exocmd.CmdShowHelpFlags(cmd.Flags(), "thanos-") os.Exit(0) + case cmd.Flags().Changed("help-clickhouse"): + exocmd.CmdShowHelpFlags(cmd.Flags(), "clickhouse-") + os.Exit(0) } exocmd.CmdSetZoneFlagFromDefault(cmd) @@ -199,6 +210,8 @@ func (c *dbaasServiceCreateCmd) CmdRun(cmd *cobra.Command, args []string) error return c.createValkey(cmd, args) case "thanos": return c.createThanos(cmd, args) + case "clickhouse": + return c.createClickhouse(cmd, args) default: return fmt.Errorf("unsupported service type %q", c.Type) } diff --git a/cmd/dbaas/dbaas_create_clickhouse.go b/cmd/dbaas/dbaas_create_clickhouse.go new file mode 100644 index 000000000..a7d55e1ae --- /dev/null +++ b/cmd/dbaas/dbaas_create_clickhouse.go @@ -0,0 +1,94 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasServiceCreateCmd) createClickhouse(_ *cobra.Command, _ []string) error { + var err error + + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return fmt.Errorf("unable to create client: %w", err) + } + + databaseService := v3.CreateDBAASServiceClickhouseRequest{ + Plan: c.Plan, + TerminationProtection: &c.TerminationProtection, + Version: c.ClickhouseVersion, + } + + if c.ClickhouseForkFrom != "" { + databaseService.ForkFromService = v3.DBAASServiceName(c.ClickhouseForkFrom) + if c.ClickhouseRecoveryBackupName != "" { + databaseService.RecoveryBackupName = c.ClickhouseRecoveryBackupName + } + } + + if len(c.ClickhouseIPFilter) > 0 { + databaseService.IPFilter = c.ClickhouseIPFilter + } + + if c.MaintenanceDOW != "" && c.MaintenanceTime != "" { + databaseService.Maintenance = &v3.CreateDBAASServiceClickhouseRequestMaintenance{ + Dow: v3.CreateDBAASServiceClickhouseRequestMaintenanceDow(c.MaintenanceDOW), + Time: c.MaintenanceTime, + } + } + + if c.ClickhouseSettings != "" { + settingsSchema, err := client.GetDBAASSettingsClickhouse(ctx) + if err != nil { + return fmt.Errorf("unable to retrieve Database Service settings: %w", err) + } + _, err = validateDatabaseServiceSettings( + c.ClickhouseSettings, + settingsSchema.Settings.Clickhouse.Properties, + ) + if err != nil { + return fmt.Errorf("invalid settings: %w", err) + } + + settings := &v3.JSONSchemaClickhouse{} + if err := json.Unmarshal([]byte(c.ClickhouseSettings), &settings); err != nil { + return err + } + + databaseService.ClickhouseSettings = settings + } + + op, err := client.CreateDBAASServiceClickhouse(ctx, c.Name, databaseService) + if err != nil { + return err + } + + utils.DecorateAsyncOperation(fmt.Sprintf("Creating DBaaS ClickHouse service %q", c.Name), func() { + op, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + + if err != nil { + return err + } + + serviceName := op.Reference.ID.String() + + if !globalstate.Quiet { + return c.OutputFunc((&dbaasServiceShowCmd{ + Name: serviceName, + Zone: c.Zone, + }).showDatabaseServiceClickhouse(ctx)) + } + + return nil +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_delete.go b/cmd/dbaas/dbaas_delete.go index ac0518d11..58639daa5 100644 --- a/cmd/dbaas/dbaas_delete.go +++ b/cmd/dbaas/dbaas_delete.go @@ -62,7 +62,12 @@ func (c *dbaasServiceDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { c.Name, strings.Join(readReplicaNames, ", ")) } - op, err := client.DeleteDBAASService(ctx, c.Name) + op, err := func() (*v3.Operation, error) { + if string(svc.Type) == "clickhouse" { + return client.DeleteDBAASServiceClickhouse(ctx, c.Name) + } + return client.DeleteDBAASService(ctx, c.Name) + }() if err != nil { if errors.Is(err, v3.ErrNotFound) { return fmt.Errorf("resource not found in zone %q", c.Zone) diff --git a/cmd/dbaas/dbaas_role.go b/cmd/dbaas/dbaas_role.go new file mode 100644 index 000000000..9016f1a28 --- /dev/null +++ b/cmd/dbaas/dbaas_role.go @@ -0,0 +1,14 @@ +package dbaas + +import ( + "github.com/spf13/cobra" +) + +var dbaasRoleCmd = &cobra.Command{ + Use: "role", + Short: "Manage DBaaS roles", +} + +func init() { + dbaasCmd.AddCommand(dbaasRoleCmd) +} diff --git a/cmd/dbaas/dbaas_role_delete.go b/cmd/dbaas/dbaas_role_delete.go new file mode 100644 index 000000000..0c600d5d2 --- /dev/null +++ b/cmd/dbaas/dbaas_role_delete.go @@ -0,0 +1,71 @@ +package dbaas + +import ( + "fmt" + + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" +) + +type dbaasRoleDeleteCmd struct { + exocmd.CliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"delete"` + Name string `cli-arg:"#" cli-usage:"NAME"` + RoleUUID string `cli-arg:"#" cli-usage:"ROLE-UUID"` + Zone string `cli-short:"z" cli-usage:"Database Service zone"` + + Force bool `cli-short:"f" cli-usage:"don't prompt for confirmation"` +} + +func (c *dbaasRoleDeleteCmd) CmdAliases() []string { return nil } +func (c *dbaasRoleDeleteCmd) CmdShort() string { return "Delete a ClickHouse role" } +func (c *dbaasRoleDeleteCmd) CmdLong() string { return "Delete a role from a ClickHouse DBaaS service by UUID." } + +func (c *dbaasRoleDeleteCmd) CmdPreRun(cmd *cobra.Command, args []string) error { + exocmd.CmdSetZoneFlagFromDefault(cmd) + return exocmd.CliCommandDefaultPreRun(c, cmd, args) +} + +func (c *dbaasRoleDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return err + } + + if !c.Force { + if !utils.AskQuestion( + ctx, + fmt.Sprintf( + "Are you sure you want to delete role %q from service %q?", + c.RoleUUID, + c.Name, + ), + ) { + return nil + } + } + + op, err := client.DeleteDBAASClickhouseRole(ctx, c.Name, v3.UUID(c.RoleUUID)) + if err != nil { + return err + } + + utils.DecorateAsyncOperation(fmt.Sprintf("Deleting role %q from service %q", c.RoleUUID, c.Name), func() { + op, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + + return err +} + +func init() { + cobra.CheckErr(exocmd.RegisterCLICommand(dbaasRoleCmd, &dbaasRoleDeleteCmd{ + CliCommandSettings: exocmd.DefaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas/dbaas_role_list.go b/cmd/dbaas/dbaas_role_list.go new file mode 100644 index 000000000..88e277823 --- /dev/null +++ b/cmd/dbaas/dbaas_role_list.go @@ -0,0 +1,158 @@ +package dbaas + +import ( + "bytes" + "fmt" + + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" +) + +type dbaasRoleListOutput struct { + Roles []dbaasRoleOutput `json:"roles"` +} + +func (o *dbaasRoleListOutput) ToJSON() { output.JSON(o) } +func (o *dbaasRoleListOutput) ToText() { output.Text(o) } + +type dbaasRoleOutput struct { + Name string `json:"name"` + UUID string `json:"uuid,omitempty"` + Privileges []dbaasRolePrivOutput `json:"privileges,omitempty"` + GrantedRoles []dbaasGrantedRoleOutput `json:"granted-roles,omitempty"` +} + +type dbaasRolePrivOutput struct { + Name string `json:"name"` + Database string `json:"database,omitempty"` + Table string `json:"table,omitempty"` + Column string `json:"column,omitempty"` + GrantOption bool `json:"grant-option,omitempty"` + IsPartialRevoke bool `json:"is-partial-revoke,omitempty"` +} + +type dbaasGrantedRoleOutput struct { + Name string `json:"name"` + UUID string `json:"uuid,omitempty"` + IsDefault bool `json:"is-default,omitempty"` + WithAdminOption bool `json:"with-admin-option,omitempty"` +} + +type dbaasRoleListCmd struct { + exocmd.CliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"list"` + Name string `cli-arg:"#" cli-usage:"NAME"` + Zone string `cli-short:"z" cli-usage:"Database Service zone"` +} + +func (c *dbaasRoleListCmd) CmdAliases() []string { return nil } +func (c *dbaasRoleListCmd) CmdShort() string { return "List ClickHouse roles" } +func (c *dbaasRoleListCmd) CmdLong() string { return "List roles for a ClickHouse DBaaS service." } + +func (c *dbaasRoleListCmd) CmdPreRun(cmd *cobra.Command, args []string) error { + exocmd.CmdSetZoneFlagFromDefault(cmd) + return exocmd.CliCommandDefaultPreRun(c, cmd, args) +} + +func (c *dbaasRoleListCmd) CmdRun(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return err + } + + roles, err := client.ListDBAASClickhouseRoles(ctx, c.Name) + if err != nil { + return err + } + + out := &dbaasRoleListOutput{} + for _, r := range roles.Roles { + roleOut := dbaasRoleOutput{ + Name: string(r.Name), + UUID: r.Uuid, + } + for _, p := range r.Privileges { + roleOut.Privileges = append(roleOut.Privileges, dbaasRolePrivOutput{ + Name: string(p.Name), + Database: p.Database, + Table: p.Table, + Column: p.Column, + GrantOption: utils.DefaultBool(p.GrantOption, false), + IsPartialRevoke: utils.DefaultBool(p.ISPartialRevoke, false), + }) + } + for _, g := range r.GrantedRoles { + roleOut.GrantedRoles = append(roleOut.GrantedRoles, dbaasGrantedRoleOutput{ + Name: g.Name, + UUID: g.Uuid, + IsDefault: utils.DefaultBool(g.ISDefault, false), + WithAdminOption: utils.DefaultBool(g.WithAdminOption, false), + }) + } + out.Roles = append(out.Roles, roleOut) + } + + return c.OutputFunc(out, nil) +} + +func (o *dbaasRoleListOutput) ToTable() { + t := table.NewTable(nil) + defer t.Render() + + if len(o.Roles) == 0 { + t.Append([]string{"No roles found", ""}) + return + } + + for _, r := range o.Roles { + t.Append([]string{"Role", r.Name}) + t.Append([]string{"UUID", r.UUID}) + + // Privileges as embedded table + buf := bytes.NewBuffer(nil) + privsTable := table.NewEmbeddedTable(buf) + privsTable.SetHeader([]string{"Access", "Database", "Table", "Column", "Grant"}) + for _, p := range r.Privileges { + privsTable.Append([]string{ + p.Name, + p.Database, + p.Table, + p.Column, + fmt.Sprintf("%v", p.GrantOption), + }) + } + privsTable.Render() + t.Append([]string{"Privileges", buf.String()}) + + // Granted roles as embedded table + buf.Reset() + grantedTable := table.NewEmbeddedTable(buf) + grantedTable.SetHeader([]string{"Role", "Default", "Admin"}) + for _, g := range r.GrantedRoles { + grantedTable.Append([]string{ + g.Name, + fmt.Sprintf("%v", g.IsDefault), + fmt.Sprintf("%v", g.WithAdminOption), + }) + } + grantedTable.Render() + t.Append([]string{"Granted Roles", buf.String()}) + + t.Append([]string{"", ""}) + } +} + +func init() { + cobra.CheckErr(exocmd.RegisterCLICommand(dbaasRoleCmd, &dbaasRoleListCmd{ + CliCommandSettings: exocmd.DefaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas/dbaas_show.go b/cmd/dbaas/dbaas_show.go index 2b2580b51..4237ba689 100644 --- a/cmd/dbaas/dbaas_show.go +++ b/cmd/dbaas/dbaas_show.go @@ -76,6 +76,7 @@ type dbServiceShowOutput struct { Valkey *dbServiceValkeyShowOutput `json:"valkey,omitempty"` Opensearch *dbServiceOpensearchShowOutput `json:"opensearch,omitempty"` Thanos *dbServiceThanosShowOutput `json:"thanos,omitempty"` + Clickhouse *dbServiceClickhouseShowOutput `json:"clickhouse,omitempty"` } func (o *dbServiceShowOutput) ToJSON() { output.JSON(o) } @@ -120,6 +121,8 @@ func (o *dbServiceShowOutput) ToTable() { formatDatabaseServiceValkeyTable(t, o.Valkey) case o.Thanos != nil: formatDatabaseServiceThanosTable(t, o.Thanos) + case o.Clickhouse != nil: + formatDatabaseServiceClickhouseTable(t, o.Clickhouse) } } @@ -213,6 +216,8 @@ func (c *dbaasServiceShowCmd) CmdRun(_ *cobra.Command, _ []string) error { return c.OutputFunc(c.showDatabaseServiceValkey(ctx)) case "thanos": return c.OutputFunc(c.showDatabaseServiceThanos(ctx)) + case "clickhouse": + return c.OutputFunc(c.showDatabaseServiceClickhouse(ctx)) default: return fmt.Errorf("unsupported service type %q", svc.Type) } diff --git a/cmd/dbaas/dbaas_show_clickhouse.go b/cmd/dbaas/dbaas_show_clickhouse.go new file mode 100644 index 000000000..7bdec9cc3 --- /dev/null +++ b/cmd/dbaas/dbaas_show_clickhouse.go @@ -0,0 +1,256 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/mitchellh/go-wordwrap" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" +) + +type dbServiceClickhouseComponentShowOutput struct { + Component string `json:"component"` + Host string `json:"host"` + Port int64 `json:"port"` + Route string `json:"route"` + SSL *bool `json:"ssl,omitempty"` + Usage string `json:"usage"` +} + +type dbServiceClickhouseUserShowOutput struct { + Username string `json:"username,omitempty"` + UUID string `json:"uuid,omitempty"` +} + +type dbServiceClickhouseShowOutput struct { + Components []dbServiceClickhouseComponentShowOutput `json:"components"` + ConnectionInfo *dbServiceClickhouseConnectionInfoShowOutput `json:"connection_info,omitempty"` + IPFilter []string `json:"ip_filter"` + PrometheusURI *dbServiceClickhousePrometheusURIShowOutput `json:"prometheus_uri,omitempty"` + URI string `json:"uri"` + URIParams map[string]interface{} `json:"uri_params"` + Users []dbServiceClickhouseUserShowOutput `json:"users"` + Version string `json:"version"` +} + +type dbServiceClickhouseConnectionInfoShowOutput struct { + URI []string `json:"uri,omitempty"` + MysqlURI string `json:"mysql_uri,omitempty"` + ArrowflightURI string `json:"arrowflight_uri,omitempty"` +} + +type dbServiceClickhousePrometheusURIShowOutput struct { + Host string `json:"host"` + Port int64 `json:"port"` +} + +func formatDatabaseServiceClickhouseTable(t *table.Table, o *dbServiceClickhouseShowOutput) { + t.Append([]string{"URI", o.URI}) + t.Append([]string{"IP Filter", strings.Join(o.IPFilter, ", ")}) + t.Append([]string{"Version", o.Version}) + + if o.ConnectionInfo != nil && len(o.ConnectionInfo.URI) > 0 { + t.Append([]string{"Connection URIs", strings.Join(o.ConnectionInfo.URI, ", ")}) + } + if o.ConnectionInfo != nil && o.ConnectionInfo.MysqlURI != "" { + t.Append([]string{"MySQL URI", o.ConnectionInfo.MysqlURI}) + } + if o.ConnectionInfo != nil && o.ConnectionInfo.ArrowflightURI != "" { + t.Append([]string{"ArrowFlight URI", o.ConnectionInfo.ArrowflightURI}) + } + + t.Append([]string{"Components", func() string { + buf := bytes.NewBuffer(nil) + ct := table.NewEmbeddedTable(buf) + ct.SetHeader([]string{" ", "Address", "Route", "Usage"}) + for _, c := range o.Components { + ct.Append([]string{ + c.Component, + fmt.Sprintf("%s:%d", c.Host, c.Port), + "route:" + c.Route, + "usage:" + c.Usage, + }) + } + ct.Render() + return buf.String() + }()}) + + t.Append([]string{"Users", func() string { + if len(o.Users) > 0 { + return strings.Join( + func() []string { + users := make([]string, len(o.Users)) + for i := range o.Users { + users[i] = o.Users[i].Username + } + return users + }(), + "\n") + } + return "n/a" + }()}) +} + +func (c *dbaasServiceShowCmd) showDatabaseServiceClickhouse(ctx context.Context) (output.Outputter, error) { + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return nil, err + } + + databaseService, err := client.GetDBAASServiceClickhouse(ctx, c.Name) + if err != nil { + return nil, err + } + + switch { + case c.ShowBackups: + out := make(dbServiceBackupListOutput, 0) + if databaseService.Backups != nil { + for _, b := range databaseService.Backups { + out = append(out, dbServiceBackupListItemOutput{ + Date: b.BackupTime, + Name: b.BackupName, + Size: b.DataSize, + }) + } + } + return &out, nil + + case c.ShowNotifications: + out := make(dbServiceNotificationListOutput, 0) + if databaseService.Notifications != nil { + for _, n := range databaseService.Notifications { + out = append(out, dbServiceNotificationListItemOutput{ + Level: string(n.Level), + Message: wordwrap.WrapString(n.Message, 50), + Type: string(n.Type), + }) + } + } + return &out, nil + + case c.ShowSettings != "": + switch c.ShowSettings { + case "clickhouse": + out, err := json.MarshalIndent(databaseService.ClickhouseSettings, "", " ") + if err != nil { + return nil, fmt.Errorf("unable to marshal JSON: %w", err) + } + fmt.Println(string(out)) + default: + return nil, fmt.Errorf( + "invalid settings value %q, expected one of: %s", + c.ShowSettings, + strings.Join(clickhouseSettings, ", "), + ) + } + return nil, nil + + case c.ShowURI: + if databaseService.ConnectionInfo != nil && len(databaseService.ConnectionInfo.URI) > 0 { + fmt.Println(databaseService.ConnectionInfo.URI[0]) + } + return nil, nil + } + + out := dbServiceShowOutput{ + Zone: c.Zone, + Name: string(databaseService.Name), + Type: string(databaseService.Type), + Plan: databaseService.Plan, + CreationDate: databaseService.CreatedAT, + Nodes: databaseService.NodeCount, + NodeCPUs: databaseService.NodeCPUCount, + NodeMemory: databaseService.NodeMemory, + UpdateDate: databaseService.UpdatedAT, + DiskSize: databaseService.DiskSize, + State: string(databaseService.State), + TerminationProtection: utils.DefaultBool(databaseService.TerminationProtection, false), + + Maintenance: func() (v *dbServiceMaintenanceShowOutput) { + if databaseService.Maintenance != nil { + v = &dbServiceMaintenanceShowOutput{ + DOW: string(databaseService.Maintenance.Dow), + Time: databaseService.Maintenance.Time, + } + } + return + }(), + + Clickhouse: &dbServiceClickhouseShowOutput{ + Components: func() (v []dbServiceClickhouseComponentShowOutput) { + if databaseService.Components != nil { + for _, c := range databaseService.Components { + v = append(v, dbServiceClickhouseComponentShowOutput{ + Component: c.Component, + Host: c.Host, + Port: c.Port, + Route: string(c.Route), + SSL: c.SSL, + Usage: string(c.Usage), + }) + } + } + return + }(), + + ConnectionInfo: func() (v *dbServiceClickhouseConnectionInfoShowOutput) { + if databaseService.ConnectionInfo != nil { + v = &dbServiceClickhouseConnectionInfoShowOutput{ + URI: databaseService.ConnectionInfo.URI, + MysqlURI: databaseService.ConnectionInfo.MysqlURI, + ArrowflightURI: databaseService.ConnectionInfo.ArrowflightURI, + } + } + return + }(), + + IPFilter: func() (v []string) { + if databaseService.IPFilter != nil { + v = databaseService.IPFilter + } + return + }(), + + PrometheusURI: func() (v *dbServiceClickhousePrometheusURIShowOutput) { + if databaseService.PrometheusURI != nil { + v = &dbServiceClickhousePrometheusURIShowOutput{ + Host: databaseService.PrometheusURI.Host, + Port: databaseService.PrometheusURI.Port, + } + } + return + }(), + + URI: databaseService.URI, + URIParams: databaseService.URIParams, + + Users: func() (v []dbServiceClickhouseUserShowOutput) { + if databaseService.Users != nil { + for _, u := range databaseService.Users { + v = append(v, dbServiceClickhouseUserShowOutput{ + Username: string(u.Username), + UUID: string(u.Uuid), + }) + } + } + return + }(), + + Version: databaseService.Version, + }, + } + + return &out, nil +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_type_show.go b/cmd/dbaas/dbaas_type_show.go index db2ebb7b7..4f838643a 100644 --- a/cmd/dbaas/dbaas_type_show.go +++ b/cmd/dbaas/dbaas_type_show.go @@ -107,7 +107,8 @@ var ( "pglookout", "timescaledb", } - valkeySettings = []string{"valkey"} + valkeySettings = []string{"valkey"} + clickhouseSettings = []string{"clickhouse"} ) type dbaasTypeShowCmd struct { @@ -343,6 +344,26 @@ func (c *dbaasTypeShowCmd) CmdRun(_ *cobra.Command, _ []string) error { //nolint settings = res.Settings.Thanos.Properties } + dbaasShowSettings(settings) + + case "clickhouse": + if !utils.IsInList(clickhouseSettings, c.ShowSettings) { + return fmt.Errorf( + "invalid settings value %q, expected one of: %s", + c.ShowSettings, + strings.Join(clickhouseSettings, ", "), + ) + } + + res, err := client.GetDBAASSettingsClickhouse(ctx) + if err != nil { + return err + } + + if c.ShowSettings == "clickhouse" { + settings = res.Settings.Clickhouse.Properties + } + dbaasShowSettings(settings) } diff --git a/cmd/dbaas/dbaas_update.go b/cmd/dbaas/dbaas_update.go index e651d2a58..b14d54ea7 100644 --- a/cmd/dbaas/dbaas_update.go +++ b/cmd/dbaas/dbaas_update.go @@ -24,8 +24,9 @@ type dbaasServiceUpdateCmd struct { HelpOpensearch bool `cli-usage:"show usage for flags specific to the opensearch type"` HelpMysql bool `cli-usage:"show usage for flags specific to the mysql type"` HelpPg bool `cli-usage:"show usage for flags specific to the pg type"` - HelpValkey bool `cli-usage:"show usage for flags specific to the valkey type"` - HelpThanos bool `cli-usage:"show usage for flags specific to the thanos type"` + HelpValkey bool `cli-usage:"show usage for flags specific to the valkey type"` + HelpThanos bool `cli-usage:"show usage for flags specific to the thanos type"` + HelpClickhouse bool `cli-usage:"show usage for flags specific to the clickhouse type"` MaintenanceDOW string `cli-flag:"maintenance-dow" cli-usage:"automated Database Service maintenance day-of-week"` MaintenanceTime string `cli-usage:"automated Database Service maintenance time (format HH:MM:SS)"` Plan string `cli-usage:"Database Service plan"` @@ -110,6 +111,11 @@ type dbaasServiceUpdateCmd struct { // "thanos" type specific flags ThanosIPFilter []string `cli-flag:"thanos-ip-filter" cli-usage:"allow incoming connections from CIDR address block" cli-hidden:""` ThanosSettings string `cli-flag:"thanos-settings" cli-usage:"Thanos configuration settings (JSON format)" cli-hidden:""` + + // "clickhouse" type specific flags + ClickhouseIPFilter []string `cli-flag:"clickhouse-ip-filter" cli-usage:"allow incoming connections from CIDR address block" cli-hidden:""` + ClickhouseSettings string `cli-flag:"clickhouse-settings" cli-usage:"ClickHouse configuration settings (JSON format)" cli-hidden:""` + ClickhouseVersion string `cli-flag:"clickhouse-version" cli-usage:"ClickHouse major version" cli-hidden:""` } func (c *dbaasServiceUpdateCmd) CmdAliases() []string { return nil } @@ -150,6 +156,9 @@ func (c *dbaasServiceUpdateCmd) CmdPreRun(cmd *cobra.Command, args []string) err case cmd.Flags().Changed("help-thanos"): exocmd.CmdShowHelpFlags(cmd.Flags(), "thanos-") os.Exit(0) + case cmd.Flags().Changed("help-clickhouse"): + exocmd.CmdShowHelpFlags(cmd.Flags(), "clickhouse-") + os.Exit(0) } exocmd.CmdSetZoneFlagFromDefault(cmd) @@ -196,6 +205,8 @@ func (c *dbaasServiceUpdateCmd) CmdRun(cmd *cobra.Command, args []string) error return c.updateValkey(cmd, args) case "thanos": return c.updateThanos(cmd, args) + case "clickhouse": + return c.updateClickhouse(cmd, args) } return nil diff --git a/cmd/dbaas/dbaas_update_clickhouse.go b/cmd/dbaas/dbaas_update_clickhouse.go new file mode 100644 index 000000000..4367acb39 --- /dev/null +++ b/cmd/dbaas/dbaas_update_clickhouse.go @@ -0,0 +1,90 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasServiceUpdateCmd) updateClickhouse(cmd *cobra.Command, _ []string) error { + var updated bool + + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return fmt.Errorf("unable to create client: %w", err) + } + + databaseService := v3.UpdateDBAASServiceClickhouseRequest{} + + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.ClickhouseIPFilter)) { + databaseService.IPFilter = c.ClickhouseIPFilter + updated = true + } + + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.Plan)) { + databaseService.Plan = c.Plan + updated = true + } + + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.TerminationProtection)) { + databaseService.TerminationProtection = &c.TerminationProtection + updated = true + } + + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.MaintenanceDOW)) && + cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.MaintenanceTime)) { + databaseService.Maintenance = &v3.UpdateDBAASServiceClickhouseRequestMaintenance{ + Dow: v3.UpdateDBAASServiceClickhouseRequestMaintenanceDow(c.MaintenanceDOW), + Time: c.MaintenanceTime, + } + updated = true + } + + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.ClickhouseSettings)) { + if c.ClickhouseSettings != "" { + settings := &v3.JSONSchemaClickhouse{} + if err := json.Unmarshal([]byte(c.ClickhouseSettings), settings); err != nil { + return err + } + databaseService.ClickhouseSettings = settings + } + updated = true + } + + if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.ClickhouseVersion)) { + databaseService.Version = c.ClickhouseVersion + updated = true + } + + if updated { + op, err := client.UpdateDBAASServiceClickhouse(ctx, c.Name, databaseService) + if err != nil { + return err + } + + utils.DecorateAsyncOperation(fmt.Sprintf("Updating DBaaS ClickHouse service %q", c.Name), func() { + op, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + + if err != nil { + return err + } + } + + if !globalstate.Quiet { + return c.OutputFunc((&dbaasServiceShowCmd{ + Name: c.Name, + Zone: c.Zone, + }).showDatabaseServiceClickhouse(ctx)) + } + return nil +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_user_create.go b/cmd/dbaas/dbaas_user_create.go index 7d6659fb3..0186870e4 100644 --- a/cmd/dbaas/dbaas_user_create.go +++ b/cmd/dbaas/dbaas_user_create.go @@ -69,6 +69,8 @@ func (c *dbaasUserCreateCmd) CmdRun(cmd *cobra.Command, args []string) error { return c.createOpensearch(cmd, args) case "valkey": return c.createValkey(cmd, args) + case "clickhouse": + return c.createClickhouse(cmd, args) default: return fmt.Errorf("creating user unsupported for service of type %q", db.Type) } diff --git a/cmd/dbaas/dbaas_user_create_clickhouse.go b/cmd/dbaas/dbaas_user_create_clickhouse.go new file mode 100644 index 000000000..23f45a692 --- /dev/null +++ b/cmd/dbaas/dbaas_user_create_clickhouse.go @@ -0,0 +1,45 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "fmt" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + v3 "github.com/exoscale/egoscale/v3" + "github.com/spf13/cobra" +) + +func (c *dbaasUserCreateCmd) createClickhouse(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return err + } + + // Check that the service is ready + s, err := client.GetDBAASServiceClickhouse(ctx, c.Name) + if err != nil { + return err + } + if len(s.Users) == 0 { + return fmt.Errorf("service %q is not ready for user creation", c.Name) + } + + req := v3.CreateDBAASClickhouseUserRequest{ + Username: v3.DBAASUserUsername(c.Username), + } + + // ClickHouse user creation is synchronous and returns secrets directly + secrets, err := client.CreateDBAASClickhouseUser(ctx, c.Name, req) + if err != nil { + return err + } + + if !globalstate.Quiet { + fmt.Printf("User %q created. Password: %s\n", c.Username, secrets.Password) + } + + return nil +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_user_delete.go b/cmd/dbaas/dbaas_user_delete.go index 2bd16d6c8..3d8c56632 100644 --- a/cmd/dbaas/dbaas_user_delete.go +++ b/cmd/dbaas/dbaas_user_delete.go @@ -51,6 +51,8 @@ func (c *dbaasUserDeleteCmd) CmdRun(cmd *cobra.Command, args []string) error { return c.deleteOpensearch(cmd, args) case "valkey": return c.deleteValkey(cmd, args) + case "clickhouse": + return c.deleteClickhouse(cmd, args) default: return fmt.Errorf("deleting user unsupported for service of type %q", db.Type) } diff --git a/cmd/dbaas/dbaas_user_delete_clickhouse.go b/cmd/dbaas/dbaas_user_delete_clickhouse.go new file mode 100644 index 000000000..a975897c3 --- /dev/null +++ b/cmd/dbaas/dbaas_user_delete_clickhouse.go @@ -0,0 +1,63 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "fmt" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/utils" + v3 "github.com/exoscale/egoscale/v3" + "github.com/spf13/cobra" +) + +func (c *dbaasUserDeleteCmd) deleteClickhouse(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return err + } + + // GetDBAASServiceClickhouse does not reflect newly created users; use the + // dedicated list endpoint for username -> UUID resolution. + users, err := client.ListDBAASClickhouseUsers(ctx, c.Name) + if err != nil { + return err + } + + userUUID := "" + for _, u := range users.Users { + if string(u.Username) == c.Username { + userUUID = string(u.Uuid) + break + } + } + if userUUID == "" { + return fmt.Errorf("user %q not found for service %q", c.Username, c.Name) + } + + if !c.Force { + if !utils.AskQuestion( + ctx, + fmt.Sprintf( + "Are you sure you want to delete user %q from service %q?", + c.Username, + c.Name, + ), + ) { + return nil + } + } + + op, err := client.DeleteDBAASClickhouseUser(ctx, c.Name, v3.UUID(userUUID)) + if err != nil { + return err + } + + utils.DecorateAsyncOperation(fmt.Sprintf("Deleting user %q...", c.Username), func() { + op, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + + return err +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_user_list.go b/cmd/dbaas/dbaas_user_list.go index 13f83d01d..d15b01270 100644 --- a/cmd/dbaas/dbaas_user_list.go +++ b/cmd/dbaas/dbaas_user_list.go @@ -67,6 +67,8 @@ func (c *dbaasUserListCmd) CmdRun(cmd *cobra.Command, args []string) error { return c.listValkey(cmd, args) case "thanos": return c.listThanos(cmd, args) + case "clickhouse": + return c.listClickhouse(cmd, args) default: return fmt.Errorf("listing users unsupported for service of type %q", db.Type) diff --git a/cmd/dbaas/dbaas_user_list_clickhouse.go b/cmd/dbaas/dbaas_user_list_clickhouse.go new file mode 100644 index 000000000..8b180c9d2 --- /dev/null +++ b/cmd/dbaas/dbaas_user_list_clickhouse.go @@ -0,0 +1,33 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "github.com/spf13/cobra" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasUserListCmd) listClickhouse(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return err + } + + s, err := client.GetDBAASServiceClickhouse(ctx, c.Name) + if err != nil { + return err + } + + res := make(dbaasUsersListOutput, 0) + for _, u := range s.Users { + res = append(res, dbaasUsersListItemOutput{ + Username: string(u.Username), + }) + } + + return c.OutputFunc(&res, nil) +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_user_reset.go b/cmd/dbaas/dbaas_user_reset.go index 182c96589..67a1552fc 100644 --- a/cmd/dbaas/dbaas_user_reset.go +++ b/cmd/dbaas/dbaas_user_reset.go @@ -68,6 +68,8 @@ func (c *dbaasUserResetCmd) CmdRun(cmd *cobra.Command, args []string) error { return c.resetGrafana(cmd, args) case "valkey": return c.resetValkey(cmd, args) + case "clickhouse": + return c.resetClickhouse(cmd, args) default: return fmt.Errorf("reseting user credentials unsupported for service of type %q", db.Type) } diff --git a/cmd/dbaas/dbaas_user_reset_clickhouse.go b/cmd/dbaas/dbaas_user_reset_clickhouse.go new file mode 100644 index 000000000..b9b7493de --- /dev/null +++ b/cmd/dbaas/dbaas_user_reset_clickhouse.go @@ -0,0 +1,37 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "fmt" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + v3 "github.com/exoscale/egoscale/v3" + "github.com/spf13/cobra" +) + +func (c *dbaasUserResetCmd) resetClickhouse(_ *cobra.Command, _ []string) error { + ctx := exocmd.GContext + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return err + } + + req := v3.ResetDBAASClickhouseUserPasswordRequest{} + if c.Password != "" { + req.Password = v3.DBAASUserPassword(c.Password) + } + + // ClickHouse reset is synchronous and returns secrets directly + secrets, err := client.ResetDBAASClickhouseUserPassword(ctx, c.Name, c.Username, req) + if err != nil { + return err + } + + if !globalstate.Quiet { + fmt.Printf("Password for user %q reset. New password: %s\n", c.Username, secrets.Password) + } + + return nil +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_user_reveal.go b/cmd/dbaas/dbaas_user_reveal.go index 6e986849f..5003d1b55 100644 --- a/cmd/dbaas/dbaas_user_reveal.go +++ b/cmd/dbaas/dbaas_user_reveal.go @@ -82,6 +82,8 @@ func (c *dbaasUserRevealCmd) CmdRun(cmd *cobra.Command, args []string) error { return c.OutputFunc(c.revealGrafana(ctx)) case "valkey": return c.OutputFunc(c.revealValkey(ctx)) + case "clickhouse": + return c.OutputFunc(c.revealClickhouse(ctx)) case "thanos": return c.OutputFunc(c.revealThanos(ctx)) default: diff --git a/cmd/dbaas/dbaas_user_reveal_clickhouse.go b/cmd/dbaas/dbaas_user_reveal_clickhouse.go new file mode 100644 index 000000000..f8fcbe9d6 --- /dev/null +++ b/cmd/dbaas/dbaas_user_reveal_clickhouse.go @@ -0,0 +1,33 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "context" + "fmt" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasUserRevealCmd) revealClickhouse(ctx context.Context) (output.Outputter, error) { + if c.Username != "avnadmin" { + return nil, fmt.Errorf("reveal password is only supported for the avnadmin user") + } + + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return &dbaasUserRevealOutput{}, err + } + + s, err := client.RevealDBAASClickhouseUserPassword(ctx, c.Name, c.Username) + if err != nil { + return &dbaasUserRevealOutput{}, err + } + + return &dbaasUserRevealOutput{ + Username: c.Username, + Password: s.Password, + }, nil +} \ No newline at end of file diff --git a/cmd/dbaas/dbaas_user_show.go b/cmd/dbaas/dbaas_user_show.go index 65da8af60..5990d75bc 100644 --- a/cmd/dbaas/dbaas_user_show.go +++ b/cmd/dbaas/dbaas_user_show.go @@ -86,6 +86,8 @@ func (c *dbaasUserShowCmd) CmdRun(cmd *cobra.Command, args []string) error { return c.OutputFunc(c.showGrafana(ctx)) case "valkey": return c.OutputFunc(c.showValkey(ctx)) + case "clickhouse": + return c.OutputFunc(c.showClickhouse(ctx)) case "thanos": return c.OutputFunc(c.showThanos(ctx)) default: diff --git a/cmd/dbaas/dbaas_user_show_clickhouse.go b/cmd/dbaas/dbaas_user_show_clickhouse.go new file mode 100644 index 000000000..8e79240ae --- /dev/null +++ b/cmd/dbaas/dbaas_user_show_clickhouse.go @@ -0,0 +1,34 @@ +// AI-modified by hermes-agent - not reviewed yet +package dbaas + +import ( + "context" + "fmt" + + exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasUserShowCmd) showClickhouse(ctx context.Context) (output.Outputter, error) { + client, err := exocmd.SwitchClientZoneV3(ctx, globalstate.EgoscaleV3Client, v3.ZoneName(c.Zone)) + if err != nil { + return &dbaasUserShowOutput{}, err + } + + s, err := client.GetDBAASServiceClickhouse(ctx, c.Name) + if err != nil { + return &dbaasUserShowOutput{}, err + } + + for _, u := range s.Users { + if string(u.Username) == c.Username { + return &dbaasUserShowOutput{ + Username: c.Username, + }, nil + } + } + + return &dbaasUserShowOutput{}, fmt.Errorf("user %q not found for service %q", c.Username, c.Name) +} \ No newline at end of file diff --git a/cmd/internal/x/x.gen.go b/cmd/internal/x/x.gen.go index 9dabc8c72..9a4a0352d 100644 --- a/cmd/internal/x/x.gen.go +++ b/cmd/internal/x/x.gen.go @@ -2257,6 +2257,91 @@ func XGetDbaasClickhouseAclConfig(paramServiceName string, params *viper.Viper) return resp, decoded, nil } +// XListDbaasClickhouseRoles [BETA] List DBaaS ClickHouse roles +func XListDbaasClickhouseRoles(paramServiceName string, params *viper.Viper) (*gentleman.Response, map[string]interface{}, error) { + handlerPath := "list-dbaas-clickhouse-roles" + if xSubcommand { + handlerPath = "x " + handlerPath + } + + server := viper.GetString("server") + if server == "" { + server = xServers()[viper.GetInt("server-index")]["url"] + } + + url := server + "/dbaas-clickhouse/{service-name}/role" + url = strings.Replace(url, "{service-name}", paramServiceName, 1) + + req := cli.Client.Get().URL(url) + + cli.HandleBefore(handlerPath, params, req) + + resp, err := req.Do() + if err != nil { + return nil, nil, errors.Wrap(err, "Request failed") + } + + var decoded map[string]interface{} + + if resp.StatusCode < 400 { + if err := cli.UnmarshalResponse(resp, &decoded); err != nil { + return nil, nil, errors.Wrap(err, "Unmarshalling response failed") + } + } else { + return nil, nil, errors.Errorf("HTTP %d: %s", resp.StatusCode, resp.String()) + } + + after := cli.HandleAfter(handlerPath, params, resp, decoded) + if after != nil { + decoded = after.(map[string]interface{}) + } + + return resp, decoded, nil +} + +// XDeleteDbaasClickhouseRole [BETA] Delete a DBaaS ClickHouse role +func XDeleteDbaasClickhouseRole(paramServiceName string, paramRoleUuid string, params *viper.Viper) (*gentleman.Response, map[string]interface{}, error) { + handlerPath := "delete-dbaas-clickhouse-role" + if xSubcommand { + handlerPath = "x " + handlerPath + } + + server := viper.GetString("server") + if server == "" { + server = xServers()[viper.GetInt("server-index")]["url"] + } + + url := server + "/dbaas-clickhouse/{service-name}/role/{role-uuid}" + url = strings.Replace(url, "{service-name}", paramServiceName, 1) + url = strings.Replace(url, "{role-uuid}", paramRoleUuid, 1) + + req := cli.Client.Delete().URL(url) + + cli.HandleBefore(handlerPath, params, req) + + resp, err := req.Do() + if err != nil { + return nil, nil, errors.Wrap(err, "Request failed") + } + + var decoded map[string]interface{} + + if resp.StatusCode < 400 { + if err := cli.UnmarshalResponse(resp, &decoded); err != nil { + return nil, nil, errors.Wrap(err, "Unmarshalling response failed") + } + } else { + return nil, nil, errors.Errorf("HTTP %d: %s", resp.StatusCode, resp.String()) + } + + after := cli.HandleAfter(handlerPath, params, resp, decoded) + if after != nil { + decoded = after.(map[string]interface{}) + } + + return resp, decoded, nil +} + // XCreateDbaasClickhouseUser [BETA] Create a DBaaS ClickHouse user func XCreateDbaasClickhouseUser(paramServiceName string, params *viper.Viper, body string) (*gentleman.Response, map[string]interface{}, error) { handlerPath := "create-dbaas-clickhouse-user" @@ -2346,7 +2431,7 @@ func XListDbaasClickhouseUsers(paramServiceName string, params *viper.Viper) (*g } // XDeleteDbaasClickhouseUser [BETA] Delete a DBaaS ClickHouse user -func XDeleteDbaasClickhouseUser(paramServiceName string, paramUsername string, params *viper.Viper) (*gentleman.Response, map[string]interface{}, error) { +func XDeleteDbaasClickhouseUser(paramServiceName string, paramUserUuid string, params *viper.Viper) (*gentleman.Response, map[string]interface{}, error) { handlerPath := "delete-dbaas-clickhouse-user" if xSubcommand { handlerPath = "x " + handlerPath @@ -2357,9 +2442,9 @@ func XDeleteDbaasClickhouseUser(paramServiceName string, paramUsername string, p server = xServers()[viper.GetInt("server-index")]["url"] } - url := server + "/dbaas-clickhouse/{service-name}/user/{username}" + url := server + "/dbaas-clickhouse/{service-name}/user/{user-uuid}" url = strings.Replace(url, "{service-name}", paramServiceName, 1) - url = strings.Replace(url, "{username}", paramUsername, 1) + url = strings.Replace(url, "{user-uuid}", paramUserUuid, 1) req := cli.Client.Delete().URL(url) @@ -18231,6 +18316,76 @@ func xRegister(subcommand bool) { }() + func() { + params := viper.New() + + var examples string + + cmd := &cobra.Command{ + Use: "list-dbaas-clickhouse-roles service-name", + Short: "[BETA] List DBaaS ClickHouse roles", + Long: cli.Markdown(""), + Example: examples, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + _, decoded, err := XListDbaasClickhouseRoles(args[0], params) + if err != nil { + log.Fatal().Err(err).Msg("Error calling operation") + } + + if err := cli.Formatter.Format(decoded); err != nil { + log.Fatal().Err(err).Msg("Formatting failed") + } + + }, + } + + root.AddCommand(cmd) + + cli.SetCustomFlags(cmd) + + if cmd.Flags().HasFlags() { + params.BindPFlags(cmd.Flags()) + } + + }() + + func() { + params := viper.New() + + var examples string + + cmd := &cobra.Command{ + Use: "delete-dbaas-clickhouse-role service-name role-uuid", + Short: "[BETA] Delete a DBaaS ClickHouse role", + Long: cli.Markdown(""), + Example: examples, + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + + _, decoded, err := XDeleteDbaasClickhouseRole(args[0], args[1], params) + if err != nil { + log.Fatal().Err(err).Msg("Error calling operation") + } + + if err := cli.Formatter.Format(decoded); err != nil { + log.Fatal().Err(err).Msg("Formatting failed") + } + + }, + } + + root.AddCommand(cmd) + + cli.SetCustomFlags(cmd) + + if cmd.Flags().HasFlags() { + params.BindPFlags(cmd.Flags()) + } + + }() + func() { params := viper.New() @@ -18311,7 +18466,7 @@ func xRegister(subcommand bool) { var examples string cmd := &cobra.Command{ - Use: "delete-dbaas-clickhouse-user service-name username", + Use: "delete-dbaas-clickhouse-user service-name user-uuid", Short: "[BETA] Delete a DBaaS ClickHouse user", Long: cli.Markdown(""), Example: examples, diff --git a/go.mod b/go.mod index 3d0f9d44f..da781553b 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0 github.com/aws/smithy-go v1.27.3 github.com/dustin/go-humanize v1.0.1 - github.com/exoscale/egoscale/v3 v3.1.42 + github.com/exoscale/egoscale/v3 v3.1.44 github.com/exoscale/openapi-cli-generator v1.2.0 github.com/fatih/camelcase v1.0.0 github.com/hashicorp/go-multierror v1.1.1 diff --git a/go.sum b/go.sum index 867d82700..8a4614d88 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/exoscale/egoscale/v3 v3.1.42 h1:KlFDdm2ga1RdCdKuKlzJxLmgJjWVcDbJxF0t6FDswzw= -github.com/exoscale/egoscale/v3 v3.1.42/go.mod h1:DUTgeubl5msPAo3SKFed04AxNhyTNOrCTJHZDRYLR10= +github.com/exoscale/egoscale/v3 v3.1.44 h1:3gPKXWoL/I+4hYgUMkKN3SSGnjWOcz87olk77SpJLJM= +github.com/exoscale/egoscale/v3 v3.1.44/go.mod h1:DUTgeubl5msPAo3SKFed04AxNhyTNOrCTJHZDRYLR10= github.com/exoscale/openapi-cli-generator v1.2.0 h1:xgTff1bInBP+JZCauD7Jq9GNBFoKK31Cnv5FIAcxtrk= github.com/exoscale/openapi-cli-generator v1.2.0/go.mod h1:TZBnbT7f3hJ5ImyUphJwRM+X5xF/zCQZ6o8a42gQeTs= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= diff --git a/vendor/github.com/exoscale/egoscale/v3/schemas.go b/vendor/github.com/exoscale/egoscale/v3/schemas.go index f0baa22c0..0858a37ce 100644 --- a/vendor/github.com/exoscale/egoscale/v3/schemas.go +++ b/vendor/github.com/exoscale/egoscale/v3/schemas.go @@ -337,6 +337,37 @@ type DBAASClickhouseAclConfig struct { Users []DBAASClickhouseUserAclConfig `json:"users,omitempty"` } +type DBAASClickhouseGrantedRole struct { + ISDefault *bool `json:"is-default,omitempty"` + // Granted role name + Name string `json:"name" validate:"required"` + Uuid string `json:"uuid,omitempty"` + WithAdminOption *bool `json:"with-admin-option,omitempty"` +} + +type DBAASClickhouseRole struct { + GrantedRoles []DBAASClickhouseGrantedRole `json:"granted-roles,omitempty"` + Name DBAASUserUsername `json:"name" validate:"required,gte=1,lte=64"` + Privileges []DBAASClickhouseRolePrivilege `json:"privileges,omitempty"` + Uuid string `json:"uuid,omitempty"` +} + +type DBAASClickhouseRolePrivilege struct { + // Column + Column string `json:"column,omitempty"` + // Database + Database string `json:"database,omitempty"` + GrantOption *bool `json:"grant-option,omitempty"` + ISPartialRevoke *bool `json:"is-partial-revoke,omitempty"` + Name DBAASUserUsername `json:"name" validate:"required,gte=1,lte=64"` + // Table + Table string `json:"table,omitempty"` +} + +type DBAASClickhouseRoles struct { + Roles []DBAASClickhouseRole `json:"roles,omitempty"` +} + type DBAASClickhouseUser struct { Required *bool `json:"required,omitempty"` Username DBAASUserUsername `json:"username" validate:"required,gte=1,lte=64"` @@ -3115,6 +3146,8 @@ type JSONSchemaClickhouseServerSettings struct { type JSONSchemaClickhouse struct { // ClickHouse server settings, which can be found in the `system.server_settings` table. ServerSettings *JSONSchemaClickhouseServerSettings `json:"server_settings,omitempty"` + // The percentage of free disk space required on local storage before data is moved to object storage. A value of 0.2 means data is moved when local storage has less than 20% free space. + TieredStorageMoveFactor float64 `json:"tiered_storage_move_factor,omitempty" validate:"omitempty,gte=0,lte=1"` } type JSONSchemaGrafanaAlertingErrorORTimeout string