diff --git a/docs/stackit_beta_sqlserverflex_database.md b/docs/stackit_beta_sqlserverflex_database.md index 0ff6409a5..60dc3674a 100644 --- a/docs/stackit_beta_sqlserverflex_database.md +++ b/docs/stackit_beta_sqlserverflex_database.md @@ -31,4 +31,6 @@ stackit beta sqlserverflex database [flags] * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex * [stackit beta sqlserverflex database create](./stackit_beta_sqlserverflex_database_create.md) - Creates a SQLServer Flex database * [stackit beta sqlserverflex database delete](./stackit_beta_sqlserverflex_database_delete.md) - Deletes a SQLServer Flex database +* [stackit beta sqlserverflex database describe](./stackit_beta_sqlserverflex_database_describe.md) - Shows details of an SQLServer Flex database +* [stackit beta sqlserverflex database list](./stackit_beta_sqlserverflex_database_list.md) - Lists all SQLServer Flex databases diff --git a/docs/stackit_beta_sqlserverflex_database_describe.md b/docs/stackit_beta_sqlserverflex_database_describe.md new file mode 100644 index 000000000..97715ae2b --- /dev/null +++ b/docs/stackit_beta_sqlserverflex_database_describe.md @@ -0,0 +1,43 @@ +## stackit beta sqlserverflex database describe + +Shows details of an SQLServer Flex database + +### Synopsis + +Shows details of an SQLServer Flex database. + +``` +stackit beta sqlserverflex database describe DATABASE_NAME [flags] +``` + +### Examples + +``` + Get details of an SQLServer Flex database with name "my-database" of instance with ID "xxx" + $ stackit beta sqlserverflex database describe my-database --instance-id xxx + + Get details of an SQLServer Flex database with name "my-database" of instance with ID "xxx" in JSON format + $ stackit beta sqlserverflex database describe my-database --instance-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta sqlserverflex database describe" + --instance-id string SQLServer Flex instance ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases + diff --git a/docs/stackit_beta_sqlserverflex_database_list.md b/docs/stackit_beta_sqlserverflex_database_list.md new file mode 100644 index 000000000..637f262e1 --- /dev/null +++ b/docs/stackit_beta_sqlserverflex_database_list.md @@ -0,0 +1,47 @@ +## stackit beta sqlserverflex database list + +Lists all SQLServer Flex databases + +### Synopsis + +Lists all SQLServer Flex databases. + +``` +stackit beta sqlserverflex database list [flags] +``` + +### Examples + +``` + List all SQLServer Flex databases of instance with ID "xxx" + $ stackit beta sqlserverflex database list --instance-id xxx + + List all SQLServer Flex databases of instance with ID "xxx" in JSON format + $ stackit beta sqlserverflex database list --instance-id xxx --output-format json + + List up to 10 SQLServer Flex databases of instance with ID "xxx" + $ stackit beta sqlserverflex database list --instance-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta sqlserverflex database list" + --instance-id string SQLServer Flex instance ID + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta sqlserverflex database](./stackit_beta_sqlserverflex_database.md) - Provides functionality for SQLServer Flex databases + diff --git a/docs/stackit_beta_sqlserverflex_user_create.md b/docs/stackit_beta_sqlserverflex_user_create.md index 910a469ab..4b3a00c46 100644 --- a/docs/stackit_beta_sqlserverflex_user_create.md +++ b/docs/stackit_beta_sqlserverflex_user_create.md @@ -5,11 +5,15 @@ Creates a SQLServer Flex user ### Synopsis Creates a SQLServer Flex user for an instance. + The password is only visible upon creation and cannot be retrieved later. Alternatively, you can reset the password and access the new one by running: $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID Please refer to https://docs.stackit.cloud/stackit/en/creating-logins-and-users-in-sqlserver-flex-instances-210862358.html for additional information. +The allowed user roles for your instance can be obtained by running: + $ stackit beta sqlserverflex options --user-roles --instance-id INSTANCE_ID + ``` stackit beta sqlserverflex user create [flags] ``` @@ -18,10 +22,10 @@ stackit beta sqlserverflex user create [flags] ``` Create a SQLServer Flex user for instance with ID "xxx" and specify the username, role and database - $ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles my-role --database my-database + $ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "##STACKIT_DatabaseManager##" --database my-database Create a SQLServer Flex user for instance with ID "xxx", specifying multiple roles - $ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2" + $ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "##STACKIT_LoginManager##,##STACKIT_DatabaseManager##" ``` ### Options diff --git a/go.mod b/go.mod index 32a76562d..f5f9f574f 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/serverbackup v0.1.0 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0 github.com/stackitcloud/stackit-sdk-go/services/ske v0.17.0 - github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.2.0 + github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.3.0 github.com/zalando/go-keyring v0.2.5 golang.org/x/mod v0.19.0 golang.org/x/oauth2 v0.21.0 diff --git a/go.sum b/go.sum index 7569c2c7a..b316b522d 100644 --- a/go.sum +++ b/go.sum @@ -159,8 +159,8 @@ github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0 h1:JB1O0E9 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0/go.mod h1:Ni9RBJvcaXRIrDIuQBpJcuQvCQSj27crQSyc+WM4p0c= github.com/stackitcloud/stackit-sdk-go/services/ske v0.17.0 h1:4S3MwNmpMfjzBz9JtKbXvkos7j+7hGeFMf7XsjMLL/g= github.com/stackitcloud/stackit-sdk-go/services/ske v0.17.0/go.mod h1:0fFs4R7kg+gU7FNAIzzFvlCZJz6gyZ8CFhbK3eSrAwQ= -github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.2.0 h1:aIXxXx6u4+6C02MPb+hdItigeKeen7m+hEEG+Ej9sNs= -github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.2.0/go.mod h1:fQJOQMfasStZ8J9iGX0vTjyJoQtLqMXJ5Npb03QJk84= +github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.3.0 h1:M6tcXUMNM6XMfHVQeQzB6IjfPdAxnZar3YD+YstRStc= +github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.3.0/go.mod h1:Qnn+06i21XtagtMQ4cTwOCR3OLnXX+t1n+Vf/HH49Yw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/internal/cmd/beta/sqlserverflex/database/create/create.go b/internal/cmd/beta/sqlserverflex/database/create/create.go index 4cb15352f..fa3467d2d 100644 --- a/internal/cmd/beta/sqlserverflex/database/create/create.go +++ b/internal/cmd/beta/sqlserverflex/database/create/create.go @@ -14,7 +14,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" "github.com/spf13/cobra" @@ -125,9 +124,9 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverfl req := apiClient.CreateDatabase(ctx, model.ProjectId, model.InstanceId) payload := sqlserverflex.CreateDatabasePayload{ Name: &model.DatabaseName, - Options: utils.Ptr(map[string]string{ - "owner": model.Owner, - }), + Options: &sqlserverflex.DatabaseDocumentationCreateDatabaseRequestOptions{ + Owner: &model.Owner, + }, } req = req.CreateDatabasePayload(payload) return req diff --git a/internal/cmd/beta/sqlserverflex/database/create/create_test.go b/internal/cmd/beta/sqlserverflex/database/create/create_test.go index fc98c388a..eedda7fa7 100644 --- a/internal/cmd/beta/sqlserverflex/database/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/database/create/create_test.go @@ -6,7 +6,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" "github.com/google/go-cmp/cmp" @@ -67,9 +66,9 @@ func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateDatabaseRequest request := testClient.CreateDatabase(testCtx, testProjectId, testInstanceId) payload := sqlserverflex.CreateDatabasePayload{ Name: &testDatabaseName, - Options: utils.Ptr(map[string]string{ - "owner": testOwner, - }), + Options: &sqlserverflex.DatabaseDocumentationCreateDatabaseRequestOptions{ + Owner: &testOwner, + }, } request = request.CreateDatabasePayload(payload) for _, mod := range mods { diff --git a/internal/cmd/beta/sqlserverflex/database/database.go b/internal/cmd/beta/sqlserverflex/database/database.go index 382b646c1..3fbd641ce 100644 --- a/internal/cmd/beta/sqlserverflex/database/database.go +++ b/internal/cmd/beta/sqlserverflex/database/database.go @@ -3,6 +3,8 @@ package database import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -25,4 +27,6 @@ func NewCmd(p *print.Printer) *cobra.Command { func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(create.NewCmd(p)) cmd.AddCommand(delete.NewCmd(p)) + cmd.AddCommand(describe.NewCmd(p)) + cmd.AddCommand(list.NewCmd(p)) } diff --git a/internal/cmd/beta/sqlserverflex/database/describe/describe.go b/internal/cmd/beta/sqlserverflex/database/describe/describe.go new file mode 100644 index 000000000..21323b958 --- /dev/null +++ b/internal/cmd/beta/sqlserverflex/database/describe/describe.go @@ -0,0 +1,169 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + + "github.com/spf13/cobra" +) + +const ( + databaseNameArg = "DATABASE_NAME" + + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + DatabaseName string + InstanceId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", databaseNameArg), + Short: "Shows details of an SQLServer Flex database", + Long: "Shows details of an SQLServer Flex database.", + Args: args.SingleArg(databaseNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Get details of an SQLServer Flex database with name "my-database" of instance with ID "xxx"`, + "$ stackit beta sqlserverflex database describe my-database --instance-id xxx"), + examples.NewExample( + `Get details of an SQLServer Flex database with name "my-database" of instance with ID "xxx" in JSON format`, + "$ stackit beta sqlserverflex database describe my-database --instance-id xxx --output-format json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read SQLServer Flex database: %w", err) + } + + return outputResult(p, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "SQLServer Flex instance ID") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + databaseName := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DatabaseName: databaseName, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiGetDatabaseRequest { + req := apiClient.GetDatabase(ctx, model.ProjectId, model.InstanceId, model.DatabaseName) + return req +} + +func outputResult(p *print.Printer, outputFormat string, database *sqlserverflex.GetDatabaseResponse) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(database, "", " ") + if err != nil { + return fmt.Errorf("marshal SQLServer Flex database: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(database, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SQLServer Flex database: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + database := database.Database + table := tables.NewTable() + table.AddRow("ID", *database.Id) + table.AddSeparator() + table.AddRow("NAME", *database.Name) + table.AddSeparator() + if database.CreateDate != nil { + table.AddRow("CREATE DATE", *database.CreateDate) + table.AddSeparator() + } + if database.Collation != nil { + table.AddRow("COLLATION", *database.Collation) + table.AddSeparator() + } + if database.Options != nil { + if database.Options.CompatibilityLevel != nil { + table.AddRow("COMPATIBILITY LEVEL", *database.Options.CompatibilityLevel) + table.AddSeparator() + } + if database.Options.IsEncrypted != nil { + table.AddRow("IS ENCRYPTED", *database.Options.IsEncrypted) + table.AddSeparator() + } + if database.Options.Owner != nil { + table.AddRow("OWNER", *database.Options.Owner) + table.AddSeparator() + } + if database.Options.UserAccess != nil { + table.AddRow("USER ACCESS", *database.Options.UserAccess) + } + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go b/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go new file mode 100644 index 000000000..a48d2e0d8 --- /dev/null +++ b/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go @@ -0,0 +1,239 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sqlserverflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testDatabaseName = "my-database" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testDatabaseName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + DatabaseName: testDatabaseName, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sqlserverflex.ApiGetDatabaseRequest)) sqlserverflex.ApiGetDatabaseRequest { + request := testClient.GetDatabase(testCtx, testProjectId, testInstanceId, testDatabaseName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "database name invalid", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sqlserverflex.ApiGetDatabaseRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/sqlserverflex/database/list/list.go b/internal/cmd/beta/sqlserverflex/database/list/list.go new file mode 100644 index 000000000..7398206f6 --- /dev/null +++ b/internal/cmd/beta/sqlserverflex/database/list/list.go @@ -0,0 +1,171 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + + "github.com/spf13/cobra" +) + +const ( + instanceIdFlag = "instance-id" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + Limit *int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all SQLServer Flex databases", + Long: "Lists all SQLServer Flex databases.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all SQLServer Flex databases of instance with ID "xxx"`, + "$ stackit beta sqlserverflex database list --instance-id xxx"), + examples.NewExample( + `List all SQLServer Flex databases of instance with ID "xxx" in JSON format`, + "$ stackit beta sqlserverflex database list --instance-id xxx --output-format json"), + examples.NewExample( + `List up to 10 SQLServer Flex databases of instance with ID "xxx"`, + "$ stackit beta sqlserverflex database list --instance-id xxx --limit 10"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SQLServer Flex databases: %w", err) + } + if resp.Databases == nil || len(*resp.Databases) == 0 { + projectLabel, err := projectname.GetProjectName(ctx, p, cmd) + if err != nil { + p.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + p.Info("No databases found for instance %s on project %s\n", model.InstanceId, projectLabel) + return nil + } + databases := *resp.Databases + + // Truncate output + if model.Limit != nil && len(databases) > int(*model.Limit) { + databases = databases[:*model.Limit] + } + + return outputResult(p, model.OutputFormat, databases) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "SQLServer Flex instance ID") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiListDatabasesRequest { + req := apiClient.ListDatabases(ctx, model.ProjectId, model.InstanceId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, databases []sqlserverflex.Database) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(databases, "", " ") + if err != nil { + return fmt.Errorf("marshal SQLServer Flex database list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(databases, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SQLServer Flex database list: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "NAME") + for i := range databases { + database := databases[i] + table.AddRow(*database.Id, *database.Name) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/sqlserverflex/database/list/list_test.go b/internal/cmd/beta/sqlserverflex/database/list/list_test.go new file mode 100644 index 000000000..ae2ea8f27 --- /dev/null +++ b/internal/cmd/beta/sqlserverflex/database/list/list_test.go @@ -0,0 +1,212 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sqlserverflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sqlserverflex.ApiListDatabasesRequest)) sqlserverflex.ApiListDatabasesRequest { + request := testClient.ListDatabases(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + p := print.NewPrinter() + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sqlserverflex.ApiListDatabasesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/sqlserverflex/user/create/create.go b/internal/cmd/beta/sqlserverflex/user/create/create.go index 305845dbd..29b416460 100644 --- a/internal/cmd/beta/sqlserverflex/user/create/create.go +++ b/internal/cmd/beta/sqlserverflex/user/create/create.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/goccy/go-yaml" "github.com/spf13/cobra" @@ -36,20 +37,22 @@ func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a SQLServer Flex user", - Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", + Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s\n\n%s\n%s", "Creates a SQLServer Flex user for an instance.", "The password is only visible upon creation and cannot be retrieved later.", "Alternatively, you can reset the password and access the new one by running:", " $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID", "Please refer to https://docs.stackit.cloud/stackit/en/creating-logins-and-users-in-sqlserver-flex-instances-210862358.html for additional information.", + "The allowed user roles for your instance can be obtained by running:", + " $ stackit beta sqlserverflex options --user-roles --instance-id INSTANCE_ID", ), Example: examples.Build( examples.NewExample( `Create a SQLServer Flex user for instance with ID "xxx" and specify the username, role and database`, - "$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles my-role --database my-database"), + `$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "##STACKIT_DatabaseManager##" --database my-database`), examples.NewExample( `Create a SQLServer Flex user for instance with ID "xxx", specifying multiple roles`, - `$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "my-role-1,my-role-2"`), + `$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "##STACKIT_LoginManager##,##STACKIT_DatabaseManager##"`), ), Args: args.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -132,21 +135,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiCreateUserRequest { req := apiClient.CreateUser(ctx, model.ProjectId, model.InstanceId) - var roles []sqlserverflex.Role - if model.Roles != nil { - for _, r := range *model.Roles { - roles = append(roles, sqlserverflex.Role(r)) - } - } - req = req.CreateUserPayload(sqlserverflex.CreateUserPayload{ Username: model.Username, - Roles: &roles, + Roles: model.Roles, }) return req } -func outputResult(p *print.Printer, model *inputModel, instanceLabel string, user *sqlserverflex.User) error { +func outputResult(p *print.Printer, model *inputModel, instanceLabel string, user *sqlserverflex.SingleUser) error { switch model.OutputFormat { case print.JSONOutputFormat: details, err := json.MarshalIndent(user, "", " ") @@ -169,7 +165,7 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, use p.Outputf("Username: %s\n", *user.Username) p.Outputf("Password: %s\n", *user.Password) if user.Roles != nil && len(*user.Roles) != 0 { - p.Outputf("Roles: %v\n", *user.Roles) + p.Outputf("Roles: [%v]\n", strings.Join(*user.Roles, ", ")) } if user.Host != nil && *user.Host != "" { p.Outputf("Host: %s\n", *user.Host) diff --git a/internal/cmd/beta/sqlserverflex/user/create/create_test.go b/internal/cmd/beta/sqlserverflex/user/create/create_test.go index e0583f9fc..5d5f52ccd 100644 --- a/internal/cmd/beta/sqlserverflex/user/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/user/create/create_test.go @@ -57,7 +57,7 @@ func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateUserRequest)) s request := testClient.CreateUser(testCtx, testProjectId, testInstanceId) request = request.CreateUserPayload(sqlserverflex.CreateUserPayload{ Username: utils.Ptr("johndoe"), - Roles: utils.Ptr([]sqlserverflex.Role{"read"}), + Roles: utils.Ptr([]string{"read"}), }) for _, mod := range mods { @@ -204,7 +204,7 @@ func TestBuildRequest(t *testing.T) { model.Username = nil }), expectedRequest: fixtureRequest().CreateUserPayload(sqlserverflex.CreateUserPayload{ - Roles: utils.Ptr([]sqlserverflex.Role{"read"}), + Roles: utils.Ptr([]string{"read"}), }), }, } diff --git a/internal/cmd/beta/sqlserverflex/user/describe/describe.go b/internal/cmd/beta/sqlserverflex/user/describe/describe.go index d12b232b6..0f0e28ae5 100644 --- a/internal/cmd/beta/sqlserverflex/user/describe/describe.go +++ b/internal/cmd/beta/sqlserverflex/user/describe/describe.go @@ -117,7 +117,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverfl return req } -func outputResult(p *print.Printer, outputFormat string, user sqlserverflex.InstanceResponseUser) error { +func outputResult(p *print.Printer, outputFormat string, user sqlserverflex.UserResponseUser) error { switch outputFormat { case print.JSONOutputFormat: details, err := json.MarshalIndent(user, "", " ") @@ -144,9 +144,9 @@ func outputResult(p *print.Printer, outputFormat string, user sqlserverflex.Inst table.AddSeparator() table.AddRow("ROLES", strings.Join(*user.Roles, "\n")) } - if user.Database != nil && *user.Database != "" { + if user.DefaultDatabase != nil && *user.DefaultDatabase != "" { table.AddSeparator() - table.AddRow("DATABASE", *user.Database) + table.AddRow("DATABASE", *user.DefaultDatabase) } if user.Host != nil && *user.Host != "" { table.AddSeparator() diff --git a/internal/pkg/services/sqlserverflex/utils/utils_test.go b/internal/pkg/services/sqlserverflex/utils/utils_test.go index 8776baf4a..953965121 100644 --- a/internal/pkg/services/sqlserverflex/utils/utils_test.go +++ b/internal/pkg/services/sqlserverflex/utils/utils_test.go @@ -434,7 +434,7 @@ func TestGetUserName(t *testing.T) { { description: "base", getUserResp: &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.InstanceResponseUser{ + Item: &sqlserverflex.UserResponseUser{ Username: utils.Ptr(testUserName), }, },