Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dbaas: added commands for dbaas acl management #659

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a08b993
creating the structure, and implementation of acl_show and acl_show_k…
elkezza Dec 19, 2024
a05e812
Adding the open search-logic for the show sub-command
elkezza Dec 20, 2024
0dff30c
Adding comment to the code for better readability.
elkezza Dec 20, 2024
52a8caf
Improve readability.
elkezza Dec 20, 2024
58b4dd2
Improve readability.
elkezza Dec 20, 2024
be8f14f
Adding the list sub-commands logic.
elkezza Dec 23, 2024
a8703f6
adding the delete sub-command tree logic, still missing the opensearc…
elkezza Dec 23, 2024
7884c91
adding the update sub-command tree logic, still missing the kafka bec…
elkezza Jan 2, 2025
80ebe78
adding the `create` sub-command tree logic.
elkezza Jan 2, 2025
a234463
use `pattern` instead of `index` as it is more general name, because …
elkezza Jan 2, 2025
3d74823
defining `FindServiceAcrossZones` function instead of repeating the s…
elkezza Jan 2, 2025
c7b3194
using `FindServiceAcrossZones` function instead of repeating the same…
elkezza Jan 2, 2025
09982d5
using `FindServiceAcrossZones` function in update sub-commands instea…
elkezza Jan 2, 2025
1dbcc11
using `FindServiceAcrossZones` function in delete sub-commands instea…
elkezza Jan 2, 2025
79bb079
using `FindServiceAcrossZones` function in list sub-commands instead …
elkezza Jan 2, 2025
6376771
removing the zone flag from the show command by using `FindServiceAcr…
elkezza Jan 2, 2025
31cde21
updating the CHANGELOG.md.
elkezza Jan 2, 2025
f0cdd7f
using `v3.ZoneName`to be returned instead of string.
elkezza Jan 2, 2025
dba696d
using global context.
elkezza Jan 2, 2025
6fb7032
Update cmd/dbaas_acl_delete.go
elkezza Jan 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
19 changes: 19 additions & 0 deletions cmd/dbaas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, string, error) {
aureliar8 marked this conversation as resolved.
Show resolved Hide resolved
// 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, string(zone.Name), nil
}
}

return v3.DBAASServiceCommon{}, "", fmt.Errorf("service %q not found in any zone", serviceName)
}
14 changes: 14 additions & 0 deletions cmd/dbaas_acl.go
Original file line number Diff line number Diff line change
@@ -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)
}
77 changes: 77 additions & 0 deletions cmd/dbaas_acl_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd

import (
"context"
"fmt"
"github.com/exoscale/cli/pkg/globalstate"
v3 "github.com/exoscale/egoscale/v3"
"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 := context.Background()
elkezza marked this conversation as resolved.
Show resolved Hide resolved

// 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, v3.ZoneName(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(),
}))
}
35 changes: 35 additions & 0 deletions cmd/dbaas_acl_create_kafka.go
Original file line number Diff line number Diff line change
@@ -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
}
46 changes: 46 additions & 0 deletions cmd/dbaas_acl_create_opensearch.go
Original file line number Diff line number Diff line change
@@ -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
}
81 changes: 81 additions & 0 deletions cmd/dbaas_acl_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cmd

import (
"context"
"fmt"

"github.com/exoscale/cli/pkg/globalstate"
v3 "github.com/exoscale/egoscale/v3"
"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 (e.g., kafka, opensearch)"`
elkezza marked this conversation as resolved.
Show resolved Hide resolved
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 := context.Background()

// 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, v3.ZoneName(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(),
}))
}
42 changes: 42 additions & 0 deletions cmd/dbaas_acl_delete_kafka.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading