diff --git a/CHANGELOG.md b/CHANGELOG.md index 9348a1bed..d2ab47171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - storage: Adding recursive feature to the storage command #653 - Update help for instance protection #658 +- dbaas: added commands for dbaas acl management #659 ## Unreleased diff --git a/cmd/dbaas.go b/cmd/dbaas.go index 0ad44f025..3ef7ebc81 100644 --- a/cmd/dbaas.go +++ b/cmd/dbaas.go @@ -198,3 +198,22 @@ func dbaasGetV3(ctx context.Context, name, zone string) (v3.DBAASServiceCommon, return v3.DBAASServiceCommon{}, fmt.Errorf("%q Database Service not found in zone %q", name, zone) } + +// FindServiceAcrossZones searches for a DBaaS service across all available zones. +func FindServiceAcrossZones(ctx context.Context, client *v3.Client, serviceName string) (v3.DBAASServiceCommon, v3.ZoneName, error) { + // Fetch all available zones + zones, err := client.ListZones(ctx) + if err != nil { + return v3.DBAASServiceCommon{}, "", fmt.Errorf("error fetching zones: %w", err) + } + + // Iterate through zones to find the service + for _, zone := range zones.Zones { + db, err := dbaasGetV3(ctx, serviceName, string(zone.Name)) + if err == nil { + return db, zone.Name, nil + } + } + + return v3.DBAASServiceCommon{}, "", fmt.Errorf("service %q not found in any zone", serviceName) +} diff --git a/cmd/dbaas_acl.go b/cmd/dbaas_acl.go new file mode 100644 index 000000000..a2f1ab061 --- /dev/null +++ b/cmd/dbaas_acl.go @@ -0,0 +1,14 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var dbaasAclCmd = &cobra.Command{ + Use: "acl", + Short: "Manage DBaaS acl", +} + +func init() { + dbaasCmd.AddCommand(dbaasAclCmd) +} diff --git a/cmd/dbaas_acl_create.go b/cmd/dbaas_acl_create.go new file mode 100644 index 000000000..925f14882 --- /dev/null +++ b/cmd/dbaas_acl_create.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "fmt" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/spf13/cobra" +) + +type dbaasAclCreateCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"create"` + Name string `cli-flag:"name" cli-usage:"Name of the DBaaS service"` + Username string `cli-flag:"username" cli-usage:"Username for the ACL entry"` + ServiceType string `cli-flag:"type" cli-short:"t" cli-usage:"Type of the DBaaS service (e.g., kafka opensearch)"` + Pattern string `cli-flag:"pattern" cli-usage:"The pattern for the ACL rule (index* for OpenSearch or topic for Kafka, max 249 characters)"` + Permission string `cli-flag:"permission" cli-usage:"Permission to apply (should be one of admin, read, readwrite, write, or deny (only for OpenSearch))"` +} + +// Command aliases (none in this case) +func (c *dbaasAclCreateCmd) cmdAliases() []string { return nil } + +// Short description for the command +func (c *dbaasAclCreateCmd) cmdShort() string { + return "Create an ACL entry for a DBaaS service" +} + +// Long description for the command +func (c *dbaasAclCreateCmd) cmdLong() string { + return `This command creates an ACL entry for a specified DBaaS service, automatically searching for the service across all available zones.` +} + +func (c *dbaasAclCreateCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) +} + +// Main run logic for showing ACL details +func (c *dbaasAclCreateCmd) cmdRun(cmd *cobra.Command, args []string) error { + ctx := gContext + + // Validate required inputs + if c.Name == "" || c.Username == "" || c.ServiceType == "" || c.Permission == "" || c.Pattern == "" { + return fmt.Errorf("all --name, --username, --type, --permission and --pattern flags must be specified") + } + + // Search for the service in each zone + service, zone, err := FindServiceAcrossZones(ctx, globalstate.EgoscaleV3Client, c.Name) + if err != nil { + return fmt.Errorf("error finding service: %w", err) + } + + // Switch client to the appropriate zone + client, err := switchClientZoneV3(ctx, globalstate.EgoscaleV3Client, zone) + if err != nil { + return fmt.Errorf("error initializing client for zone %s: %w", zone, err) + } + // Validate the service type + if string(service.Type) != c.ServiceType { + return fmt.Errorf("service type mismatch: expected %q but got %q for service %q", c.ServiceType, service.Type, c.Name) + } + + switch service.Type { + case "kafka": + return c.createKafka(ctx, client, c.Name) + case "opensearch": + return c.createOpensearch(ctx, client, c.Name) + default: + return fmt.Errorf("create ACL unsupported for service type %q", service.Type) + } +} +func init() { + cobra.CheckErr(registerCLICommand(dbaasAclCmd, &dbaasAclCreateCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas_acl_create_kafka.go b/cmd/dbaas_acl_create_kafka.go new file mode 100644 index 000000000..573aaa53c --- /dev/null +++ b/cmd/dbaas_acl_create_kafka.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "context" + "fmt" + + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasAclCreateCmd) createKafka(ctx context.Context, client *v3.Client, serviceName string) error { + // Define the new Kafka ACL entry + newAcl := v3.DBAASKafkaTopicAclEntry{ + Username: c.Username, + Topic: c.Pattern, + Permission: v3.DBAASKafkaTopicAclEntryPermission(c.Permission), + } + + // Trigger the creation of the ACL entry + op, err := client.CreateDBAASKafkaTopicAclConfig(ctx, serviceName, newAcl) + if err != nil { + return fmt.Errorf("error creating ACL entry for service %q: %w", serviceName, err) + } + + // Use decorateAsyncOperation to handle the operation and provide user feedback + decorateAsyncOperation(fmt.Sprintf("Creating Kafka ACL entry for user %q", c.Username), func() { + op, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + + if err != nil { + return fmt.Errorf("error completing ACL creation: %w", err) + } + + fmt.Printf("Kafka ACL entry for user %q successfully created in service %q\n", c.Username, serviceName) + return nil +} diff --git a/cmd/dbaas_acl_create_opensearch.go b/cmd/dbaas_acl_create_opensearch.go new file mode 100644 index 000000000..abc4d0d1b --- /dev/null +++ b/cmd/dbaas_acl_create_opensearch.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "context" + "fmt" + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasAclCreateCmd) createOpensearch(ctx context.Context, client *v3.Client, serviceName string) error { + aclsConfig, err := client.GetDBAASOpensearchAclConfig(ctx, serviceName) + if err != nil { + return fmt.Errorf("error fetching ACL configuration for service %q: %w", serviceName, err) + } + + // Check if an entry with the same username already exists + for _, acl := range aclsConfig.Acls { + if string(acl.Username) == c.Username { + return fmt.Errorf("ACL entry for username %q already exists in service %q", c.Username, serviceName) + } + } + + // Create a new ACL entry + newAcl := v3.DBAASOpensearchAclConfigAcls{ + Username: v3.DBAASUserUsername(c.Username), + Rules: []v3.DBAASOpensearchAclConfigAclsRules{ + {Index: c.Pattern, Permission: v3.EnumOpensearchRulePermission(c.Permission)}, + }, + } + + // Append the new entry to the existing ACLs + aclsConfig.Acls = append(aclsConfig.Acls, newAcl) + + // Update the configuration with the new entry + op, err := client.UpdateDBAASOpensearchAclConfig(ctx, serviceName, *aclsConfig) + if err != nil { + return fmt.Errorf("error updating ACL configuration for service %q: %w", serviceName, err) + } + + // Use decorateAsyncOperation to wait for the operation and provide user feedback + decorateAsyncOperation(fmt.Sprintf("Creating ACL entry for user %q", c.Username), func() { + op, err = client.Wait(ctx, op, v3.OperationStateSuccess) + }) + + fmt.Printf("ACL entry for username %q created successfully in service %q\n", c.Username, serviceName) + return nil +} diff --git a/cmd/dbaas_acl_delete.go b/cmd/dbaas_acl_delete.go new file mode 100644 index 000000000..326b96cdb --- /dev/null +++ b/cmd/dbaas_acl_delete.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "fmt" + + "github.com/exoscale/cli/pkg/globalstate" + "github.com/spf13/cobra" +) + +type dbaasAclDeleteCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"delete"` + Name string `cli-flag:"name" cli-usage:"Name of the DBaaS service"` + ServiceType string `cli-flag:"type" cli-short:"t" cli-usage:"DBaaS service type (currently only kafka supported)"` + Username string `cli-flag:"username" cli-usage:"Username of the ACL entry"` +} + +// Command aliases (none in this case) +func (c *dbaasAclDeleteCmd) cmdAliases() []string { return nil } + +// Short description for the command +func (c *dbaasAclDeleteCmd) cmdShort() string { return "Delete an ACL entry for a DBaaS service" } + +// Long description for the command +func (c *dbaasAclDeleteCmd) cmdLong() string { + return `This command deletes a specified ACL entry for a DBaaS service, such as Kafka or OpenSearch, across all available zones.` +} + +func (c *dbaasAclDeleteCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) // Default validations +} + +// Main run logic for showing ACL details +func (c *dbaasAclDeleteCmd) cmdRun(cmd *cobra.Command, args []string) error { + ctx := gContext + + // Validate required flags + if c.Name == "" || c.ServiceType == "" || c.Username == "" { + return fmt.Errorf("all flags --name, --type, and --username must be specified") + } + + // Search for the service in each zone + service, zone, err := FindServiceAcrossZones(ctx, globalstate.EgoscaleV3Client, c.Name) + if err != nil { + return fmt.Errorf("error finding service: %w", err) + } + + client, err := switchClientZoneV3(ctx, globalstate.EgoscaleV3Client, zone) + if err != nil { + return fmt.Errorf("error initializing client for zone %s: %w", zone, err) + } + + // Validate the service type + if string(service.Type) != c.ServiceType { + return fmt.Errorf("mismatched service type: expected %q but got %q for service %q", c.ServiceType, service.Type, c.Name) + } + + // Call the appropriate delete logic based on the service type + switch service.Type { + case "kafka": + err = c.deleteKafkaACL(ctx, client, c.Name, c.Username) + default: + return fmt.Errorf("deleting ACL unsupported for service type %q", service.Type) + } + + if err != nil { + return err + } + + cmd.Println(fmt.Sprintf("Successfully deleted ACL entry for username %q in service %q.", c.Username, c.Name)) + return nil +} + +func init() { + cobra.CheckErr(registerCLICommand(dbaasAclCmd, &dbaasAclDeleteCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas_acl_delete_kafka.go b/cmd/dbaas_acl_delete_kafka.go new file mode 100644 index 000000000..636d8c4e2 --- /dev/null +++ b/cmd/dbaas_acl_delete_kafka.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "context" + "fmt" + + v3 "github.com/exoscale/egoscale/v3" +) + +// deleteKafkaACL deletes a Kafka ACL entry for the specified username. +func (c *dbaasAclDeleteCmd) deleteKafkaACL(ctx context.Context, client *v3.Client, serviceName, username string) error { + // Fetch Kafka ACLs for the service + acls, err := client.GetDBAASKafkaAclConfig(ctx, serviceName) + if err != nil { + return fmt.Errorf("error fetching Kafka ACL configuration: %w", err) + } + + // Find ACL entries for the given username and delete them + var found bool + for _, acl := range acls.TopicAcl { + if acl.Username == username { + found = true + // Use the correct delete function to remove the topic ACL + op, err := client.DeleteDBAASKafkaTopicAclConfig(ctx, serviceName, string(acl.ID)) + if err != nil { + return fmt.Errorf("error deleting ACL entry %q for topic %q: %w", acl.ID, acl.Topic, err) + } + + // Wait for the operation to complete (if applicable) + _, waitErr := client.Wait(ctx, op, v3.OperationStateSuccess) + if waitErr != nil { + return fmt.Errorf("error waiting for ACL deletion operation: %w", waitErr) + } + } + } + + if !found { + return fmt.Errorf("no ACL entry found for username %q in service %q", username, serviceName) + } + + return nil +} diff --git a/cmd/dbaas_acl_list.go b/cmd/dbaas_acl_list.go new file mode 100644 index 000000000..ad93bd54a --- /dev/null +++ b/cmd/dbaas_acl_list.go @@ -0,0 +1,83 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + + "github.com/exoscale/cli/pkg/globalstate" + "github.com/exoscale/cli/pkg/output" +) + +type dbaasAclListCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"list"` + Name string `cli-flag:"name" cli-usage:"Name of the DBaaS service"` + ServiceType string `cli-flag:"type" cli-short:"t" cli-usage:"Type of the DBaaS service (e.g., kafka, opensearch)"` +} + +// Command aliases (none in this case) +func (c *dbaasAclListCmd) cmdAliases() []string { return nil } + +// Short description for the command +func (c *dbaasAclListCmd) cmdShort() string { return "List ACL entries for a DBaaS service" } + +// Long description for the command +func (c *dbaasAclListCmd) cmdLong() string { + return `This command lists ACL entries for a specified DBaaS service, including Kafka and OpenSearch, across all available zones.` +} + +// Pre-run validation for required flags +func (c *dbaasAclListCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) // Run default validations +} + +// Main run logic for listing ACLs +func (c *dbaasAclListCmd) cmdRun(cmd *cobra.Command, args []string) error { + ctx := gContext + + // Validate required flags + if c.Name == "" || c.ServiceType == "" { + return fmt.Errorf("both --name and --type flags must be specified") + } + + // Search for the service in each zone + service, zone, err := FindServiceAcrossZones(ctx, globalstate.EgoscaleV3Client, c.Name) + if err != nil { + return fmt.Errorf("error finding service: %w", err) + } + + client, err := switchClientZoneV3(ctx, globalstate.EgoscaleV3Client, zone) + if err != nil { + return fmt.Errorf("error initializing client for zone %s: %w", zone, err) + } + + // Validate the service type + if string(service.Type) != c.ServiceType { + return fmt.Errorf("mismatched service type: expected %q but got %q for service %q", c.ServiceType, service.Type, c.Name) + } + + // Determine the appropriate listing logic based on the service type + var output output.Outputter + switch service.Type { + case "kafka": + output, err = c.listKafkaACL(ctx, client, c.Name) + case "opensearch": + output, err = c.listOpenSearchACL(ctx, client, c.Name) + default: + return fmt.Errorf("listing ACL unsupported for service type %q", service.Type) + } + + if err != nil { + return err + } + + // Output the fetched details + return c.outputFunc(output, nil) +} + +func init() { + cobra.CheckErr(registerCLICommand(dbaasAclCmd, &dbaasAclListCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas_acl_list_kafka.go b/cmd/dbaas_acl_list_kafka.go new file mode 100644 index 000000000..f1d156f97 --- /dev/null +++ b/cmd/dbaas_acl_list_kafka.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + v3 "github.com/exoscale/egoscale/v3" +) + +// dbaasAclListKafkaOutput defines the Kafka ACL output structure. +type dbaasAclListKafkaOutput struct { + TopicAcls []v3.DBAASKafkaTopicAclEntry `json:"topic_acls,omitempty"` +} + +// ToJSON outputs the result in JSON format. +func (o *dbaasAclListKafkaOutput) ToJSON() { output.JSON(o) } + +// ToText outputs the result in plain text format. +func (o *dbaasAclListKafkaOutput) ToText() { output.Text(o) } + +// ToTable outputs the result in a tabular format. +func (o *dbaasAclListKafkaOutput) ToTable() { + tabular := table.NewTable(os.Stdout) + tabular.SetHeader([]string{"Username", "Topic", "Permission"}) + + // Display Topic ACL entries. + for _, acl := range o.TopicAcls { + tabular.Append([]string{acl.Username, acl.Topic, string(acl.Permission)}) + } + + tabular.Render() +} + +// listKafkaACL fetches Kafka ACLs and prepares the output. +func (c *dbaasAclListCmd) listKafkaACL(ctx context.Context, client *v3.Client, serviceName string) (output.Outputter, error) { + acls, err := client.GetDBAASKafkaAclConfig(ctx, serviceName) + if err != nil { + return nil, fmt.Errorf("error fetching Kafka ACL configuration: %w", err) + } + + return &dbaasAclListKafkaOutput{ + TopicAcls: acls.TopicAcl, + }, nil +} diff --git a/cmd/dbaas_acl_list_opensearch.go b/cmd/dbaas_acl_list_opensearch.go new file mode 100644 index 000000000..88c63bd16 --- /dev/null +++ b/cmd/dbaas_acl_list_opensearch.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + v3 "github.com/exoscale/egoscale/v3" +) + +// dbaasAclListOpenSearchOutput defines the OpenSearch ACL output structure. +type dbaasAclListOpenSearchOutput struct { + Acls []v3.DBAASOpensearchAclConfigAcls `json:"acls,omitempty"` // ACLs grouped by username + AclEnabled bool `json:"acl_enabled,omitempty"` // ACL Enabled status + ExtendedAclEnabled bool `json:"extended_acl_enabled,omitempty"` // Extended ACL Enabled status +} + +// ToJSON outputs the result in JSON format. +func (o *dbaasAclListOpenSearchOutput) ToJSON() { output.JSON(o) } + +// ToText outputs the result in plain text format. +func (o *dbaasAclListOpenSearchOutput) ToText() { output.Text(o) } + +// ToTable outputs the result in a tabular format. +func (o *dbaasAclListOpenSearchOutput) ToTable() { + tabular := table.NewTable(os.Stdout) + tabular.SetHeader([]string{"Field", "Value"}) + + // Display general ACL configurations. + tabular.Append([]string{"ACL Enabled", fmt.Sprintf("%t", o.AclEnabled)}) + tabular.Append([]string{"Extended ACL Enabled", fmt.Sprintf("%t", o.ExtendedAclEnabled)}) + + // Display rules grouped under usernames. + for _, acl := range o.Acls { + tabular.Append([]string{"Username", string(acl.Username)}) + for _, rule := range acl.Rules { + tabular.Append([]string{" Rule", fmt.Sprintf("Pattern: %s, Permission: %s", rule.Index, string(rule.Permission))}) + } + } + + tabular.Render() +} + +// listOpenSearchACL fetches OpenSearch ACLs and prepares the output. +func (c *dbaasAclListCmd) listOpenSearchACL(ctx context.Context, client *v3.Client, serviceName string) (output.Outputter, error) { + aclsConfig, err := client.GetDBAASOpensearchAclConfig(ctx, serviceName) + if err != nil { + return nil, fmt.Errorf("error fetching OpenSearch ACL configuration: %w", err) + } + + return &dbaasAclListOpenSearchOutput{ + Acls: aclsConfig.Acls, + AclEnabled: *aclsConfig.AclEnabled, + ExtendedAclEnabled: *aclsConfig.ExtendedAclEnabled, + }, nil +} diff --git a/cmd/dbaas_acl_show.go b/cmd/dbaas_acl_show.go new file mode 100644 index 000000000..222613818 --- /dev/null +++ b/cmd/dbaas_acl_show.go @@ -0,0 +1,108 @@ +package cmd + +import ( + "fmt" + "github.com/exoscale/cli/pkg/globalstate" + "os" + + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + "github.com/spf13/cobra" +) + +type dbaasAclShowOutput struct { + Username string `json:"username,omitempty"` + Permission string `json:"permission,omitempty"` + Topic string `json:"topic,omitempty"` +} + +func (o *dbaasAclShowOutput) ToJSON() { output.JSON(o) } +func (o *dbaasAclShowOutput) ToText() { output.Text(o) } + +func (o *dbaasAclShowOutput) ToTable() { + table := table.NewTable(os.Stdout) + table.SetHeader([]string{"ACL Entry"}) + defer table.Render() + + table.Append([]string{"Username", o.Username}) + table.Append([]string{"Topic", o.Topic}) + table.Append([]string{"Permission", o.Permission}) +} + +// Main command for showing ACLs +type dbaasAclShowCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"show"` + Name string `cli-flag:"name" cli-usage:"Name of the DBaaS service"` + Username string `cli-flag:"username" cli-usage:"Username of the ACL entry"` + ServiceType string `cli-flag:"type" cli-short:"t" cli-usage:"type of the DBaaS service (e.g., kafka, opensearch)"` +} + +// Command aliases (none in this case) +func (c *dbaasAclShowCmd) cmdAliases() []string { return nil } + +// Short description for the command +func (c *dbaasAclShowCmd) cmdShort() string { return "Show the details of an acl" } + +// Long description for the command +func (c *dbaasAclShowCmd) cmdLong() string { + return `This command show an acl entty and its details for a specified DBAAS service.` +} + +// Pre-run validation for required flags and default zone setting +func (c *dbaasAclShowCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) // Run default validations +} + +// Main run logic for showing ACL details +func (c *dbaasAclShowCmd) cmdRun(cmd *cobra.Command, args []string) error { + ctx := gContext + + // Validate required flags + if c.Name == "" || c.Username == "" || c.ServiceType == "" { + return fmt.Errorf("both --name, --username and --type flags must be specified") + } + + // Search for the service in each zone + service, zone, err := FindServiceAcrossZones(ctx, globalstate.EgoscaleV3Client, c.Name) + if err != nil { + return fmt.Errorf("error finding service: %w", err) + } + + // Switch client to the appropriate zone + client, err := switchClientZoneV3(ctx, globalstate.EgoscaleV3Client, zone) + if err != nil { + return fmt.Errorf("error initializing client for zone %s: %w", zone, err) + } + + // Validate the service type + if string(service.Type) != c.ServiceType { + return fmt.Errorf("service type mismatch: expected %q but got %q for service %q", c.ServiceType, service.Type, c.Name) + } + + // Call the appropriate method based on the service type + var output output.Outputter + switch service.Type { + case "kafka": + output, err = c.showKafka(ctx, client, c.Name) + case "opensearch": + output, err = c.showOpensearch(ctx, client, c.Name) + default: + return fmt.Errorf("listing ACL unsupported for service of type %q", service.Type) + } + + if err != nil { + return err + } + + // Output the fetched details + return c.outputFunc(output, nil) +} + +// Register the command +func init() { + cobra.CheckErr(registerCLICommand(dbaasAclCmd, &dbaasAclShowCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas_acl_show_kafka.go b/cmd/dbaas_acl_show_kafka.go new file mode 100644 index 000000000..2f3310c7c --- /dev/null +++ b/cmd/dbaas_acl_show_kafka.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "context" + "fmt" + "github.com/exoscale/cli/pkg/output" + v3 "github.com/exoscale/egoscale/v3" +) + +// Fetch OpenSearch ACL configuration and process its details +func (c *dbaasAclShowCmd) showKafka(ctx context.Context, client *v3.Client, serviceName string) (output.Outputter, error) { + + // Fetch Kafka ACLs for the specified service + acls, err := client.GetDBAASKafkaAclConfig(ctx, serviceName) + if err != nil { + return nil, fmt.Errorf("error fetching ACL configuration for service %q: %w", serviceName, err) + } + + // Search for the specific username in the fetched ACLs + for _, acl := range acls.TopicAcl { + if acl.Username == c.Username { + return &dbaasAclShowOutput{ + Username: acl.Username, + Topic: acl.Topic, + Permission: string(acl.Permission), + }, nil + } + } + + return nil, fmt.Errorf("ACL entry for username %q not found in service %q", c.Username, serviceName) +} diff --git a/cmd/dbaas_acl_show_opensearch.go b/cmd/dbaas_acl_show_opensearch.go new file mode 100644 index 000000000..85fb6d843 --- /dev/null +++ b/cmd/dbaas_acl_show_opensearch.go @@ -0,0 +1,75 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/exoscale/cli/pkg/output" + "github.com/exoscale/cli/table" + v3 "github.com/exoscale/egoscale/v3" +) + +type dbaasAclShowOpensearchOutput struct { + Username string `json:"username,omitempty"` + Rules []v3.DBAASOpensearchAclConfigAclsRules `json:"rules,omitempty"` + AclEnabled bool `json:"acl_enabled,omitempty"` + ExtendedAclEnabled bool `json:"extended_acl_enabled,omitempty"` +} + +func (o *dbaasAclShowOpensearchOutput) ToJSON() { output.JSON(o) } + +func (o *dbaasAclShowOpensearchOutput) ToText() { output.Text(o) } + +// ToTable Define table output formatting for OpenSearch +func (o *dbaasAclShowOpensearchOutput) ToTable() { + table := table.NewTable(os.Stdout) + table.SetHeader([]string{"Field", "Value"}) + defer table.Render() + + // Display whether ACL and extended ACL are enabled + table.Append([]string{"ACL Enabled", fmt.Sprintf("%t", o.AclEnabled)}) + table.Append([]string{"Extended ACL Enabled", fmt.Sprintf("%t", o.ExtendedAclEnabled)}) + + // Iterate over rules and display each + for _, rule := range o.Rules { + table.Append([]string{"Rule", fmt.Sprintf("ACL pattern: %s, Permission: %s", rule.Index, rule.Permission)}) + } +} + +// Fetch OpenSearch ACL configuration and process its details +func (c *dbaasAclShowCmd) showOpensearch(ctx context.Context, client *v3.Client, serviceName string) (output.Outputter, error) { + + // Fetch OpenSearch ACL configuration for the specified service + aclsConfig, err := client.GetDBAASOpensearchAclConfig(ctx, serviceName) + if err != nil { + return nil, fmt.Errorf("error fetching ACL configuration for service %q: %w", serviceName, err) + } + + // Check if ACLs are enabled + aclEnabled := false + if aclsConfig.AclEnabled != nil { + aclEnabled = *aclsConfig.AclEnabled + } + + // Check if extended ACLs are enabled + extendedAclEnabled := false + if aclsConfig.ExtendedAclEnabled != nil { + extendedAclEnabled = *aclsConfig.ExtendedAclEnabled + } + + // Search for the specific username in the fetched ACLs + for _, acl := range aclsConfig.Acls { + if string(acl.Username) == c.Username { + // Return the ACL details for the matched username + return &dbaasAclShowOpensearchOutput{ + Username: string(acl.Username), + Rules: acl.Rules, + AclEnabled: aclEnabled, + ExtendedAclEnabled: extendedAclEnabled, + }, nil + } + } + // If no matching username is found, return an error + return nil, fmt.Errorf("ACL entry for username %q not found in service %q", c.Username, serviceName) +} diff --git a/cmd/dbaas_acl_update.go b/cmd/dbaas_acl_update.go new file mode 100644 index 000000000..39cf57087 --- /dev/null +++ b/cmd/dbaas_acl_update.go @@ -0,0 +1,77 @@ +package cmd + +import ( + "fmt" + "github.com/exoscale/cli/pkg/globalstate" + "github.com/spf13/cobra" +) + +type dbaasAclUpdateCmd struct { + cliCommandSettings `cli-cmd:"-"` + + _ bool `cli-cmd:"update"` + Name string `cli-flag:"name" cli-usage:"Name of the DBaaS service"` + Username string `cli-flag:"username" cli-usage:"Current username of the ACL entry to update"` + NewUsername string `cli-flag:"new-username" cli-usage:"New username to replace the current one (optional)"` + ServiceType string `cli-flag:"type" cli-short:"t" cli-usage:"Type of the DBaaS service (e.g., opensearch)"` + Pattern string `cli-flag:"pattern" cli-usage:"The pattern for the ACL rule (index* for OpenSearch or topic for Kafka, max 249 characters)"` + Permission string `cli-flag:"permission" cli-usage:"Permission to apply (should be one of admin, read, readwrite, write, or deny (only for OpenSearch))"` +} + +// Command aliases (none in this case) +func (c *dbaasAclUpdateCmd) cmdAliases() []string { return nil } + +// Short description for the command +func (c *dbaasAclUpdateCmd) cmdShort() string { + return "Update an ACL entry for a DBaaS service" +} + +// Long description for the command +func (c *dbaasAclUpdateCmd) cmdLong() string { + return `This command updates an ACL entry for a specified DBaaS service. You can also update the username with the --new-username flag.` +} + +func (c *dbaasAclUpdateCmd) cmdPreRun(cmd *cobra.Command, args []string) error { + return cliCommandDefaultPreRun(c, cmd, args) +} + +// Main run logic for showing ACL details +func (c *dbaasAclUpdateCmd) cmdRun(cmd *cobra.Command, args []string) error { + ctx := gContext + + // Validate required flags + if c.Name == "" || c.Username == "" || c.ServiceType == "" { + return fmt.Errorf("both --name, --username, and --type flags must be specified") + } + + // Search for the service in each zone + service, zone, err := FindServiceAcrossZones(ctx, globalstate.EgoscaleV3Client, c.Name) + if err != nil { + return fmt.Errorf("error finding service: %w", err) + } + + // Switch client to the appropriate zone + client, err := switchClientZoneV3(ctx, globalstate.EgoscaleV3Client, zone) + if err != nil { + return fmt.Errorf("error initializing client for zone %s: %w", zone, err) + } + + // Validate the service type + if string(service.Type) != c.ServiceType { + return fmt.Errorf("service type mismatch: expected %q but got %q for service %q", c.ServiceType, service.Type, c.Name) + } + + // Determine the appropriate update logic based on the service type + switch service.Type { + case "opensearch": + return c.updateOpensearch(ctx, client, c.Name) + default: + return fmt.Errorf("update ACL unsupported for service type %q", service.Type) + } +} + +func init() { + cobra.CheckErr(registerCLICommand(dbaasAclCmd, &dbaasAclUpdateCmd{ + cliCommandSettings: defaultCLICmdSettings(), + })) +} diff --git a/cmd/dbaas_acl_update_opensearch.go b/cmd/dbaas_acl_update_opensearch.go new file mode 100644 index 000000000..c089d8f98 --- /dev/null +++ b/cmd/dbaas_acl_update_opensearch.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "context" + "fmt" + + v3 "github.com/exoscale/egoscale/v3" +) + +func (c *dbaasAclUpdateCmd) updateOpensearch(ctx context.Context, client *v3.Client, serviceName string) error { + + aclsConfig, err := client.GetDBAASOpensearchAclConfig(ctx, serviceName) + if err != nil { + return fmt.Errorf("error fetching ACL configuration for service %q: %w", serviceName, err) + } + + // Ensure ACL entry for the specified username exists + var updatedAcls []v3.DBAASOpensearchAclConfigAcls + var updatedEntry *v3.DBAASOpensearchAclConfigAcls + found := false + + for _, acl := range aclsConfig.Acls { + if string(acl.Username) == c.Username { + found = true + + // Update username if --new-username is provided + newUsername := c.Username + if c.NewUsername != "" { + newUsername = c.NewUsername + } + + updatedEntry = &v3.DBAASOpensearchAclConfigAcls{ + Username: v3.DBAASUserUsername(newUsername), + Rules: []v3.DBAASOpensearchAclConfigAclsRules{ + {Index: c.Pattern, Permission: v3.EnumOpensearchRulePermission(c.Permission)}, + }, + } + } else { + updatedAcls = append(updatedAcls, acl) + } + } + + if !found { + return fmt.Errorf("ACL entry for username %q not found in service %q", c.Username, serviceName) + } + + if updatedEntry != nil { + updatedAcls = append(updatedAcls, *updatedEntry) + } + + // Update the configuration + aclsConfig.Acls = updatedAcls + _, err = client.UpdateDBAASOpensearchAclConfig(ctx, serviceName, *aclsConfig) + if err != nil { + return fmt.Errorf("error updating ACL configuration for service %q: %w", serviceName, err) + } + + fmt.Printf("ACL entry for username %q updated successfully in service %q\n", c.Username, serviceName) + return nil +}