diff --git a/docs/stackit_beta_alb.md b/docs/stackit_beta_alb.md index eaa733d1c..633c2ac54 100644 --- a/docs/stackit_beta_alb.md +++ b/docs/stackit_beta_alb.md @@ -31,10 +31,13 @@ stackit beta alb [flags] * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands * [stackit beta alb create](./stackit_beta_alb_create.md) - Creates an application loadbalancer +* [stackit beta alb credentials](./stackit_beta_alb_credentials.md) - Provides functionality for application loadbalancer credentials * [stackit beta alb delete](./stackit_beta_alb_delete.md) - Deletes an application loadbalancer * [stackit beta alb describe](./stackit_beta_alb_describe.md) - Describes an application loadbalancer * [stackit beta alb list](./stackit_beta_alb_list.md) - Lists albs +* [stackit beta alb plans](./stackit_beta_alb_plans.md) - Lists the application load balancer plans * [stackit beta alb pool](./stackit_beta_alb_pool.md) - Manages target pools for application loadbalancers +* [stackit beta alb quotas](./stackit_beta_alb_quotas.md) - Shows the application load balancer quotas * [stackit beta alb template](./stackit_beta_alb_template.md) - creates configuration templates to use for resource creation * [stackit beta alb update](./stackit_beta_alb_update.md) - Updates an application loadbalancer diff --git a/docs/stackit_beta_alb_credentials.md b/docs/stackit_beta_alb_credentials.md new file mode 100644 index 000000000..2e91dfe4e --- /dev/null +++ b/docs/stackit_beta_alb_credentials.md @@ -0,0 +1,38 @@ +## stackit beta alb credentials + +Provides functionality for application loadbalancer credentials + +### Synopsis + +Provides functionality for application loadbalancer credentials + +``` +stackit beta alb credentials [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta alb credentials" +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers +* [stackit beta alb credentials create](./stackit_beta_alb_credentials_create.md) - Creates a credential +* [stackit beta alb credentials delete](./stackit_beta_alb_credentials_delete.md) - Deletes credentials +* [stackit beta alb credentials describe](./stackit_beta_alb_credentials_describe.md) - Describes credentials +* [stackit beta alb credentials list](./stackit_beta_alb_credentials_list.md) - Lists all credentials +* [stackit beta alb credentials update](./stackit_beta_alb_credentials_update.md) - Update credentials + diff --git a/docs/stackit_beta_alb_credentials_create.md b/docs/stackit_beta_alb_credentials_create.md new file mode 100644 index 000000000..21ac038f4 --- /dev/null +++ b/docs/stackit_beta_alb_credentials_create.md @@ -0,0 +1,42 @@ +## stackit beta alb credentials create + +Creates a credential + +### Synopsis + +Creates a credential. + +``` +stackit beta alb credentials create [flags] +``` + +### Examples + +``` + Create a new credential, the password is requested interactively or read from ENV variable ALB_CREDENTIALS_PASSWORD + $ stackit beta alb credential create --username some.user --displayname master-creds +``` + +### Options + +``` + -d, --displayname string the displayname for the credentials + -h, --help Help for "stackit beta alb credentials create" + -u, --username string the username for the credentials +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb credentials](./stackit_beta_alb_credentials.md) - Provides functionality for application loadbalancer credentials + diff --git a/docs/stackit_beta_alb_credentials_delete.md b/docs/stackit_beta_alb_credentials_delete.md new file mode 100644 index 000000000..e1971858b --- /dev/null +++ b/docs/stackit_beta_alb_credentials_delete.md @@ -0,0 +1,40 @@ +## stackit beta alb credentials delete + +Deletes credentials + +### Synopsis + +Deletes credentials. + +``` +stackit beta alb credentials delete CREDENTIAL_REF [flags] +``` + +### Examples + +``` + Delete credential with name "credential-12345" + $ stackit beta alb credentials delete credential-12345 +``` + +### Options + +``` + -h, --help Help for "stackit beta alb credentials delete" +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb credentials](./stackit_beta_alb_credentials.md) - Provides functionality for application loadbalancer credentials + diff --git a/docs/stackit_beta_alb_credentials_describe.md b/docs/stackit_beta_alb_credentials_describe.md new file mode 100644 index 000000000..309beb5d9 --- /dev/null +++ b/docs/stackit_beta_alb_credentials_describe.md @@ -0,0 +1,40 @@ +## stackit beta alb credentials describe + +Describes credentials + +### Synopsis + +Describes credentials. + +``` +stackit beta alb credentials describe CREDENTIAL_REF [flags] +``` + +### Examples + +``` + Get details about credentials with name "credential-12345" + $ stackit beta alb credential describe credential-12345 +``` + +### Options + +``` + -h, --help Help for "stackit beta alb credentials describe" +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb credentials](./stackit_beta_alb_credentials.md) - Provides functionality for application loadbalancer credentials + diff --git a/docs/stackit_beta_alb_credentials_list.md b/docs/stackit_beta_alb_credentials_list.md new file mode 100644 index 000000000..87c3e0b33 --- /dev/null +++ b/docs/stackit_beta_alb_credentials_list.md @@ -0,0 +1,47 @@ +## stackit beta alb credentials list + +Lists all credentials + +### Synopsis + +Lists all credentials. + +``` +stackit beta alb credentials list [flags] +``` + +### Examples + +``` + Lists all credentials + $ stackit beta alb credential list + + Lists all credentials in JSON format + $ stackit beta alb credential list --output-format json + + Lists up to 10 credentials + $ stackit beta alb credential list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta alb credentials list" + --limit int Number of credentials 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb credentials](./stackit_beta_alb_credentials.md) - Provides functionality for application loadbalancer credentials + diff --git a/docs/stackit_beta_alb_credentials_update.md b/docs/stackit_beta_alb_credentials_update.md new file mode 100644 index 000000000..88b8f19c6 --- /dev/null +++ b/docs/stackit_beta_alb_credentials_update.md @@ -0,0 +1,48 @@ +## stackit beta alb credentials update + +Update credentials + +### Synopsis + +Update credentials. + +``` +stackit beta alb credentials update CREDENTIAL_REF_ARG [flags] +``` + +### Examples + +``` + Update the username + $ stackit beta alb credentials update --username test-cred2 credentials-12345 + + Update the displayname + $ stackit beta alb credentials update --displayname new-name credentials-12345 + + Update the password (is retrieved interactively or from ENV variable ) + $ stackit beta alb credentials update --password credentials-12345 +``` + +### Options + +``` + -d, --displayname string the displayname for the credentials + -h, --help Help for "stackit beta alb credentials update" + -u, --username string the username for the credentials +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb credentials](./stackit_beta_alb_credentials.md) - Provides functionality for application loadbalancer credentials + diff --git a/docs/stackit_beta_alb_plans.md b/docs/stackit_beta_alb_plans.md new file mode 100644 index 000000000..3e46e1185 --- /dev/null +++ b/docs/stackit_beta_alb_plans.md @@ -0,0 +1,40 @@ +## stackit beta alb plans + +Lists the application load balancer plans + +### Synopsis + +Lists the available application load balancer plans. + +``` +stackit beta alb plans [flags] +``` + +### Examples + +``` + List all application load balancer plans + $ stackit beta alb plans +``` + +### Options + +``` + -h, --help Help for "stackit beta alb plans" +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers + diff --git a/docs/stackit_beta_alb_quotas.md b/docs/stackit_beta_alb_quotas.md new file mode 100644 index 000000000..26f9168ce --- /dev/null +++ b/docs/stackit_beta_alb_quotas.md @@ -0,0 +1,40 @@ +## stackit beta alb quotas + +Shows the application load balancer quotas + +### Synopsis + +Shows the application load balancer quotas for the application load balancers. + +``` +stackit beta alb quotas [flags] +``` + +### Examples + +``` + List all application load balancer quotas + $ stackit beta alb quotas +``` + +### Options + +``` + -h, --help Help for "stackit beta alb quotas" +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers + diff --git a/internal/cmd/beta/alb/alb.go b/internal/cmd/beta/alb/alb.go index a1288fcf6..07ac1aefe 100644 --- a/internal/cmd/beta/alb/alb.go +++ b/internal/cmd/beta/alb/alb.go @@ -6,7 +6,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/plans" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/pool" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/quotas" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/template" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -39,5 +41,7 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { describe.NewCmd(p), delete.NewCmd(p), pool.NewCmd(p), + plans.NewCmd(p), + quotas.NewCmd(p), ) } diff --git a/internal/cmd/beta/alb/credentials/create/create.go b/internal/cmd/beta/alb/credentials/create/create.go index 550f20492..bfa428aaa 100644 --- a/internal/cmd/beta/alb/credentials/create/create.go +++ b/internal/cmd/beta/alb/credentials/create/create.go @@ -111,7 +111,7 @@ func readPassword() (string, error) { return "", fmt.Errorf("cannot read password: %w", err) } fmt.Println() - if bytes.Equal(password, confirmation) { + if !bytes.Equal(password, confirmation) { return "", fmt.Errorf("the password and the confirmation do not match") } @@ -169,9 +169,11 @@ func outputResult(p *print.Printer, outputFormat string, item *alb.CreateCredent } p.Outputln(string(details)) default: - p.Outputf("Created credential %q", - utils.PtrString(item.Credential), - ) + if item.Credential != nil { + p.Outputf("Created credential %s\n", + utils.PtrString(item.Credential.CredentialsRef), + ) + } } return nil } diff --git a/internal/cmd/beta/alb/credentials/delete/delete.go b/internal/cmd/beta/alb/credentials/delete/delete.go index 1fcc05d25..9a2c85d18 100644 --- a/internal/cmd/beta/alb/credentials/delete/delete.go +++ b/internal/cmd/beta/alb/credentials/delete/delete.go @@ -15,7 +15,7 @@ import ( ) const ( - credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, this are not valid credentials + credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, these are not valid credentials ) type inputModel struct { @@ -26,13 +26,13 @@ type inputModel struct { func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialRefArg), - Short: "Deletes a credential", - Long: "Deletes a credential.", + Short: "Deletes credentials", + Long: "Deletes credentials.", Args: args.SingleArg(credentialRefArg, nil), Example: examples.Build( examples.NewExample( - `Delete credential with name "CREDENTIAL_REF"`, - "$ stackit beta alb credentials delete CREDENTIAL_REF", + `Delete credential with name "credential-12345"`, + "$ stackit beta alb credentials delete credential-12345", ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -49,7 +49,7 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credential %q?", model.CredentialsRef) + prompt := fmt.Sprintf("Are you sure you want to delete credentials %q?", model.CredentialsRef) err = p.PromptForConfirmation(prompt) if err != nil { return err diff --git a/internal/cmd/beta/alb/credentials/describe/describe.go b/internal/cmd/beta/alb/credentials/describe/describe.go index 901351983..0b8684a25 100644 --- a/internal/cmd/beta/alb/credentials/describe/describe.go +++ b/internal/cmd/beta/alb/credentials/describe/describe.go @@ -20,7 +20,7 @@ import ( ) const ( - credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, this are not valid credentials + credentialRefArg = "CREDENTIAL_REF" // nolint:gosec // false alert, these are not valid credentials ) type inputModel struct { @@ -31,12 +31,12 @@ type inputModel struct { func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialRefArg), - Short: "Describes a credential", - Long: "Describes a credential.", + Short: "Describes credentials", + Long: "Describes credentials.", Args: args.SingleArg(credentialRefArg, nil), Example: examples.Build( examples.NewExample( - `Get details about a credential with name "credential-12345"`, + `Get details about credentials with name "credential-12345"`, "$ stackit beta alb credential describe credential-12345", ), ), @@ -57,7 +57,7 @@ func NewCmd(p *print.Printer) *cobra.Command { req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() if err != nil { - return fmt.Errorf("read credential: %w", err) + return fmt.Errorf("read credentials: %w", err) } if credential := resp; credential != nil && credential.Credential != nil { @@ -101,7 +101,7 @@ func outputResult(p *print.Printer, outputFormat string, response alb.Credential details, err := json.MarshalIndent(response, "", " ") if err != nil { - return fmt.Errorf("marshal credential: %w", err) + return fmt.Errorf("marshal credentials: %w", err) } p.Outputln(string(details)) @@ -110,7 +110,7 @@ func outputResult(p *print.Printer, outputFormat string, response alb.Credential details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) if err != nil { - return fmt.Errorf("marshal credential: %w", err) + return fmt.Errorf("marshal credentials: %w", err) } p.Outputln(string(details)) diff --git a/internal/cmd/beta/alb/credentials/update/update.go b/internal/cmd/beta/alb/credentials/update/update.go index cbc13606a..65b9af4eb 100644 --- a/internal/cmd/beta/alb/credentials/update/update.go +++ b/internal/cmd/beta/alb/credentials/update/update.go @@ -12,6 +12,7 @@ import ( "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/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "golang.org/x/term" @@ -21,28 +22,26 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/alb" ) -const passwordEnv = "ALB_CREDENTIALS_PASSWORD" //nolint:gosec // false alert, this are not valid credentials +const passwordEnv = "ALB_CREDENTIALS_PASSWORD" //nolint:gosec // false alert, these are not valid credentials const ( usernameFlag = "username" displaynameFlag = "displayname" - passwordFlag = "password" - credentialRefArg = "CREDENTIAL_REF_ARG" //nolint:gosec // false alert, this are not valid credentials + credentialRefArg = "CREDENTIAL_REF_ARG" //nolint:gosec // false alert, these are not valid credentials ) type inputModel struct { *globalflags.GlobalFlagModel Username *string Displayname *string - Password *bool CredentialsRef *string } func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", credentialRefArg), - Short: "Update a credential", - Long: "Update a credential.", + Short: "Update credentials", + Long: "Update credentials.", Args: args.SingleArg(credentialRefArg, nil), Example: examples.Build( examples.NewExample( @@ -68,8 +67,17 @@ func NewCmd(p *print.Printer) *cobra.Command { return err } + req, err := buildRequest(ctx, &model, apiClient, readPassword) + if err != nil { + return err + } + projectLabel, err := projectname.GetProjectName(ctx, p, cmd) + if err != nil { + p.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update credential %q?", *model.CredentialsRef) + prompt := fmt.Sprintf("Are you sure you want to update credential %q for %q?", *model.CredentialsRef, projectLabel) err = p.PromptForConfirmation(prompt) if err != nil { return fmt.Errorf("update credential: %w", err) @@ -77,10 +85,6 @@ func NewCmd(p *print.Printer) *cobra.Command { } // Call API - req, err := buildRequest(ctx, &model, apiClient, readPassword) - if err != nil { - return err - } resp, err := req.Execute() if err != nil { return fmt.Errorf("update credential: %w", err) @@ -99,26 +103,28 @@ func NewCmd(p *print.Printer) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(usernameFlag, "u", "", "the username for the credentials") cmd.Flags().StringP(displaynameFlag, "d", "", "the displayname for the credentials") - cmd.Flags().BoolP(passwordFlag, "w", false, "change the password for the credentials") + cobra.CheckErr(flags.MarkFlagsRequired(cmd, displaynameFlag, usernameFlag, displaynameFlag)) } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient, readPassword func() (string, error)) (req alb.ApiUpdateCredentialsRequest, err error) { req = apiClient.UpdateCredentials(ctx, model.ProjectId, model.Region, *model.CredentialsRef) var password *string - if model.Password != nil && *model.Password { - p, err := readPassword() - if err != nil { - return req, err - } - password = &p + p, err := readPassword() + if err != nil { + return req, err } + password = &p payload := alb.UpdateCredentialsPayload{ DisplayName: model.Displayname, Password: password, Username: model.Username, } + if model.Displayname == nil && model.Username == nil { + return req, fmt.Errorf("no attribute to change passed") + } + return req.UpdateCredentialsPayload(payload), nil } func readPassword() (string, error) { @@ -138,7 +144,7 @@ func readPassword() (string, error) { return "", fmt.Errorf("cannot read password: %w", err) } fmt.Println() - if bytes.Equal(password, confirmation) { + if !bytes.Equal(password, confirmation) { return "", fmt.Errorf("the password and the confirmation do not match") } @@ -149,7 +155,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) inputM GlobalFlagModel: globalflags.Parse(p, cmd), Username: flags.FlagToStringPointer(p, cmd, usernameFlag), Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag), - Password: flags.FlagToBoolPointer(p, cmd, passwordFlag), CredentialsRef: &inputArgs[0], } diff --git a/internal/cmd/beta/alb/credentials/update/update_test.go b/internal/cmd/beta/alb/credentials/update/update_test.go index 9c7fbce80..b953ce982 100644 --- a/internal/cmd/beta/alb/credentials/update/update_test.go +++ b/internal/cmd/beta/alb/credentials/update/update_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/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -34,7 +33,6 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st globalflags.RegionFlag: testRegion, usernameFlag: testUsername, displaynameFlag: testDisplayname, - passwordFlag: "true", } for _, mod := range mods { mod(flagValues) @@ -52,7 +50,6 @@ func fixtureInputModel(mods ...func(model *inputModel)) inputModel { Username: &testUsername, Displayname: &testDisplayname, CredentialsRef: &testCredentialRef, - Password: utils.Ptr(true), } for _, mod := range mods { mod(&model) @@ -103,52 +100,23 @@ func TestParseInput(t *testing.T) { globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, }, - isValid: true, + isValid: false, expectedModel: fixtureInputModel(func(model *inputModel) { model.Username = nil - model.Password = nil model.Displayname = nil }), }, { + description: "required values", args: []string{testCredentialRef}, - description: "only username", flagValues: map[string]string{ globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, usernameFlag: testUsername, - }, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.Displayname = nil - model.Password = nil - }), - isValid: true, - }, { - description: "only displayname", - args: []string{testCredentialRef}, - flagValues: map[string]string{ - globalflags.ProjectIdFlag: testProjectId, - globalflags.RegionFlag: testRegion, displaynameFlag: testDisplayname, }, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.Username = nil - model.Password = nil - }), - isValid: true, - }, { - description: "only password", - args: []string{testCredentialRef}, - flagValues: map[string]string{ - globalflags.ProjectIdFlag: testProjectId, - globalflags.RegionFlag: testRegion, - passwordFlag: "true", - }, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.Username = nil - model.Displayname = nil - }), - isValid: true, + isValid: true, + expectedModel: fixtureInputModel(), }, } diff --git a/internal/cmd/beta/alb/delete/delete.go b/internal/cmd/beta/alb/delete/delete.go index 78d552a7f..570c63eb6 100644 --- a/internal/cmd/beta/alb/delete/delete.go +++ b/internal/cmd/beta/alb/delete/delete.go @@ -56,7 +56,7 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the application loadbalancer %q for project %q?", model.Name, projectLabel) + prompt := fmt.Sprintf("Are you sure you want to delete the credentials %q for project %q?", model.Name, projectLabel) err = p.PromptForConfirmation(prompt) if err != nil { return err diff --git a/internal/cmd/beta/alb/list/list.go b/internal/cmd/beta/alb/list/list.go index c2e6a937d..4050e8791 100644 --- a/internal/cmd/beta/alb/list/list.go +++ b/internal/cmd/beta/alb/list/list.go @@ -1,4 +1,3 @@ -<<<<<<< HEAD package list import ( @@ -177,180 +176,3 @@ func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalance return nil } } -||||||| parent of db5dfa32 (feat(alb): list command) -======= -package list - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/goccy/go-yaml" - "github.com/spf13/cobra" - "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/alb/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" -) - -type inputModel struct { - *globalflags.GlobalFlagModel - Limit *int64 -} - -const ( - labelSelectorFlag = "label-selector" - limitFlag = "limit" -) - -func NewCmd(p *print.Printer) *cobra.Command { - cmd := &cobra.Command{ - Use: "list", - Short: "Lists albs", - Long: "Lists application load balancers.", - Args: args.NoArgs, - Example: examples.Build( - examples.NewExample( - `List all load balancers`, - `$ stackit beta alb list`, - ), - examples.NewExample( - `List the first 10 application load balancers`, - `$ stackit beta alb list --limit=10`, - ), - ), - RunE: func(cmd *cobra.Command, _ []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 - } - - projectLabel, err := projectname.GetProjectName(ctx, p, cmd) - if err != nil { - p.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } else if projectLabel == "" { - projectLabel = model.ProjectId - } - - // Call API - request := buildRequest(ctx, model, apiClient) - - response, err := request.Execute() - if err != nil { - return fmt.Errorf("list load balancerse: %w", err) - } - - if items := response.LoadBalancers; items != nil && len(*items) == 0 { - p.Info("No load balancers found for project %q", projectLabel) - } else { - if model.Limit != nil && len(*items) > int(*model.Limit) { - *items = (*items)[:*model.Limit] - } - if err := outputResult(p, model.OutputFormat, *items); err != nil { - return fmt.Errorf("output loadbalancers: %w", err) - } - } - - return nil - }, - } - - configureFlags(cmd) - return cmd -} - -func configureFlags(cmd *cobra.Command) { - cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") -} - -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, - Limit: limit, - } - - 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 *alb.APIClient) alb.ApiListLoadBalancersRequest { - request := apiClient.ListLoadBalancers(ctx, model.ProjectId, model.Region) - - return request -} -func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalancer) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal loadbalancer list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal loadbalancer list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - table := tables.NewTable() - table.SetHeader("NAME", "EXTERNAL ADDRESS", "REGION", "STATUS", "VERSION") - for _, item := range items { - table.AddRow(utils.PtrString(item.Name), - utils.PtrString(item.ExternalAddress), - utils.PtrString(item.Region), - utils.PtrString(item.Status), - utils.PtrString(item.Version), - utils.PtrString(item.ExternalAddress), - ) - } - err := table.Display(p) - if err != nil { - return fmt.Errorf("render table: %w", err) - } - - return nil - } -} ->>>>>>> db5dfa32 (feat(alb): list command) diff --git a/internal/cmd/beta/alb/list/list_test.go b/internal/cmd/beta/alb/list/list_test.go index 530204b2d..77c915266 100644 --- a/internal/cmd/beta/alb/list/list_test.go +++ b/internal/cmd/beta/alb/list/list_test.go @@ -1,4 +1,3 @@ -<<<<<<< HEAD package list import ( @@ -206,214 +205,3 @@ func Test_outputResult(t *testing.T) { }) } } -||||||| parent of db5dfa32 (feat(alb): list command) -======= -package list - -import ( - "context" - "strconv" - "testing" - - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" -) - -var projectIdFlag = globalflags.ProjectIdFlag - -type testCtxKey struct{} - -var ( - testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &alb.APIClient{} - testProjectId = uuid.NewString() - testRegion = "eu01" - testLimit int64 = 10 -) - -func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { - flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: strconv.Itoa(int(testLimit)), - } - for _, mod := range mods { - mod(flagValues) - } - return flagValues -} - -func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { - model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, - Limit: &testLimit, - } - for _, mod := range mods { - mod(model) - } - return model -} - -func fixtureRequest(mods ...func(request *alb.ApiListLoadBalancersRequest)) alb.ApiListLoadBalancersRequest { - request := testClient.ListLoadBalancers(context.Background(), testProjectId, testRegion) - 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, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(p) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot 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) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - 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 alb.ApiListLoadBalancersRequest - }{ - { - 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), - cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"), - ) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func Test_outputResult(t *testing.T) { - type args struct { - outputFormat string - items []alb.LoadBalancer - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "empty", - args: args{ - outputFormat: "", - items: []alb.LoadBalancer{}, - }, - wantErr: false, - }, - { - name: "output format json", - args: args{ - outputFormat: print.JSONOutputFormat, - items: []alb.LoadBalancer{}, - }, - wantErr: false, - }, - } - p := print.NewPrinter() - p.Cmd = NewCmd(p) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { - t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} ->>>>>>> db5dfa32 (feat(alb): list command) diff --git a/internal/cmd/beta/alb/plans/plans.go b/internal/cmd/beta/alb/plans/plans.go new file mode 100644 index 000000000..13719baac --- /dev/null +++ b/internal/cmd/beta/alb/plans/plans.go @@ -0,0 +1,146 @@ +package plans + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "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/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/alb/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/alb" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "plans", + Short: "Lists the application load balancer plans", + Long: "Lists the available application load balancer plans.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all application load balancer plans`, + `$ stackit beta alb plans`, + ), + ), + RunE: func(cmd *cobra.Command, _ []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 + } + + projectLabel, err := projectname.GetProjectName(ctx, p, cmd) + if err != nil { + p.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + // Call API + request := buildRequest(ctx, model, apiClient) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("list plans: %w", err) + } + + if items := response.ValidPlans; items == nil || len(*items) == 0 { + p.Info("No plans found for project %q", projectLabel) + } else { + if err := outputResult(p, model.OutputFormat, *items); err != nil { + return fmt.Errorf("output plans: %w", err) + } + } + + return nil + }, + } + + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + 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 *alb.APIClient) alb.ApiListPlansRequest { + request := apiClient.ListPlans(ctx, model.Region) + + return request +} + +func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(items, "", " ") + if err != nil { + return fmt.Errorf("marshal plans: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal plans: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("PLAN ID", "NAME", "FLAVOR", "MAX CONNS", "DESCRIPTION") + for _, item := range items { + table.AddRow(utils.PtrString(item.PlanId), + utils.PtrString(item.Name), + utils.PtrString(item.FlavorName), + utils.PtrString(item.MaxConnections), + utils.Truncate(item.Description, 70), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/alb/plans/plans_test.go b/internal/cmd/beta/alb/plans/plans_test.go new file mode 100644 index 000000000..3184f5696 --- /dev/null +++ b/internal/cmd/beta/alb/plans/plans_test.go @@ -0,0 +1,203 @@ +package plans + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/alb" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &alb.APIClient{} + testProjectId = uuid.NewString() + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *alb.ApiListPlansRequest)) alb.ApiListPlansRequest { + request := testClient.ListPlans(testCtx, testRegion) + 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, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + if err := globalflags.Configure(cmd.Flags()); err != nil { + t.Errorf("cannot 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) + } + } + + if err := cmd.ValidateRequiredFlags(); err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + 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 alb.ApiListPlansRequest + }{ + { + 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), + cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + items []alb.PlanDetails + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: "", + items: []alb.PlanDetails{}, + }, + wantErr: false, + }, + { + name: "output format json", + args: args{ + outputFormat: print.JSONOutputFormat, + items: []alb.PlanDetails{}, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(p) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/alb/quotas/quotas.go b/internal/cmd/beta/alb/quotas/quotas.go new file mode 100644 index 000000000..07bc2eb21 --- /dev/null +++ b/internal/cmd/beta/alb/quotas/quotas.go @@ -0,0 +1,132 @@ +package quotas + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "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/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/alb" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "quotas", + Short: "Shows the application load balancer quotas", + Long: "Shows the application load balancer quotas for the application load balancers.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all application load balancer quotas`, + `$ stackit beta alb quotas`, + ), + ), + RunE: func(cmd *cobra.Command, _ []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 + request := buildRequest(ctx, model, apiClient) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("get quotas: %w", err) + } + + if response == nil { + p.Outputln("no quotas found") + return nil + } + + if err := outputResult(p, model.OutputFormat, *response); err != nil { + return fmt.Errorf("output loadbalancers: %w", err) + } + + return nil + }, + } + + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + 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 *alb.APIClient) alb.ApiGetQuotaRequest { + request := apiClient.GetQuota(ctx, model.ProjectId, model.Region) + + return request +} + +func outputResult(p *print.Printer, outputFormat string, response alb.GetQuotaResponse) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(response, "", " ") + if err != nil { + return fmt.Errorf("marshal quotas: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal quotas: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.AddRow("REGION", utils.PtrString(response.Region)) + table.AddSeparator() + table.AddRow("MAX LOADBALANCERS", utils.PtrString(response.MaxLoadBalancers)) + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/beta/alb/quotas/quotas_test.go b/internal/cmd/beta/alb/quotas/quotas_test.go new file mode 100644 index 000000000..b840d9ef0 --- /dev/null +++ b/internal/cmd/beta/alb/quotas/quotas_test.go @@ -0,0 +1,203 @@ +package quotas + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/alb" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &alb.APIClient{} + testProjectId = uuid.NewString() + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *alb.ApiGetQuotaRequest)) alb.ApiGetQuotaRequest { + request := testClient.GetQuota(testCtx, testProjectId, testRegion) + 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, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + if err := globalflags.Configure(cmd.Flags()); err != nil { + t.Errorf("cannot 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) + } + } + + if err := cmd.ValidateRequiredFlags(); err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + 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 alb.ApiGetQuotaRequest + }{ + { + 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), + cmpopts.IgnoreFields(alb.ApiListLoadBalancersRequest{}, "ctx"), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + response alb.GetQuotaResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: "", + response: alb.GetQuotaResponse{}, + }, + wantErr: false, + }, + { + name: "output format json", + args: args{ + outputFormat: print.JSONOutputFormat, + response: alb.GetQuotaResponse{}, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(p) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index 4a3339144..37b5e4bd9 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -5,7 +5,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/print" diff --git a/internal/pkg/services/alb/utils/utils_test.go b/internal/pkg/services/alb/utils/utils_test.go index e3788f064..489b7a2c5 100644 --- a/internal/pkg/services/alb/utils/utils_test.go +++ b/internal/pkg/services/alb/utils/utils_test.go @@ -1,13 +1,4 @@ -<<<<<<< HEAD package utils type AlbClientMocked struct { } -||||||| parent of db5dfa32 (feat(alb): list command) -======= -package utils - -type AlbClientMocked struct { -} - ->>>>>>> db5dfa32 (feat(alb): list command) diff --git a/internal/pkg/utils/strings.go b/internal/pkg/utils/strings.go index 36cfda139..401287fa1 100644 --- a/internal/pkg/utils/strings.go +++ b/internal/pkg/utils/strings.go @@ -2,6 +2,7 @@ package utils import ( "strings" + "unicode/utf8" ) // JoinStringKeys concatenates the string keys of a map, each separatore by the @@ -33,3 +34,25 @@ func JoinStringPtr(vals *[]string, sep string) string { } return strings.Join(*vals, sep) } + +// Truncate trims the passed string (if it is not nil). If the input string is +// longer than the given length, it is truncated to _maxLen_ and a ellipsis (…) +// is attached. Therefore the resulting string has at most length _maxLen-1_ +func Truncate(s *string, maxLen int) string { + if s == nil { + return "" + } + + if utf8.RuneCountInString(*s) > maxLen { + var builder strings.Builder + for i, r := range *s { + if i >= maxLen { + break + } + builder.WriteRune(r) + } + builder.WriteRune('…') + return builder.String() + } + return *s +} diff --git a/internal/pkg/utils/strings_test.go b/internal/pkg/utils/strings_test.go new file mode 100644 index 000000000..a7fb023bc --- /dev/null +++ b/internal/pkg/utils/strings_test.go @@ -0,0 +1,32 @@ +package utils + +import ( + "testing" + + "github.com/stackitcloud/stackit-sdk-go/core/utils" +) + +func TestTruncate(t *testing.T) { + type args struct { + s *string + maxLen int + } + tests := []struct { + name string + args args + want string + }{ + {"nil string", args{nil, 3}, ""}, + {"empty string", args{utils.Ptr(""), 10}, ""}, + {"length below maxlength", args{utils.Ptr("foo"), 10}, "foo"}, + {"exactly maxlength", args{utils.Ptr("foo"), 3}, "foo"}, + {"above maxlength", args{utils.Ptr("foobarbaz"), 3}, "foo…"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Truncate(tt.args.s, tt.args.maxLen); got != tt.want { + t.Errorf("Truncate() = %v, want %v", got, tt.want) + } + }) + } +}