mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## stackit beta edge-cloud instance create | ||
| Creates an edge instance | ||
| ### Synopsis | ||
| Creates a STACKIT Edge Cloud (STEC) instance. The instance will take a moment to become fully functional. | ||
| ``` | ||
| stackit beta edge-cloud instance create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Creates an edge instance with the name "xxx" and plan-id "yyy" | ||
| $ stackit beta edge-cloud instance create --name "xxx" --plan-id "yyy" | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -d, --description string A user chosen description to distinguish multiple instances. | ||
| -h, --help Help for "stackit beta edge-cloud instance create" | ||
| -n, --name string The displayed name to distinguish multiple instances. | ||
| --plan-id string Service Plan configures the size of the Instance. | ||
| ``` | ||
| ### 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 edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. | ||
| ## stackit beta edge-cloud instance delete | ||
| Deletes an edge instance | ||
| ### Synopsis | ||
| Deletes a STACKIT Edge Cloud (STEC) instance. The instance will be deleted permanently. | ||
| ``` | ||
| stackit beta edge-cloud instance delete [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Delete an edge instance with id "xxx" | ||
| $ stackit beta edge-cloud instance delete --id "xxx" | ||
| Delete an edge instance with name "xxx" | ||
| $ stackit beta edge-cloud instance delete --name "xxx" | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud instance delete" | ||
| -i, --id string The project-unique identifier of this instance. | ||
| -n, --name string The displayed name to distinguish multiple instances. | ||
| ``` | ||
| ### 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 edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. | ||
| ## stackit beta edge-cloud instance describe | ||
| Describes an edge instance | ||
| ### Synopsis | ||
| Describes a STACKIT Edge Cloud (STEC) instance. | ||
| ``` | ||
| stackit beta edge-cloud instance describe [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Describe an edge instance with id "xxx" | ||
| $ stackit beta edge-cloud instance describe --id <ID> | ||
| Describe an edge instance with name "xxx" | ||
| $ stackit beta edge-cloud instance describe --name <NAME> | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud instance describe" | ||
| -i, --id string The project-unique identifier of this instance. | ||
| -n, --name string The displayed name to distinguish multiple instances. | ||
| ``` | ||
| ### 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 edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. | ||
| ## stackit beta edge-cloud instance list | ||
| Lists edge instances | ||
| ### Synopsis | ||
| Lists STACKIT Edge Cloud (STEC) instances of a project. | ||
| ``` | ||
| stackit beta edge-cloud instance list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Lists all edge instances of a given project | ||
| $ stackit beta edge-cloud instance list | ||
| Lists all edge instances of a given project and limits the output to two instances | ||
| $ stackit beta edge-cloud instance list --limit 2 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud instance list" | ||
| --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 | ||
| --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 edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. | ||
| ## stackit beta edge-cloud instance update | ||
| Updates an edge instance | ||
| ### Synopsis | ||
| Updates a STACKIT Edge Cloud (STEC) instance. | ||
| ``` | ||
| stackit beta edge-cloud instance update [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Updates the description of an edge instance with id "xxx" | ||
| $ stackit beta edge-cloud instance update --id "xxx" --description "yyy" | ||
| Updates the plan of an edge instance with name "xxx" | ||
| $ stackit beta edge-cloud instance update --name "xxx" --plan-id "yyy" | ||
| Updates the description and plan of an edge instance with id "xxx" | ||
| $ stackit beta edge-cloud instance update --id "xxx" --description "yyy" --plan-id "zzz" | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -d, --description string A user chosen description to distinguish multiple instances. | ||
| -h, --help Help for "stackit beta edge-cloud instance update" | ||
| -i, --id string The project-unique identifier of this instance. | ||
| -n, --name string The displayed name to distinguish multiple instances. | ||
| --plan-id string Service Plan configures the size of the Instance. | ||
| ``` | ||
| ### 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 edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. | ||
| ## stackit beta edge-cloud instance | ||
| Provides functionality for edge instances. | ||
| ### Synopsis | ||
| Provides functionality for STACKIT Edge Cloud (STEC) instance management. | ||
| ``` | ||
| stackit beta edge-cloud instance [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud instance" | ||
| ``` | ||
| ### 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 edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. | ||
| * [stackit beta edge-cloud instance create](./stackit_beta_edge-cloud_instance_create.md) - Creates an edge instance | ||
| * [stackit beta edge-cloud instance delete](./stackit_beta_edge-cloud_instance_delete.md) - Deletes an edge instance | ||
| * [stackit beta edge-cloud instance describe](./stackit_beta_edge-cloud_instance_describe.md) - Describes an edge instance | ||
| * [stackit beta edge-cloud instance list](./stackit_beta_edge-cloud_instance_list.md) - Lists edge instances | ||
| * [stackit beta edge-cloud instance update](./stackit_beta_edge-cloud_instance_update.md) - Updates an edge instance | ||
| ## stackit beta edge-cloud kubeconfig create | ||
| Creates or updates a local kubeconfig file of an edge instance | ||
| ### Synopsis | ||
| Creates or updates a local kubeconfig file of a STACKIT Edge Cloud (STEC) instance. If the config exists in the kubeconfig file, the information will be updated. | ||
| By default, the kubeconfig information of the edge instance is merged into the current kubeconfig file which is determined by Kubernetes client logic. If the kubeconfig file doesn't exist, a new one will be created. | ||
| You can override this behavior by specifying a custom filepath with the --filepath flag or disable writing with the --disable-writing flag. | ||
| An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 3600 seconds. | ||
| Note: the format for the duration is <value><unit>, e.g. 30d for 30 days. You may not combine units. | ||
| ``` | ||
| stackit beta edge-cloud kubeconfig create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create or update a kubeconfig for the edge instance with id "xxx". If the config exists in the kubeconfig file, the information will be updated. | ||
| $ stackit beta edge-cloud kubeconfig create --id "xxx" | ||
| Create or update a kubeconfig for the edge instance with name "xxx" in a custom filepath. | ||
| $ stackit beta edge-cloud kubeconfig create --name "xxx" --filepath "yyy" | ||
| Get a kubeconfig for the edge instance with name "xxx" without writing it to a file and format the output as json. | ||
| $ stackit beta edge-cloud kubeconfig create --name "xxx" --disable-writing --output-format json | ||
| Create a kubeconfig for the edge instance with id "xxx". This will replace your current kubeconfig file. | ||
| $ stackit beta edge-cloud kubeconfig create --id "xxx" --overwrite | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| --disable-writing Disable writing the kubeconfig to a file. | ||
| -e, --expiration string Expiration time for the kubeconfig, e.g. 5d. By default, the token is valid for 1h. | ||
| -f, --filepath string Path to the kubeconfig file. A default is chosen by Kubernetes if not set. | ||
| -h, --help Help for "stackit beta edge-cloud kubeconfig create" | ||
| -i, --id string The project-unique identifier of this instance. | ||
| -n, --name string The displayed name to distinguish multiple instances. | ||
| --overwrite Force overwrite the kubeconfig file if it exists. | ||
| --switch-context Switch to the context in the kubeconfig file to the new context. | ||
| ``` | ||
| ### 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 edge-cloud kubeconfig](./stackit_beta_edge-cloud_kubeconfig.md) - Provides functionality for edge kubeconfig. | ||
| ## stackit beta edge-cloud kubeconfig | ||
| Provides functionality for edge kubeconfig. | ||
| ### Synopsis | ||
| Provides functionality for STACKIT Edge Cloud (STEC) kubeconfig management. | ||
| ``` | ||
| stackit beta edge-cloud kubeconfig [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud kubeconfig" | ||
| ``` | ||
| ### 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 edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. | ||
| * [stackit beta edge-cloud kubeconfig create](./stackit_beta_edge-cloud_kubeconfig_create.md) - Creates or updates a local kubeconfig file of an edge instance | ||
| ## stackit beta edge-cloud plans list | ||
| Lists available edge service plans | ||
| ### Synopsis | ||
| Lists available STACKIT Edge Cloud (STEC) service plans of a project | ||
| ``` | ||
| stackit beta edge-cloud plans list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Lists all edge plans for a given project | ||
| $ stackit beta edge-cloud plan list | ||
| Lists all edge plans for a given project and limits the output to two plans | ||
| $ stackit beta edge-cloud plan list --limit 2 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud plans list" | ||
| --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 | ||
| --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 edge-cloud plans](./stackit_beta_edge-cloud_plans.md) - Provides functionality for edge service plans. | ||
| ## stackit beta edge-cloud plans | ||
| Provides functionality for edge service plans. | ||
| ### Synopsis | ||
| Provides functionality for STACKIT Edge Cloud (STEC) service plan management. | ||
| ``` | ||
| stackit beta edge-cloud plans [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud 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 edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. | ||
| * [stackit beta edge-cloud plans list](./stackit_beta_edge-cloud_plans_list.md) - Lists available edge service plans | ||
| ## stackit beta edge-cloud token create | ||
| Creates a token for an edge instance | ||
| ### Synopsis | ||
| Creates a token for a STACKIT Edge Cloud (STEC) instance. | ||
| An expiration time can be set for the token. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 3600 seconds. | ||
| Note: the format for the duration is <value><unit>, e.g. 30d for 30 days. You may not combine units. | ||
| ``` | ||
| stackit beta edge-cloud token create [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Create a token for the edge instance with id "xxx". | ||
| $ stackit beta edge-cloud token create --id "xxx" | ||
| Create a token for the edge instance with name "xxx". The token will be valid for one day. | ||
| $ stackit beta edge-cloud token create --name "xxx" --expiration 1d | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -e, --expiration string Expiration time for the kubeconfig, e.g. 5d. By default, the token is valid for 1h. | ||
| -h, --help Help for "stackit beta edge-cloud token create" | ||
| -i, --id string The project-unique identifier of this instance. | ||
| -n, --name string The displayed name to distinguish multiple instances. | ||
| ``` | ||
| ### 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 edge-cloud token](./stackit_beta_edge-cloud_token.md) - Provides functionality for edge service token. | ||
| ## stackit beta edge-cloud token | ||
| Provides functionality for edge service token. | ||
| ### Synopsis | ||
| Provides functionality for STACKIT Edge Cloud (STEC) token management. | ||
| ``` | ||
| stackit beta edge-cloud token [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud token" | ||
| ``` | ||
| ### 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 edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. | ||
| * [stackit beta edge-cloud token create](./stackit_beta_edge-cloud_token_create.md) - Creates a token for an edge instance | ||
| ## stackit beta edge-cloud | ||
| Provides functionality for edge services. | ||
| ### Synopsis | ||
| Provides functionality for STACKIT Edge Cloud (STEC) services. | ||
| ``` | ||
| stackit beta edge-cloud [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta edge-cloud" | ||
| ``` | ||
| ### 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](./stackit_beta.md) - Contains beta STACKIT CLI commands | ||
| * [stackit beta edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. | ||
| * [stackit beta edge-cloud kubeconfig](./stackit_beta_edge-cloud_kubeconfig.md) - Provides functionality for edge kubeconfig. | ||
| * [stackit beta edge-cloud plans](./stackit_beta_edge-cloud_plans.md) - Provides functionality for edge service plans. | ||
| * [stackit beta edge-cloud token](./stackit_beta_edge-cloud_token.md) - Provides functionality for edge service token. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package edge | ||
| import ( | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/kubeconfig" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/plans" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/token" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| ) | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "edge-cloud", | ||
| Short: "Provides functionality for edge services.", | ||
| Long: "Provides functionality for STACKIT Edge Cloud (STEC) services.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, params) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { | ||
| cmd.AddCommand(instance.NewCmd(params)) | ||
| cmd.AddCommand(plans.NewCmd(params)) | ||
| cmd.AddCommand(kubeconfig.NewCmd(params)) | ||
| cmd.AddCommand(token.NewCmd(params)) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package create | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "strings" | ||
| "testing" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testName = "test" | ||
| testPlanId = uuid.NewString() | ||
| testDescription = "Initial instance description" | ||
| testInstanceId = uuid.NewString() | ||
| ) | ||
| // mockExecutable is a mock for the Executable interface used by the SDK | ||
| type mockExecutable struct { | ||
| executeFails bool | ||
| resp *edge.Instance | ||
| } | ||
| func (m *mockExecutable) PostInstancesPayload(_ edge.PostInstancesPayload) edge.ApiPostInstancesRequest { | ||
| // This method is needed to satisfy the interface. It allows chaining in buildRequest. | ||
| return m | ||
| } | ||
| func (m *mockExecutable) Execute() (*edge.Instance, error) { | ||
| if m.executeFails { | ||
| return nil, errors.New("API error") | ||
| } | ||
| if m.resp != nil { | ||
| return m.resp, nil | ||
| } | ||
| return &edge.Instance{Id: &testInstanceId}, nil | ||
| } | ||
| // mockAPIClient is a mock for the client.APIClient interface | ||
| type mockAPIClient struct { | ||
| postInstancesMock edge.ApiPostInstancesRequest | ||
| } | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| if m.postInstancesMock != nil { | ||
| return m.postInstancesMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| // Unused methods to satisfy the client.APIClient interface | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| return nil | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| commonInstance.DisplayNameFlag: testName, | ||
| commonInstance.DescriptionFlag: testDescription, | ||
| commonInstance.PlanIdFlag: testPlanId, | ||
| } | ||
| 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, | ||
| }, | ||
| DisplayName: testName, | ||
| Description: testDescription, | ||
| PlanId: testPlanId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "create success", | ||
| want: fixtureInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}, globalflags.GlobalFlagModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name missing", | ||
| wantErr: "required flag(s) \"name\" not set", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.DisplayNameFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too long", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = "this-name-is-way-too-long-for-the-validation" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too short", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = "in" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name invalid", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = "1test" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "plan invalid", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.PlanIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "description too long", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DescriptionFlag] = strings.Repeat("a", 257) | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want *createRequestSpec | ||
| }{ | ||
| { | ||
| name: "success", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| postInstancesMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| want: &createRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| Payload: edge.PostInstancesPayload{ | ||
| DisplayName: &testName, | ||
| Description: &testDescription, | ||
| PlanId: &testPlanId, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, _ := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if got != nil { | ||
| if got.Execute == nil { | ||
| t.Error("expected non-nil Execute function") | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(createRequestSpec{}, "Execute")) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want *edge.Instance | ||
| args args | ||
| }{ | ||
| { | ||
| name: "create success", | ||
| want: &edge.Instance{Id: &testInstanceId}, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| postInstancesMock: &mockExecutable{ | ||
| resp: &edge.Instance{Id: &testInstanceId}, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "create API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| postInstancesMock: &mockExecutable{ | ||
| executeFails: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := run(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| instance *edge.Instance | ||
| projectLabel string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| args args | ||
| }{ | ||
| { | ||
| name: "no instance", | ||
| wantErr: &commonErr.NoInstanceError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "output json", | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.JSONOutputFormat | ||
| }), | ||
| instance: &edge.Instance{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output yaml", | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| }), | ||
| instance: &edge.Instance{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output default", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| instance: &edge.Instance{Id: &testInstanceId}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(&types.CmdParams{Printer: p}) | ||
| err := outputResult(p, tt.args.model.OutputFormat, tt.args.model.Async, tt.args.projectLabel, tt.args.instance) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package create | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" | ||
| ) | ||
| // Command constructor | ||
| // Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags | ||
| // instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname | ||
| // is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we | ||
| // won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates an edge instance", | ||
| Long: "Creates a STACKIT Edge Cloud (STEC) instance. The instance will take a moment to become fully functional.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Creates an edge instance with the %s "xxx" and %s "yyy"`, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance create --%s "xxx" --%s "yyy"`, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) | ||
| if err != nil { | ||
| params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| // If project label can't be determined, fall back to project ID | ||
| projectLabel = model.ProjectId | ||
| } | ||
| // Prompt for confirmation | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a new edge instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| resp, err := run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if resp == nil { | ||
| return fmt.Errorf("create instance: empty response from API") | ||
| } | ||
| if resp.Id == nil { | ||
| return fmt.Errorf("create instance: instance id missing in response") | ||
| } | ||
| instanceId := *resp.Id | ||
| // Wait for async operation, if async mode not enabled | ||
| if !model.Async { | ||
| s := spinner.New(params.Printer) | ||
| s.Start("Creating instance") | ||
| // The waiter handler needs a concrete client type. We can safely cast here as the real implementation will always match. | ||
| client, ok := apiClient.(*edge.APIClient) | ||
| if !ok { | ||
| return fmt.Errorf("failed to configure API client") | ||
| } | ||
| _, err = wait.CreateOrUpdateInstanceWaitHandler(ctx, client, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("wait for edge instance creation: %w", err) | ||
| } | ||
| s.Stop() | ||
| } | ||
| // Handle output to printer | ||
| return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| // inputModel represents the user input for creating an edge instance. | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| DisplayName string | ||
| Description string | ||
| PlanId string | ||
| } | ||
| // createRequestSpec captures the details of the request for testing. | ||
| type createRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Region string | ||
| Payload edge.PostInstancesPayload | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() (*edge.Instance, error) | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) | ||
| cmd.Flags().StringP(commonInstance.DescriptionFlag, commonInstance.DescriptionShorthand, "", commonInstance.DescriptionUsage) | ||
| cmd.Flags().String(commonInstance.PlanIdFlag, "", commonInstance.PlanIdUsage) | ||
| cobra.CheckErr(flags.MarkFlagsRequired(cmd, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag)) | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| displayNameValue := flags.FlagToStringPointer(p, cmd, commonInstance.DisplayNameFlag) | ||
| if err := commonInstance.ValidateDisplayName(displayNameValue); err != nil { | ||
| return nil, err | ||
| } | ||
| planIdValue := flags.FlagToStringPointer(p, cmd, commonInstance.PlanIdFlag) | ||
| if err := commonInstance.ValidatePlanId(planIdValue); err != nil { | ||
| return nil, err | ||
| } | ||
| descriptionValue := flags.FlagWithDefaultToStringValue(p, cmd, commonInstance.DescriptionFlag) | ||
| if err := commonInstance.ValidateDescription(descriptionValue); err != nil { | ||
| return nil, err | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| DisplayName: *displayNameValue, | ||
| Description: descriptionValue, | ||
| PlanId: *planIdValue, | ||
| } | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Instance, error) { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp, err := spec.Execute() | ||
| if err != nil { | ||
| return nil, cliErr.NewRequestFailedError(err) | ||
| } | ||
| return resp, nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*createRequestSpec, error) { | ||
| req := apiClient.PostInstances(ctx, model.ProjectId, model.Region) | ||
| // Build request payload | ||
| payload := edge.PostInstancesPayload{ | ||
| DisplayName: &model.DisplayName, | ||
| Description: &model.Description, | ||
| PlanId: &model.PlanId, | ||
| } | ||
| req = req.PostInstancesPayload(payload) | ||
| return &createRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Region: model.Region, | ||
| Payload: payload, | ||
| Execute: req.Execute, | ||
| }, nil | ||
| } | ||
| // Output result based on the configured output format | ||
| func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, instance *edge.Instance) error { | ||
| if instance == nil { | ||
| // This is only to prevent nil pointer deref. | ||
| // As long as the API behaves as defined by it's spec, instance can not be empty (HTTP 200 with an empty body) | ||
| return commonErr.NewNoInstanceError("") | ||
| } | ||
| return p.OutputResult(outputFormat, instance, func() error { | ||
| operationState := "Created" | ||
| if async { | ||
| operationState = "Triggered creation of" | ||
| } | ||
| p.Outputf("%s instance for project %q. Instance ID: %q.\n", operationState, projectLabel, utils.PtrString(instance.Id)) | ||
| return nil | ||
| }) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package delete | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "testing" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testInstanceId = "instance" | ||
| testDisplayName = "test" | ||
| ) | ||
| // mockExecutable implements the SDK delete request interface for testing. | ||
| type mockExecutable struct { | ||
| executeFails bool | ||
| executeNotFound bool | ||
| } | ||
| func (m *mockExecutable) Execute() error { | ||
| if m.executeNotFound { | ||
| return &oapierror.GenericOpenAPIError{ | ||
| StatusCode: http.StatusNotFound, | ||
| Body: []byte(`{"message":"not found"}`), | ||
| } | ||
| } | ||
| if m.executeFails { | ||
| return errors.New("execute failed") | ||
| } | ||
| return nil | ||
| } | ||
| // mockAPIClient provides the minimal API client behavior required by the tests. | ||
| type mockAPIClient struct { | ||
| deleteInstanceMock edge.ApiDeleteInstanceRequest | ||
| deleteInstanceByNameMock edge.ApiDeleteInstanceByNameRequest | ||
| } | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| if m.deleteInstanceMock != nil { | ||
| return m.deleteInstanceMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| if m.deleteInstanceByNameMock != nil { | ||
| return m.deleteInstanceByNameMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| // Unused methods to satisfy the client.APIClient interface. | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| return nil | ||
| } | ||
| func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| commonInstance.InstanceIdFlag: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(useDisplayName bool, mods ...func(*inputModel)) *inputModel { | ||
| identifier := &commonValidation.Identifier{ | ||
| Flag: commonInstance.InstanceIdFlag, | ||
| Value: testInstanceId, | ||
| } | ||
| if useDisplayName { | ||
| identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.DisplayNameFlag, | ||
| Value: testDisplayName, | ||
| } | ||
| } | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| identifier: identifier, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureByIdInputModel(mods ...func(*inputModel)) *inputModel { | ||
| return fixtureInputModel(false, mods...) | ||
| } | ||
| func fixtureByNameInputModel(mods ...func(*inputModel)) *inputModel { | ||
| return fixtureInputModel(true, mods...) | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: fixtureByIdInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}, globalflags.GlobalFlagModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: fixtureByNameInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}, globalflags.GlobalFlagModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by id and name", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id empty", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id too long", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id too short", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too short", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foo" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too long", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| wantErr error | ||
| }{ | ||
| { | ||
| name: "delete by id success", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "delete by id API error", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceMock: &mockExecutable{executeFails: true}, | ||
| }, | ||
| }, | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| }, | ||
| { | ||
| name: "delete by id not found", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceMock: &mockExecutable{executeNotFound: true}, | ||
| }, | ||
| }, | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| }, | ||
| { | ||
| name: "delete by name success", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceByNameMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "delete by name API error", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceByNameMock: &mockExecutable{executeFails: true}, | ||
| }, | ||
| }, | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| }, | ||
| { | ||
| name: "delete by name not found", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceByNameMock: &mockExecutable{executeNotFound: true}, | ||
| }, | ||
| }, | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| }, | ||
| { | ||
| name: "no identifier", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| }, | ||
| { | ||
| name: "invalid identifier", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{Flag: "unknown", Value: "value"} | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| wantErr: &cliErr.BuildRequestError{}, | ||
| }, | ||
| { | ||
| name: "nil model", | ||
| args: args{ | ||
| model: nil, | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := run(testCtx, tt.args.model, tt.args.client) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want *deleteRequestSpec | ||
| wantErr error | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| want: &deleteRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceId: testInstanceId, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| deleteInstanceByNameMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| want: &deleteRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceName: testDisplayName, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no identifier", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| }, | ||
| { | ||
| name: "invalid identifier", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{Flag: "unknown", Value: "val"} | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| wantErr: &cliErr.BuildRequestError{}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if got != nil { | ||
| if got.Execute == nil { | ||
| t.Error("expected non-nil Execute function") | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(deleteRequestSpec{}, "Execute")) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestGetWaiterFactory(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want bool | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id identifier", | ||
| want: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name identifier", | ||
| want: true, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "nil model", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| want: false, | ||
| args: args{ | ||
| model: nil, | ||
| }, | ||
| }, | ||
| { | ||
| name: "nil identifier", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| want: false, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "invalid identifier", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| want: false, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{Flag: "unsupported", Value: "value"} | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := getWaiterFactory(testCtx, tt.args.model) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.want && got == nil { | ||
| t.Fatal("expected non-nil waiter factory") | ||
| } | ||
| if !tt.want && got != nil { | ||
| t.Fatal("expected nil waiter factory") | ||
| } | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package delete | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" | ||
| ) | ||
| // Struct to model user input (arguments and/or flags) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| identifier *commonValidation.Identifier | ||
| } | ||
| // deleteRequestSpec captures the details of a request for testing. | ||
| type deleteRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Region string | ||
| InstanceId string // Set if deleting by ID | ||
| InstanceName string // Set if deleting by Name | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() error | ||
| } | ||
| // OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. | ||
| // InstanceWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. | ||
| type instanceWaiter interface { | ||
| WaitWithContext(context.Context) (*edge.Instance, error) | ||
| } | ||
| // A function that creates an instance waiter | ||
| type instanceWaiterFactory = func(client *edge.APIClient) instanceWaiter | ||
| // Command constructor | ||
| // Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags | ||
| // instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname | ||
| // is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we | ||
| // won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "delete", | ||
| Short: "Deletes an edge instance", | ||
| Long: "Deletes a STACKIT Edge Cloud (STEC) instance. The instance will be deleted permanently.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Delete an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance delete --%s "xxx"`, commonInstance.InstanceIdFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Delete an edge instance with %s "xxx"`, commonInstance.DisplayNameFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance delete --%s "xxx"`, commonInstance.DisplayNameFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) | ||
| if err != nil { | ||
| params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| // If project label can't be determined, fall back to project ID | ||
| projectLabel = model.ProjectId | ||
| } | ||
| // Prompt for confirmation | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the edge instance %q of project %q?", model.identifier.Value, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| err = run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| operationState := "Triggered deletion of" | ||
| if !model.Async { | ||
| s := spinner.New(params.Printer) | ||
| s.Start("Deleting instance") | ||
| // Determine identifier and waiter to use | ||
| waiterFactory, err := getWaiterFactory(ctx, model) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // The waiter factory needs a concrete client type. We can safely cast here as the real implementation will always match. | ||
| client, ok := apiClient.(*edge.APIClient) | ||
| if !ok { | ||
| return fmt.Errorf("failed to configure API client") | ||
| } | ||
| waiter := waiterFactory(client) | ||
| if _, err = waiter.WaitWithContext(ctx); err != nil { | ||
| return fmt.Errorf("wait for edge instance deletion: %w", err) | ||
| } | ||
| operationState = "Deleted" | ||
| s.Stop() | ||
| } | ||
| params.Printer.Info("%s instance with %q %q of project %q.\n", operationState, model.identifier.Flag, model.identifier.Value, projectLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) | ||
| cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) | ||
| identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} | ||
| cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName | ||
| cmd.MarkFlagsOneRequired(identifierFlags...) | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Generate input model based on chosen flags | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| model.identifier = id | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) error { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := spec.Execute(); err != nil { | ||
| return cliErr.NewRequestFailedError(err) | ||
| } | ||
| return nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| // It handles the logic of choosing between DeleteInstance and DeleteInstanceByName. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*deleteRequestSpec, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| spec := &deleteRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Region: model.Region, | ||
| } | ||
| // Switch the concrete client based on the identifier flag used | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| spec.InstanceId = model.identifier.Value | ||
| req := apiClient.DeleteInstance(ctx, model.ProjectId, model.Region, model.identifier.Value) | ||
| spec.Execute = req.Execute | ||
| case commonInstance.DisplayNameFlag: | ||
| spec.InstanceName = model.identifier.Value | ||
| req := apiClient.DeleteInstanceByName(ctx, model.ProjectId, model.Region, model.identifier.Value) | ||
| spec.Execute = req.Execute | ||
| default: | ||
| return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) | ||
| } | ||
| return spec, nil | ||
| } | ||
| // Returns a factory function to create the appropriate waiter based on the input model. | ||
| func getWaiterFactory(ctx context.Context, model *inputModel) (instanceWaiterFactory, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| factory := func(c *edge.APIClient) instanceWaiter { | ||
| return wait.DeleteInstanceWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) | ||
| } | ||
| return factory, nil | ||
| case commonInstance.DisplayNameFlag: | ||
| factory := func(c *edge.APIClient) instanceWaiter { | ||
| return wait.DeleteInstanceByNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) | ||
| } | ||
| return factory, nil | ||
| default: | ||
| return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package describe | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "testing" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testInstanceId = "instance" | ||
| testDisplayName = "test" | ||
| ) | ||
| // mockExecutable is a mock for the Executable interface | ||
| type mockExecutable struct { | ||
| executeFails bool | ||
| executeNotFound bool | ||
| executeResp *edge.Instance | ||
| } | ||
| func (m *mockExecutable) Execute() (*edge.Instance, error) { | ||
| if m.executeFails { | ||
| return nil, errors.New("API error") | ||
| } | ||
| if m.executeNotFound { | ||
| return nil, &oapierror.GenericOpenAPIError{ | ||
| StatusCode: http.StatusNotFound, | ||
| } | ||
| } | ||
| return m.executeResp, nil | ||
| } | ||
| // mockAPIClient is a mock for the edge.APIClient interface | ||
| type mockAPIClient struct { | ||
| getInstanceMock edge.ApiGetInstanceRequest | ||
| getInstanceByNameMock edge.ApiGetInstanceByNameRequest | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| if m.getInstanceMock != nil { | ||
| return m.getInstanceMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| if m.getInstanceByNameMock != nil { | ||
| return m.getInstanceByNameMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| // Unused methods to satisfy the interface | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| return nil | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| commonInstance.InstanceIdFlag: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(false, mods...) | ||
| } | ||
| func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(true, mods...) | ||
| } | ||
| func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| } | ||
| if useName { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.DisplayNameFlag, | ||
| Value: testDisplayName, | ||
| } | ||
| } else { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.InstanceIdFlag, | ||
| Value: testInstanceId, | ||
| } | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: fixtureByIdInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: fixtureByNameInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by id and name", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instanceId missing", | ||
| want: fixtureByNameInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "instanceId empty", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instanceId too long", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instanceId too short", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too short", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foo" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too long", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want *edge.Instance | ||
| args args | ||
| }{ | ||
| { | ||
| name: "get by id success", | ||
| want: &edge.Instance{ | ||
| Id: &testInstanceId, | ||
| DisplayName: &testDisplayName, | ||
| }, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstanceMock: &mockExecutable{ | ||
| executeResp: &edge.Instance{ | ||
| Id: &testInstanceId, | ||
| DisplayName: &testDisplayName, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get by name success", | ||
| want: &edge.Instance{ | ||
| Id: &testInstanceId, | ||
| DisplayName: &testDisplayName, | ||
| }, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstanceByNameMock: &mockExecutable{ | ||
| executeResp: &edge.Instance{ | ||
| Id: &testInstanceId, | ||
| DisplayName: &testDisplayName, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance not found error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstanceMock: &mockExecutable{ | ||
| executeNotFound: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get by id API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstanceMock: &mockExecutable{ | ||
| executeFails: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get by name API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstanceByNameMock: &mockExecutable{ | ||
| executeFails: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "identifier invalid", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: "unknown-flag", | ||
| Value: "some-value", | ||
| } | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := run(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type outputArgs struct { | ||
| model *inputModel | ||
| instance *edge.Instance | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| args outputArgs | ||
| }{ | ||
| { | ||
| name: "no instance", | ||
| wantErr: &commonErr.NoInstanceError{}, | ||
| args: outputArgs{ | ||
| model: fixtureByIdInputModel(), | ||
| instance: nil, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output json", | ||
| args: outputArgs{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.OutputFormat = print.JSONOutputFormat | ||
| model.identifier = nil | ||
| }), | ||
| instance: &edge.Instance{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output yaml", | ||
| args: outputArgs{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| model.identifier = nil | ||
| }), | ||
| instance: &edge.Instance{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output default", | ||
| args: outputArgs{ | ||
| model: fixtureByIdInputModel(), | ||
| instance: &edge.Instance{Id: &testInstanceId}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(&types.CmdParams{Printer: p}) | ||
| err := outputResult(p, tt.args.model.OutputFormat, tt.args.instance) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want *describeRequestSpec | ||
| args args | ||
| }{ | ||
| { | ||
| name: "get by id", | ||
| want: &describeRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceId: testInstanceId, | ||
| }, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstanceMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get by name", | ||
| want: &describeRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceName: testDisplayName, | ||
| }, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstanceByNameMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "identifier invalid", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: "unknown-flag", | ||
| Value: "some-value", | ||
| } | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(describeRequestSpec{}, "Execute")) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package describe | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| identifier *commonValidation.Identifier | ||
| } | ||
| // describeRequestSpec captures the details of the request for testing. | ||
| type describeRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Region string | ||
| InstanceId string // Set if describing by ID | ||
| InstanceName string // Set if describing by Name | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() (*edge.Instance, error) | ||
| } | ||
| // Command constructor | ||
| // Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags | ||
| // instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname | ||
| // is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we | ||
| // won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "describe", | ||
| Short: "Describes an edge instance", | ||
| Long: "Describes a STACKIT Edge Cloud (STEC) instance.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Describe an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance describe --%s <ID>`, commonInstance.InstanceIdFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Describe an edge instance with %s "xxx"`, commonInstance.DisplayNameFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance describe --%s <NAME>`, commonInstance.DisplayNameFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| resp, err := run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Handle output to printer | ||
| return outputResult(params.Printer, model.OutputFormat, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) | ||
| cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) | ||
| identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} | ||
| cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName | ||
| cmd.MarkFlagsOneRequired(identifierFlags...) | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Generate input model based on chosen flags | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| model.identifier = id | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Instance, error) { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp, err := spec.Execute() | ||
| if err != nil { | ||
| return nil, cliErr.NewRequestFailedError(err) | ||
| } | ||
| return resp, nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| // It handles the logic of choosing between GetInstance and GetInstanceByName. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*describeRequestSpec, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| spec := &describeRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Region: model.Region, | ||
| } | ||
| // Switch the concrete client based on the identifier flag used | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| spec.InstanceId = model.identifier.Value | ||
| req := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.identifier.Value) | ||
| spec.Execute = req.Execute | ||
| case commonInstance.DisplayNameFlag: | ||
| spec.InstanceName = model.identifier.Value | ||
| req := apiClient.GetInstanceByName(ctx, model.ProjectId, model.Region, model.identifier.Value) | ||
| spec.Execute = req.Execute | ||
| default: | ||
| return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) | ||
| } | ||
| return spec, nil | ||
| } | ||
| // Output result based on the configured output format | ||
| func outputResult(p *print.Printer, outputFormat string, instance *edge.Instance) error { | ||
| if instance == nil { | ||
| // This is only to prevent nil pointer deref. | ||
| // As long as the API behaves as defined by it's spec, instance can not be empty (HTTP 200 with an empty body) | ||
| return commonErr.NewNoInstanceError("") | ||
| } | ||
| return p.OutputResult(outputFormat, instance, func() error { | ||
| table := tables.NewTable() | ||
| // Describe: output all fields. Be sure to filter for any non-required fields. | ||
| table.AddRow("CREATED", utils.PtrString(instance.Created)) | ||
| table.AddSeparator() | ||
| table.AddRow("ID", utils.PtrString(instance.Id)) | ||
| table.AddSeparator() | ||
| table.AddRow("NAME", utils.PtrString(instance.DisplayName)) | ||
| table.AddSeparator() | ||
| if instance.HasDescription() { | ||
| table.AddRow("DESCRIPTION", utils.PtrString(instance.Description)) | ||
| table.AddSeparator() | ||
| } | ||
| table.AddRow("UI", utils.PtrString(instance.FrontendUrl)) | ||
| table.AddSeparator() | ||
| table.AddRow("STATE", utils.PtrString(instance.Status)) | ||
| table.AddSeparator() | ||
| table.AddRow("PLAN", utils.PtrString(instance.PlanId)) | ||
| table.AddSeparator() | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| }) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package instance | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/delete" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/describe" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/update" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "instance", | ||
| Short: "Provides functionality for edge instances.", | ||
| Long: "Provides functionality for STACKIT Edge Cloud (STEC) instance management.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, params) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { | ||
| cmd.AddCommand(list.NewCmd(params)) | ||
| cmd.AddCommand(describe.NewCmd(params)) | ||
| cmd.AddCommand(create.NewCmd(params)) | ||
| cmd.AddCommand(update.NewCmd(params)) | ||
| cmd.AddCommand(delete.NewCmd(params)) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package list | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| ) | ||
| // mockExecutable is a mock for the Executable interface | ||
| type mockExecutable struct { | ||
| executeFails bool | ||
| executeResp *edge.InstanceList | ||
| } | ||
| func (m *mockExecutable) Execute() (*edge.InstanceList, error) { | ||
| if m.executeFails { | ||
| return nil, errors.New("API error") | ||
| } | ||
| if m.executeResp != nil { | ||
| return m.executeResp, nil | ||
| } | ||
| return &edge.InstanceList{ | ||
| Instances: &[]edge.Instance{ | ||
| {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, | ||
| {Id: utils.Ptr("instance-2"), DisplayName: utils.Ptr("nameb")}, | ||
| }, | ||
| }, nil | ||
| } | ||
| // mockAPIClient is a mock for the edge.APIClient interface | ||
| type mockAPIClient struct { | ||
| getInstancesMock edge.ApiGetInstancesRequest | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| if m.getInstancesMock != nil { | ||
| return m.getInstancesMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| // Unused methods to satisfy the interface | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| return nil | ||
| } | ||
| 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 TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "success", | ||
| want: fixtureInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "with limit", | ||
| want: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = utils.Ptr(int64(10)) | ||
| }), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "10" | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "limit invalid", | ||
| wantErr: "invalid syntax", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "limit less than 1", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want []edge.Instance | ||
| args args | ||
| }{ | ||
| { | ||
| name: "list success", | ||
| want: []edge.Instance{ | ||
| {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, | ||
| {Id: utils.Ptr("instance-2"), DisplayName: utils.Ptr("nameb")}, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list success with limit", | ||
| want: []edge.Instance{ | ||
| {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = utils.Ptr(int64(1)) | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list success with limit greater than items", | ||
| want: []edge.Instance{ | ||
| {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, | ||
| {Id: utils.Ptr("instance-2"), DisplayName: utils.Ptr("nameb")}, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = utils.Ptr(int64(5)) | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list success with no items", | ||
| want: []edge.Instance{}, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstancesMock: &mockExecutable{ | ||
| executeResp: &edge.InstanceList{Instances: &[]edge.Instance{}}, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstancesMock: &mockExecutable{ | ||
| executeFails: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := run(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| instances []edge.Instance | ||
| projectLabel string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| args args | ||
| }{ | ||
| { | ||
| name: "no instance", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "output json", | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.JSONOutputFormat | ||
| }), | ||
| instances: []edge.Instance{ | ||
| {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, | ||
| }, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| { | ||
| name: "output yaml", | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| }), | ||
| instances: []edge.Instance{ | ||
| {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, | ||
| }, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| { | ||
| name: "output default with instances", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| instances: []edge.Instance{ | ||
| { | ||
| Id: utils.Ptr("instance-1"), | ||
| DisplayName: utils.Ptr("namea"), | ||
| FrontendUrl: utils.Ptr("https://example.com"), | ||
| }, | ||
| { | ||
| Id: utils.Ptr("instance-2"), | ||
| DisplayName: utils.Ptr("nameb"), | ||
| FrontendUrl: utils.Ptr("https://example2.com"), | ||
| }, | ||
| }, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| { | ||
| name: "output default with no instances", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| instances: []edge.Instance{}, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(&types.CmdParams{Printer: p}) | ||
| err := outputResult(p, tt.args.model.OutputFormat, tt.args.projectLabel, tt.args.instances) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want *listRequestSpec | ||
| args args | ||
| }{ | ||
| { | ||
| name: "success", | ||
| want: &listRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| getInstancesMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "success with limit", | ||
| want: &listRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| Limit: utils.Ptr(int64(10)), | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = utils.Ptr(int64(10)) | ||
| }), | ||
| client: &mockAPIClient{ | ||
| getInstancesMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(listRequestSpec{}, "Execute")) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package list | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| const ( | ||
| limitFlag = "limit" | ||
| ) | ||
| // Struct to model user input (arguments and/or flags) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| } | ||
| // listRequestSpec captures the details of the request for testing. | ||
| type listRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Region string | ||
| Limit *int64 | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() (*edge.InstanceList, error) | ||
| } | ||
| // Command constructor | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists edge instances", | ||
| Long: "Lists STACKIT Edge Cloud (STEC) instances of a project.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Lists all edge instances of a given project`, | ||
| `$ stackit beta edge-cloud instance list`), | ||
| examples.NewExample( | ||
| `Lists all edge instances of a given project and limits the output to two instances`, | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance list --%s 2`, limitFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) | ||
| if err != nil { | ||
| params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| // If project label can't be determined, fall back to project ID | ||
| projectLabel = model.ProjectId | ||
| } | ||
| // Call API | ||
| resp, err := run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) | ||
| if limit != nil && *limit < 1 { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: limitFlag, | ||
| Details: "must be greater than 0", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Limit: limit, | ||
| } | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) ([]edge.Instance, error) { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp, err := spec.Execute() | ||
| if err != nil { | ||
| return nil, cliErr.NewRequestFailedError(err) | ||
| } | ||
| if resp == nil { | ||
| return nil, fmt.Errorf("list instances: empty response from API") | ||
| } | ||
| if resp.Instances == nil { | ||
| return nil, fmt.Errorf("list instances: instances missing in response") | ||
| } | ||
| instances := *resp.Instances | ||
| // Truncate output if limit is set | ||
| if spec.Limit != nil && len(instances) > int(*spec.Limit) { | ||
| instances = instances[:*spec.Limit] | ||
| } | ||
| return instances, nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*listRequestSpec, error) { | ||
| req := apiClient.GetInstances(ctx, model.ProjectId, model.Region) | ||
| return &listRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Region: model.Region, | ||
| Limit: model.Limit, | ||
| Execute: req.Execute, | ||
| }, nil | ||
| } | ||
| // Output result based on the configured output format | ||
| func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []edge.Instance) error { | ||
| return p.OutputResult(outputFormat, instances, func() error { | ||
| // No instances found for project | ||
| if len(instances) == 0 { | ||
| p.Outputf("No instances found for project %q\n", projectLabel) | ||
| return nil | ||
| } | ||
| // Display instances found for project in a table | ||
| table := tables.NewTable() | ||
| // List: only output the most important fields. Be sure to filter for any non-required fields. | ||
| table.SetHeader("ID", "NAME", "UI", "STATE") | ||
| for i := range instances { | ||
| instance := instances[i] | ||
| table.AddRow( | ||
| utils.PtrString(instance.Id), | ||
| utils.PtrString(instance.DisplayName), | ||
| utils.PtrString(instance.FrontendUrl), | ||
| utils.PtrString(instance.Status)) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| }) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package update | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "testing" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testInstanceId = "instance" | ||
| testDisplayName = "test" | ||
| testDescription = "new description" | ||
| testPlanId = uuid.NewString() | ||
| ) | ||
| type mockExecutable struct { | ||
| executeFails bool | ||
| executeNotFound bool | ||
| capturedUpdatePayload *edge.UpdateInstancePayload | ||
| capturedUpdateByNamePayload *edge.UpdateInstanceByNamePayload | ||
| } | ||
| func (m *mockExecutable) Execute() error { | ||
| if m.executeFails { | ||
| return errors.New("API error") | ||
| } | ||
| if m.executeNotFound { | ||
| return &oapierror.GenericOpenAPIError{ | ||
| StatusCode: http.StatusNotFound, | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| func (m *mockExecutable) UpdateInstancePayload(payload edge.UpdateInstancePayload) edge.ApiUpdateInstanceRequest { | ||
| if m.capturedUpdatePayload != nil { | ||
| *m.capturedUpdatePayload = payload | ||
| } | ||
| return m | ||
| } | ||
| func (m *mockExecutable) UpdateInstanceByNamePayload(payload edge.UpdateInstanceByNamePayload) edge.ApiUpdateInstanceByNameRequest { | ||
| if m.capturedUpdateByNamePayload != nil { | ||
| *m.capturedUpdateByNamePayload = payload | ||
| } | ||
| return m | ||
| } | ||
| type mockAPIClient struct { | ||
| updateInstanceMock edge.ApiUpdateInstanceRequest | ||
| updateInstanceByNameMock edge.ApiUpdateInstanceByNameRequest | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| if m.updateInstanceMock != nil { | ||
| return m.updateInstanceMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| if m.updateInstanceByNameMock != nil { | ||
| return m.updateInstanceByNameMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| // Unused methods to satisfy the interface | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| return nil | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| commonInstance.InstanceIdFlag: testInstanceId, | ||
| commonInstance.DescriptionFlag: testDescription, | ||
| commonInstance.PlanIdFlag: testPlanId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(false, mods...) | ||
| } | ||
| func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(true, mods...) | ||
| } | ||
| func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Description: &testDescription, | ||
| PlanId: &testPlanId, | ||
| } | ||
| if useName { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.DisplayNameFlag, | ||
| Value: testDisplayName, | ||
| } | ||
| } else { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.InstanceIdFlag, | ||
| Value: testInstanceId, | ||
| } | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: fixtureByIdInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: fixtureByNameInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by id and name", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no update flags", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.DescriptionFlag) | ||
| delete(flagValues, commonInstance.PlanIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "plan id invalid", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.PlanIdFlag] = "not-a-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want *updateRequestSpec | ||
| wantErr error | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| updateInstanceMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| want: &updateRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceId: testInstanceId, | ||
| Payload: edge.UpdateInstancePayload{ | ||
| Description: &testDescription, | ||
| PlanId: &testPlanId, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| updateInstanceByNameMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| want: &updateRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceName: testDisplayName, | ||
| Payload: edge.UpdateInstancePayload{ | ||
| Description: &testDescription, | ||
| PlanId: &testPlanId, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no identifier", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| }, | ||
| { | ||
| name: "invalid identifier", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{Flag: "unknown", Value: "val"} | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| wantErr: &cliErr.BuildRequestError{}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if got != nil { | ||
| if got.Execute == nil { | ||
| t.Error("expected non-nil Execute function") | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(updateRequestSpec{}, "Execute")) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| args args | ||
| }{ | ||
| { | ||
| name: "update by id success", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| updateInstanceMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "update by name success", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| updateInstanceByNameMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance not found error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| updateInstanceMock: &mockExecutable{ | ||
| executeNotFound: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "update by id API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{ | ||
| updateInstanceMock: &mockExecutable{ | ||
| executeFails: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "update by name API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{ | ||
| updateInstanceByNameMock: &mockExecutable{ | ||
| executeFails: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "identifier invalid", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: "unknown-flag", | ||
| Value: "some-value", | ||
| } | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := run(testCtx, tt.args.model, tt.args.client) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } | ||
| func TestGetWaiterFactory(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want bool | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: true, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| want: false, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "unknown identifier", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| want: false, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier.Flag = "unknown" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := getWaiterFactory(testCtx, tt.args.model) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.want && got == nil { | ||
| t.Fatal("expected non-nil waiter factory") | ||
| } | ||
| if !tt.want && got != nil { | ||
| t.Fatal("expected nil waiter factory") | ||
| } | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package update | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" | ||
| ) | ||
| // Struct to model user input (arguments and/or flags) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| identifier *commonValidation.Identifier | ||
| Description *string | ||
| PlanId *string | ||
| } | ||
| // updateRequestSpec captures the details of the request for testing. | ||
| type updateRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Region string | ||
| InstanceId string // Set if updating by ID | ||
| InstanceName string // Set if updating by Name | ||
| Payload edge.UpdateInstancePayload | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() error | ||
| } | ||
| // OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. | ||
| // InstanceWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. | ||
| type instanceWaiter interface { | ||
| WaitWithContext(context.Context) (*edge.Instance, error) | ||
| } | ||
| // A function that creates an instance waiter | ||
| type instanceWaiterFactory = func(client *edge.APIClient) instanceWaiter | ||
| // Command constructor | ||
| // Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags | ||
| // instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname | ||
| // is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we | ||
| // won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "update", | ||
| Short: "Updates an edge instance", | ||
| Long: "Updates a STACKIT Edge Cloud (STEC) instance.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Updates the description of an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance update --%s "xxx" --%s "yyy"`, commonInstance.InstanceIdFlag, commonInstance.DescriptionFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Updates the plan of an edge instance with %s "xxx"`, commonInstance.DisplayNameFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance update --%s "xxx" --%s "yyy"`, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Updates the description and plan of an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud instance update --%s "xxx" --%s "yyy" --%s "zzz"`, commonInstance.InstanceIdFlag, commonInstance.DescriptionFlag, commonInstance.PlanIdFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) | ||
| if err != nil { | ||
| params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| // If project label can't be determined, fall back to project ID | ||
| projectLabel = model.ProjectId | ||
| } | ||
| // Prompt for confirmation | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update the edge instance %q of project %q?", model.identifier.Value, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| err = run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Wait for async operation, if async mode not enabled | ||
| operationState := "Triggered update of" | ||
| if !model.Async { | ||
| // Wait for async operation, if async mode not enabled | ||
| // Show spinner while waiting | ||
| s := spinner.New(params.Printer) | ||
| s.Start("Updating instance") | ||
| // Determine identifier and waiter to use | ||
| waiterFactory, err := getWaiterFactory(ctx, model) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // The waiter handler needs a concrete client type. We can safely cast here as the real implementation will always match. | ||
| client, ok := apiClient.(*edge.APIClient) | ||
| if !ok { | ||
| return fmt.Errorf("failed to configure API client") | ||
| } | ||
| waiter := waiterFactory(client) | ||
| if _, err = waiter.WaitWithContext(ctx); err != nil { | ||
| return fmt.Errorf("wait for edge instance update: %w", err) | ||
| } | ||
| operationState = "Updated" | ||
| s.Stop() | ||
| } | ||
| params.Printer.Info("%s instance with %q %q of project %q.\n", operationState, model.identifier.Flag, model.identifier.Value, projectLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) | ||
| cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) | ||
| cmd.Flags().StringP(commonInstance.DescriptionFlag, commonInstance.DescriptionShorthand, "", commonInstance.DescriptionUsage) | ||
| cmd.Flags().StringP(commonInstance.PlanIdFlag, "", "", commonInstance.PlanIdUsage) | ||
| identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} | ||
| cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName | ||
| cmd.MarkFlagsOneRequired(identifierFlags...) | ||
| // Make sure at least one updatable field is provided, otherwise it would be a no-op | ||
| updatedFields := []string{commonInstance.DescriptionFlag, commonInstance.PlanIdFlag} | ||
| cmd.MarkFlagsOneRequired(updatedFields...) | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Generate input model based on chosen flags | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| model.identifier = id | ||
| if planIdValue := flags.FlagToStringPointer(p, cmd, commonInstance.PlanIdFlag); planIdValue != nil { | ||
| if err := commonInstance.ValidatePlanId(planIdValue); err != nil { | ||
| return nil, err | ||
| } | ||
| model.PlanId = planIdValue | ||
| } | ||
| if descriptionValue := flags.FlagToStringPointer(p, cmd, commonInstance.DescriptionFlag); descriptionValue != nil { | ||
| if err := commonInstance.ValidateDescription(*descriptionValue); err != nil { | ||
| return nil, err | ||
| } | ||
| model.Description = descriptionValue | ||
| } | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) error { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = spec.Execute() | ||
| if err != nil { | ||
| return cliErr.NewRequestFailedError(err) | ||
| } | ||
| return nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| // It handles the logic of choosing between UpdateInstance and UpdateInstanceByName. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*updateRequestSpec, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| spec := &updateRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Region: model.Region, | ||
| Payload: edge.UpdateInstancePayload{ | ||
| Description: model.Description, | ||
| PlanId: model.PlanId, | ||
| }, | ||
| } | ||
| // Switch the concrete client based on the identifier flag used | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| spec.InstanceId = model.identifier.Value | ||
| req := apiClient.UpdateInstance(ctx, model.ProjectId, model.Region, model.identifier.Value) | ||
| req = req.UpdateInstancePayload(spec.Payload) | ||
| spec.Execute = req.Execute | ||
| case commonInstance.DisplayNameFlag: | ||
| spec.InstanceName = model.identifier.Value | ||
| req := apiClient.UpdateInstanceByName(ctx, model.ProjectId, model.Region, model.identifier.Value) | ||
| req = req.UpdateInstanceByNamePayload(edge.UpdateInstanceByNamePayload{ | ||
| Description: spec.Payload.Description, | ||
| PlanId: spec.Payload.PlanId, | ||
| }) | ||
| spec.Execute = req.Execute | ||
| default: | ||
| return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) | ||
| } | ||
| return spec, nil | ||
| } | ||
| // Returns a factory function to create the appropriate waiter based on the input model. | ||
| func getWaiterFactory(ctx context.Context, model *inputModel) (instanceWaiterFactory, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| factory := func(c *edge.APIClient) instanceWaiter { | ||
| return wait.CreateOrUpdateInstanceWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) | ||
| } | ||
| return factory, nil | ||
| case commonInstance.DisplayNameFlag: | ||
| factory := func(c *edge.APIClient) instanceWaiter { | ||
| return wait.CreateOrUpdateInstanceByNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) | ||
| } | ||
| return factory, nil | ||
| default: | ||
| return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package create | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "testing" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testInstanceId = "instance" | ||
| testDisplayName = "test" | ||
| testExpiration = "1h" | ||
| ) | ||
| const ( | ||
| testKubeconfig = ` | ||
| apiVersion: v1 | ||
| clusters: | ||
| - cluster: | ||
| server: https://server-1.com | ||
| name: cluster-1 | ||
| contexts: | ||
| - context: | ||
| cluster: cluster-1 | ||
| user: user-1 | ||
| name: context-1 | ||
| current-context: context-1 | ||
| kind: Config | ||
| preferences: {} | ||
| users: | ||
| - name: user-1 | ||
| user: {} | ||
| ` | ||
| ) | ||
| // Helper function to create a new instance of Kubeconfig | ||
| // | ||
| //nolint:gocritic // ptrToRefParam: Required by edge.Kubeconfig API which expects *map[string]interface{} | ||
| func testKubeconfigMap() *map[string]interface{} { | ||
| var kubeconfigMap map[string]interface{} | ||
| err := yaml.Unmarshal([]byte(testKubeconfig), &kubeconfigMap) | ||
| if err != nil { | ||
| // This should never happen in tests with valid YAML | ||
| panic(err) | ||
| } | ||
| return utils.Ptr(kubeconfigMap) | ||
| } | ||
| // mockKubeconfigWaiter is a mock for the kubeconfigWaiter interface | ||
| type mockKubeconfigWaiter struct { | ||
| waitFails bool | ||
| waitNotFound bool | ||
| waitResp *edge.Kubeconfig | ||
| } | ||
| func (m *mockKubeconfigWaiter) WaitWithContext(_ context.Context) (*edge.Kubeconfig, error) { | ||
| if m.waitFails { | ||
| return nil, errors.New("wait error") | ||
| } | ||
| if m.waitNotFound { | ||
| return nil, &oapierror.GenericOpenAPIError{ | ||
| StatusCode: http.StatusNotFound, | ||
| } | ||
| } | ||
| if m.waitResp != nil { | ||
| return m.waitResp, nil | ||
| } | ||
| // Default kubeconfig response | ||
| return &edge.Kubeconfig{ | ||
| Kubeconfig: testKubeconfigMap(), | ||
| }, nil | ||
| } | ||
| // testWaiterFactoryProvider is a test implementation that returns mock waiters. | ||
| type testWaiterFactoryProvider struct { | ||
| waiter kubeconfigWaiter | ||
| } | ||
| func (t *testWaiterFactoryProvider) getKubeconfigWaiter(_ context.Context, model *inputModel, _ client.APIClient) (kubeconfigWaiter, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, &commonErr.NoIdentifierError{} | ||
| } | ||
| // Validate identifier like the real implementation | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag: | ||
| // Return our mock waiter directly, bypassing the client type casting issue | ||
| return t.waiter, nil | ||
| default: | ||
| return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) | ||
| } | ||
| } | ||
| // mockAPIClient is a mock for the edge.APIClient interface | ||
| type mockAPIClient struct{} | ||
| // Unused methods to satisfy the interface | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| commonInstance.InstanceIdFlag: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(false, mods...) | ||
| } | ||
| func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(true, mods...) | ||
| } | ||
| func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| DisableWriting: false, | ||
| Filepath: nil, | ||
| Overwrite: false, | ||
| Expiration: uint64(3600), // Default 1 hour | ||
| SwitchContext: false, | ||
| } | ||
| if useName { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.DisplayNameFlag, | ||
| Value: testDisplayName, | ||
| } | ||
| } else { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.InstanceIdFlag, | ||
| Value: testInstanceId, | ||
| } | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: fixtureByIdInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: fixtureByNameInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "with expiration", | ||
| want: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.Expiration = uint64(3600) | ||
| }), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = testExpiration | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by id and name", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id missing", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id empty", | ||
| wantErr: "id may not be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id too long", | ||
| wantErr: "id is too long", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id too short", | ||
| wantErr: "id is too short", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too short", | ||
| wantErr: "name is too short", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foo" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too long", | ||
| wantErr: "name is too long", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "disable writing and invalid output format", | ||
| wantErr: "valid output formats for this command are", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.DisableWritingFlag] = "true" | ||
| flagValues[globalflags.OutputFormatFlag] = print.PrettyOutputFormat | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "disable writing and default output format", | ||
| wantErr: "must be used with --output-format", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.DisableWritingFlag] = "true" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "disable writing and valid output format", | ||
| want: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.DisableWriting = true | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| }), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.DisableWritingFlag] = "true" | ||
| flagValues[globalflags.OutputFormatFlag] = print.YAMLOutputFormat | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "invalid expiration format", | ||
| wantErr: "invalid time string format", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = "invalid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "expiration too short", | ||
| wantErr: "expiration is too small", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = "1s" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "expiration too long", | ||
| wantErr: "expiration is too large", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = "13M" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| waiter kubeconfigWaiter | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| args args | ||
| }{ | ||
| { | ||
| name: "run by id success", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockKubeconfigWaiter{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "run by name success", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockKubeconfigWaiter{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockKubeconfigWaiter{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance not found error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockKubeconfigWaiter{waitNotFound: true}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get kubeconfig by id API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockKubeconfigWaiter{waitFails: true}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get kubeconfig by name API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockKubeconfigWaiter{waitFails: true}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "identifier invalid", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: "unknown-flag", | ||
| Value: "some-value", | ||
| } | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockKubeconfigWaiter{}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| // Override production waiterProvider package level variable for testing | ||
| prodWaiterProvider := waiterProvider | ||
| waiterProvider = &testWaiterFactoryProvider{waiter: tt.args.waiter} | ||
| defer func() { waiterProvider = prodWaiterProvider }() | ||
| _, err := run(testCtx, tt.args.model, tt.args.client) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want *createRequestSpec | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: &createRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceId: testInstanceId, | ||
| Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), | ||
| }, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: &createRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceName: testDisplayName, | ||
| Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), | ||
| }, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "identifier invalid", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: "unknown-flag", | ||
| Value: "some-value", | ||
| } | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(createRequestSpec{}, "Execute")) | ||
| }) | ||
| } | ||
| } | ||
| func TestGetWaiterFactory(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want bool | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: true, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| want: false, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "unknown identifier", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| want: false, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier.Flag = "unknown" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := getWaiterFactory(testCtx, tt.args.model) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.want && got == nil { | ||
| t.Fatal("expected non-nil waiter factory") | ||
| } | ||
| if !tt.want && got != nil { | ||
| t.Fatal("expected nil waiter factory") | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| kubeconfig *edge.Kubeconfig | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| args args | ||
| }{ | ||
| { | ||
| name: "no kubeconfig", | ||
| wantErr: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| kubeconfig: nil, | ||
| }, | ||
| }, | ||
| { | ||
| name: "kubeconfig with nil kubeconfig data", | ||
| wantErr: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: nil}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output json with disable writing", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.JSONOutputFormat | ||
| model.DisableWriting = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output yaml with disable writing", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| model.DisableWriting = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output default with disable writing", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.DisableWriting = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output by name with json format and disable writing", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.JSONOutputFormat | ||
| model.DisableWriting = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output by name with yaml format and disable writing", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| model.DisableWriting = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "output by name default with disable writing", | ||
| args: args{ | ||
| model: fixtureByNameInputModel(func(model *inputModel) { | ||
| model.DisableWriting = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "file writing enabled (default behavior)", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.AssumeYes = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "file writing with overwrite enabled", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.Overwrite = true | ||
| model.AssumeYes = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "file writing with switch context enabled", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.SwitchContext = true | ||
| model.AssumeYes = true | ||
| }), | ||
| kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(&types.CmdParams{Printer: p}) | ||
| err := outputResult(p, tt.args.model.OutputFormat, tt.args.model, tt.args.kubeconfig) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package create | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| identifier *commonValidation.Identifier | ||
| DisableWriting bool | ||
| Filepath *string | ||
| Overwrite bool | ||
| Expiration uint64 | ||
| SwitchContext bool | ||
| } | ||
| // createRequestSpec captures the details of the request for testing. | ||
| type createRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Region string | ||
| InstanceId string | ||
| InstanceName string | ||
| Expiration int64 | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() (*edge.Kubeconfig, error) | ||
| } | ||
| // OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. | ||
| // KubeconfigWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. | ||
| type kubeconfigWaiter interface { | ||
| WaitWithContext(context.Context) (*edge.Kubeconfig, error) | ||
| } | ||
| // A function that creates a kubeconfig waiter | ||
| type kubeconfigWaiterFactory = func(client *edge.APIClient) kubeconfigWaiter | ||
| // waiterFactoryProvider is an interface that provides kubeconfig waiters so we can inject different impl. while testing. | ||
| type waiterFactoryProvider interface { | ||
| getKubeconfigWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (kubeconfigWaiter, error) | ||
| } | ||
| // productionWaiterFactoryProvider is the real implementation used in production. | ||
| // It handles the concrete client type casting required by the SDK's wait handlers. | ||
| type productionWaiterFactoryProvider struct{} | ||
| func (p *productionWaiterFactoryProvider) getKubeconfigWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (kubeconfigWaiter, error) { | ||
| waiterFactory, err := getWaiterFactory(ctx, model) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| // The waiter handler needs a concrete client type. We can safely cast here as the real implementation will always match. | ||
| edgeClient, ok := apiClient.(*edge.APIClient) | ||
| if !ok { | ||
| return nil, cliErr.NewBuildRequestError("failed to configure API client", nil) | ||
| } | ||
| return waiterFactory(edgeClient), nil | ||
| } | ||
| // waiterProvider is the package-level variable used to get the waiter. | ||
| // It is initialized with the production implementation but can be overridden in tests. | ||
| var waiterProvider waiterFactoryProvider = &productionWaiterFactoryProvider{} | ||
| // Command constructor | ||
| // Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags | ||
| // instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname | ||
| // is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we | ||
| // won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates or updates a local kubeconfig file of an edge instance", | ||
| Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s", | ||
| "Creates or updates a local kubeconfig file of a STACKIT Edge Cloud (STEC) instance. If the config exists in the kubeconfig file, the information will be updated.", | ||
| "By default, the kubeconfig information of the edge instance is merged into the current kubeconfig file which is determined by Kubernetes client logic. If the kubeconfig file doesn't exist, a new one will be created.", | ||
| fmt.Sprintf("You can override this behavior by specifying a custom filepath with the --%s flag or disable writing with the --%s flag.", commonKubeconfig.FilepathFlag, commonKubeconfig.DisableWritingFlag), | ||
| fmt.Sprintf("An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is %d seconds.", commonKubeconfig.ExpirationSecondsDefault), | ||
| "Note: the format for the duration is <value><unit>, e.g. 30d for 30 days. You may not combine units."), | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Create or update a kubeconfig for the edge instance with %s "xxx". If the config exists in the kubeconfig file, the information will be updated.`, commonInstance.InstanceIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx"`, commonInstance.InstanceIdFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Create or update a kubeconfig for the edge instance with %s "xxx" in a custom filepath.`, commonInstance.DisplayNameFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx" --filepath "yyy"`, commonInstance.DisplayNameFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Get a kubeconfig for the edge instance with %s "xxx" without writing it to a file and format the output as json.`, commonInstance.DisplayNameFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx" --disable-writing --output-format json`, commonInstance.DisplayNameFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Create a kubeconfig for the edge instance with %s "xxx". This will replace your current kubeconfig file.`, commonInstance.InstanceIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx" --overwrite`, commonInstance.InstanceIdFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Prompt for confirmation is handled in outputResult | ||
| if model.Async { | ||
| return fmt.Errorf("async mode is not supported for kubeconfig create") | ||
| } | ||
| // Call API via waiter (which handles both the API call and waiting) | ||
| kubeconfig, err := run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Handle file operations or output to printer | ||
| return outputResult(params.Printer, model.OutputFormat, model, kubeconfig) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) | ||
| cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) | ||
| cmd.Flags().Bool(commonKubeconfig.DisableWritingFlag, false, commonKubeconfig.DisableWritingUsage) | ||
| cmd.Flags().StringP(commonKubeconfig.FilepathFlag, commonKubeconfig.FilepathShorthand, "", commonKubeconfig.FilepathUsage) | ||
| cmd.Flags().StringP(commonKubeconfig.ExpirationFlag, commonKubeconfig.ExpirationShorthand, "", commonKubeconfig.ExpirationUsage) | ||
| cmd.Flags().Bool(commonKubeconfig.OverwriteFlag, false, commonKubeconfig.OverwriteUsage) | ||
| cmd.Flags().Bool(commonKubeconfig.SwitchContextFlag, false, commonKubeconfig.SwitchContextUsage) | ||
| identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} | ||
| cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName | ||
| cmd.MarkFlagsOneRequired(identifierFlags...) | ||
| cmd.MarkFlagsMutuallyExclusive(commonKubeconfig.DisableWritingFlag, commonKubeconfig.FilepathFlag) // DisableWriting xor Filepath | ||
| cmd.MarkFlagsMutuallyExclusive(commonKubeconfig.DisableWritingFlag, commonKubeconfig.OverwriteFlag) // DisableWriting xor Overwrite | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Generate input model based on chosen flags | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Filepath: flags.FlagToStringPointer(p, cmd, commonKubeconfig.FilepathFlag), | ||
| Overwrite: flags.FlagToBoolValue(p, cmd, commonKubeconfig.OverwriteFlag), | ||
| SwitchContext: flags.FlagToBoolValue(p, cmd, commonKubeconfig.SwitchContextFlag), | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| model.identifier = id | ||
| // Parse and validate kubeconfig expiration time | ||
| if expString := flags.FlagToStringPointer(p, cmd, commonKubeconfig.ExpirationFlag); expString != nil { | ||
| expTime, err := utils.ConvertToSeconds(*expString) | ||
| if err != nil { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: commonKubeconfig.ExpirationFlag, | ||
| Details: err.Error(), | ||
| } | ||
| } | ||
| if err := commonKubeconfig.ValidateExpiration(&expTime); err != nil { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: commonKubeconfig.ExpirationFlag, | ||
| Details: err.Error(), | ||
| } | ||
| } | ||
| model.Expiration = expTime | ||
| } else { | ||
| // Default expiration is 1 hour | ||
| defaultExp := uint64(commonKubeconfig.ExpirationSecondsDefault) | ||
| model.Expiration = defaultExp | ||
| } | ||
| disableWriting := flags.FlagToBoolValue(p, cmd, commonKubeconfig.DisableWritingFlag) | ||
| model.DisableWriting = disableWriting | ||
| // Make sure to only output if the format is explicitly set | ||
| if disableWriting { | ||
| if globalFlags.OutputFormat == "" || globalFlags.OutputFormat == print.NoneOutputFormat { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: commonKubeconfig.DisableWritingFlag, | ||
| Details: fmt.Sprintf("must be used with --%s", globalflags.OutputFormatFlag), | ||
| } | ||
| } | ||
| if globalFlags.OutputFormat != print.JSONOutputFormat && globalFlags.OutputFormat != print.YAMLOutputFormat { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: globalflags.OutputFormatFlag, | ||
| Details: fmt.Sprintf("valid output formats for this command are: %s", fmt.Sprintf("%s, %s", print.JSONOutputFormat, print.YAMLOutputFormat)), | ||
| } | ||
| } | ||
| } | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Kubeconfig, error) { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp, err := spec.Execute() | ||
| if err != nil { | ||
| return nil, cliErr.NewRequestFailedError(err) | ||
| } | ||
| return resp, nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*createRequestSpec, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| spec := &createRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Region: model.Region, | ||
| Expiration: int64(model.Expiration), // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe | ||
| } | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| spec.InstanceId = model.identifier.Value | ||
| case commonInstance.DisplayNameFlag: | ||
| spec.InstanceName = model.identifier.Value | ||
| default: | ||
| return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) | ||
| } | ||
| // Closure used to decouple the actual SDK call for easier testing | ||
| spec.Execute = func() (*edge.Kubeconfig, error) { | ||
| // Get the waiter from the provider (handles client type casting internally) | ||
| waiter, err := waiterProvider.getKubeconfigWaiter(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return waiter.WaitWithContext(ctx) | ||
| } | ||
| return spec, nil | ||
| } | ||
| // Returns a factory function to create the appropriate waiter based on the input model. | ||
| func getWaiterFactory(ctx context.Context, model *inputModel) (kubeconfigWaiterFactory, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| // The KubeconfigWaitHandlers don't wait for the kubeconfig to be created, but for the instance to be ready to return a kubeconfig. | ||
| // Convert uint64 to int64 to match the API's type. | ||
| var expiration = int64(model.Expiration) // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| factory := func(c *edge.APIClient) kubeconfigWaiter { | ||
| return wait.KubeconfigWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) | ||
| } | ||
| return factory, nil | ||
| case commonInstance.DisplayNameFlag: | ||
| factory := func(c *edge.APIClient) kubeconfigWaiter { | ||
| return wait.KubeconfigByInstanceNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) | ||
| } | ||
| return factory, nil | ||
| default: | ||
| return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) | ||
| } | ||
| } | ||
| // Output result based on the configured output format | ||
| func outputResult(p *print.Printer, outputFormat string, model *inputModel, kubeconfig *edge.Kubeconfig) error { | ||
| // Ensure kubeconfig data is present | ||
| if kubeconfig == nil || kubeconfig.Kubeconfig == nil { | ||
| return fmt.Errorf("no kubeconfig returned from the API") | ||
| } | ||
| kubeconfigMap := *kubeconfig.Kubeconfig | ||
| // Determine output format for terminal or file output | ||
| var format string | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| // JSON if explicitly requested | ||
| format = print.JSONOutputFormat | ||
| case print.YAMLOutputFormat: | ||
| // YAML if explicitly requested | ||
| format = print.YAMLOutputFormat | ||
| default: | ||
| if model.DisableWriting { | ||
| // If not explicitly requested, use JSON as default for terminal output | ||
| format = print.JSONOutputFormat | ||
| } else { | ||
| // If not explicitly requested, use YAML as default for file output | ||
| format = print.YAMLOutputFormat | ||
| } | ||
| } | ||
| // Marshal kubeconfig data based on the determined format | ||
| kubeconfigData, err := marshalKubeconfig(kubeconfigMap, format) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Handle file writing and output | ||
| if !model.DisableWriting { | ||
| // Build options for writing kubeconfig | ||
| opts := commonKubeconfig.NewWriteOptions(). | ||
| WithOverwrite(model.Overwrite). | ||
| WithSwitchContext(model.SwitchContext) | ||
| // Add confirmation callback if not assumeYes | ||
| if !model.AssumeYes { | ||
| confirmFn := func(message string) error { | ||
| return p.PromptForConfirmation(message) | ||
| } | ||
| opts = opts.WithConfirmation(confirmFn) | ||
| } | ||
| path, err := commonKubeconfig.WriteKubeconfig(model.Filepath, kubeconfigData, opts) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Inform the user about the successful write operation | ||
| p.Outputf("Wrote kubeconfig for instance %q to %q.\n", model.identifier.Value, *path) | ||
| if model.SwitchContext { | ||
| p.Outputln("Switched context as requested.") | ||
| } | ||
| } else { | ||
| p.Outputln(kubeconfigData) | ||
| } | ||
| return nil | ||
| } | ||
| // Marshal kubeconfig data to the specified format | ||
| func marshalKubeconfig(kubeconfigMap map[string]interface{}, format string) (string, error) { | ||
| switch format { | ||
| case print.JSONOutputFormat: | ||
| kubeconfigJSON, err := json.MarshalIndent(kubeconfigMap, "", " ") | ||
| if err != nil { | ||
| return "", fmt.Errorf("marshal kubeconfig to JSON: %w", err) | ||
| } | ||
| return string(kubeconfigJSON), nil | ||
| case print.YAMLOutputFormat: | ||
| kubeconfigYAML, err := yaml.MarshalWithOptions(kubeconfigMap, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) | ||
| if err != nil { | ||
| return "", fmt.Errorf("marshal kubeconfig to YAML: %w", err) | ||
| } | ||
| return string(kubeconfigYAML), nil | ||
| default: | ||
| return "", fmt.Errorf("%w: %s", commonErr.NewNoIdentifierError(""), format) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package kubeconfig | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/kubeconfig/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "kubeconfig", | ||
| Short: "Provides functionality for edge kubeconfig.", | ||
| Long: "Provides functionality for STACKIT Edge Cloud (STEC) kubeconfig management.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, params) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { | ||
| cmd.AddCommand(create.NewCmd(params)) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package list | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "testing" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| ) | ||
| // mockExecutable is a mock for the Executable interface | ||
| type mockExecutable struct { | ||
| executeFails bool | ||
| executeResp *edge.PlanList | ||
| } | ||
| func (m *mockExecutable) Execute() (*edge.PlanList, error) { | ||
| if m.executeFails { | ||
| return nil, errors.New("API error") | ||
| } | ||
| if m.executeResp != nil { | ||
| return m.executeResp, nil | ||
| } | ||
| return &edge.PlanList{ | ||
| ValidPlans: &[]edge.Plan{ | ||
| {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, | ||
| {Id: utils.Ptr("plan-2"), Name: utils.Ptr("Premium")}, | ||
| }, | ||
| }, nil | ||
| } | ||
| // mockAPIClient is a mock for the edge.APIClient interface | ||
| type mockAPIClient struct { | ||
| getPlansMock edge.ApiListPlansProjectRequest | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| if m.getPlansMock != nil { | ||
| return m.getPlansMock | ||
| } | ||
| return &mockExecutable{} | ||
| } | ||
| // Unused methods to satisfy the interface | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| limitFlag: "10", | ||
| } | ||
| 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: utils.Ptr(int64(10)), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "list success", | ||
| want: fixtureInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "limit invalid value", | ||
| wantErr: "invalid syntax", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "limit is zero", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "limit is negative", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "-0" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want []edge.Plan | ||
| args args | ||
| }{ | ||
| { | ||
| name: "list success", | ||
| want: []edge.Plan{ | ||
| {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, | ||
| {Id: utils.Ptr("plan-2"), Name: utils.Ptr("Premium")}, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list success with limit", | ||
| want: []edge.Plan{ | ||
| {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = utils.Ptr(int64(1)) | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list success with limit greater than items", | ||
| want: []edge.Plan{ | ||
| {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, | ||
| {Id: utils.Ptr("plan-2"), Name: utils.Ptr("Premium")}, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = utils.Ptr(int64(5)) | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list success with no items", | ||
| want: []edge.Plan{}, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| getPlansMock: &mockExecutable{ | ||
| executeResp: &edge.PlanList{ValidPlans: &[]edge.Plan{}}, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "list API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| getPlansMock: &mockExecutable{ | ||
| executeFails: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := run(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| plans []edge.Plan | ||
| projectLabel string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| args args | ||
| }{ | ||
| { | ||
| name: "output json", | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.JSONOutputFormat | ||
| }), | ||
| plans: []edge.Plan{ | ||
| {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, | ||
| }, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| { | ||
| name: "output yaml", | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| }), | ||
| plans: []edge.Plan{ | ||
| {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, | ||
| }, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| { | ||
| name: "output default with plans", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| plans: []edge.Plan{ | ||
| { | ||
| Id: utils.Ptr("plan-1"), | ||
| Name: utils.Ptr("Standard"), | ||
| Description: utils.Ptr("Standard plan description"), | ||
| }, | ||
| { | ||
| Id: utils.Ptr("plan-2"), | ||
| Name: utils.Ptr("Premium"), | ||
| Description: utils.Ptr("Premium plan description"), | ||
| }, | ||
| }, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| { | ||
| name: "output default with no plans", | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| plans: []edge.Plan{}, | ||
| projectLabel: "test-project", | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(&types.CmdParams{Printer: p}) | ||
| err := outputResult(p, tt.args.model.OutputFormat, tt.args.projectLabel, tt.args.plans) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want *listRequestSpec | ||
| args args | ||
| }{ | ||
| { | ||
| name: "success", | ||
| want: &listRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.Limit = nil | ||
| }), | ||
| client: &mockAPIClient{ | ||
| getPlansMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "success with limit", | ||
| want: &listRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Limit: utils.Ptr(int64(10)), | ||
| }, | ||
| args: args{ | ||
| model: fixtureInputModel(), | ||
| client: &mockAPIClient{ | ||
| getPlansMock: &mockExecutable{}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(listRequestSpec{}, "Execute")) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package list | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| // User input struct for the command | ||
| const ( | ||
| limitFlag = "limit" | ||
| ) | ||
| // Struct to model user input (arguments and/or flags) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| Limit *int64 | ||
| } | ||
| // listRequestSpec captures the details of the request for testing. | ||
| type listRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Limit *int64 | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() (*edge.PlanList, error) | ||
| } | ||
| // Command constructor | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists available edge service plans", | ||
| Long: "Lists available STACKIT Edge Cloud (STEC) service plans of a project", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Lists all edge plans for a given project`, | ||
| `$ stackit beta edge-cloud plan list`), | ||
| examples.NewExample( | ||
| `Lists all edge plans for a given project and limits the output to two plans`, | ||
| fmt.Sprintf(`$ stackit beta edge-cloud plan list --%s 2`, limitFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) | ||
| if err != nil { | ||
| params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) | ||
| // If project label can't be determined, fall back to project ID | ||
| projectLabel = model.ProjectId | ||
| } | ||
| // Call API | ||
| resp, err := run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) | ||
| if limit != nil && *limit < 1 { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: limitFlag, | ||
| Details: "must be greater than 0", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| Limit: limit, | ||
| } | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) ([]edge.Plan, error) { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp, err := spec.Execute() | ||
| if err != nil { | ||
| return nil, cliErr.NewRequestFailedError(err) | ||
| } | ||
| if resp == nil { | ||
| return nil, fmt.Errorf("list plans: empty response from API") | ||
| } | ||
| if resp.ValidPlans == nil { | ||
| return nil, fmt.Errorf("list plans: valid plans missing in response") | ||
| } | ||
| plans := *resp.ValidPlans | ||
| // Truncate output | ||
| if spec.Limit != nil && len(plans) > int(*spec.Limit) { | ||
| plans = plans[:*spec.Limit] | ||
| } | ||
| return plans, nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*listRequestSpec, error) { | ||
| req := apiClient.ListPlansProject(ctx, model.ProjectId) | ||
| return &listRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Limit: model.Limit, | ||
| Execute: req.Execute, | ||
| }, nil | ||
| } | ||
| // Output result based on the configured output format | ||
| func outputResult(p *print.Printer, outputFormat, projectLabel string, plans []edge.Plan) error { | ||
| return p.OutputResult(outputFormat, plans, func() error { | ||
| // No plans found for project | ||
| if len(plans) == 0 { | ||
| p.Outputf("No plans found for project %q\n", projectLabel) | ||
| return nil | ||
| } | ||
| // Display plans found for project in a table | ||
| table := tables.NewTable() | ||
| // List: only output the most important fields. Be sure to filter for any non-required fields. | ||
| table.SetHeader("ID", "NAME", "DESCRIPTION", "MAX EDGE HOSTS") | ||
| for i := range plans { | ||
| plan := plans[i] | ||
| table.AddRow( | ||
| utils.PtrString(plan.Id), | ||
| utils.PtrString(plan.Name), | ||
| utils.PtrString(plan.Description), | ||
| utils.PtrString(plan.MaxEdgeHosts)) | ||
| } | ||
| err := table.Display(p) | ||
| if err != nil { | ||
| return fmt.Errorf("render table: %w", err) | ||
| } | ||
| return nil | ||
| }) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package plans | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/plans/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "plans", | ||
| Short: "Provides functionality for edge service plans.", | ||
| Long: "Provides functionality for STACKIT Edge Cloud (STEC) service plan management.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, params) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { | ||
| cmd.AddCommand(list.NewCmd(params)) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package create | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "testing" | ||
| "github.com/google/uuid" | ||
| "github.com/spf13/cobra" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| type testCtxKey struct{} | ||
| var ( | ||
| testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| testProjectId = uuid.NewString() | ||
| testRegion = "eu01" | ||
| testInstanceId = "instance" | ||
| testDisplayName = "test" | ||
| testExpiration = "1h" | ||
| ) | ||
| // mockTokenWaiter is a mock for the tokenWaiter interface | ||
| type mockTokenWaiter struct { | ||
| waitFails bool | ||
| waitNotFound bool | ||
| waitResp *edge.Token | ||
| } | ||
| func (m *mockTokenWaiter) WaitWithContext(_ context.Context) (*edge.Token, error) { | ||
| if m.waitFails { | ||
| return nil, errors.New("wait error") | ||
| } | ||
| if m.waitNotFound { | ||
| return nil, &oapierror.GenericOpenAPIError{ | ||
| StatusCode: http.StatusNotFound, | ||
| } | ||
| } | ||
| if m.waitResp != nil { | ||
| return m.waitResp, nil | ||
| } | ||
| // Default token response | ||
| tokenString := "test-token-string" | ||
| return &edge.Token{ | ||
| Token: &tokenString, | ||
| }, nil | ||
| } | ||
| // testWaiterFactoryProvider is a test implementation that returns mock waiters. | ||
| type testWaiterFactoryProvider struct { | ||
| waiter tokenWaiter | ||
| } | ||
| func (t *testWaiterFactoryProvider) getTokenWaiter(_ context.Context, model *inputModel, _ client.APIClient) (tokenWaiter, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, &commonErr.NoIdentifierError{} | ||
| } | ||
| // Validate identifier like the real implementation | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag: | ||
| // Return our mock waiter directly, bypassing the client type casting issue | ||
| return t.waiter, nil | ||
| default: | ||
| return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) | ||
| } | ||
| } | ||
| // mockAPIClient is a mock for the edge.APIClient interface | ||
| type mockAPIClient struct{} | ||
| // Unused methods to satisfy the interface | ||
| func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) PostInstances(_ context.Context, _, _ string) edge.ApiPostInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetInstances(_ context.Context, _, _ string) edge.ApiGetInstancesRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { | ||
| return nil | ||
| } | ||
| func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { | ||
| return nil | ||
| } | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| globalflags.ProjectIdFlag: testProjectId, | ||
| globalflags.RegionFlag: testRegion, | ||
| commonInstance.InstanceIdFlag: testInstanceId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(false, mods...) | ||
| } | ||
| func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| return fixtureInputModel(true, mods...) | ||
| } | ||
| func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| ProjectId: testProjectId, | ||
| Region: testRegion, | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| }, | ||
| Expiration: uint64(commonKubeconfig.ExpirationSecondsDefault), // Default 1 hour | ||
| } | ||
| if useName { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.DisplayNameFlag, | ||
| Value: testDisplayName, | ||
| } | ||
| } else { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: commonInstance.InstanceIdFlag, | ||
| Value: testInstanceId, | ||
| } | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| type args struct { | ||
| flags map[string]string | ||
| cmpOpts []testUtils.ValueComparisonOption | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| want *inputModel | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: fixtureByIdInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: fixtureByNameInputModel(), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "with expiration", | ||
| want: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.Expiration = uint64(3600) | ||
| }), | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = testExpiration | ||
| }), | ||
| cmpOpts: []testUtils.ValueComparisonOption{ | ||
| testUtils.WithAllowUnexported(inputModel{}), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by id and name", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.DisplayNameFlag] = testDisplayName | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "no flag values", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: map[string]string{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id missing", | ||
| wantErr: &cliErr.ProjectIdError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, globalflags.ProjectIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id empty", | ||
| wantErr: "value cannot be empty", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "project id invalid", | ||
| wantErr: "invalid UUID length", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id missing", | ||
| wantErr: true, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id empty", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id too long", | ||
| wantErr: &cliErr.FlagValidationError{}, | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance id too short", | ||
| wantErr: "id is too short", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonInstance.InstanceIdFlag] = "id" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too short", | ||
| wantErr: "name is too short", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foo" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "name too long", | ||
| wantErr: "name is too long", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, commonInstance.InstanceIdFlag) | ||
| flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "invalid expiration format", | ||
| wantErr: "invalid time string format", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = "invalid" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "expiration too short", | ||
| wantErr: "expiration is too small", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = "1s" | ||
| }), | ||
| }, | ||
| }, | ||
| { | ||
| name: "expiration too long", | ||
| wantErr: "expiration is too large", | ||
| args: args{ | ||
| flags: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[commonKubeconfig.ExpirationFlag] = "13M" | ||
| }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| caseOpts := []testUtils.ParseInputCaseOption{} | ||
| if len(tt.args.cmpOpts) > 0 { | ||
| caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) | ||
| } | ||
| testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.args.flags, | ||
| WantModel: tt.want, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: NewCmd, | ||
| ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { | ||
| return parseInput(p, cmd) | ||
| }, | ||
| }, caseOpts...) | ||
| }) | ||
| } | ||
| } | ||
| func TestRun(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| waiter tokenWaiter | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| wantToken bool | ||
| args args | ||
| }{ | ||
| { | ||
| name: "run by id success", | ||
| wantToken: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockTokenWaiter{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "run by name success", | ||
| wantToken: true, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockTokenWaiter{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockTokenWaiter{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "instance not found error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockTokenWaiter{waitNotFound: true}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get token by id API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockTokenWaiter{waitFails: true}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "get token by name API error", | ||
| wantErr: &cliErr.RequestFailedError{}, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockTokenWaiter{waitFails: true}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "identifier invalid", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: "unknown-flag", | ||
| Value: "some-value", | ||
| } | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| waiter: &mockTokenWaiter{}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| // Override production waiterProvider package level variable for testing | ||
| prodWaiterProvider := waiterProvider | ||
| waiterProvider = &testWaiterFactoryProvider{waiter: tt.args.waiter} | ||
| defer func() { waiterProvider = prodWaiterProvider }() | ||
| got, err := run(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.wantToken && got == nil { | ||
| t.Fatal("expected non-nil token") | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| client client.APIClient | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr error | ||
| want *createRequestSpec | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: &createRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceId: testInstanceId, | ||
| Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), | ||
| }, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: &createRequestSpec{ | ||
| ProjectID: testProjectId, | ||
| Region: testRegion, | ||
| InstanceName: testDisplayName, | ||
| Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), | ||
| }, | ||
| args: args{ | ||
| model: fixtureByNameInputModel(), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| { | ||
| name: "identifier invalid", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{ | ||
| model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = &commonValidation.Identifier{ | ||
| Flag: "unknown-flag", | ||
| Value: "some-value", | ||
| } | ||
| }), | ||
| client: &mockAPIClient{}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := buildRequest(testCtx, tt.args.model, tt.args.client) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(createRequestSpec{}, "Execute")) | ||
| }) | ||
| } | ||
| } | ||
| func TestGetWaiterFactory(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| want bool | ||
| wantErr error | ||
| args args | ||
| }{ | ||
| { | ||
| name: "by id", | ||
| want: true, | ||
| args: args{model: fixtureByIdInputModel()}, | ||
| }, | ||
| { | ||
| name: "by name", | ||
| want: true, | ||
| args: args{model: fixtureByNameInputModel()}, | ||
| }, | ||
| { | ||
| name: "no id or name", | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| args: args{model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier = nil | ||
| })}, | ||
| }, | ||
| { | ||
| name: "unknown identifier", | ||
| wantErr: &commonErr.InvalidIdentifierError{}, | ||
| args: args{model: fixtureInputModel(false, func(model *inputModel) { | ||
| model.identifier.Flag = "unknown" | ||
| })}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got, err := getWaiterFactory(testCtx, tt.args.model) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.want && got == nil { | ||
| t.Fatal("expected non-nil waiter factory") | ||
| } | ||
| if !tt.want && got != nil { | ||
| t.Fatal("expected nil waiter factory") | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestOutputResult(t *testing.T) { | ||
| type args struct { | ||
| model *inputModel | ||
| token *edge.Token | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| wantErr any | ||
| args args | ||
| }{ | ||
| { | ||
| name: "default output format", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| token: &edge.Token{ | ||
| Token: func() *string { s := "test-token"; return &s }(), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "JSON output format", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.JSONOutputFormat | ||
| }), | ||
| token: &edge.Token{ | ||
| Token: func() *string { s := "test-token"; return &s }(), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "YAML output format", | ||
| args: args{ | ||
| model: fixtureByIdInputModel(func(model *inputModel) { | ||
| model.OutputFormat = print.YAMLOutputFormat | ||
| }), | ||
| token: &edge.Token{ | ||
| Token: func() *string { s := "test-token"; return &s }(), | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "nil token", | ||
| wantErr: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| token: nil, | ||
| }, | ||
| }, | ||
| { | ||
| name: "nil token string", | ||
| wantErr: true, | ||
| args: args{ | ||
| model: fixtureByIdInputModel(), | ||
| token: &edge.Token{Token: nil}, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| p.Cmd = NewCmd(&types.CmdParams{Printer: p}) | ||
| err := outputResult(p, tt.args.model.OutputFormat, tt.args.token) | ||
| testUtils.AssertError(t, err, tt.wantErr) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package create | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| cliErr "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/edge/client" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" | ||
| commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| identifier *commonValidation.Identifier | ||
| Expiration uint64 | ||
| } | ||
| // createRequestSpec captures the details of the request for testing. | ||
| type createRequestSpec struct { | ||
| // Exported fields allow tests to inspect the request inputs | ||
| ProjectID string | ||
| Region string | ||
| InstanceId string | ||
| InstanceName string | ||
| Expiration int64 | ||
| // Execute is a closure that wraps the actual SDK call | ||
| Execute func() (*edge.Token, error) | ||
| } | ||
| // OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. | ||
| // tokenWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. | ||
| type tokenWaiter interface { | ||
| WaitWithContext(context.Context) (*edge.Token, error) | ||
| } | ||
| // A function that creates a token waiter | ||
| type tokenWaiterFactory = func(client *edge.APIClient) tokenWaiter | ||
| // waiterFactoryProvider is an interface that provides token waiters so we can inject different impl. while testing. | ||
| type waiterFactoryProvider interface { | ||
| getTokenWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (tokenWaiter, error) | ||
| } | ||
| // productionWaiterFactoryProvider is the real implementation used in production. | ||
| // It handles the concrete client type casting required by the SDK's wait handlers. | ||
| type productionWaiterFactoryProvider struct{} | ||
| func (p *productionWaiterFactoryProvider) getTokenWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (tokenWaiter, error) { | ||
| waiterFactory, err := getWaiterFactory(ctx, model) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| // The waiter handler needs a concrete client type. We can safely cast here as the real implementation will always match. | ||
| edgeClient, ok := apiClient.(*edge.APIClient) | ||
| if !ok { | ||
| return nil, cliErr.NewBuildRequestError("failed to configure API client", nil) | ||
| } | ||
| return waiterFactory(edgeClient), nil | ||
| } | ||
| // waiterProvider is the package-level variable used to get the waiter. | ||
| // It is initialized with the production implementation but can be overridden in tests. | ||
| var waiterProvider waiterFactoryProvider = &productionWaiterFactoryProvider{} | ||
| // Command constructor | ||
| // Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags | ||
| // instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname | ||
| // is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we | ||
| // won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "create", | ||
| Short: "Creates a token for an edge instance", | ||
| Long: fmt.Sprintf("%s\n\n%s\n%s", | ||
| "Creates a token for a STACKIT Edge Cloud (STEC) instance.", | ||
| fmt.Sprintf("An expiration time can be set for the token. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is %d seconds.", commonKubeconfig.ExpirationSecondsDefault), | ||
| "Note: the format for the duration is <value><unit>, e.g. 30d for 30 days. You may not combine units."), | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Create a token for the edge instance with %s "xxx".`, commonInstance.InstanceIdFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud token create --%s "xxx"`, commonInstance.InstanceIdFlag)), | ||
| examples.NewExample( | ||
| fmt.Sprintf(`Create a token for the edge instance with %s "xxx". The token will be valid for one day.`, commonInstance.DisplayNameFlag), | ||
| fmt.Sprintf(`$ stackit beta edge-cloud token create --%s "xxx" --expiration 1d`, commonInstance.DisplayNameFlag)), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| // Parse user input (arguments and/or flags) | ||
| model, err := parseInput(params.Printer, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if model.Async { | ||
| return fmt.Errorf("async mode is not supported for token create") | ||
| } | ||
| // Call API | ||
| resp, err := run(ctx, model, apiClient) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Handle output to printer | ||
| return outputResult(params.Printer, model.OutputFormat, resp) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) | ||
| cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) | ||
| cmd.Flags().StringP(commonKubeconfig.ExpirationFlag, commonKubeconfig.ExpirationShorthand, "", commonKubeconfig.ExpirationUsage) | ||
| identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} | ||
| cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName | ||
| cmd.MarkFlagsOneRequired(identifierFlags...) | ||
| } | ||
| // Parse user input (arguments and/or flags) | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &cliErr.ProjectIdError{} | ||
| } | ||
| // Generate input model based on chosen flags | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| } | ||
| // Parse and validate user input then add it to the model | ||
| id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| model.identifier = id | ||
| // Parse and validate kubeconfig expiration time | ||
| if expString := flags.FlagToStringPointer(p, cmd, commonKubeconfig.ExpirationFlag); expString != nil { | ||
| expTime, err := utils.ConvertToSeconds(*expString) | ||
| if err != nil { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: commonKubeconfig.ExpirationFlag, | ||
| Details: err.Error(), | ||
| } | ||
| } | ||
| if err := commonKubeconfig.ValidateExpiration(&expTime); err != nil { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: commonKubeconfig.ExpirationFlag, | ||
| Details: err.Error(), | ||
| } | ||
| } | ||
| model.Expiration = expTime | ||
| } else { | ||
| // Default expiration is 1 hour | ||
| defaultExp := uint64(commonKubeconfig.ExpirationSecondsDefault) | ||
| model.Expiration = defaultExp | ||
| } | ||
| // Make sure to only output if the format is not none | ||
| if globalFlags.OutputFormat == print.NoneOutputFormat { | ||
| return nil, &cliErr.FlagValidationError{ | ||
| Flag: globalflags.OutputFormatFlag, | ||
| Details: fmt.Sprintf("valid formats for this command are: %s", fmt.Sprintf("%s, %s, %s", print.PrettyOutputFormat, print.JSONOutputFormat, print.YAMLOutputFormat)), | ||
| } | ||
| } | ||
| // Log the parsed model if --verbosity is set to debug | ||
| p.DebugInputModel(model) | ||
| return &model, nil | ||
| } | ||
| // Run is the main execution function used by the command runner. | ||
| // It is decoupled from TTY output to have the ability to mock the API client during testing. | ||
| func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Token, error) { | ||
| spec, err := buildRequest(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp, err := spec.Execute() | ||
| if err != nil { | ||
| return nil, cliErr.NewRequestFailedError(err) | ||
| } | ||
| return resp, nil | ||
| } | ||
| // buildRequest constructs the spec that can be tested. | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*createRequestSpec, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| spec := &createRequestSpec{ | ||
| ProjectID: model.ProjectId, | ||
| Region: model.Region, | ||
| Expiration: int64(model.Expiration), // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe | ||
| } | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| spec.InstanceId = model.identifier.Value | ||
| case commonInstance.DisplayNameFlag: | ||
| spec.InstanceName = model.identifier.Value | ||
| default: | ||
| return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) | ||
| } | ||
| // Closure used to decouple the actual SDK call for easier testing | ||
| spec.Execute = func() (*edge.Token, error) { | ||
| // Get the waiter from the provider (handles client type casting internally) | ||
| waiter, err := waiterProvider.getTokenWaiter(ctx, model, apiClient) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return waiter.WaitWithContext(ctx) | ||
| } | ||
| return spec, nil | ||
| } | ||
| // Returns a factory function to create the appropriate waiter based on the input model. | ||
| func getWaiterFactory(ctx context.Context, model *inputModel) (tokenWaiterFactory, error) { | ||
| if model == nil || model.identifier == nil { | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| // The tokenWaitHandlers don't wait for the token to be created, but for the instance to be ready to return a token. | ||
| // Convert uint64 to int64 to match the API's type. | ||
| var expiration = int64(model.Expiration) // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe | ||
| switch model.identifier.Flag { | ||
| case commonInstance.InstanceIdFlag: | ||
| factory := func(c *edge.APIClient) tokenWaiter { | ||
| return wait.TokenWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) | ||
| } | ||
| return factory, nil | ||
| case commonInstance.DisplayNameFlag: | ||
| factory := func(c *edge.APIClient) tokenWaiter { | ||
| return wait.TokenByInstanceNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) | ||
| } | ||
| return factory, nil | ||
| default: | ||
| return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) | ||
| } | ||
| } | ||
| // Output result based on the configured output format | ||
| func outputResult(p *print.Printer, outputFormat string, token *edge.Token) error { | ||
| if token == nil || token.Token == nil { | ||
| // This is only to prevent nil pointer deref. | ||
| // As long as the API behaves as defined by it's spec, instance can not be empty (HTTP 200 with an empty body) | ||
| return fmt.Errorf("no token returned from the API") | ||
| } | ||
| tokenString := *token.Token | ||
| return p.OutputResult(outputFormat, token, func() error { | ||
| p.Outputln(tokenString) | ||
| return nil | ||
| }) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package token | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/token/create" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(params *types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "token", | ||
| Short: "Provides functionality for edge service token.", | ||
| Long: "Provides functionality for STACKIT Edge Cloud (STEC) token management.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, params) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { | ||
| cmd.AddCommand(create.NewCmd(params)) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package client | ||
| import ( | ||
| "context" | ||
| "github.com/spf13/viper" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/config" | ||
| genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/edge" | ||
| ) | ||
| // APIClient is an interface that consolidates all client functionality to allow for mocking of the API client during testing. | ||
| type APIClient interface { | ||
| PostInstances(ctx context.Context, projectId, region string) edge.ApiPostInstancesRequest | ||
| DeleteInstance(ctx context.Context, projectId, region, instanceId string) edge.ApiDeleteInstanceRequest | ||
| DeleteInstanceByName(ctx context.Context, projectId, region, instanceName string) edge.ApiDeleteInstanceByNameRequest | ||
| GetInstance(ctx context.Context, projectId, region, instanceId string) edge.ApiGetInstanceRequest | ||
| GetInstanceByName(ctx context.Context, projectId, region, instanceName string) edge.ApiGetInstanceByNameRequest | ||
| GetInstances(ctx context.Context, projectId, region string) edge.ApiGetInstancesRequest | ||
| UpdateInstance(ctx context.Context, projectId, region, instanceId string) edge.ApiUpdateInstanceRequest | ||
| UpdateInstanceByName(ctx context.Context, projectId, region, instanceName string) edge.ApiUpdateInstanceByNameRequest | ||
| GetKubeconfigByInstanceId(ctx context.Context, projectId, region, instanceId string) edge.ApiGetKubeconfigByInstanceIdRequest | ||
| GetKubeconfigByInstanceName(ctx context.Context, projectId, region, instanceName string) edge.ApiGetKubeconfigByInstanceNameRequest | ||
| GetTokenByInstanceId(ctx context.Context, projectId, region, instanceId string) edge.ApiGetTokenByInstanceIdRequest | ||
| GetTokenByInstanceName(ctx context.Context, projectId, region, instanceName string) edge.ApiGetTokenByInstanceNameRequest | ||
| ListPlansProject(ctx context.Context, projectId string) edge.ApiListPlansProjectRequest | ||
| } | ||
| // ConfigureClient configures and returns a new API client for the Edge service. | ||
| func ConfigureClient(p *print.Printer, cliVersion string) (APIClient, error) { | ||
| return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.EdgeCustomEndpointKey), false, edge.NewAPIClient) | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| // Unit tests for package error | ||
| package error | ||
| import ( | ||
| "testing" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| ) | ||
| func TestNoIdentifierError(t *testing.T) { | ||
| type args struct { | ||
| operation string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| operation: "", | ||
| }, | ||
| want: "no identifier provided", | ||
| }, | ||
| { | ||
| name: "with operation", | ||
| args: args{ | ||
| operation: "create", | ||
| }, | ||
| want: "no identifier provided for create", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := (&NoIdentifierError{Operation: tt.args.operation}).Error() | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestInvalidIdentifierError(t *testing.T) { | ||
| type args struct { | ||
| id string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| id: "", | ||
| }, | ||
| want: "unsupported identifier provided", | ||
| }, | ||
| { | ||
| name: "with identifier", | ||
| args: args{ | ||
| id: "x-123", | ||
| }, | ||
| want: "unsupported identifier provided: x-123", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := (&InvalidIdentifierError{Identifier: tt.args.id}).Error() | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestInstanceExistsError(t *testing.T) { | ||
| type args struct { | ||
| name string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{name: ""}, | ||
| want: "instance already exists"}, | ||
| { | ||
| name: "with display name", | ||
| args: args{name: "my-inst"}, | ||
| want: "instance already exists: my-inst", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := (&InstanceExistsError{DisplayName: tt.args.name}).Error() | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestNoInstanceError(t *testing.T) { | ||
| type args struct { | ||
| ctx string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| ctx: "", | ||
| }, | ||
| want: "no instance provided", | ||
| }, | ||
| { | ||
| name: "with context", | ||
| args: args{ | ||
| ctx: "in project", | ||
| }, | ||
| want: "no instance provided in project", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := (&NoInstanceError{Context: tt.args.ctx}).Error() | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestConstructorsReturnExpected(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| got any | ||
| want any | ||
| }{ | ||
| { | ||
| name: "NoIdentifier operation", | ||
| got: NewNoIdentifierError("op").Operation, | ||
| want: "op", | ||
| }, | ||
| { | ||
| name: "InvalidIdentifier identifier", | ||
| got: NewInvalidIdentifierError("id").Identifier, | ||
| want: "id", | ||
| }, | ||
| { | ||
| name: "InstanceExists displayName", | ||
| got: NewInstanceExistsError("name").DisplayName, | ||
| want: "name", | ||
| }, | ||
| { | ||
| name: "NoInstance context", | ||
| got: NewNoInstanceError("ctx").Context, | ||
| want: "ctx", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| wantErr, wantIsErr := tt.want.(error) | ||
| gotErr, gotIsErr := tt.got.(error) | ||
| if wantIsErr { | ||
| if !gotIsErr { | ||
| t.Fatalf("expected error but got %T", tt.got) | ||
| } | ||
| testUtils.AssertError(t, gotErr, wantErr) | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, tt.got, tt.want) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| // Package error provides custom error types for STACKIT Edge Cloud operations. | ||
| // | ||
| // This package defines structured error types that provide better error handling | ||
| // and type checking compared to simple string errors. Each error type can carry | ||
| // additional context and implements the standard error interface. | ||
| package error | ||
| import ( | ||
| "fmt" | ||
| ) | ||
| // NoIdentifierError indicates that no identifier was provided when one was required. | ||
| type NoIdentifierError struct { | ||
| Operation string // Optional: which operation failed | ||
| } | ||
| func (e *NoIdentifierError) Error() string { | ||
| if e.Operation != "" { | ||
| return fmt.Sprintf("no identifier provided for %s", e.Operation) | ||
| } | ||
| return "no identifier provided" | ||
| } | ||
| // InvalidIdentifierError indicates that an unsupported identifier was provided. | ||
| type InvalidIdentifierError struct { | ||
| Identifier string // The invalid identifier that was provided | ||
| } | ||
| func (e *InvalidIdentifierError) Error() string { | ||
| if e.Identifier != "" { | ||
| return fmt.Sprintf("unsupported identifier provided: %s", e.Identifier) | ||
| } | ||
| return "unsupported identifier provided" | ||
| } | ||
| // InstanceExistsError indicates that a specific instance already exists. | ||
| type InstanceExistsError struct { | ||
| DisplayName string // Optional: the display name that was searched for | ||
| } | ||
| func (e *InstanceExistsError) Error() string { | ||
| if e.DisplayName != "" { | ||
| return fmt.Sprintf("instance already exists: %s", e.DisplayName) | ||
| } | ||
| return "instance already exists" | ||
| } | ||
| // NoInstanceError indicates that no instance was provided in a context where one was expected. | ||
| type NoInstanceError struct { | ||
| Context string // Optional: context where no instance was found (e.g., "in response", "in project") | ||
| } | ||
| func (e *NoInstanceError) Error() string { | ||
| if e.Context != "" { | ||
| return fmt.Sprintf("no instance provided %s", e.Context) | ||
| } | ||
| return "no instance provided" | ||
| } | ||
| // NewNoIdentifierError creates a new NoIdentifierError with optional context. | ||
| func NewNoIdentifierError(operation string) *NoIdentifierError { | ||
| return &NoIdentifierError{Operation: operation} | ||
| } | ||
| // NewInvalidIdentifierError creates a new InvalidIdentifierError with the provided identifier. | ||
| func NewInvalidIdentifierError(identifier string) *InvalidIdentifierError { | ||
| return &InvalidIdentifierError{ | ||
| Identifier: identifier, | ||
| } | ||
| } | ||
| // NewInstanceExistsError creates a new InstanceExistsError with optional instance details. | ||
| func NewInstanceExistsError(displayName string) *InstanceExistsError { | ||
| return &InstanceExistsError{ | ||
| DisplayName: displayName, | ||
| } | ||
| } | ||
| // NewNoInstanceError creates a new NoInstanceError with optional context. | ||
| func NewNoInstanceError(context string) *NoInstanceError { | ||
| return &NoInstanceError{Context: context} | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package instance | ||
| import ( | ||
| "fmt" | ||
| "strings" | ||
| "testing" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| ) | ||
| func TestValidateDisplayName(t *testing.T) { | ||
| type args struct { | ||
| displayName *string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| want error | ||
| }{ | ||
| // Valid cases | ||
| { | ||
| name: "valid minimum length", | ||
| args: &args{displayName: utils.Ptr("test")}, | ||
| }, | ||
| { | ||
| name: "valid maximum length", | ||
| args: &args{displayName: utils.Ptr("testname")}, | ||
| }, | ||
| { | ||
| name: "valid with hyphens", | ||
| args: &args{displayName: utils.Ptr("test-app")}, | ||
| }, | ||
| { | ||
| name: "valid with numbers", | ||
| args: &args{displayName: utils.Ptr("test123")}, | ||
| }, | ||
| { | ||
| name: "valid starting with letter", | ||
| args: &args{displayName: utils.Ptr("a-test")}, | ||
| }, | ||
| // Error cases - nil pointer | ||
| { | ||
| name: "nil display name", | ||
| args: &args{displayName: nil}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", DisplayNameFlag), | ||
| }, | ||
| }, | ||
| // Error cases - length validation | ||
| { | ||
| name: "too short", | ||
| args: &args{displayName: utils.Ptr("abc")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", DisplayNameFlag, displayNameMinimumChars), | ||
| }, | ||
| }, | ||
| { | ||
| name: "too long", | ||
| args: &args{displayName: utils.Ptr("verylongname")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DisplayNameFlag, displayNameMaximumChars), | ||
| }, | ||
| }, | ||
| // Error cases - regex validation | ||
| { | ||
| name: "starts with number", | ||
| args: &args{displayName: utils.Ptr("1test")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), | ||
| }, | ||
| }, | ||
| { | ||
| name: "starts with hyphen", | ||
| args: &args{displayName: utils.Ptr("-test")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), | ||
| }, | ||
| }, | ||
| { | ||
| name: "ends with hyphen", | ||
| args: &args{displayName: utils.Ptr("test-")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), | ||
| }, | ||
| }, | ||
| { | ||
| name: "contains uppercase", | ||
| args: &args{displayName: utils.Ptr("Test")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), | ||
| }, | ||
| }, | ||
| { | ||
| name: "contains special characters", | ||
| args: &args{displayName: utils.Ptr("test@")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := ValidateDisplayName(tt.args.displayName) | ||
| testUtils.AssertError(t, err, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestValidatePlanId(t *testing.T) { | ||
| type args struct { | ||
| planId *string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| want error | ||
| }{ | ||
| // Valid cases | ||
| { | ||
| name: "valid UUID v4", | ||
| args: &args{planId: utils.Ptr("550e8400-e29b-41d4-a716-446655440000")}, | ||
| }, | ||
| { | ||
| name: "valid UUID lowercase", | ||
| args: &args{planId: utils.Ptr("6ba7b810-9dad-11d1-80b4-00c04fd430c8")}, | ||
| }, | ||
| { | ||
| name: "valid UUID uppercase", | ||
| args: &args{planId: utils.Ptr("6BA7B810-9DAD-11D1-80B4-00C04FD430C8")}, | ||
| }, | ||
| { | ||
| name: "valid UUID without hyphens", | ||
| args: &args{planId: utils.Ptr("550e8400e29b41d4a716446655440000")}, | ||
| }, | ||
| // Error cases - nil pointer | ||
| { | ||
| name: "nil plan id", | ||
| args: &args{planId: nil}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: PlanIdFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", PlanIdFlag), | ||
| }, | ||
| }, | ||
| // Error cases - invalid UUID format | ||
| { | ||
| name: "invalid UUID - too short", | ||
| args: &args{planId: utils.Ptr("550e8400-e29b-41d4-a716")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: PlanIdFlag, | ||
| Details: fmt.Sprintf("%s is not a valid UUID: parse 550e8400-e29b-41d4-a716 as UUID: invalid UUID length: 23", PlanIdFlag), | ||
| }, | ||
| }, | ||
| { | ||
| name: "invalid UUID - invalid characters", | ||
| args: &args{planId: utils.Ptr("550e8400-e29b-41d4-a716-44665544000g")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: PlanIdFlag, | ||
| Details: fmt.Sprintf("%s is not a valid UUID: parse 550e8400-e29b-41d4-a716-44665544000g as UUID: invalid UUID format", PlanIdFlag), | ||
| }, | ||
| }, | ||
| { | ||
| name: "not a UUID", | ||
| args: &args{planId: utils.Ptr("not-a-uuid")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: PlanIdFlag, | ||
| Details: fmt.Sprintf("%s is not a valid UUID: parse not-a-uuid as UUID: invalid UUID length: 10", PlanIdFlag), | ||
| }, | ||
| }, | ||
| { | ||
| name: "empty string", | ||
| args: &args{planId: utils.Ptr("")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: PlanIdFlag, | ||
| Details: fmt.Sprintf("%s is not a valid UUID: parse as UUID: invalid UUID length: 0", PlanIdFlag), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := ValidatePlanId(tt.args.planId) | ||
| testUtils.AssertError(t, err, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestValidateDescription(t *testing.T) { | ||
| type args struct { | ||
| description string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| want error | ||
| }{ | ||
| // Valid cases | ||
| { | ||
| name: "empty description", | ||
| args: &args{description: ""}, | ||
| }, | ||
| { | ||
| name: "short description", | ||
| args: &args{description: "A short description"}, | ||
| }, | ||
| { | ||
| name: "description at maximum length", | ||
| args: &args{description: strings.Repeat("a", descriptionMaxLength)}, | ||
| }, | ||
| { | ||
| name: "description with special characters", | ||
| args: &args{description: "Description with special chars: !@#$%^&*()"}, | ||
| }, | ||
| { | ||
| name: "description with unicode", | ||
| args: &args{description: "Description with unicode: 你好世界 🌍"}, | ||
| }, | ||
| // Error cases | ||
| { | ||
| name: "description too long", | ||
| args: &args{description: strings.Repeat("a", descriptionMaxLength+1)}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DescriptionFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DescriptionFlag, descriptionMaxLength), | ||
| }, | ||
| }, | ||
| { | ||
| name: "description way too long", | ||
| args: &args{description: strings.Repeat("a", descriptionMaxLength+100)}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: DescriptionFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DescriptionFlag, descriptionMaxLength), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := ValidateDescription(tt.args.description) | ||
| testUtils.AssertError(t, err, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestValidateInstanceId(t *testing.T) { | ||
| type args struct { | ||
| instanceId *string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| want error | ||
| }{ | ||
| // Valid cases | ||
| { | ||
| name: "valid instance id at minimum length", | ||
| args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMinLength))}, | ||
| }, | ||
| { | ||
| name: "valid instance id at maximum length", | ||
| args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMaxLength))}, | ||
| }, | ||
| { | ||
| name: "valid instance id with mixed characters", | ||
| args: &args{instanceId: utils.Ptr("test-instance")}, | ||
| }, | ||
| // Error cases - nil pointer | ||
| { | ||
| name: "nil instance id", | ||
| args: &args{instanceId: nil}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), | ||
| }, | ||
| }, | ||
| // Error cases - empty string | ||
| { | ||
| name: "empty string", | ||
| args: &args{instanceId: utils.Ptr("")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), | ||
| }, | ||
| }, | ||
| // Error cases - length validation | ||
| { | ||
| name: "too short", | ||
| args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMinLength-1))}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", InstanceIdFlag, instanceIdMinLength), | ||
| }, | ||
| }, | ||
| { | ||
| name: "way too short", | ||
| args: &args{instanceId: utils.Ptr("a")}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", InstanceIdFlag, instanceIdMinLength), | ||
| }, | ||
| }, | ||
| { | ||
| name: "too long", | ||
| args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMaxLength+1))}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", InstanceIdFlag, instanceIdMaxLength), | ||
| }, | ||
| }, | ||
| { | ||
| name: "way too long", | ||
| args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMaxLength+10))}, | ||
| want: &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", InstanceIdFlag, instanceIdMaxLength), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := ValidateInstanceId(tt.args.instanceId) | ||
| testUtils.AssertError(t, err, tt.want) | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package instance | ||
| import ( | ||
| "fmt" | ||
| "regexp" | ||
| cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| cliUtils "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| ) | ||
| // Validation constants taken from OpenApi spec. | ||
| const ( | ||
| displayNameMinimumChars = 4 | ||
| displayNameMaximumChars = 8 | ||
| displayNameRegex = `^[a-z]([-a-z0-9]*[a-z0-9])?$` | ||
| descriptionMaxLength = 256 | ||
| instanceIdMaxLength = 16 | ||
| instanceIdMinLength = displayNameMinimumChars + 1 // Instance ID is generated by extending the display name. | ||
| ) | ||
| // User input flags for instance commands | ||
| const ( | ||
| DisplayNameFlag = "name" // > displayNameMinimumChars <= displayNameMaximumChars characters + regex displayNameRegex | ||
| DescriptionFlag = "description" // <= descriptionMaxLength characters | ||
| PlanIdFlag = "plan-id" // UUID | ||
| InstanceIdFlag = "id" // instance id (unique per project) | ||
| ) | ||
| // Flag usage texts | ||
| const ( | ||
| DisplayNameUsage = "The displayed name to distinguish multiple instances." | ||
| DescriptionUsage = "A user chosen description to distinguish multiple instances." | ||
| PlanIdUsage = "Service Plan configures the size of the Instance." | ||
| InstanceIdUsage = "The project-unique identifier of this instance." | ||
| ) | ||
| // Flag shorthands | ||
| const ( | ||
| DisplayNameShorthand = "n" | ||
| DescriptionShorthand = "d" | ||
| InstanceIdShorthand = "i" | ||
| ) | ||
| // OpenApi generated code will have different types for by-instance-id and by-display-name API calls, which are currently impl. as separate endpoints. | ||
| // To make the code more flexible, we use a struct to hold the request model. | ||
| type RequestModel struct { | ||
| Value any | ||
| } | ||
| func ValidateDisplayName(displayName *string) error { | ||
| if displayName == nil { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", DisplayNameFlag), | ||
| } | ||
| } | ||
| if len(*displayName) > displayNameMaximumChars { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DisplayNameFlag, displayNameMaximumChars), | ||
| } | ||
| } | ||
| if len(*displayName) < displayNameMinimumChars { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", DisplayNameFlag, displayNameMinimumChars), | ||
| } | ||
| } | ||
| displayNameRegex := regexp.MustCompile(displayNameRegex) | ||
| if !displayNameRegex.MatchString(*displayName) { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: DisplayNameFlag, | ||
| Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| func ValidatePlanId(planId *string) error { | ||
| if planId == nil { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: PlanIdFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", PlanIdFlag), | ||
| } | ||
| } | ||
| err := cliUtils.ValidateUUID(*planId) | ||
| if err != nil { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: PlanIdFlag, | ||
| Details: fmt.Sprintf("%s is not a valid UUID: %v", PlanIdFlag, err), | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| func ValidateDescription(description string) error { | ||
| if len(description) > descriptionMaxLength { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: DescriptionFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DescriptionFlag, descriptionMaxLength), | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| func ValidateInstanceId(instanceId *string) error { | ||
| if instanceId == nil { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), | ||
| } | ||
| } | ||
| if *instanceId == "" { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), | ||
| } | ||
| } | ||
| if len(*instanceId) < instanceIdMinLength { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", InstanceIdFlag, instanceIdMinLength), | ||
| } | ||
| } | ||
| if len(*instanceId) > instanceIdMaxLength { | ||
| return &cliErr.FlagValidationError{ | ||
| Flag: InstanceIdFlag, | ||
| Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", InstanceIdFlag, instanceIdMaxLength), | ||
| } | ||
| } | ||
| return nil | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package kubeconfig | ||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "k8s.io/client-go/tools/clientcmd" | ||
| ) | ||
| var ( | ||
| testErrorMessage = "test error message" | ||
| errStringErrTest = errors.New(testErrorMessage) | ||
| ) | ||
| const ( | ||
| kubeconfig_1_yaml = ` | ||
| apiVersion: v1 | ||
| clusters: | ||
| - cluster: | ||
| server: https://server-1.com | ||
| name: cluster-1 | ||
| contexts: | ||
| - context: | ||
| cluster: cluster-1 | ||
| user: user-1 | ||
| name: context-1 | ||
| current-context: context-1 | ||
| kind: Config | ||
| preferences: {} | ||
| users: | ||
| - name: user-1 | ||
| user: {} | ||
| ` | ||
| kubeconfig_2_yaml = ` | ||
| apiVersion: v1 | ||
| clusters: | ||
| - cluster: | ||
| server: https://server-2.com | ||
| name: cluster-2 | ||
| contexts: | ||
| - context: | ||
| cluster: cluster-2 | ||
| user: user-2 | ||
| name: context-2 | ||
| current-context: context-2 | ||
| kind: Config | ||
| users: | ||
| - name: user-2 | ||
| user: {} | ||
| ` | ||
| overwriteKubeconfigTarget = ` | ||
| apiVersion: v1 | ||
| clusters: | ||
| - cluster: | ||
| server: https://server-1.com | ||
| name: cluster-1 | ||
| contexts: | ||
| - context: | ||
| cluster: cluster-1 | ||
| user: user-1 | ||
| name: context-1 | ||
| current-context: context-1 | ||
| kind: Config | ||
| users: | ||
| - name: user-1 | ||
| user: | ||
| token: old-token | ||
| ` | ||
| overwriteKubeconfigSource = ` | ||
| apiVersion: v1 | ||
| clusters: | ||
| - cluster: | ||
| server: https://server-1-new.com | ||
| name: cluster-1 | ||
| contexts: | ||
| - context: | ||
| cluster: cluster-1 | ||
| user: user-1 | ||
| name: context-1 | ||
| current-context: context-1 | ||
| kind: Config | ||
| users: | ||
| - name: user-1 | ||
| user: | ||
| token: new-token | ||
| ` | ||
| ) | ||
| func TestValidateExpiration(t *testing.T) { | ||
| type args struct { | ||
| expiration *uint64 | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| want error | ||
| }{ | ||
| // Valid cases | ||
| { | ||
| name: "nil expiration", | ||
| args: &args{ | ||
| expiration: nil, | ||
| }, | ||
| }, | ||
| { | ||
| name: "valid expiration - minimum value", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(expirationSecondsMin)), | ||
| }, | ||
| }, | ||
| { | ||
| name: "valid expiration - maximum value", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(expirationSecondsMax)), | ||
| }, | ||
| }, | ||
| { | ||
| name: "valid expiration - default value", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(ExpirationSecondsDefault)), | ||
| }, | ||
| }, | ||
| { | ||
| name: "valid expiration - middle value", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(86400)), // 1 day | ||
| }, | ||
| }, | ||
| // Error cases - below minimum | ||
| { | ||
| name: "expiration too small - below minimum", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(expirationSecondsMin - 1)), | ||
| }, | ||
| want: fmt.Errorf("%s is too small (minimum is %d seconds)", ExpirationFlag, expirationSecondsMin), | ||
| }, | ||
| { | ||
| name: "expiration too small - zero", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(0)), | ||
| }, | ||
| want: fmt.Errorf("%s is too small (minimum is %d seconds)", ExpirationFlag, expirationSecondsMin), | ||
| }, | ||
| // Error cases - above maximum | ||
| { | ||
| name: "expiration too large - above maximum", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(expirationSecondsMax + 1)), | ||
| }, | ||
| want: fmt.Errorf("%s is too large (maximum is %d seconds)", ExpirationFlag, expirationSecondsMax), | ||
| }, | ||
| { | ||
| name: "expiration too large - way above maximum", | ||
| args: &args{ | ||
| expiration: utils.Ptr(uint64(9999999999999999999)), | ||
| }, | ||
| want: fmt.Errorf("%s is too large (maximum is %d seconds)", ExpirationFlag, expirationSecondsMax), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := ValidateExpiration(tt.args.expiration) | ||
| testUtils.AssertError(t, err, tt.want) | ||
| }) | ||
| } | ||
| } | ||
| func TestErrors(t *testing.T) { | ||
| type args struct { | ||
| err error | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| wantErr error | ||
| }{ | ||
| // EmptyKubeconfigError | ||
| { | ||
| name: "EmptyKubeconfigError", | ||
| args: &args{ | ||
| err: &EmptyKubeconfigError{}, | ||
| }, | ||
| wantErr: &EmptyKubeconfigError{}, | ||
| }, | ||
| // LoadKubeconfigError | ||
| { | ||
| name: "LoadKubeconfigError", | ||
| args: &args{ | ||
| err: &LoadKubeconfigError{Err: errStringErrTest}, | ||
| }, | ||
| wantErr: errStringErrTest, | ||
| }, | ||
| // WriteKubeconfigError | ||
| { | ||
| name: "WriteKubeconfigError", | ||
| args: &args{ | ||
| err: &WriteKubeconfigError{Err: errStringErrTest}, | ||
| }, | ||
| wantErr: errStringErrTest, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| testUtils.AssertError(t, tt.args.err, tt.wantErr) | ||
| }) | ||
| } | ||
| } | ||
| // Already have comprehensive tests for WriteKubeconfig | ||
| func TestWriteOptions(t *testing.T) { | ||
| confirmFn := func(_ string) error { return nil } | ||
| type args struct { | ||
| modify func(WriteOptions) WriteOptions | ||
| check func(*testing.T, WriteOptions) | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| }{ | ||
| // Default options | ||
| { | ||
| name: "NewWriteOptions creates default options", | ||
| args: &args{ | ||
| modify: func(o WriteOptions) WriteOptions { return o }, | ||
| check: func(t *testing.T, opts WriteOptions) { | ||
| if opts.Overwrite { | ||
| t.Error("expected Overwrite to be false by default") | ||
| } | ||
| if opts.SwitchContext { | ||
| t.Error("expected SwitchContext to be false by default") | ||
| } | ||
| if opts.ConfirmFn != nil { | ||
| t.Error("expected ConfirmFn to be nil by default") | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| // Individual option tests | ||
| { | ||
| name: "WithOverwrite sets overwrite flag", | ||
| args: &args{ | ||
| modify: func(o WriteOptions) WriteOptions { return o.WithOverwrite(true) }, | ||
| check: func(t *testing.T, opts WriteOptions) { | ||
| if !opts.Overwrite { | ||
| t.Error("expected Overwrite to be true") | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "WithSwitchContext sets switch context flag", | ||
| args: &args{ | ||
| modify: func(o WriteOptions) WriteOptions { return o.WithSwitchContext(true) }, | ||
| check: func(t *testing.T, opts WriteOptions) { | ||
| if !opts.SwitchContext { | ||
| t.Error("expected SwitchContext to be true") | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "WithConfirmation sets confirmation callback", | ||
| args: &args{ | ||
| modify: func(o WriteOptions) WriteOptions { return o.WithConfirmation(confirmFn) }, | ||
| check: func(t *testing.T, opts WriteOptions) { | ||
| if opts.ConfirmFn == nil { | ||
| t.Error("expected ConfirmFn to be set") | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| // Chained options | ||
| { | ||
| name: "options are chainable", | ||
| args: &args{ | ||
| modify: func(o WriteOptions) WriteOptions { | ||
| return o.WithOverwrite(true). | ||
| WithSwitchContext(true). | ||
| WithConfirmation(confirmFn) | ||
| }, | ||
| check: func(t *testing.T, opts WriteOptions) { | ||
| if !opts.Overwrite { | ||
| t.Error("expected Overwrite to be true") | ||
| } | ||
| if !opts.SwitchContext { | ||
| t.Error("expected SwitchContext to be true") | ||
| } | ||
| if opts.ConfirmFn == nil { | ||
| t.Error("expected ConfirmFn to be set") | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| opts := tt.args.modify(NewWriteOptions()) | ||
| tt.args.check(t, opts) | ||
| }) | ||
| } | ||
| } | ||
| func TestGetDefaultKubeconfigPath(t *testing.T) { | ||
| type args struct { | ||
| kubeconfigEnv *string // nil means unset | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| want string | ||
| }{ | ||
| // KUBECONFIG not set | ||
| { | ||
| name: "returns a non-empty path when KUBECONFIG is not set", | ||
| args: &args{kubeconfigEnv: nil}, | ||
| want: "", | ||
| }, | ||
| // Single path | ||
| { | ||
| name: "returns path from KUBECONFIG if set", | ||
| args: &args{kubeconfigEnv: utils.Ptr("/test/kubeconfig_1_yaml")}, | ||
| want: "/test/kubeconfig_1_yaml", | ||
| }, | ||
| // Multiple paths | ||
| { | ||
| name: "returns first path from KUBECONFIG if multiple are set", | ||
| args: &args{kubeconfigEnv: utils.Ptr("/test/kubeconfig_1_yaml" + string(os.PathListSeparator) + "/test/kubeconfig_2_yaml")}, | ||
| want: "/test/kubeconfig_1_yaml", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| // Save original env and restore after test | ||
| oldKubeconfig := os.Getenv("KUBECONFIG") | ||
| defer func() { | ||
| if err := os.Setenv("KUBECONFIG", oldKubeconfig); err != nil { | ||
| t.Logf("failed to restore KUBECONFIG: %v", err) | ||
| } | ||
| }() | ||
| // Setup test environment | ||
| if tt.args.kubeconfigEnv == nil { | ||
| if err := os.Unsetenv("KUBECONFIG"); err != nil { | ||
| t.Fatalf("failed to unset KUBECONFIG: %v", err) | ||
| } | ||
| } else { | ||
| if err := os.Setenv("KUBECONFIG", *tt.args.kubeconfigEnv); err != nil { | ||
| t.Fatalf("failed to set KUBECONFIG: %v", err) | ||
| } | ||
| } | ||
| // Run test | ||
| got := getDefaultKubeconfigPath() | ||
| // If want is empty only make sure the returned path is not empty | ||
| // In that case we don't care about what path is default, only that one is. | ||
| want := filepath.Clean(tt.want) | ||
| if want == filepath.Clean("") { | ||
| if filepath.Clean(got) != "" { | ||
| return | ||
| } | ||
| } | ||
| // Verify results | ||
| testUtils.AssertValue(t, filepath.Clean(got), want) | ||
| }) | ||
| } | ||
| } | ||
| func TestGetKubeconfigPath(t *testing.T) { | ||
| type args struct { | ||
| path *string | ||
| checkPath func(t *testing.T, path string) | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| wantErr error | ||
| }{ | ||
| { | ||
| name: "uses default path when nil provided", | ||
| args: &args{ | ||
| path: nil, | ||
| checkPath: func(t *testing.T, path string) { | ||
| if path == "" { | ||
| t.Error("expected non-empty path") | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "validates and returns absolute path when valid path provided", | ||
| args: &args{ | ||
| path: utils.Ptr("/tmp/kubeconfig"), | ||
| checkPath: func(t *testing.T, path string) { | ||
| if !filepath.IsAbs(path) { | ||
| t.Error("expected absolute path") | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| name: "returns error for invalid path", | ||
| args: &args{ | ||
| path: utils.Ptr("."), | ||
| }, | ||
| wantErr: &InvalidKubeconfigPathError{Path: "."}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| path, err := getKubeconfigPath(tt.args.path) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.args.checkPath != nil { | ||
| tt.args.checkPath(t, path) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestIsValidFilePath(t *testing.T) { | ||
| type args struct { | ||
| path *string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| want bool | ||
| }{ | ||
| { | ||
| name: "valid path", | ||
| args: &args{ | ||
| path: utils.Ptr("/test/kubeconfig"), | ||
| }, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "nil path", | ||
| args: &args{ | ||
| path: nil, | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "empty path", | ||
| args: &args{ | ||
| path: utils.Ptr(""), | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "single dot", | ||
| args: &args{ | ||
| path: utils.Ptr("."), | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "single slash", | ||
| args: &args{ | ||
| path: utils.Ptr("/"), | ||
| }, | ||
| want: false, | ||
| }, | ||
| { | ||
| name: "relative path with parent directory", | ||
| args: &args{ | ||
| path: utils.Ptr("../kubeconfig"), | ||
| }, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "path with spaces", | ||
| args: &args{ | ||
| path: utils.Ptr("/test/kube config"), | ||
| }, | ||
| want: true, | ||
| }, | ||
| { | ||
| name: "complex but valid path", | ||
| args: &args{ | ||
| path: utils.Ptr("/test/kube-config.d/cluster1/config"), | ||
| }, | ||
| want: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if got := isValidFilePath(tt.args.path); got != tt.want { | ||
| t.Errorf("isValidFilePath() = %v, want %v", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestWriteKubeconfig(t *testing.T) { | ||
| testPath := filepath.Join(t.TempDir(), "config") | ||
| defaultTempFile := filepath.Join(t.TempDir(), "default-kubeconfig") | ||
| type args struct { | ||
| path *string | ||
| content string | ||
| options WriteOptions | ||
| setupEnv func() | ||
| checkFile func(t *testing.T, path string) | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args *args | ||
| wantPath *string | ||
| wantErr any | ||
| }{ | ||
| { | ||
| name: "writes new file with default options", | ||
| args: &args{ | ||
| path: &testPath, | ||
| content: kubeconfig_1_yaml, | ||
| options: NewWriteOptions(), | ||
| checkFile: func(t *testing.T, path string) { | ||
| if !isExistingFile(&path) { | ||
| t.Error("file was not created") | ||
| } | ||
| }, | ||
| }, | ||
| wantPath: &testPath, | ||
| }, | ||
| { | ||
| name: "handles invalid file path", | ||
| args: &args{ | ||
| path: utils.Ptr("."), | ||
| content: kubeconfig_1_yaml, | ||
| options: NewWriteOptions(), | ||
| }, | ||
| wantErr: &InvalidKubeconfigPathError{Path: "."}, | ||
| }, | ||
| { | ||
| name: "handles empty kubeconfig", | ||
| args: &args{ | ||
| path: &testPath, | ||
| content: "", | ||
| options: NewWriteOptions(), | ||
| }, | ||
| wantErr: &EmptyKubeconfigError{}, | ||
| }, | ||
| { | ||
| name: "uses default path when nil provided", | ||
| args: &args{ | ||
| path: nil, | ||
| content: kubeconfig_1_yaml, | ||
| options: NewWriteOptions(), | ||
| setupEnv: func() { | ||
| t.Setenv("KUBECONFIG", defaultTempFile) | ||
| }, | ||
| }, | ||
| wantPath: &defaultTempFile, | ||
| }, | ||
| { | ||
| name: "overwrites existing file when option is set", | ||
| args: &args{ | ||
| path: &testPath, | ||
| content: kubeconfig_2_yaml, | ||
| options: NewWriteOptions().WithOverwrite(true), | ||
| setupEnv: func() { | ||
| // Pre-write first file | ||
| if _, err := WriteKubeconfig(&testPath, kubeconfig_1_yaml, NewWriteOptions()); err != nil { | ||
| t.Fatalf("failed to setup test: %v", err) | ||
| } | ||
| }, | ||
| checkFile: func(t *testing.T, path string) { | ||
| content, err := os.ReadFile(path) | ||
| if err != nil { | ||
| t.Fatalf("failed to read kubeconfig: %v", err) | ||
| } | ||
| if !strings.Contains(string(content), "server-2.com") { | ||
| t.Error("file was not overwritten") | ||
| } | ||
| }, | ||
| }, | ||
| wantPath: &testPath, | ||
| }, | ||
| { | ||
| name: "respects user confirmation - confirmed", | ||
| args: &args{ | ||
| path: &testPath, | ||
| content: kubeconfig_1_yaml, | ||
| options: NewWriteOptions().WithConfirmation(func(_ string) error { | ||
| return nil | ||
| }), | ||
| }, | ||
| wantPath: &testPath, | ||
| }, | ||
| { | ||
| name: "respects user confirmation - denied", | ||
| args: &args{ | ||
| path: &testPath, | ||
| content: kubeconfig_1_yaml, | ||
| options: NewWriteOptions().WithConfirmation(func(_ string) error { | ||
| return errStringErrTest | ||
| }), | ||
| }, | ||
| wantErr: errStringErrTest, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if tt.args.setupEnv != nil { | ||
| tt.args.setupEnv() | ||
| } | ||
| got, gotErr := WriteKubeconfig(tt.args.path, tt.args.content, tt.args.options) | ||
| if !testUtils.AssertError(t, gotErr, tt.wantErr) { | ||
| return | ||
| } | ||
| testUtils.AssertValue(t, got, tt.wantPath) | ||
| if tt.args.checkFile != nil { | ||
| tt.args.checkFile(t, *got) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestMergeKubeconfig(t *testing.T) { | ||
| type args struct { | ||
| path *string | ||
| content string | ||
| switchCtx bool | ||
| setupEnv func() | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| verify func(t *testing.T, path string) | ||
| wantErr error | ||
| }{ | ||
| { | ||
| name: "merges configs with conflicting names", | ||
| args: args{ | ||
| path: utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), | ||
| content: overwriteKubeconfigSource, | ||
| switchCtx: true, | ||
| setupEnv: func() { | ||
| // Pre-write first file | ||
| if _, err := WriteKubeconfig(utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), overwriteKubeconfigTarget, NewWriteOptions()); err != nil { | ||
| t.Fatalf("failed to setup test: %v", err) | ||
| } | ||
| }, | ||
| }, | ||
| verify: func(t *testing.T, path string) { | ||
| config, err := clientcmd.LoadFromFile(path) | ||
| if err != nil { | ||
| t.Fatalf("failed to load merged config: %v", err) | ||
| } | ||
| cluster := config.Clusters["cluster-1"] | ||
| if cluster.Server != "https://server-1-new.com" { | ||
| t.Errorf("expected server to be 'https://server-1-new.com', got '%s'", cluster.Server) | ||
| } | ||
| user := config.AuthInfos["user-1"] | ||
| if user.Token != "new-token" { | ||
| t.Errorf("expected token to be 'new-token', got '%s'", user.Token) | ||
| } | ||
| }, | ||
| }, | ||
| { | ||
| name: "handles nil file path", | ||
| args: args{ | ||
| path: nil, | ||
| content: kubeconfig_1_yaml, | ||
| switchCtx: false, | ||
| }, | ||
| wantErr: fmt.Errorf("no kubeconfig file provided to be merged"), | ||
| }, | ||
| { | ||
| name: "handles invalid config", | ||
| args: args{ | ||
| path: utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), | ||
| content: "invalid yaml", | ||
| switchCtx: false, | ||
| }, | ||
| wantErr: &LoadKubeconfigError{}, | ||
| }, | ||
| { | ||
| name: "handles empty config", | ||
| args: args{ | ||
| path: utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), | ||
| content: "", | ||
| switchCtx: false, | ||
| }, | ||
| wantErr: &EmptyKubeconfigError{}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if tt.args.setupEnv != nil { | ||
| tt.args.setupEnv() | ||
| } | ||
| err := mergeKubeconfig(tt.args.path, tt.args.content, tt.args.switchCtx) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.verify != nil { | ||
| if tt.args.path == nil { | ||
| t.Fatalf("expected path to be set") | ||
| } | ||
| tt.verify(t, *tt.args.path) | ||
| } | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package kubeconfig | ||
| import ( | ||
| "fmt" | ||
| "maps" | ||
| "math" | ||
| "os" | ||
| "path/filepath" | ||
| "k8s.io/client-go/tools/clientcmd" | ||
| ) | ||
| // Validation constants taken from OpenApi spec. | ||
| const ( | ||
| expirationSecondsMax = 15552000 // 60 * 60 * 24 * 180 seconds = 180 days | ||
| expirationSecondsMin = 600 // 60 * 10 seconds = 10 minutes | ||
| ) | ||
| // Defaults taken from OpenApi spec. | ||
| const ( | ||
| ExpirationSecondsDefault = 3600 // 60 * 60 seconds = 1 hour | ||
| ) | ||
| // User input flags for kubeconfig commands | ||
| const ( | ||
| ExpirationFlag = "expiration" | ||
| DisableWritingFlag = "disable-writing" | ||
| FilepathFlag = "filepath" | ||
| OverwriteFlag = "overwrite" | ||
| SwitchContextFlag = "switch-context" | ||
| ) | ||
| // Flag usage texts | ||
| const ( | ||
| ExpirationUsage = "Expiration time for the kubeconfig, e.g. 5d. By default, the token is valid for 1h." | ||
| FilepathUsage = "Path to the kubeconfig file. A default is chosen by Kubernetes if not set." | ||
| DisableWritingUsage = "Disable writing the kubeconfig to a file." | ||
| OverwriteUsage = "Force overwrite the kubeconfig file if it exists." | ||
| SwitchContextUsage = "Switch to the context in the kubeconfig file to the new context." | ||
| ) | ||
| // Flag shorthands | ||
| const ( | ||
| ExpirationShorthand = "e" | ||
| DisableWritingShorthand = "" | ||
| FilepathShorthand = "f" | ||
| OverwriteShorthand = "" | ||
| SwitchContextShorthand = "" | ||
| ) | ||
| func ValidateExpiration(expiration *uint64) error { | ||
| if expiration != nil { | ||
| // We're using utils.ConvertToSeconds to convert the user input string to seconds, which is using | ||
| // math.MaxUint64 internally, if no special limits are set. However: the OpenApi v3 Spec | ||
| // only allows integers (int64). So we could end up in a overflow IF expirationSecondsMax | ||
| // ever is changed beyond the maximum value of int64. This check makes sure this won't happen. | ||
| maxExpiration := uint64(math.Min(expirationSecondsMax, math.MaxInt64)) | ||
| if *expiration > maxExpiration { | ||
| return fmt.Errorf("%s is too large (maximum is %d seconds)", ExpirationFlag, maxExpiration) | ||
| } | ||
| // If expiration is ever changed to int64 this check makes sure we never end up with negative expiration times. | ||
| minExpiration := uint64(math.Max(expirationSecondsMin, 0)) | ||
| if *expiration < minExpiration { | ||
| return fmt.Errorf("%s is too small (minimum is %d seconds)", ExpirationFlag, minExpiration) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| // EmptyKubeconfigError is returned when the kubeconfig content is empty. | ||
| type EmptyKubeconfigError struct{} | ||
| // Error returns the error message. | ||
| func (e *EmptyKubeconfigError) Error() string { | ||
| return "no data for kubeconfig" | ||
| } | ||
| // LoadKubeconfigError is returned when loading the kubeconfig fails. | ||
| type LoadKubeconfigError struct { | ||
| Err error | ||
| } | ||
| // Error returns the error message. | ||
| func (e *LoadKubeconfigError) Error() string { | ||
| return fmt.Sprintf("load kubeconfig: %v", e.Err) | ||
| } | ||
| // Unwrap returns the underlying error. | ||
| func (e *LoadKubeconfigError) Unwrap() error { | ||
| return e.Err | ||
| } | ||
| // WriteKubeconfigError is returned when writing the kubeconfig fails. | ||
| type WriteKubeconfigError struct { | ||
| Err error | ||
| } | ||
| // Error returns the error message. | ||
| func (e *WriteKubeconfigError) Error() string { | ||
| return fmt.Sprintf("write kubeconfig: %v", e.Err) | ||
| } | ||
| // Unwrap returns the underlying error. | ||
| func (e *WriteKubeconfigError) Unwrap() error { | ||
| return e.Err | ||
| } | ||
| // InvalidKubeconfigPathError is returned when an invalid kubeconfig path is provided. | ||
| type InvalidKubeconfigPathError struct { | ||
| Path string | ||
| } | ||
| // Error returns the error message. | ||
| func (e *InvalidKubeconfigPathError) Error() string { | ||
| return fmt.Sprintf("invalid path: %s", e.Path) | ||
| } | ||
| // mergeKubeconfig merges new kubeconfig data into a kubeconfig file. | ||
| // | ||
| // If the destination file does not exist, it will be created. If the file exists, | ||
| // the new data (clusters, contexts, and users) is merged into the existing | ||
| // configuration, overwriting entries with the same name and replacing the | ||
| // current-context if defined in the new data. | ||
| // | ||
| // The function takes the following parameters: | ||
| // - configPath: The path to the destination file. The file and the directory tree | ||
| // for the file will be created if it does not exist. | ||
| // - data: The new kubeconfig content to merge. Merge is performed based on standard | ||
| // kubeconfig structure. | ||
| // - switchContext: If true, the function will switch to the new context in the | ||
| // kubeconfig file after merging. | ||
| // | ||
| // It returns a nil error on success. On failure, it returns an error indicating | ||
| // if the provided data was empty, malformed, or if there were issues reading from | ||
| // or writing to the filesystem. | ||
| func mergeKubeconfig(filePath *string, data string, switchContext bool) error { | ||
| if filePath == nil { | ||
| return fmt.Errorf("no kubeconfig file provided to be merged") | ||
| } | ||
| path := *filePath | ||
| // Check if the new kubeconfig data is empty | ||
| if data == "" { | ||
| return &EmptyKubeconfigError{} | ||
| } | ||
| // Load and validate the data into a kubeconfig object | ||
| newConfig, err := clientcmd.Load([]byte(data)) | ||
| if err != nil { | ||
| return &LoadKubeconfigError{Err: err} | ||
| } | ||
| // If the destination kubeconfig does not exist, create a new one. IsNotExist will ignore other errors. | ||
| // Other errors are handled separately by the following clientcmd.LoadFromFile clientcmd.LoadFromFile | ||
| if _, err := os.Stat(path); os.IsNotExist(err) { | ||
| return writeKubeconfig(&path, data) | ||
| } | ||
| // If the file exists load and validate the existing kubeconfig into a config object | ||
| existingConfig, err := clientcmd.LoadFromFile(path) | ||
| if err != nil { | ||
| return &LoadKubeconfigError{Err: err} | ||
| } | ||
| // Merge the new kubeconfig data into the existing config object | ||
| maps.Copy(existingConfig.AuthInfos, newConfig.AuthInfos) | ||
| maps.Copy(existingConfig.Clusters, newConfig.Clusters) | ||
| maps.Copy(existingConfig.Contexts, newConfig.Contexts) | ||
| // If no CurrentContext is set or switchContext is true, set the CurrentContext to the CurrentContext of the new kubeconfig | ||
| if newConfig.CurrentContext != "" && (switchContext || existingConfig.CurrentContext == "") { | ||
| existingConfig.CurrentContext = newConfig.CurrentContext | ||
| } | ||
| // Save the merged config to the file, creating missing directories as needed. | ||
| if err := clientcmd.WriteToFile(*existingConfig, path); err != nil { | ||
| return &WriteKubeconfigError{Err: err} | ||
| } | ||
| return nil | ||
| } | ||
| // writeKubeconfig writes kubeconfig data to a file, overwriting it if it exists. | ||
| // | ||
| // The function takes the following parameters: | ||
| // - configPath: The path to the destination file. The file and the directory tree | ||
| // for the file will be created if it does not exist. | ||
| // - data: The new kubeconfig content to write to the file. | ||
| // | ||
| // It returns a nil error on success. On failure, it returns an error indicating | ||
| // if the provided data was empty, malformed, or if there were issues reading from | ||
| // or writing to the filesystem. | ||
| func writeKubeconfig(filePath *string, data string) error { | ||
| if filePath == nil { | ||
| return fmt.Errorf("no kubeconfig file provided to be written") | ||
| } | ||
| path := *filePath | ||
| // Check if the new kubeconfig data is empty | ||
| if data == "" { | ||
| return &EmptyKubeconfigError{} | ||
| } | ||
| // Load and validate the data into a kubeconfig object | ||
| config, err := clientcmd.Load([]byte(data)) | ||
| if err != nil { | ||
| return &LoadKubeconfigError{Err: err} | ||
| } | ||
| // Save the merged config to the file, creating missing directories as needed. | ||
| if err := clientcmd.WriteToFile(*config, path); err != nil { | ||
| return &WriteKubeconfigError{Err: err} | ||
| } | ||
| return nil | ||
| } | ||
| // getDefaultKubeconfigPath returns the default location for the kubeconfig file, | ||
| // following standard Kubernetes loading rules. | ||
| // | ||
| // It returns a string containing the absolute path to the default kubeconfig file. | ||
| func getDefaultKubeconfigPath() string { | ||
| return clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() | ||
| } | ||
| // Returns the absolute path to the kubeconfig file. | ||
| // If a file path is provided, it is validated and, if valid, returned as an absolute path. | ||
| // If nil is provided the default kubeconfig path is loaded and returned as an absolute path. | ||
| func getKubeconfigPath(filePath *string) (string, error) { | ||
| if filePath == nil { | ||
| return getDefaultKubeconfigPath(), nil | ||
| } | ||
| if isValidFilePath(filePath) { | ||
| return filepath.Abs(*filePath) | ||
| } | ||
| return "", &InvalidKubeconfigPathError{Path: *filePath} | ||
| } | ||
| // Basic filesystem path validation. Returns true if the provided string is a path. Returns false otherwise. | ||
| func isValidFilePath(filePath *string) bool { | ||
| if filePath == nil || *filePath == "" { | ||
| return false | ||
| } | ||
| // Clean the path and check if it's valid | ||
| cleaned := filepath.Clean(*filePath) | ||
| if cleaned == "." || cleaned == string(filepath.Separator) { | ||
| return false | ||
| } | ||
| // Try to get absolute path (this will fail for invalid paths) | ||
| _, err := filepath.Abs(*filePath) | ||
| // If no error, the path is valid (return true). Otherwise, it's invalid (return false). | ||
| return err == nil | ||
| } | ||
| // Basic filesystem file existence check. Returns true if the file exists. Returns false otherwise. | ||
| func isExistingFile(filePath *string) bool { | ||
| // Check if the kubeconfig file exists | ||
| _, errStat := os.Stat(*filePath) | ||
| return !os.IsNotExist(errStat) | ||
| } | ||
| // ConfirmationCallback is a function that prompts for confirmation with the given message | ||
| // and returns true if confirmed, false otherwise | ||
| type ConfirmationCallback func(message string) error | ||
| // WriteOptions contains options for writing kubeconfig files | ||
| type WriteOptions struct { | ||
| Overwrite bool | ||
| SwitchContext bool | ||
| ConfirmFn ConfirmationCallback | ||
| } | ||
| // WithOverwrite sets whether to overwrite existing files instead of merging | ||
| func (w WriteOptions) WithOverwrite(overwrite bool) WriteOptions { | ||
| w.Overwrite = overwrite | ||
| return w | ||
| } | ||
| // WithSwitchContext sets whether to switch to the new context after writing | ||
| func (w WriteOptions) WithSwitchContext(switchContext bool) WriteOptions { | ||
| w.SwitchContext = switchContext | ||
| return w | ||
| } | ||
| // WithConfirmation sets the confirmation callback function | ||
| func (w WriteOptions) WithConfirmation(fn ConfirmationCallback) WriteOptions { | ||
| w.ConfirmFn = fn | ||
| return w | ||
| } | ||
| // NewWriteOptions creates a new WriteOptions with default values | ||
| func NewWriteOptions() WriteOptions { | ||
| return WriteOptions{ | ||
| Overwrite: false, | ||
| SwitchContext: false, | ||
| ConfirmFn: nil, | ||
| } | ||
| } | ||
| // WriteKubeconfig writes the provided kubeconfig data to a file on the filesystem. | ||
| // By default, if the file already exists, it will be merged with the provided data. | ||
| // This behavior can be controlled using the provided options. | ||
| // | ||
| // The function takes the following parameters: | ||
| // - filePath: The path to the destination file. The file and the directory tree for the | ||
| // file will be created if it does not exist. If nil, the default kubeconfig path is used. | ||
| // - kubeconfig: The kubeconfig content to write. | ||
| // - options: Options for controlling the write behavior. | ||
| // | ||
| // It returns the file path actually used to write to on success. | ||
| func WriteKubeconfig(filePath *string, kubeconfig string, options WriteOptions) (*string, error) { | ||
| // Check if the provided filePath is valid or use the default kubeconfig path no filePath is provided | ||
| path, err := getKubeconfigPath(filePath) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if isExistingFile(&path) { | ||
| // If the file exists | ||
| if !options.Overwrite { | ||
| // If overwrite was not requested the default it to merge | ||
| if options.ConfirmFn != nil { | ||
| // If confirmation callback is provided, prompt the user for confirmation | ||
| prompt := fmt.Sprintf("Update your kubeconfig %q?", path) | ||
| err := options.ConfirmFn(prompt) | ||
| if err != nil { | ||
| // If the user doesn't confirm do not proceed with the merge | ||
| return nil, err | ||
| } | ||
| } | ||
| err := mergeKubeconfig(&path, kubeconfig, options.SwitchContext) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &path, err | ||
| } | ||
| // If overwrite was requested overwrite the existing file | ||
| if options.ConfirmFn != nil { | ||
| // If confirmation callback is provided, prompt the user for confirmation | ||
| prompt := fmt.Sprintf("Replace your kubeconfig %q?", path) | ||
| err := options.ConfirmFn(prompt) | ||
| if err != nil { | ||
| // If the user doesn't confirm do not proceed with the overwrite | ||
| return nil, err | ||
| } | ||
| // Fallthrough | ||
| } | ||
| } | ||
| // If the file doesn't exist or in case the user confirmed the overwrite (fallthrough) write the file | ||
| err = writeKubeconfig(&path, kubeconfig) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &path, err | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package validation | ||
| import ( | ||
| "testing" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" | ||
| ) | ||
| func TestGetValidatedInstanceIdentifier(t *testing.T) { | ||
| t.Parallel() | ||
| tests := []struct { | ||
| name string | ||
| setup func(*cobra.Command) | ||
| want *Identifier | ||
| wantErr any | ||
| }{ | ||
| { | ||
| name: "instance id success", | ||
| setup: func(cmd *cobra.Command) { | ||
| cmd.Flags().String(commonInstance.InstanceIdFlag, "", "") | ||
| _ = cmd.Flags().Set(commonInstance.InstanceIdFlag, "edgesvc01") | ||
| }, | ||
| want: &Identifier{Flag: commonInstance.InstanceIdFlag, Value: "edgesvc01"}, | ||
| }, | ||
| { | ||
| name: "display name success", | ||
| setup: func(cmd *cobra.Command) { | ||
| cmd.Flags().String(commonInstance.DisplayNameFlag, "", "") | ||
| _ = cmd.Flags().Set(commonInstance.DisplayNameFlag, "edge01") | ||
| }, | ||
| want: &Identifier{Flag: commonInstance.DisplayNameFlag, Value: "edge01"}, | ||
| }, | ||
| { | ||
| name: "instance id validation error", | ||
| setup: func(cmd *cobra.Command) { | ||
| cmd.Flags().String(commonInstance.InstanceIdFlag, "", "") | ||
| _ = cmd.Flags().Set(commonInstance.InstanceIdFlag, "id") | ||
| }, | ||
| wantErr: "too short", | ||
| }, | ||
| { | ||
| name: "display name validation error", | ||
| setup: func(cmd *cobra.Command) { | ||
| cmd.Flags().String(commonInstance.DisplayNameFlag, "", "") | ||
| _ = cmd.Flags().Set(commonInstance.DisplayNameFlag, "x") | ||
| }, | ||
| wantErr: "too short", | ||
| }, | ||
| { | ||
| name: "no identifier", | ||
| setup: func(_ *cobra.Command) {}, | ||
| wantErr: &commonErr.NoIdentifierError{}, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| printer := print.NewPrinter() | ||
| cmd := &cobra.Command{Use: "test"} | ||
| tt.setup(cmd) | ||
| got, err := GetValidatedInstanceIdentifier(printer, cmd) | ||
| if !testUtils.AssertError(t, err, tt.wantErr) { | ||
| return | ||
| } | ||
| if tt.want != nil { | ||
| testUtils.AssertValue(t, got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package validation | ||
| import ( | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" | ||
| commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" | ||
| ) | ||
| // Struct to model the instance identifier provided by the user (either instance-id or display-name) | ||
| type Identifier struct { | ||
| Flag string | ||
| Value string | ||
| } | ||
| // GetValidatedInstanceIdentifier gets and validates the instance identifier provided by the user through the command-line flags. | ||
| // It checks for either an instance ID or a display name and validates the provided value. | ||
| // | ||
| // p is the printer used for logging. | ||
| // cmd is the cobra command that holds the flags. | ||
| // | ||
| // Returns an Identifier struct containing the flag and its value if a valid identifier is provided, otherwise returns an error. | ||
| // Indirect unit tests of GetValidatedInstanceIdentifier are done within the respective CLI packages. | ||
| func GetValidatedInstanceIdentifier(p *print.Printer, cmd *cobra.Command) (*Identifier, error) { | ||
| switch { | ||
| case cmd.Flags().Changed(commonInstance.InstanceIdFlag): | ||
| instanceIdValue := flags.FlagToStringPointer(p, cmd, commonInstance.InstanceIdFlag) | ||
| if err := commonInstance.ValidateInstanceId(instanceIdValue); err != nil { | ||
| return nil, err | ||
| } | ||
| return &Identifier{ | ||
| Flag: commonInstance.InstanceIdFlag, | ||
| Value: *instanceIdValue, | ||
| }, nil | ||
| case cmd.Flags().Changed(commonInstance.DisplayNameFlag): | ||
| displayNameValue := flags.FlagToStringPointer(p, cmd, commonInstance.DisplayNameFlag) | ||
| if err := commonInstance.ValidateDisplayName(displayNameValue); err != nil { | ||
| return nil, err | ||
| } | ||
| return &Identifier{ | ||
| Flag: commonInstance.DisplayNameFlag, | ||
| Value: *displayNameValue, | ||
| }, nil | ||
| default: | ||
| return nil, commonErr.NewNoIdentifierError("") | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package testutils | ||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "reflect" | ||
| "testing" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| ) | ||
| type customError struct{ msg string } | ||
| func (e *customError) Error() string { return e.msg } | ||
| type anotherError struct{ code int } | ||
| func (e *anotherError) Error() string { return fmt.Sprintf("code=%d", e.code) } | ||
| type mockTB struct { | ||
| testing.TB | ||
| failed bool | ||
| msg string | ||
| } | ||
| func (m *mockTB) Helper() {} | ||
| func (m *mockTB) Errorf(format string, args ...any) { | ||
| m.failed = true | ||
| m.msg = fmt.Sprintf(format, args...) | ||
| } | ||
| func TestAssertError(t *testing.T) { | ||
| t.Parallel() | ||
| sentinel := errors.New("sentinel") | ||
| tests := map[string]struct { | ||
| got error // The input provided as got to AssertError() | ||
| want any // The input provided as want to AssertError() | ||
| wantErr bool // Whether this comparison is expected to fail | ||
| }{ | ||
| "exact match": { | ||
| got: &customError{msg: "boom"}, | ||
| want: &customError{}, | ||
| wantErr: false, | ||
| }, | ||
| "error string message match": { | ||
| got: errors.New("same message"), | ||
| want: "same message", | ||
| wantErr: false, | ||
| }, | ||
| "error string mismatch": { | ||
| got: errors.New("different"), | ||
| want: "same message", | ||
| wantErr: true, | ||
| }, | ||
| "sentinel via errors.Is": { | ||
| got: fmt.Errorf("wrap: %w", sentinel), | ||
| want: sentinel, | ||
| wantErr: false, | ||
| }, | ||
| "any error (true)": { | ||
| got: errors.New("any"), | ||
| want: true, | ||
| wantErr: false, | ||
| }, | ||
| "nil expectation (nil)": { | ||
| got: nil, | ||
| want: nil, | ||
| wantErr: false, | ||
| }, | ||
| "nil expectation (false)": { | ||
| got: nil, | ||
| want: false, | ||
| wantErr: false, | ||
| }, | ||
| "nil error input with error expectation": { | ||
| got: nil, | ||
| want: true, | ||
| wantErr: true, | ||
| }, | ||
| "unexpected error (nil want)": { | ||
| got: errors.New("unexpected"), | ||
| want: nil, | ||
| wantErr: true, | ||
| }, | ||
| "type match without message": { | ||
| got: &customError{msg: "alpha"}, | ||
| want: &customError{msg: "beta"}, | ||
| wantErr: false, | ||
| }, | ||
| "type mismatch": { | ||
| got: &customError{msg: "alpha"}, | ||
| want: &anotherError{}, | ||
| wantErr: true, | ||
| }, | ||
| "no error when none expected": { | ||
| got: nil, | ||
| want: false, | ||
| wantErr: false, | ||
| }, | ||
| "error but want false": { | ||
| got: errors.New("boom"), | ||
| want: false, | ||
| wantErr: true, | ||
| }, | ||
| } | ||
| for name, tt := range tests { | ||
| t.Run(name, func(t *testing.T) { | ||
| t.Parallel() | ||
| mock := &mockTB{} | ||
| result := AssertError(mock, tt.got, tt.want) | ||
| // if the test failed but we didn't expect it to fail | ||
| if mock.failed != tt.wantErr { | ||
| t.Fatalf("AssertError() failed = %v, wantErr %v (msg: %s)", mock.failed, tt.wantErr, mock.msg) | ||
| } | ||
| // if we expected an error the result of AssertError() should be false (this is what AssertError() does in case of error) | ||
| if tt.wantErr && result != false { | ||
| t.Fatalf("AssertError() returned = %v, want %v", result, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestCheckErrorMatch(t *testing.T) { | ||
| t.Parallel() | ||
| underlying := &customError{msg: "root"} | ||
| wrapped := fmt.Errorf("wrap: %w", underlying) | ||
| if !checkErrorMatch(wrapped, &customError{}) { | ||
| t.Fatalf("expected wrapped customError to match via errors.As") | ||
| } | ||
| notMatch := errors.New("other") | ||
| if checkErrorMatch(notMatch, &anotherError{}) { | ||
| t.Fatalf("expected mismatch for unrelated error types") | ||
| } | ||
| } | ||
| func TestAssertValue(t *testing.T) { | ||
| t.Parallel() | ||
| type payload struct { | ||
| Visible string | ||
| hidden int | ||
| } | ||
| customDiff := func(got, want any) string { | ||
| if reflect.DeepEqual(got, want) { | ||
| return "" | ||
| } | ||
| return "custom diff" | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| got any // The input provided as got to AssertValue() | ||
| want any // The input provided as want to AssertValue() | ||
| wantErr bool // Whether this comparison is expected to fail | ||
| opts []ValueComparisonOption | ||
| }{ | ||
| { | ||
| name: "allow unexported success", | ||
| got: payload{Visible: "ok", hidden: 1}, | ||
| want: payload{Visible: "ok", hidden: 1}, | ||
| opts: []ValueComparisonOption{WithAllowUnexported(payload{})}, | ||
| }, | ||
| { | ||
| name: "allow unexported mismatch", | ||
| got: payload{Visible: "oops", hidden: 1}, | ||
| want: payload{Visible: "ok", hidden: 1}, | ||
| opts: []ValueComparisonOption{WithAllowUnexported(payload{})}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "cmp options sort", | ||
| got: []string{"b", "a", "c"}, | ||
| want: []string{"a", "b", "c"}, | ||
| opts: []ValueComparisonOption{WithAssertionCmpOptions(cmpopts.SortSlices(func(a, b string) bool { return a < b }))}, | ||
| }, | ||
| { | ||
| name: "custom diff mismatch", | ||
| got: 1, | ||
| want: 2, | ||
| opts: []ValueComparisonOption{WithDiffFunc(customDiff)}, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "default diff success", | ||
| got: 42, | ||
| want: 42, | ||
| }, | ||
| { | ||
| name: "default diff mismatch", | ||
| got: 1, | ||
| want: 2, | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "diff func overrides cmp options", | ||
| got: []string{"b"}, | ||
| want: []string{"a"}, | ||
| opts: []ValueComparisonOption{ | ||
| WithAssertionCmpOptions(cmpopts.SortSlices(func(a, b string) bool { return a < b })), | ||
| WithDiffFunc(func(_, _ any) string { return "" }), | ||
| }, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| mock := &mockTB{} | ||
| AssertValue(mock, tt.got, tt.want, tt.opts...) | ||
| // if the test failed but we didn't expect it to fail | ||
| if mock.failed != tt.wantErr { | ||
| t.Fatalf("AssertValue failed = %v, want %v (msg: %s)", mock.failed, tt.wantErr, mock.msg) | ||
| } | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package testutils | ||
| // Package test provides utilities for validating CLI command test results with | ||
| // explicit helpers for error expectations and value comparisons. By splitting | ||
| // error and value handling the package keeps assertions simple and removes the | ||
| // need for dynamic type checks in every test case. | ||
| // | ||
| // Example usage: | ||
| // | ||
| // // Expect a specific error type | ||
| // if !test.AssertError(t, run(), &cliErr.FlagValidationError{}) { | ||
| // return | ||
| // } | ||
| // | ||
| // // Expect any error | ||
| // if !test.AssertError(t, run(), true) { | ||
| // return | ||
| // } | ||
| // | ||
| // // Expect error message substring | ||
| // if !test.AssertError(t, run(), "not found") { | ||
| // return | ||
| // } | ||
| // | ||
| // // Compare complex structs with private fields | ||
| // test.AssertValue(t, got, want, test.WithAllowUnexported(MyStruct{})) | ||
| import ( | ||
| "errors" | ||
| "reflect" | ||
| "strings" | ||
| "testing" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| ) | ||
| // AssertError verifies that an observed error satisfies the expected condition. | ||
| // | ||
| // Returns: | ||
| // - bool: True if the test should continue to value checks (i.e., no error occurred). | ||
| // | ||
| // Behavior: | ||
| // 1. If err is nil: | ||
| // - If want is nil or false: Success. | ||
| // - If want is anything else: Fails test (Expected error but got nil). | ||
| // 2. If err is non-nil: | ||
| // - If want is nil or false: Fails test (Unexpected error). | ||
| // - If want is true: Success (Any error accepted). | ||
| // - If want is string: Asserts err.Error() contains the string. | ||
| // - If want is error: Asserts errors.Is(err, want) or type match. | ||
| func AssertError(t testing.TB, got error, want any) bool { | ||
| t.Helper() | ||
| // Case 1: No error occurred | ||
| if got == nil { | ||
| if want == nil || want == false { | ||
| return true | ||
| } | ||
| t.Errorf("got nil error, want %v", want) | ||
| return false | ||
| } | ||
| // Case 2: Error occurred | ||
| if want == nil || want == false { | ||
| t.Errorf("got unexpected error: %v", got) | ||
| return false | ||
| } | ||
| if want == true { | ||
| return false // Error expected and received, stop test | ||
| } | ||
| // Handle string error type expectation | ||
| if wantStr, ok := want.(string); ok { | ||
| if !strings.Contains(got.Error(), wantStr) { | ||
| t.Errorf("got error %q, want substring %q", got, wantStr) | ||
| } | ||
| return false | ||
| } | ||
| // Handle specific error type expectation | ||
| if wantErr, ok := want.(error); ok { | ||
| if checkErrorMatch(got, wantErr) { | ||
| return false | ||
| } | ||
| t.Errorf("got error %v, want %v", got, wantErr) | ||
| return false | ||
| } | ||
| t.Errorf("invalid want type %T for AssertError", want) | ||
| return false | ||
| } | ||
| func checkErrorMatch(got, want error) bool { | ||
| if errors.Is(got, want) { | ||
| return true | ||
| } | ||
| // Fallback to type check using errors.As to handle wrapped errors | ||
| if want != nil { | ||
| typ := reflect.TypeOf(want) | ||
| // errors.As requires a pointer to the target type. | ||
| // reflect.New(typ) returns *T where T is the type of want. | ||
| target := reflect.New(typ).Interface() | ||
| if errors.As(got, target) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| // DiffFunc compares two values and returns a diff string. An empty string means | ||
| // equality. | ||
| type DiffFunc func(got, want any) string | ||
| // ValueComparisonOption configures how HandleValueResult applies cmp options or | ||
| // diffing strategies. | ||
| type ValueComparisonOption func(*valueComparisonConfig) | ||
| type valueComparisonConfig struct { | ||
| diffFunc DiffFunc | ||
| cmpOptions []cmp.Option | ||
| } | ||
| func (config *valueComparisonConfig) getDiffFunc() DiffFunc { | ||
| if config.diffFunc != nil { | ||
| return config.diffFunc | ||
| } | ||
| return func(got, want any) string { | ||
| return cmp.Diff(got, want, config.cmpOptions...) | ||
| } | ||
| } | ||
| // WithCmpOptions accumulates cmp.Options used during value comparison. | ||
| func WithAssertionCmpOptions(opts ...cmp.Option) ValueComparisonOption { | ||
| return func(config *valueComparisonConfig) { | ||
| config.cmpOptions = append(config.cmpOptions, opts...) | ||
| } | ||
| } | ||
| // WithAllowUnexported enables comparison of unexported fields for the provided | ||
| // struct types. | ||
| func WithAllowUnexported(types ...any) ValueComparisonOption { | ||
| return WithAssertionCmpOptions(cmp.AllowUnexported(types...)) | ||
| } | ||
| // WithDiffFunc sets a custom diffing function. Providing this option overrides | ||
| // the default cmp-based diff logic. | ||
| func WithDiffFunc(diffFunc DiffFunc) ValueComparisonOption { | ||
| return func(config *valueComparisonConfig) { | ||
| config.diffFunc = diffFunc | ||
| } | ||
| } | ||
| // WithIgnoreFields ignores the specified fields on the provided type during comparison. | ||
| // It uses cmpopts.IgnoreFields to ensure type-safe filtering. | ||
| func WithIgnoreFields(typ any, names ...string) ValueComparisonOption { | ||
| return WithAssertionCmpOptions(cmpopts.IgnoreFields(typ, names...)) | ||
| } | ||
| // AssertValue compares two values with cmp.Diff while allowing callers to | ||
| // tweak the diff strategy via ValueComparisonOption. A non-empty diff is | ||
| // reported as an error containing the diff output. | ||
| func AssertValue[T any](t testing.TB, got, want T, opts ...ValueComparisonOption) { | ||
| t.Helper() | ||
| // Configure comparison options | ||
| config := &valueComparisonConfig{} | ||
| for _, opt := range opts { | ||
| opt(config) | ||
| } | ||
| // Perform comparison and report diff | ||
| diff := config.getDiffFunc()(got, want) | ||
| if diff != "" { | ||
| t.Errorf("values do not match: %s", diff) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package testutils | ||
| import ( | ||
| "errors" | ||
| "testing" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| ) | ||
| type parseInputTestModel struct { | ||
| Value string | ||
| Args []string | ||
| RepeatValue []string | ||
| hidden string | ||
| } | ||
| func newTestCmdFactory(flagSetup func(*cobra.Command)) func(*types.CmdParams) *cobra.Command { | ||
| return func(*types.CmdParams) *cobra.Command { | ||
| cmd := &cobra.Command{Use: "test"} | ||
| if flagSetup != nil { | ||
| flagSetup(cmd) | ||
| } | ||
| return cmd | ||
| } | ||
| } | ||
| func TestRunParseInputCase(t *testing.T) { | ||
| sentinel := errors.New("parse failed") | ||
| tests := []struct { | ||
| name string | ||
| flagSetup func(*cobra.Command) | ||
| flags map[string]string | ||
| repeatFlags map[string][]string | ||
| args []string | ||
| cmpOpts []ParseInputCaseOption | ||
| wantModel *parseInputTestModel | ||
| wantErr any | ||
| parseFunc func(*print.Printer, *cobra.Command, []string) (*parseInputTestModel, error) | ||
| expectParseCall bool | ||
| }{ | ||
| { | ||
| name: "success", | ||
| flagSetup: func(cmd *cobra.Command) { | ||
| cmd.Flags().String("name", "", "") | ||
| }, | ||
| flags: map[string]string{"name": "edge"}, | ||
| cmpOpts: []ParseInputCaseOption{WithParseInputCmpOptions(WithAllowUnexported(parseInputTestModel{}))}, | ||
| wantModel: &parseInputTestModel{Value: "edge", hidden: "protected"}, | ||
| parseFunc: func(_ *print.Printer, cmd *cobra.Command, _ []string) (*parseInputTestModel, error) { | ||
| val, _ := cmd.Flags().GetString("name") | ||
| return &parseInputTestModel{Value: val, hidden: "protected"}, nil | ||
| }, | ||
| expectParseCall: true, | ||
| }, | ||
| { | ||
| name: "flag set failure", | ||
| flagSetup: func(cmd *cobra.Command) { | ||
| cmd.Flags().Int("count", 0, "") | ||
| }, | ||
| flags: map[string]string{"count": "invalid"}, | ||
| wantErr: "invalid syntax", | ||
| parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { | ||
| return &parseInputTestModel{}, nil | ||
| }, | ||
| expectParseCall: false, | ||
| }, | ||
| { | ||
| name: "flag group validation", | ||
| flagSetup: func(cmd *cobra.Command) { | ||
| cmd.Flags().String("first", "", "") | ||
| cmd.Flags().String("second", "", "") | ||
| cmd.MarkFlagsRequiredTogether("first", "second") | ||
| }, | ||
| flags: map[string]string{"first": "only"}, | ||
| wantErr: "must all be set", | ||
| parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { | ||
| return &parseInputTestModel{}, nil | ||
| }, | ||
| expectParseCall: false, | ||
| }, | ||
| { | ||
| name: "parse func error", | ||
| flagSetup: func(cmd *cobra.Command) { | ||
| cmd.Flags().Bool("ok", false, "") | ||
| }, | ||
| flags: map[string]string{"ok": "true"}, | ||
| wantErr: sentinel, | ||
| parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { | ||
| return nil, sentinel | ||
| }, | ||
| expectParseCall: true, | ||
| }, | ||
| { | ||
| name: "args success", | ||
| flagSetup: func(cmd *cobra.Command) { | ||
| cmd.Args = cobra.ExactArgs(1) | ||
| }, | ||
| args: []string{"arg1"}, | ||
| cmpOpts: []ParseInputCaseOption{WithParseInputCmpOptions(WithAllowUnexported(parseInputTestModel{}))}, | ||
| wantModel: &parseInputTestModel{Args: []string{"arg1"}}, | ||
| parseFunc: func(_ *print.Printer, _ *cobra.Command, args []string) (*parseInputTestModel, error) { | ||
| return &parseInputTestModel{Args: args}, nil | ||
| }, | ||
| expectParseCall: true, | ||
| }, | ||
| { | ||
| name: "args validation failure", | ||
| flagSetup: func(cmd *cobra.Command) { | ||
| cmd.Args = cobra.NoArgs | ||
| }, | ||
| args: []string{"arg1"}, | ||
| wantErr: "unknown command", | ||
| parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { | ||
| return &parseInputTestModel{}, nil | ||
| }, | ||
| expectParseCall: false, | ||
| }, | ||
| { | ||
| name: "repeat flags success", | ||
| flagSetup: func(cmd *cobra.Command) { | ||
| cmd.Flags().StringSlice("tags", []string{}, "") | ||
| }, | ||
| repeatFlags: map[string][]string{"tags": {"tag1", "tag2"}}, | ||
| cmpOpts: []ParseInputCaseOption{WithParseInputCmpOptions(WithAllowUnexported(parseInputTestModel{}))}, | ||
| wantModel: &parseInputTestModel{RepeatValue: []string{"tag1", "tag2"}}, | ||
| parseFunc: func(_ *print.Printer, cmd *cobra.Command, _ []string) (*parseInputTestModel, error) { | ||
| val, _ := cmd.Flags().GetStringSlice("tags") | ||
| return &parseInputTestModel{RepeatValue: val}, nil | ||
| }, | ||
| expectParseCall: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| cmdFactory := newTestCmdFactory(tt.flagSetup) | ||
| var parseCalled bool | ||
| parseFn := tt.parseFunc | ||
| if parseFn == nil { | ||
| parseFn = func(*print.Printer, *cobra.Command, []string) (*parseInputTestModel, error) { | ||
| return &parseInputTestModel{}, nil | ||
| } | ||
| } | ||
| RunParseInputCase(t, ParseInputTestCase[*parseInputTestModel]{ | ||
| Name: tt.name, | ||
| Flags: tt.flags, | ||
| RepeatFlags: tt.repeatFlags, | ||
| Args: tt.args, | ||
| WantModel: tt.wantModel, | ||
| WantErr: tt.wantErr, | ||
| CmdFactory: cmdFactory, | ||
| ParseInputFunc: func(pr *print.Printer, cmd *cobra.Command, args []string) (*parseInputTestModel, error) { | ||
| parseCalled = true | ||
| return parseFn(pr, cmd, args) | ||
| }, | ||
| }, tt.cmpOpts...) | ||
| if parseCalled != tt.expectParseCall { | ||
| t.Fatalf("parseCalled = %v, expect %v", parseCalled, tt.expectParseCall) | ||
| } | ||
| }) | ||
| } | ||
| } |
| // SPDX-License-Identifier: Apache-2.0 | ||
| // SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG | ||
| package testutils | ||
| import ( | ||
| "testing" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/types" | ||
| ) | ||
| // ParseInputTestCase aggregates all required elements to exercise a CLI parseInput | ||
| // function. It centralizes the common flag setup, validation, and result | ||
| // assertions used throughout the edge command test suites. | ||
| type ParseInputTestCase[T any] struct { | ||
| Name string | ||
| // Args simulates positional arguments passed to the command. | ||
| Args []string | ||
| // Flags sets simple single-value flags. | ||
| Flags map[string]string | ||
| // RepeatFlags sets flags that can be specified multiple times (e.g. slice flags). | ||
| RepeatFlags map[string][]string | ||
| WantModel T | ||
| WantErr any | ||
| CmdFactory func(*types.CmdParams) *cobra.Command | ||
| // ParseInputFunc is the function under test. It must accept the printer, command, and args. | ||
| ParseInputFunc func(*print.Printer, *cobra.Command, []string) (T, error) | ||
| } | ||
| // ParseInputCaseOption allows configuring the test execution behavior. | ||
| type ParseInputCaseOption func(*parseInputCaseConfig) | ||
| type parseInputCaseConfig struct { | ||
| cmpOpts []ValueComparisonOption | ||
| } | ||
| // WithParseInputCmpOptions sets custom comparison options for AssertValue. | ||
| func WithParseInputCmpOptions(opts ...ValueComparisonOption) ParseInputCaseOption { | ||
| return func(cfg *parseInputCaseConfig) { | ||
| cfg.cmpOpts = append(cfg.cmpOpts, opts...) | ||
| } | ||
| } | ||
| func defaultParseInputCaseConfig() *parseInputCaseConfig { | ||
| return &parseInputCaseConfig{} | ||
| } | ||
| // RunParseInputCase executes a single parse-input test case using the provided | ||
| // configuration. It mirrors the typical table-driven pattern while removing the | ||
| // boilerplate repeated across tests. The helper short-circuits as soon as an | ||
| // expected error is encountered. | ||
| func RunParseInputCase[T any](t *testing.T, tc ParseInputTestCase[T], opts ...ParseInputCaseOption) { | ||
| t.Helper() | ||
| cfg := defaultParseInputCaseConfig() | ||
| for _, opt := range opts { | ||
| opt(cfg) | ||
| } | ||
| if tc.CmdFactory == nil { | ||
| t.Fatalf("parse input case %q missing CmdFactory", tc.Name) | ||
| } | ||
| if tc.ParseInputFunc == nil { | ||
| t.Fatalf("parse input case %q missing ParseInputFunc", tc.Name) | ||
| } | ||
| printer := print.NewPrinter() | ||
| cmd := tc.CmdFactory(&types.CmdParams{Printer: printer}) | ||
| if cmd == nil { | ||
| t.Fatalf("parse input case %q produced nil command", tc.Name) | ||
| } | ||
| if printer.Cmd == nil { | ||
| printer.Cmd = cmd | ||
| } | ||
| if err := globalflags.Configure(cmd.Flags()); err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| // Set regular flag values. | ||
| for flag, value := range tc.Flags { | ||
| if err := cmd.Flags().Set(flag, value); err != nil { | ||
| AssertError(t, err, tc.WantErr) | ||
| return | ||
| } | ||
| } | ||
| // Set repeated flag values. | ||
| for flag, values := range tc.RepeatFlags { | ||
| for _, value := range values { | ||
| if err := cmd.Flags().Set(flag, value); err != nil { | ||
| AssertError(t, err, tc.WantErr) | ||
| return | ||
| } | ||
| } | ||
| } | ||
| // Test cobra argument validation. | ||
| if err := cmd.ValidateArgs(tc.Args); err != nil { | ||
| AssertError(t, err, tc.WantErr) | ||
| return | ||
| } | ||
| // Test cobra required flags validation. | ||
| if err := cmd.ValidateRequiredFlags(); err != nil { | ||
| AssertError(t, err, tc.WantErr) | ||
| return | ||
| } | ||
| // Test cobra flag group validation. | ||
| if err := cmd.ValidateFlagGroups(); err != nil { | ||
| AssertError(t, err, tc.WantErr) | ||
| return | ||
| } | ||
| // Test parse input function. | ||
| got, err := tc.ParseInputFunc(printer, cmd, tc.Args) | ||
| if !AssertError(t, err, tc.WantErr) { | ||
| return | ||
| } | ||
| AssertValue(t, got, tc.WantModel, cfg.cmpOpts...) | ||
| } |
@@ -16,5 +16,5 @@ name: Renovate | ||
| - name: Self-hosted Renovate | ||
| uses: renovatebot/github-action@v44.2.0 | ||
| uses: renovatebot/github-action@v44.2.4 | ||
| with: | ||
| configurationFile: .github/renovate.json | ||
| token: ${{ secrets.RENOVATE_TOKEN }} |
+11
-0
@@ -56,2 +56,12 @@ version: 2 | ||
| - id: freebsd-builds | ||
| env: | ||
| - CGO_ENABLED=0 | ||
| goos: | ||
| - freebsd | ||
| goarch: | ||
| - arm64 | ||
| - amd64 | ||
| binary: "stackit" | ||
| archives: | ||
@@ -65,2 +75,3 @@ - id: windows-archives | ||
| - macos-builds | ||
| - freebsd-builds | ||
| formats: [ 'tar.gz' ] | ||
@@ -67,0 +78,0 @@ files: |
@@ -45,2 +45,3 @@ ## stackit beta | ||
| * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers | ||
| * [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. | ||
| * [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake | ||
@@ -47,0 +48,0 @@ * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS |
@@ -35,2 +35,3 @@ ## stackit config set | ||
| --dns-custom-endpoint string DNS API base URL, used in calls to this API | ||
| --edge-custom-endpoint string Edge API base URL, used in calls to this API | ||
| -h, --help Help for "stackit config set" | ||
@@ -37,0 +38,0 @@ --iaas-custom-endpoint string IaaS API base URL, used in calls to this API |
@@ -33,2 +33,3 @@ ## stackit config unset | ||
| --dns-custom-endpoint DNS API base URL. If unset, uses the default base URL | ||
| --edge-custom-endpoint Edge API base URL. If unset, uses the default base URL | ||
| -h, --help Help for "stackit config unset" | ||
@@ -35,0 +36,0 @@ --iaas-custom-endpoint IaaS API base URL. If unset, uses the default base URL |
+27
-25
@@ -7,3 +7,3 @@ module github.com/stackitcloud/stackit-cli | ||
| github.com/fatih/color v1.18.0 | ||
| github.com/goccy/go-yaml v1.19.1 | ||
| github.com/goccy/go-yaml v1.19.2 | ||
| github.com/golang-jwt/jwt/v5 v5.3.0 | ||
@@ -13,3 +13,3 @@ github.com/google/go-cmp v0.7.0 | ||
| github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf | ||
| github.com/jedib0t/go-pretty/v6 v6.7.7 | ||
| github.com/jedib0t/go-pretty/v6 v6.7.8 | ||
| github.com/lmittmann/tint v1.1.2 | ||
@@ -21,5 +21,6 @@ github.com/mattn/go-colorable v0.1.14 | ||
| github.com/stackitcloud/stackit-sdk-go/core v0.20.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.3 | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.10.1 | ||
| github.com/stackitcloud/stackit-sdk-go/services/alb v0.8.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/authorization v0.11.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.3 | ||
| github.com/stackitcloud/stackit-sdk-go/services/edge v0.2.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/git v0.10.1 | ||
@@ -33,3 +34,3 @@ github.com/stackitcloud/stackit-sdk-go/services/iaas v1.3.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.3 | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.3 | ||
| github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.14.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.4 | ||
@@ -42,6 +43,6 @@ github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.3 | ||
| github.com/zalando/go-keyring v0.2.6 | ||
| golang.org/x/mod v0.31.0 | ||
| golang.org/x/mod v0.32.0 | ||
| golang.org/x/oauth2 v0.34.0 | ||
| golang.org/x/term v0.38.0 | ||
| golang.org/x/text v0.32.0 | ||
| golang.org/x/term v0.39.0 | ||
| golang.org/x/text v0.33.0 | ||
| k8s.io/apimachinery v0.34.2 | ||
@@ -52,3 +53,3 @@ k8s.io/client-go v0.34.2 | ||
| require ( | ||
| golang.org/x/net v0.48.0 // indirect | ||
| golang.org/x/net v0.49.0 // indirect | ||
| golang.org/x/time v0.11.0 // indirect | ||
@@ -63,2 +64,3 @@ gopkg.in/inf.v0 v0.9.1 // indirect | ||
| codeberg.org/chavacava/garif v0.2.0 // indirect | ||
| codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect | ||
| dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect | ||
@@ -73,11 +75,11 @@ dev.gaijin.team/go/golib v0.6.0 // indirect | ||
| github.com/Antonboom/testifylint v1.6.4 // indirect | ||
| github.com/BurntSushi/toml v1.5.0 // indirect | ||
| github.com/BurntSushi/toml v1.6.0 // indirect | ||
| github.com/Djarvur/go-err113 v0.1.1 // indirect | ||
| github.com/Masterminds/semver/v3 v3.4.0 // indirect | ||
| github.com/MirrexOne/unqueryvet v1.3.0 // indirect | ||
| github.com/MirrexOne/unqueryvet v1.4.0 // indirect | ||
| github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect | ||
| github.com/alecthomas/chroma/v2 v2.20.0 // indirect | ||
| github.com/alecthomas/chroma/v2 v2.21.1 // indirect | ||
| github.com/alecthomas/go-check-sumtype v0.3.1 // indirect | ||
| github.com/alexkohler/nakedret/v2 v2.0.6 // indirect | ||
| github.com/alexkohler/prealloc v1.0.0 // indirect | ||
| github.com/alexkohler/prealloc v1.0.1 // indirect | ||
| github.com/alfatraining/structtag v1.0.0 // indirect | ||
@@ -118,4 +120,4 @@ github.com/alingse/asasalint v0.0.11 // indirect | ||
| github.com/fzipp/gocyclo v0.6.0 // indirect | ||
| github.com/ghostiam/protogetter v0.3.17 // indirect | ||
| github.com/go-critic/go-critic v0.14.2 // indirect | ||
| github.com/ghostiam/protogetter v0.3.18 // indirect | ||
| github.com/go-critic/go-critic v0.14.3 // indirect | ||
| github.com/go-toolsmith/astcast v1.1.0 // indirect | ||
@@ -131,3 +133,3 @@ github.com/go-toolsmith/astcopy v1.1.0 // indirect | ||
| github.com/gobwas/glob v0.2.3 // indirect | ||
| github.com/godoc-lint/godoc-lint v0.10.2 // indirect | ||
| github.com/godoc-lint/godoc-lint v0.11.1 // indirect | ||
| github.com/gofrs/flock v0.13.0 // indirect | ||
@@ -139,4 +141,4 @@ github.com/golang/protobuf v1.5.3 // indirect | ||
| github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect | ||
| github.com/golangci/golangci-lint/v2 v2.7.2 // indirect | ||
| github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect | ||
| github.com/golangci/golangci-lint/v2 v2.8.0 // indirect | ||
| github.com/golangci/golines v0.14.0 // indirect | ||
| github.com/golangci/misspell v0.7.0 // indirect | ||
@@ -167,4 +169,5 @@ github.com/golangci/plugin-module-register v0.1.2 // indirect | ||
| github.com/ldez/exptostd v0.4.5 // indirect | ||
| github.com/ldez/gomoddirectives v0.7.1 // indirect | ||
| github.com/ldez/gomoddirectives v0.8.0 // indirect | ||
| github.com/ldez/grignotin v0.10.1 // indirect | ||
| github.com/ldez/structtags v0.6.1 // indirect | ||
| github.com/ldez/tagliatelle v0.7.2 // indirect | ||
@@ -192,3 +195,2 @@ github.com/ldez/usetesting v0.5.0 // indirect | ||
| github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect | ||
| github.com/polyfloyd/go-errorlint v1.8.0 // indirect | ||
| github.com/prometheus/client_golang v1.12.1 // indirect | ||
@@ -211,3 +213,3 @@ github.com/prometheus/client_model v0.2.0 // indirect | ||
| github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect | ||
| github.com/securego/gosec/v2 v2.22.11-0.20251204091113-daccba6b93d7 // indirect | ||
| github.com/securego/gosec/v2 v2.22.11 // indirect | ||
| github.com/sirupsen/logrus v1.9.3 // indirect | ||
@@ -248,4 +250,4 @@ github.com/sivchari/containedctx v1.0.3 // indirect | ||
| golang.org/x/sync v0.19.0 // indirect | ||
| golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect | ||
| golang.org/x/tools v0.40.0 // indirect | ||
| golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 // indirect | ||
| golang.org/x/tools v0.41.0 // indirect | ||
| google.golang.org/protobuf v1.36.8 // indirect | ||
@@ -279,3 +281,3 @@ honnef.co/go/tools v0.6.1 // indirect | ||
| github.com/spf13/cast v1.10.0 // indirect | ||
| github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.2 | ||
| github.com/stackitcloud/stackit-sdk-go/services/kms v1.2.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.2 | ||
@@ -290,3 +292,3 @@ github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.3 | ||
| github.com/subosito/gotenv v1.6.0 // indirect | ||
| golang.org/x/sys v0.39.0 // indirect | ||
| golang.org/x/sys v0.40.0 // indirect | ||
| gopkg.in/yaml.v3 v3.0.1 // indirect | ||
@@ -293,0 +295,0 @@ k8s.io/api v0.34.2 // indirect |
+18
-0
@@ -218,2 +218,20 @@ # Installation | ||
| ### FreeBSD | ||
| The STACKIT CLI can be installed through the [FreeBSD ports or packages](https://docs.freebsd.org/en/books/handbook/ports/). | ||
| To install the port: | ||
| ```shell | ||
| cd /usr/ports/sysutils/stackit/ && make install clean | ||
| ``` | ||
| To add the package, run one of these commands: | ||
| ```shell | ||
| pkg install sysutils/stackit | ||
| # OR | ||
| pkg install stackit | ||
| ``` | ||
| ### Pre-compiled binary | ||
@@ -220,0 +238,0 @@ |
@@ -57,8 +57,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create the affinity group %q?", model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create the affinity group %q?", model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -65,0 +63,0 @@ |
@@ -68,8 +68,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete affinity group %q?", affinityGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete affinity group %q?", affinityGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -69,8 +69,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an application loadbalancer for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create an application loadbalancer for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -60,8 +60,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the application loadbalancer %q for project %q?", model.Name, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the application loadbalancer %q for project %q?", model.Name, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -68,0 +66,0 @@ |
@@ -58,8 +58,6 @@ package add | ||
| if !model.AssumeYes { | ||
| prompt := "Are your sure you want to add credentials?" | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := "Are your sure you want to add credentials?" | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -66,0 +64,0 @@ |
@@ -53,8 +53,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %q?", model.CredentialsRef) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %q?", model.CredentialsRef) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -61,0 +59,0 @@ |
@@ -67,8 +67,6 @@ package update | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update credential %q for %q?", *model.CredentialsRef, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return fmt.Errorf("update credential: %w", err) | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update credential %q for %q?", *model.CredentialsRef, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return fmt.Errorf("update credential: %w", err) | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -69,8 +69,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update an application target pool for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update an application target pool for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -70,8 +70,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update an application loadbalancer for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update an application loadbalancer for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -78,0 +76,0 @@ |
@@ -9,2 +9,3 @@ package beta | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake" | ||
@@ -47,4 +48,5 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms" | ||
| cmd.AddCommand(alb.NewCmd(params)) | ||
| cmd.AddCommand(edge.NewCmd(params)) | ||
| cmd.AddCommand(intake.NewCmd(params)) | ||
| cmd.AddCommand(kms.NewCmd(params)) | ||
| } |
@@ -76,8 +76,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an Intake Runner for project %q?", projectLabel) | ||
| err = p.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create an Intake Runner for project %q?", projectLabel) | ||
| err = p.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -84,0 +82,0 @@ |
@@ -57,8 +57,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete Intake Runner %q?", model.RunnerId) | ||
| err = p.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete Intake Runner %q?", model.RunnerId) | ||
| err = p.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -65,0 +63,0 @@ |
@@ -88,7 +88,5 @@ package create | ||
| if !model.AssumeYes { | ||
| err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS Key?") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS Key?") | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -95,0 +93,0 @@ |
@@ -67,8 +67,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete key %q? (This cannot be undone)", keyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete key %q? (This cannot be undone)", keyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -82,8 +82,6 @@ package importKey | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to import a new version for the KMS Key %q inside the key ring %q?", keyName, keyRingName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to import a new version for the KMS Key %q inside the key ring %q?", keyName, keyRingName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -90,0 +88,0 @@ |
@@ -67,8 +67,6 @@ package restore | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to restore key %q? (This cannot be undone)", keyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to restore key %q? (This cannot be undone)", keyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -67,8 +67,6 @@ package rotate | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to rotate the key %q? (this cannot be undone)", keyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to rotate the key %q? (this cannot be undone)", keyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -67,7 +67,5 @@ package create | ||
| if !model.AssumeYes { | ||
| err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS key ring?") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS key ring?") | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -61,8 +61,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete key ring %q? (this cannot be undone)", keyRingLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete key ring %q? (this cannot be undone)", keyRingLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -69,0 +67,0 @@ |
@@ -74,7 +74,5 @@ package create | ||
| if !model.AssumeYes { | ||
| err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS wrapping key?") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS wrapping key?") | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -81,0 +79,0 @@ |
@@ -65,8 +65,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the wrapping key %q? (this cannot be undone)", wrappingKeyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the wrapping key %q? (this cannot be undone)", wrappingKeyName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -73,0 +71,0 @@ |
@@ -70,8 +70,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a export policy for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a export policy for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -78,0 +76,0 @@ |
@@ -59,8 +59,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete export policy %q? (This cannot be undone)", exportPolicyLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete export policy %q? (This cannot be undone)", exportPolicyLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -67,0 +65,0 @@ |
@@ -81,8 +81,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update export policy %q for project %q?", exportPolicyLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update export policy %q for project %q?", exportPolicyLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -89,0 +87,0 @@ |
@@ -85,8 +85,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a resource-pool for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a resource-pool for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -93,0 +91,0 @@ |
@@ -62,8 +62,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete resource pool %q? (This cannot be undone)", resourcePoolName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete resource pool %q? (This cannot be undone)", resourcePoolName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -70,0 +68,0 @@ |
@@ -90,8 +90,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update resource-pool %q for project %q?", resourcePoolName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update resource-pool %q for project %q?", resourcePoolName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -98,0 +96,0 @@ |
@@ -75,8 +75,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a SFS share for resource pool %q?", resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a SFS share for resource pool %q?", resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -68,8 +68,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete SFS share %q? (This cannot be undone)", shareLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete SFS share %q? (This cannot be undone)", shareLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -84,8 +84,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update SFS share %q for resource pool %q?", shareLabel, resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update SFS share %q for resource pool %q?", shareLabel, resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -92,0 +90,0 @@ |
@@ -71,8 +71,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a snapshot for resource pool %q?", resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a snapshot for resource pool %q?", resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -79,0 +77,0 @@ |
@@ -64,8 +64,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q for resource pool %q?", model.SnapshotName, resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q for resource pool %q?", model.SnapshotName, resourcePoolLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -72,0 +70,0 @@ |
@@ -63,8 +63,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create database %q? (This cannot be undone)", model.DatabaseName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create database %q? (This cannot be undone)", model.DatabaseName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -61,8 +61,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete database %q? (This cannot be undone)", model.DatabaseName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete database %q? (This cannot be undone)", model.DatabaseName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -69,0 +67,0 @@ |
@@ -106,8 +106,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a SQLServer Flex instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a SQLServer Flex instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -114,0 +112,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -97,8 +97,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -105,0 +103,0 @@ |
@@ -78,8 +78,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -86,0 +84,0 @@ |
@@ -74,8 +74,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -75,8 +75,6 @@ package resetpassword | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -68,8 +68,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete profile %q? (This cannot be undone)", model.Profile) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete profile %q? (This cannot be undone)", model.Profile) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -6,2 +6,3 @@ { | ||
| "dns_custom_endpoint": "", | ||
| "edge_custom_endpoint": "", | ||
| "iaas_custom_endpoint": "", | ||
@@ -35,2 +36,2 @@ "identity_provider_custom_client_id": "", | ||
| "verbosity": "info" | ||
| } | ||
| } |
@@ -29,2 +29,3 @@ package set | ||
| dnsCustomEndpointFlag = "dns-custom-endpoint" | ||
| edgeCustomEndpointFlag = "edge-custom-endpoint" | ||
| loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint" | ||
@@ -147,2 +148,3 @@ logMeCustomEndpointFlag = "logme-custom-endpoint" | ||
| cmd.Flags().String(dnsCustomEndpointFlag, "", "DNS API base URL, used in calls to this API") | ||
| cmd.Flags().String(edgeCustomEndpointFlag, "", "Edge API base URL, used in calls to this API") | ||
| cmd.Flags().String(loadBalancerCustomEndpointFlag, "", "Load Balancer API base URL, used in calls to this API") | ||
@@ -187,2 +189,4 @@ cmd.Flags().String(logMeCustomEndpointFlag, "", "LogMe API base URL, used in calls to this API") | ||
| cobra.CheckErr(err) | ||
| err = viper.BindPFlag(config.EdgeCustomEndpointKey, cmd.Flags().Lookup(edgeCustomEndpointFlag)) | ||
| cobra.CheckErr(err) | ||
| err = viper.BindPFlag(config.LoadBalancerCustomEndpointKey, cmd.Flags().Lookup(loadBalancerCustomEndpointFlag)) | ||
@@ -189,0 +193,0 @@ cobra.CheckErr(err) |
@@ -28,2 +28,3 @@ package unset | ||
| dnsCustomEndpointFlag: true, | ||
| edgeCustomEndpointFlag: true, | ||
| loadBalancerCustomEndpointFlag: true, | ||
@@ -71,2 +72,3 @@ logMeCustomEndpointFlag: true, | ||
| DNSCustomEndpoint: true, | ||
| EdgeCustomEndpoint: true, | ||
| LoadBalancerCustomEndpoint: true, | ||
@@ -130,2 +132,3 @@ LogMeCustomEndpoint: true, | ||
| model.DNSCustomEndpoint = false | ||
| model.EdgeCustomEndpoint = false | ||
| model.LoadBalancerCustomEndpoint = false | ||
@@ -225,2 +228,12 @@ model.LogMeCustomEndpoint = false | ||
| { | ||
| description: "edge custom endpoint empty", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]bool) { | ||
| flagValues[edgeCustomEndpointFlag] = false | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.EdgeCustomEndpoint = false | ||
| }), | ||
| }, | ||
| { | ||
| description: "secrets manager custom endpoint empty", | ||
@@ -227,0 +240,0 @@ flagValues: fixtureFlagValues(func(flagValues map[string]bool) { |
@@ -33,2 +33,3 @@ package unset | ||
| dnsCustomEndpointFlag = "dns-custom-endpoint" | ||
| edgeCustomEndpointFlag = "edge-custom-endpoint" | ||
| loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint" | ||
@@ -74,2 +75,3 @@ logMeCustomEndpointFlag = "logme-custom-endpoint" | ||
| DNSCustomEndpoint bool | ||
| EdgeCustomEndpoint bool | ||
| LoadBalancerCustomEndpoint bool | ||
@@ -159,2 +161,5 @@ LogMeCustomEndpoint bool | ||
| } | ||
| if model.EdgeCustomEndpoint { | ||
| viper.Set(config.EdgeCustomEndpointKey, "") | ||
| } | ||
| if model.LoadBalancerCustomEndpoint { | ||
@@ -256,2 +261,3 @@ viper.Set(config.LoadBalancerCustomEndpointKey, "") | ||
| cmd.Flags().Bool(dnsCustomEndpointFlag, false, "DNS API base URL. If unset, uses the default base URL") | ||
| cmd.Flags().Bool(edgeCustomEndpointFlag, false, "Edge API base URL. If unset, uses the default base URL") | ||
| cmd.Flags().Bool(loadBalancerCustomEndpointFlag, false, "Load Balancer API base URL. If unset, uses the default base URL") | ||
@@ -297,2 +303,3 @@ cmd.Flags().Bool(logMeCustomEndpointFlag, false, "LogMe API base URL. If unset, uses the default base URL") | ||
| DNSCustomEndpoint: flags.FlagToBoolValue(p, cmd, dnsCustomEndpointFlag), | ||
| EdgeCustomEndpoint: flags.FlagToBoolValue(p, cmd, edgeCustomEndpointFlag), | ||
| LoadBalancerCustomEndpoint: flags.FlagToBoolValue(p, cmd, loadBalancerCustomEndpointFlag), | ||
@@ -299,0 +306,0 @@ LogMeCustomEndpoint: flags.FlagToBoolValue(p, cmd, logMeCustomEndpointFlag), |
@@ -76,8 +76,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a record set for zone %s?", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a record set for zone %s?", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -84,0 +82,0 @@ |
@@ -73,8 +73,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete record set %s of zone %s? (This cannot be undone)", recordSetLabel, zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete record set %s of zone %s? (This cannot be undone)", recordSetLabel, zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -81,0 +79,0 @@ |
@@ -96,8 +96,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update record set %s of zone %s?", recordSetLabel, zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update record set %s of zone %s?", recordSetLabel, zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -104,0 +102,0 @@ |
@@ -78,8 +78,6 @@ package clone | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to clone the zone %q?", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to clone the zone %q?", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -86,0 +84,0 @@ |
@@ -91,8 +91,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a zone for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a zone for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -99,0 +97,0 @@ |
@@ -62,8 +62,6 @@ package delete | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete zone %q? (This cannot be undone)", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete zone %q? (This cannot be undone)", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -70,0 +68,0 @@ |
@@ -85,8 +85,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update zone %s?", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update zone %s?", zoneLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -93,0 +91,0 @@ |
@@ -70,8 +70,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create the instance %q?", model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create the instance %q?", model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -78,0 +76,0 @@ |
@@ -68,8 +68,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the stackit git instance %q for %q?", instanceName, projectName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the stackit git instance %q for %q?", instanceName, projectName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -130,8 +130,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create the image %q?", model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create the image %q?", model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -138,0 +136,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the image %q for %q?", imageName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the image %q for %q?", imageName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -142,8 +142,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update the image %q?", imageLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update the image %q?", imageLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -150,0 +148,0 @@ |
@@ -70,8 +70,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := "Are your sure you want to create a key pair?" | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := "Are your sure you want to create a key pair?" | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -78,0 +76,0 @@ |
@@ -53,8 +53,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete key pair %q?", model.KeyPairName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete key pair %q?", model.KeyPairName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -61,0 +59,0 @@ |
@@ -54,8 +54,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update key pair %q?", *model.KeyPairName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return fmt.Errorf("update key pair: %w", err) | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update key pair %q?", *model.KeyPairName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return fmt.Errorf("update key pair: %w", err) | ||
| } | ||
@@ -62,0 +60,0 @@ |
@@ -81,8 +81,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a load balancer for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a load balancer for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -89,0 +87,0 @@ |
@@ -54,8 +54,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete load balancer %q? (This cannot be undone)", model.LoadBalancerName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete load balancer %q? (This cannot be undone)", model.LoadBalancerName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -62,0 +60,0 @@ |
@@ -79,8 +79,6 @@ package add | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to add observability credentials for Load Balancer on project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to add observability credentials for Load Balancer on project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -87,0 +85,0 @@ |
@@ -75,18 +75,16 @@ package cleanup | ||
| if !model.AssumeYes { | ||
| prompt := "Will delete the following unused observability credentials: \n" | ||
| for _, credential := range credentials { | ||
| if credential.DisplayName == nil || credential.Username == nil { | ||
| return fmt.Errorf("list unused Load Balancer observability credentials: credentials %q missing display name or username", *credential.CredentialsRef) | ||
| } | ||
| name := *credential.DisplayName | ||
| username := *credential.Username | ||
| prompt += fmt.Sprintf(" - %s (username: %q)\n", name, username) | ||
| prompt := "Will delete the following unused observability credentials: \n" | ||
| for _, credential := range credentials { | ||
| if credential.DisplayName == nil || credential.Username == nil { | ||
| return fmt.Errorf("list unused Load Balancer observability credentials: credentials %q missing display name or username", *credential.CredentialsRef) | ||
| } | ||
| prompt += fmt.Sprintf("Are you sure you want to delete unused observability credentials on project %q? (This cannot be undone)", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| name := *credential.DisplayName | ||
| username := *credential.Username | ||
| prompt += fmt.Sprintf(" - %s (username: %q)\n", name, username) | ||
| } | ||
| prompt += fmt.Sprintf("Are you sure you want to delete unused observability credentials on project %q? (This cannot be undone)", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -93,0 +91,0 @@ for _, credential := range credentials { |
@@ -67,8 +67,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete observability credentials %q on project %q?(This cannot be undone)", credentialsLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete observability credentials %q on project %q?(This cannot be undone)", credentialsLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -98,8 +98,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update observability credentials %q for Load Balancer on project %q?", credentialsLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update observability credentials %q for Load Balancer on project %q?", credentialsLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -106,0 +104,0 @@ |
@@ -65,8 +65,6 @@ package addtarget | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to add a target with IP %q to target pool %q of load balancer %q?", model.IP, model.TargetPoolName, model.LBName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to add a target with IP %q to target pool %q of load balancer %q?", model.IP, model.TargetPoolName, model.LBName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -73,0 +71,0 @@ |
@@ -67,8 +67,6 @@ package removetarget | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to remove target %q from target pool %q of load balancer %q?", targetLabel, model.TargetPoolName, model.LBName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to remove target %q from target pool %q of load balancer %q?", targetLabel, model.TargetPoolName, model.LBName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -69,8 +69,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update load balancer %q?", model.LoadBalancerName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update load balancer %q?", model.LoadBalancerName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -66,8 +66,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -71,8 +71,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -79,0 +77,0 @@ |
@@ -94,8 +94,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a LogMe instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a LogMe instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -102,0 +100,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -93,8 +93,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -101,0 +99,0 @@ |
@@ -67,8 +67,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -71,8 +71,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -79,0 +77,0 @@ |
@@ -94,8 +94,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a MariaDB instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a MariaDB instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -102,0 +100,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -91,8 +91,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -99,0 +97,0 @@ |
@@ -80,8 +80,6 @@ package restore | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to restore MongoDB Flex instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to restore MongoDB Flex instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -88,0 +86,0 @@ |
@@ -91,8 +91,6 @@ package updateschedule | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -99,0 +97,0 @@ |
@@ -96,8 +96,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a MongoDB Flex instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a MongoDB Flex instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -104,0 +102,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -91,8 +91,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -99,0 +97,0 @@ |
@@ -80,8 +80,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -88,0 +86,0 @@ |
@@ -75,8 +75,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -75,8 +75,6 @@ package resetpassword | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -76,8 +76,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -84,0 +82,0 @@ |
@@ -117,8 +117,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network area for organization %q?", orgLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network area for organization %q?", orgLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -151,3 +149,3 @@ | ||
| } | ||
| if !model.AssumeYes { | ||
| if !model.Async { | ||
| s := spinner.New(params.Printer) | ||
@@ -154,0 +152,0 @@ s.Start("Create network area region") |
@@ -72,8 +72,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete network area %q?", networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete network area %q?", networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -80,0 +78,0 @@ |
@@ -67,8 +67,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network range for STACKIT Network Area (SNA) %q?", networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network range for STACKIT Network Area (SNA) %q?", networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -74,8 +74,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete network range %q on STACKIT Network Area (SNA) %q?", networkRangeLabel, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete network range %q on STACKIT Network Area (SNA) %q?", networkRangeLabel, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -93,8 +93,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -101,0 +99,0 @@ |
@@ -71,8 +71,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -79,0 +77,0 @@ |
@@ -86,8 +86,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -94,0 +92,0 @@ |
@@ -101,8 +101,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a static route for STACKIT Network Area (SNA) %q?", networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a static route for STACKIT Network Area (SNA) %q?", networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -109,0 +107,0 @@ |
@@ -67,8 +67,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete static route %q on STACKIT Network Area (SNA) %q?", model.RouteId, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete static route %q on STACKIT Network Area (SNA) %q?", model.RouteId, networkAreaLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -106,8 +106,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update a network area for organization %q?", orgLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update a network area for organization %q?", orgLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -114,0 +112,0 @@ |
@@ -89,8 +89,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network interface for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network interface for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -97,0 +95,0 @@ |
@@ -57,8 +57,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the network interface %q? (This cannot be undone)", model.NicId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the network interface %q? (This cannot be undone)", model.NicId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -65,0 +63,0 @@ |
@@ -83,8 +83,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update the network interface %q?", model.NicId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update the network interface %q?", model.NicId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -91,0 +89,0 @@ |
@@ -111,8 +111,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -119,0 +117,0 @@ |
@@ -69,8 +69,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete network %q?", networkLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete network %q?", networkLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -96,8 +96,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update network %q?", networkLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update network %q?", networkLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -104,0 +102,0 @@ |
@@ -56,8 +56,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create bucket %q? (This cannot be undone)", model.BucketName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create bucket %q? (This cannot be undone)", model.BucketName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -64,0 +62,0 @@ |
@@ -55,8 +55,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete bucket %q? (This cannot be undone)", model.BucketName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete bucket %q? (This cannot be undone)", model.BucketName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -63,0 +61,0 @@ |
@@ -55,8 +55,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a credentials group with name %q?", model.CredentialsGroupName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a credentials group with name %q?", model.CredentialsGroupName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -63,0 +61,0 @@ |
@@ -61,8 +61,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials group %q? (This cannot be undone)", credentialsGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials group %q? (This cannot be undone)", credentialsGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -69,0 +67,0 @@ |
@@ -69,8 +69,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials in group %q?", credentialsGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials in group %q?", credentialsGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -68,8 +68,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %q of credentials group %q? (This cannot be undone)", credentialsLabel, credentialsGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %q of credentials group %q? (This cannot be undone)", credentialsLabel, credentialsGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -55,8 +55,6 @@ package disable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disable Object Storage for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to disable Object Storage for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -63,0 +61,0 @@ |
@@ -55,8 +55,6 @@ package enable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to enable Object Storage for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to enable Object Storage for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -63,0 +61,0 @@ |
@@ -64,8 +64,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -72,0 +70,0 @@ |
@@ -64,8 +64,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials for username %q of instance %q? (This cannot be undone)", model.Username, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials for username %q of instance %q? (This cannot be undone)", model.Username, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -72,0 +70,0 @@ |
@@ -63,8 +63,6 @@ package disable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disable Grafana public read access for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to disable Grafana public read access for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -63,8 +63,6 @@ package enable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to enable Grafana public read access for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to enable Grafana public read access for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -63,8 +63,6 @@ package disable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disable single sign-on for Grafana for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to disable single sign-on for Grafana for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -63,8 +63,6 @@ package enable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to enable single sign-on for Grafana for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to enable single sign-on for Grafana for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -74,8 +74,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an Observability instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create an Observability instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -79,8 +79,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -87,0 +85,0 @@ |
@@ -92,8 +92,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create scrape configuration %q on Observability instance %q?", *model.Payload.JobName, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create scrape configuration %q on Observability instance %q?", *model.Payload.JobName, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -100,0 +98,0 @@ |
@@ -66,8 +66,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete scrape configuration %q on Observability instance %q? (This cannot be undone)", model.JobName, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete scrape configuration %q on Observability instance %q? (This cannot be undone)", model.JobName, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -72,8 +72,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update scrape configuration %q?", model.JobName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update scrape configuration %q?", model.JobName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -80,0 +78,0 @@ |
@@ -66,8 +66,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -71,8 +71,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -79,0 +77,0 @@ |
@@ -96,8 +96,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an OpenSearch instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create an OpenSearch instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -104,0 +102,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -94,8 +94,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -102,0 +100,0 @@ |
@@ -61,8 +61,6 @@ package add | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to add the %s role to %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to add the %s role to %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -69,0 +67,0 @@ |
@@ -71,12 +71,10 @@ package remove | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to remove the %s role from %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) | ||
| if model.Force { | ||
| prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) | ||
| } | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to remove the %s role from %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) | ||
| if model.Force { | ||
| prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) | ||
| } | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ // Call API |
@@ -65,8 +65,6 @@ package updateschedule | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -73,0 +71,0 @@ |
@@ -81,8 +81,6 @@ package clone | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to clone instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to clone instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -89,0 +87,0 @@ |
@@ -97,8 +97,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a PostgreSQL Flex instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a PostgreSQL Flex instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -105,0 +103,0 @@ |
@@ -74,8 +74,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -91,8 +91,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q? (This may cause downtime)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q? (This may cause downtime)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -99,0 +97,0 @@ |
@@ -78,8 +78,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -86,0 +84,0 @@ |
@@ -75,8 +75,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -74,8 +74,6 @@ package resetpassword | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -73,8 +73,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -81,0 +79,0 @@ |
@@ -80,8 +80,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a project under the parent with ID %q?", *model.ParentId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a project under the parent with ID %q?", *model.ParentId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -88,0 +86,0 @@ |
@@ -58,8 +58,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -66,0 +64,0 @@ |
@@ -74,8 +74,6 @@ package add | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to add the role %q to %s on project %q?", *model.Role, model.Subject, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to add the role %q to %s on project %q?", *model.Role, model.Subject, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -77,12 +77,10 @@ package remove | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to remove the role %q from %s on project %q?", *model.Role, model.Subject, projectLabel) | ||
| if model.Force { | ||
| prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) | ||
| } | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to remove the role %q from %s on project %q?", *model.Role, model.Subject, projectLabel) | ||
| if model.Force { | ||
| prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) | ||
| } | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -89,0 +87,0 @@ // Call API |
@@ -77,8 +77,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -85,0 +83,0 @@ |
@@ -68,8 +68,6 @@ package associate | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to associate public IP %q with resource %v?", publicIpLabel, *model.AssociatedResourceId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to associate public IP %q with resource %v?", publicIpLabel, *model.AssociatedResourceId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -74,8 +74,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a public IP for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a public IP for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -66,8 +66,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete public IP %q? (This cannot be undone)", publicIpLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete public IP %q? (This cannot be undone)", publicIpLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -64,8 +64,6 @@ package disassociate | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disassociate public IP %q from the associated resource %q?", publicIpLabel, associatedResourceId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to disassociate public IP %q from the associated resource %q?", publicIpLabel, associatedResourceId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -72,0 +70,0 @@ |
@@ -69,8 +69,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update public IP %q?", publicIpLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update public IP %q?", publicIpLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -66,8 +66,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -71,8 +71,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -79,0 +77,0 @@ |
@@ -96,8 +96,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an RabbitMQ instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create an RabbitMQ instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -104,0 +102,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -94,8 +94,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -102,0 +100,0 @@ |
@@ -67,8 +67,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -71,8 +71,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -79,0 +77,0 @@ |
@@ -94,8 +94,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Redis instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Redis instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -102,0 +100,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -91,8 +91,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -99,0 +97,0 @@ |
@@ -65,3 +65,5 @@ package cmd | ||
| p.Cmd = cmd | ||
| p.Verbosity = print.Level(globalflags.Parse(p, cmd).Verbosity) | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| p.Verbosity = print.Level(globalFlags.Verbosity) | ||
| p.AssumeYes = globalFlags.AssumeYes | ||
@@ -213,2 +215,3 @@ argsString := print.BuildDebugStrFromSlice(os.Args) | ||
| // PersistentPreRun is not called when the command is wrongly called | ||
| // In this case Printer.AssumeYes isn't set either, but `false` as default is acceptable | ||
| p.Cmd = cmd | ||
@@ -215,0 +218,0 @@ p.Verbosity = print.InfoLevel |
@@ -69,8 +69,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Secrets Manager instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Secrets Manager instance for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -60,8 +60,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -68,0 +66,0 @@ |
@@ -67,8 +67,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -74,8 +74,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -75,8 +75,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %s of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete user %s of instance %q? (This cannot be undone)", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -79,8 +79,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update user %s of instance %q?", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update user %s of instance %q?", userLabel, instanceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -87,0 +85,0 @@ |
@@ -59,8 +59,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create the security group %q?", *model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create the security group %q?", *model.Name) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -67,0 +65,0 @@ |
@@ -63,8 +63,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the security group %q for %q?", groupLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the security group %q for %q?", groupLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -71,0 +69,0 @@ |
@@ -104,8 +104,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a security group rule for security group %q for project %q?", securityGroupLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a security group rule for security group %q for project %q?", securityGroupLabel, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -112,0 +110,0 @@ |
@@ -74,8 +74,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete security group rule %q from security group %q?", securityGroupRuleLabel, securityGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete security group rule %q from security group %q?", securityGroupRuleLabel, securityGroupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -74,8 +74,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update the security group %q?", groupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update the security group %q?", groupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -82,8 +82,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Backup for server %s?", model.ServerId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Backup for server %s?", model.ServerId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -90,0 +88,0 @@ |
@@ -57,8 +57,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server backup %q? (This cannot be undone)", model.BackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server backup %q? (This cannot be undone)", model.BackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -65,0 +63,0 @@ |
@@ -77,8 +77,6 @@ package disable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disable the backup service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to disable the backup service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -85,0 +83,0 @@ |
@@ -68,8 +68,6 @@ package enable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to enable the Server Backup service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to enable the Server Backup service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -66,8 +66,6 @@ package restore | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to restore server backup %q? (This cannot be undone)", model.BackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to restore server backup %q? (This cannot be undone)", model.BackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -90,8 +90,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Backup Schedule for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Backup Schedule for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -98,0 +96,0 @@ |
@@ -69,8 +69,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server backup schedule %q? (This cannot be undone)", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server backup schedule %q? (This cannot be undone)", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -85,8 +85,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update Server Backup Schedule %q?", model.BackupScheduleId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update Server Backup Schedule %q?", model.BackupScheduleId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -93,0 +91,0 @@ |
@@ -59,8 +59,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server volume backup %q? (This cannot be undone)", model.VolumeId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server volume backup %q? (This cannot be undone)", model.VolumeId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -67,0 +65,0 @@ |
@@ -61,8 +61,6 @@ package restore | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to restore volume backup %q? (This cannot be undone)", model.VolumeBackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to restore volume backup %q? (This cannot be undone)", model.VolumeBackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -69,0 +67,0 @@ |
@@ -78,8 +78,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Command for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a Command for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -86,0 +84,0 @@ |
@@ -131,8 +131,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a server for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a server for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -139,0 +137,0 @@ |
@@ -66,8 +66,6 @@ package deallocate | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to deallocate server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to deallocate server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -69,8 +69,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -77,0 +75,0 @@ |
@@ -81,9 +81,8 @@ package attach | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network interface for network %q and attach it to server %q?", networkLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a network interface for network %q and attach it to server %q?", networkLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
@@ -99,9 +98,8 @@ req := buildRequestCreateAndAttach(ctx, model, apiClient) | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to attach network interface %q to server %q?", *model.NicId, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to attach network interface %q to server %q?", *model.NicId, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
@@ -108,0 +106,0 @@ req := buildRequestAttach(ctx, model, apiClient) |
@@ -83,9 +83,8 @@ package detach | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to detach and delete all network interfaces of network %q from server %q? (This cannot be undone)", networkLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to detach and delete all network interfaces of network %q from server %q? (This cannot be undone)", networkLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
@@ -101,9 +100,8 @@ req := buildRequestDetachAndDelete(ctx, model, apiClient) | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to detach network interface %q from server %q?", *model.NicId, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to detach network interface %q from server %q?", *model.NicId, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
@@ -110,0 +108,0 @@ req := buildRequestDetach(ctx, model, apiClient) |
@@ -77,8 +77,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a os-update for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a os-update for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -85,0 +83,0 @@ |
@@ -67,8 +67,6 @@ package disable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disable the os-update service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to disable the os-update service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -68,8 +68,6 @@ package enable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to enable the server os-update service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to enable the server os-update service for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -85,8 +85,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a os-update Schedule for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a os-update Schedule for server %s?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -93,0 +91,0 @@ |
@@ -56,8 +56,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server os-update schedule %q? (This cannot be undone)", model.ScheduleId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete server os-update schedule %q? (This cannot be undone)", model.ScheduleId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -64,0 +62,0 @@ |
@@ -77,8 +77,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update Server os-update Schedule %q?", model.ScheduleId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update Server os-update Schedule %q?", model.ScheduleId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -85,0 +83,0 @@ |
@@ -74,8 +74,6 @@ package attach | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to attach public IP %q to server %q?", publicIpLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to attach public IP %q to server %q?", publicIpLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -75,8 +75,6 @@ package detach | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to detach public IP %q from server %q?", publicIpLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to detach public IP %q from server %q?", publicIpLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -73,8 +73,6 @@ package reboot | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to reboot server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to reboot server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -81,0 +79,0 @@ |
@@ -70,8 +70,6 @@ package rescue | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to rescue server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to rescue server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -78,0 +76,0 @@ |
@@ -70,8 +70,6 @@ package resize | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to resize server %q to machine type %q?", serverLabel, *model.MachineType) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to resize server %q to machine type %q?", serverLabel, *model.MachineType) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -78,0 +76,0 @@ |
@@ -66,8 +66,6 @@ package attach | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to attach service account %q to server %q?", model.ServiceAccMail, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to attach service account %q to server %q?", model.ServiceAccMail, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -66,8 +66,6 @@ package detach | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are your sure you want to detach service account %q from a server %q?", model.ServiceAccMail, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are your sure you want to detach service account %q from a server %q?", model.ServiceAccMail, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -66,8 +66,6 @@ package stop | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to stop server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to stop server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -66,8 +66,6 @@ package unrescue | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to unrescue server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to unrescue server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -73,8 +73,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update server %q?", serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -81,0 +79,0 @@ |
@@ -81,8 +81,6 @@ package attach | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to attach volume %q to server %q?", volumeLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to attach volume %q to server %q?", volumeLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -89,0 +87,0 @@ |
@@ -73,8 +73,6 @@ package detach | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to detach volume %q from server %q?", volumeLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to detach volume %q from server %q?", volumeLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -81,0 +79,0 @@ |
@@ -77,8 +77,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update attached volume %q of server %q?", volumeLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update attached volume %q of server %q?", volumeLabel, serverLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -85,0 +83,0 @@ |
@@ -62,8 +62,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a service account for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a service account for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -70,0 +68,0 @@ |
@@ -53,8 +53,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete service account %s? (This cannot be undone)", model.Email) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete service account %s? (This cannot be undone)", model.Email) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -61,0 +59,0 @@ |
@@ -72,13 +72,11 @@ package create | ||
| if !model.AssumeYes { | ||
| validUntilInfo := "The key will be valid until deleted" | ||
| if model.ExpiresInDays != nil { | ||
| validUntilInfo = fmt.Sprintf("The key will be valid for %d days", *model.ExpiresInDays) | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a key for service account %s? %s", model.ServiceAccountEmail, validUntilInfo) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| validUntilInfo := "The key will be valid until deleted" | ||
| if model.ExpiresInDays != nil { | ||
| validUntilInfo = fmt.Sprintf("The key will be valid for %d days", *model.ExpiresInDays) | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a key for service account %s? %s", model.ServiceAccountEmail, validUntilInfo) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -85,0 +83,0 @@ // Call API |
@@ -59,8 +59,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the key %s from service account %s?", model.KeyId, model.ServiceAccountEmail) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete the key %s from service account %s?", model.KeyId, model.ServiceAccountEmail) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -67,0 +65,0 @@ |
@@ -76,8 +76,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update the key with ID %q?", model.KeyId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update the key with ID %q?", model.KeyId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -84,0 +82,0 @@ |
@@ -66,8 +66,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create an access token for service account %s?", model.ServiceAccountEmail) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create an access token for service account %s?", model.ServiceAccountEmail) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -62,8 +62,6 @@ package revoke | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to revoke the access token with ID %q?", model.TokenId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to revoke the access token with ID %q?", model.TokenId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -70,0 +68,0 @@ |
@@ -85,8 +85,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a cluster for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a cluster for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -93,0 +91,0 @@ |
@@ -55,8 +55,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete cluster %q? (This cannot be undone)", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete cluster %q? (This cannot be undone)", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -63,0 +61,0 @@ |
@@ -60,8 +60,6 @@ package hibernate | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to trigger hibernate for %q in project %q?", model.ClusterName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to trigger hibernate for %q in project %q?", model.ClusterName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -68,0 +66,0 @@ |
@@ -60,8 +60,6 @@ package maintenance | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to trigger maintenance for %q in project %q?", model.ClusterName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to trigger maintenance for %q in project %q?", model.ClusterName, projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -68,0 +66,0 @@ |
@@ -73,8 +73,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update cluster %q?", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update cluster %q?", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -81,0 +79,0 @@ |
@@ -72,8 +72,6 @@ package completerotation | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to complete the rotation of the credentials for SKE cluster %q?", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to complete the rotation of the credentials for SKE cluster %q?", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -80,0 +78,0 @@ |
@@ -75,8 +75,6 @@ package startrotation | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to start the rotation of the credentials for SKE cluster %q?", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to start the rotation of the credentials for SKE cluster %q?", model.ClusterName) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -83,0 +81,0 @@ |
@@ -58,8 +58,6 @@ package disable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to disable SKE for project %q? (This will delete all associated clusters)", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to disable SKE for project %q? (This will delete all associated clusters)", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -66,0 +64,0 @@ |
@@ -58,8 +58,6 @@ package enable | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to enable SKE for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to enable SKE for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -66,0 +64,0 @@ |
@@ -93,3 +93,3 @@ package create | ||
| if !model.AssumeYes && !model.DisableWriting { | ||
| if !model.DisableWriting { | ||
| var prompt string | ||
@@ -96,0 +96,0 @@ if model.Overwrite { |
@@ -22,2 +22,3 @@ package login | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/auth" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
@@ -154,4 +155,4 @@ "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| } | ||
| config := &clusterConfig{} | ||
| err = json.Unmarshal(execCredential.Spec.Cluster.Config.Raw, config) | ||
| clusterConfig := &clusterConfig{} | ||
| err = json.Unmarshal(execCredential.Spec.Cluster.Config.Raw, clusterConfig) | ||
| if err != nil { | ||
@@ -161,10 +162,15 @@ return nil, fmt.Errorf("unmarshal: %w", err) | ||
| config.cacheKey = fmt.Sprintf("ske-login-%x", sha256.Sum256([]byte(execCredential.Spec.Cluster.Server))) | ||
| authEmail, err := auth.GetAuthEmail() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("error getting auth email: %w", err) | ||
| } | ||
| clusterConfig.cacheKey = fmt.Sprintf("ske-login-%x", sha256.Sum256([]byte(execCredential.Spec.Cluster.Server+"\x00"+authEmail))) | ||
| // NOTE: Fallback if region is not set in the kubeconfig (this was the case in the past) | ||
| if config.Region == "" { | ||
| config.Region = globalflags.Parse(p, cmd).Region | ||
| if clusterConfig.Region == "" { | ||
| clusterConfig.Region = globalflags.Parse(p, cmd).Region | ||
| } | ||
| return config, nil | ||
| return clusterConfig, nil | ||
| } | ||
@@ -171,0 +177,0 @@ |
@@ -99,8 +99,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", sourceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", sourceLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -107,0 +105,0 @@ |
@@ -61,8 +61,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete backup %q? (This cannot be undone)", backupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete backup %q? (This cannot be undone)", backupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -69,0 +67,0 @@ |
@@ -74,8 +74,6 @@ package restore | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to restore %q with backup %q? (This cannot be undone)", sourceLabel, backupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to restore %q with backup %q? (This cannot be undone)", sourceLabel, backupLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -82,0 +80,0 @@ |
@@ -68,8 +68,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update backup %q? (This cannot be undone)", model.BackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update backup %q? (This cannot be undone)", model.BackupId) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -76,0 +74,0 @@ |
@@ -91,8 +91,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create a volume for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create a volume for project %q?", projectLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -99,0 +97,0 @@ |
@@ -67,8 +67,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete volume %q?", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete volume %q?", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -75,0 +73,0 @@ |
@@ -66,8 +66,6 @@ package resize | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to resize volume %q?", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to resize volume %q?", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -74,0 +72,0 @@ |
@@ -82,8 +82,6 @@ package create | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to create snapshot from volume %q? (This cannot be undone)", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to create snapshot from volume %q? (This cannot be undone)", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -90,0 +88,0 @@ |
@@ -64,8 +64,6 @@ package delete | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q? (This cannot be undone)", snapshotLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q? (This cannot be undone)", snapshotLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -72,0 +70,0 @@ |
@@ -70,8 +70,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update snapshot %q?", snapshotLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update snapshot %q?", snapshotLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -78,0 +76,0 @@ |
@@ -77,8 +77,6 @@ package update | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to update volume %q?", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| prompt := fmt.Sprintf("Are you sure you want to update volume %q?", volumeLabel) | ||
| err = params.Printer.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
@@ -85,0 +83,0 @@ |
@@ -28,2 +28,3 @@ package config | ||
| DNSCustomEndpointKey = "dns_custom_endpoint" | ||
| EdgeCustomEndpointKey = "edge_custom_endpoint" | ||
| LoadBalancerCustomEndpointKey = "load_balancer_custom_endpoint" | ||
@@ -89,2 +90,3 @@ LogMeCustomEndpointKey = "logme_custom_endpoint" | ||
| DNSCustomEndpointKey, | ||
| EdgeCustomEndpointKey, | ||
| LoadBalancerCustomEndpointKey, | ||
@@ -184,2 +186,3 @@ LogMeCustomEndpointKey, | ||
| viper.SetDefault(DNSCustomEndpointKey, "") | ||
| viper.SetDefault(EdgeCustomEndpointKey, "") | ||
| viper.SetDefault(ObservabilityCustomEndpointKey, "") | ||
@@ -186,0 +189,0 @@ viper.SetDefault(AuthorizationCustomEndpointKey, "") |
@@ -6,2 +6,3 @@ { | ||
| "dns_custom_endpoint": "", | ||
| "edge_custom_endpoint": "", | ||
| "iaas_custom_endpoint": "", | ||
@@ -35,2 +36,2 @@ "identity_provider_custom_client_id": "", | ||
| "verbosity": "info" | ||
| } | ||
| } |
@@ -9,2 +9,3 @@ package errors | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| ) | ||
@@ -17,2 +18,9 @@ | ||
| var ( | ||
| testErrorMessage = "test error message" | ||
| errStringErrTest = errors.New(testErrorMessage) | ||
| errOpenApi404 = &oapierror.GenericOpenAPIError{StatusCode: 404, Body: []byte(`{"message":"not found"}`)} | ||
| errOpenApi500 = &oapierror.GenericOpenAPIError{StatusCode: 500, Body: []byte(`invalid-json`)} | ||
| ) | ||
| func setupCmd() { | ||
@@ -691,1 +699,236 @@ cmd = &cobra.Command{ | ||
| } | ||
| func TestInvalidFormatError(t *testing.T) { | ||
| type args struct { | ||
| format string | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| format: "", | ||
| }, | ||
| want: "unsupported format provided", | ||
| }, | ||
| { | ||
| name: "with format", | ||
| args: args{ | ||
| format: "yaml", | ||
| }, | ||
| want: "unsupported format provided: yaml", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := (&InvalidFormatError{Format: tt.args.format}).Error() | ||
| if got != tt.want { | ||
| t.Errorf("got %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequestError(t *testing.T) { | ||
| type args struct { | ||
| reason string | ||
| err error | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "empty", | ||
| args: args{ | ||
| reason: "", | ||
| err: nil, | ||
| }, | ||
| want: "could not build request", | ||
| }, | ||
| { | ||
| name: "reason only", | ||
| args: args{ | ||
| reason: testErrorMessage, | ||
| err: nil, | ||
| }, | ||
| want: fmt.Sprintf("could not build request: %s", testErrorMessage), | ||
| }, | ||
| { | ||
| name: "error only", | ||
| args: args{ | ||
| reason: "", | ||
| err: errStringErrTest, | ||
| }, | ||
| want: fmt.Sprintf("could not build request: %s", testErrorMessage), | ||
| }, | ||
| { | ||
| name: "reason and error", | ||
| args: args{ | ||
| reason: testErrorMessage, | ||
| err: errStringErrTest, | ||
| }, | ||
| want: fmt.Sprintf("could not build request (%s): %s", testErrorMessage, testErrorMessage), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := (&BuildRequestError{Reason: tt.args.reason, Err: tt.args.err}).Error() | ||
| if got != tt.want { | ||
| t.Errorf("got %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestRequestFailedError(t *testing.T) { | ||
| type args struct { | ||
| err error | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "nil underlying", | ||
| args: args{ | ||
| err: nil, | ||
| }, | ||
| want: "request failed", | ||
| }, | ||
| { | ||
| name: "non-openapi error", | ||
| args: args{ | ||
| err: errStringErrTest, | ||
| }, | ||
| want: fmt.Sprintf("request failed: %s", testErrorMessage), | ||
| }, | ||
| { | ||
| name: "openapi error with message", | ||
| args: args{ | ||
| err: errOpenApi404, | ||
| }, | ||
| want: "request failed (404): not found", | ||
| }, | ||
| { | ||
| name: "openapi error without message", | ||
| args: args{ | ||
| err: errOpenApi500, | ||
| }, | ||
| want: "request failed (500): invalid-json", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := (&RequestFailedError{Err: tt.args.err}).Error() | ||
| if got != tt.want { | ||
| t.Errorf("got %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestExtractMessageFromBody(t *testing.T) { | ||
| type args struct { | ||
| body []byte | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args args | ||
| want string | ||
| }{ | ||
| { | ||
| name: "empty body", | ||
| args: args{ | ||
| body: []byte(""), | ||
| }, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "invalid json", | ||
| args: args{ | ||
| body: []byte("not-json"), | ||
| }, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "missing message field", | ||
| args: args{ | ||
| body: []byte(`{"error":"oops"}`), | ||
| }, | ||
| want: "", | ||
| }, | ||
| { | ||
| name: "with message field", | ||
| args: args{ | ||
| body: []byte(`{"message":"the reason"}`), | ||
| }, | ||
| want: "the reason", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := extractOpenApiMessageFromBody(tt.args.body) | ||
| if got != tt.want { | ||
| t.Errorf("got %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestConstructorsReturnExpected(t *testing.T) { | ||
| buildRequestError := NewBuildRequestError(testErrorMessage, errStringErrTest) | ||
| tests := []struct { | ||
| name string | ||
| got any | ||
| want any | ||
| }{ | ||
| { | ||
| name: "InvalidFormat format", | ||
| got: NewInvalidFormatError("fmt").Format, | ||
| want: "fmt", | ||
| }, | ||
| { | ||
| name: "BuildRequestError error", | ||
| got: buildRequestError.Err, | ||
| want: errStringErrTest, | ||
| }, | ||
| { | ||
| name: "BuildRequestError reason", | ||
| got: buildRequestError.Reason, | ||
| want: testErrorMessage, | ||
| }, | ||
| { | ||
| name: "RequestFailed error", | ||
| got: NewRequestFailedError(errStringErrTest).Err, | ||
| want: errStringErrTest, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| wantErr, wantIsErr := tt.want.(error) | ||
| gotErr, gotIsErr := tt.got.(error) | ||
| if wantIsErr { | ||
| if !gotIsErr { | ||
| t.Fatalf("expected error but got %T", tt.got) | ||
| } | ||
| if !errors.Is(gotErr, wantErr) { | ||
| t.Errorf("got error %v, want %v", gotErr, wantErr) | ||
| } | ||
| return | ||
| } | ||
| if tt.got != tt.want { | ||
| t.Errorf("got %v, want %v", tt.got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package errors | ||
| import ( | ||
| "encoding/json" | ||
| sysErrors "errors" | ||
| "fmt" | ||
@@ -8,2 +10,3 @@ "strings" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-sdk-go/core/oapierror" | ||
| ) | ||
@@ -552,1 +555,134 @@ | ||
| } | ||
| // ___FORMATTING_ERRORS_________________________________________________________ | ||
| // InvalidFormatError indicates that an unsupported format was provided. | ||
| type InvalidFormatError struct { | ||
| Format string // The invalid format that was provided | ||
| } | ||
| func (e *InvalidFormatError) Error() string { | ||
| if e.Format != "" { | ||
| return fmt.Sprintf("unsupported format provided: %s", e.Format) | ||
| } | ||
| return "unsupported format provided" | ||
| } | ||
| // NewInvalidFormatError creates a new InvalidFormatError with the provided format. | ||
| func NewInvalidFormatError(format string) *InvalidFormatError { | ||
| return &InvalidFormatError{ | ||
| Format: format, | ||
| } | ||
| } | ||
| // ___BUILD_REQUEST_ERRORS______________________________________________________ | ||
| // BuildRequestError indicates that a request could not be built. | ||
| type BuildRequestError struct { | ||
| Reason string // Optional: specific reason why the request failed to build | ||
| Err error // Optional: underlying error | ||
| } | ||
| func (e *BuildRequestError) Error() string { | ||
| if e.Reason != "" && e.Err != nil { | ||
| return fmt.Sprintf("could not build request (%s): %v", e.Reason, e.Err) | ||
| } | ||
| if e.Reason != "" { | ||
| return fmt.Sprintf("could not build request: %s", e.Reason) | ||
| } | ||
| if e.Err != nil { | ||
| return fmt.Sprintf("could not build request: %v", e.Err) | ||
| } | ||
| return "could not build request" | ||
| } | ||
| func (e *BuildRequestError) Unwrap() error { | ||
| return e.Err | ||
| } | ||
| // NewBuildRequestError creates a new BuildRequestError with optional reason and underlying error. | ||
| func NewBuildRequestError(reason string, err error) *BuildRequestError { | ||
| return &BuildRequestError{ | ||
| Reason: reason, | ||
| Err: err, | ||
| } | ||
| } | ||
| // ___REQUESTS_ERRORS___________________________________________________________ | ||
| // RequestFailedError indicates that an API request failed. | ||
| // If the provided error is an OpenAPI error, the status code and message from the error body will be included in the error message. | ||
| type RequestFailedError struct { | ||
| Err error // Optional: underlying error | ||
| } | ||
| func (e *RequestFailedError) Error() string { | ||
| var msg = "request failed" | ||
| if e.Err != nil { | ||
| var oApiErr *oapierror.GenericOpenAPIError | ||
| if sysErrors.As(e.Err, &oApiErr) { | ||
| // Extract status code from OpenAPI error header if it exists | ||
| if oApiErr.StatusCode > 0 { | ||
| msg += fmt.Sprintf(" (%d)", oApiErr.StatusCode) | ||
| } | ||
| // Try to extract message from OpenAPI error body | ||
| if bodyMsg := extractOpenApiMessageFromBody(oApiErr.Body); bodyMsg != "" { | ||
| msg += fmt.Sprintf(": %s", bodyMsg) | ||
| } else if trimmedBody := strings.TrimSpace(string(oApiErr.Body)); trimmedBody != "" { | ||
| msg += fmt.Sprintf(": %s", trimmedBody) | ||
| } else { | ||
| // Otherwise use the Go error | ||
| msg += fmt.Sprintf(": %v", e.Err) | ||
| } | ||
| } else { | ||
| // If this can't be cased into a OpenApi error use the Go error | ||
| msg += fmt.Sprintf(": %v", e.Err) | ||
| } | ||
| } | ||
| return msg | ||
| } | ||
| func (e *RequestFailedError) Unwrap() error { | ||
| return e.Err | ||
| } | ||
| // NewRequestFailedError creates a new RequestFailedError with optional details. | ||
| func NewRequestFailedError(err error) *RequestFailedError { | ||
| return &RequestFailedError{ | ||
| Err: err, | ||
| } | ||
| } | ||
| // ___HELPERS___________________________________________________________________ | ||
| // extractOpenApiMessageFromBody attempts to parse a JSON body and extract the "message" | ||
| // field. It returns an empty string if parsing fails or if no message is found. | ||
| func extractOpenApiMessageFromBody(body []byte) string { | ||
| trimmedBody := strings.TrimSpace(string(body)) | ||
| // Return early if empty. | ||
| if trimmedBody == "" { | ||
| return "" | ||
| } | ||
| // Try to unmarshal as a structured error first | ||
| var errorBody struct { | ||
| Message string `json:"message"` | ||
| } | ||
| if err := json.Unmarshal(body, &errorBody); err == nil && errorBody.Message != "" { | ||
| if msg := strings.TrimSpace(errorBody.Message); msg != "" { | ||
| return msg | ||
| } | ||
| } | ||
| // If that fails, try to unmarshal as a plain string | ||
| var plainBody string | ||
| if err := json.Unmarshal(body, &plainBody); err == nil && plainBody != "" { | ||
| if msg := strings.TrimSpace(plainBody); msg != "" { | ||
| return msg | ||
| } | ||
| return "" | ||
| } | ||
| // All parsing attempts failed or yielded no message | ||
| return "" | ||
| } |
@@ -512,2 +512,3 @@ package print | ||
| isAborted bool | ||
| assumeYes bool | ||
| }{ | ||
@@ -651,2 +652,9 @@ // Note: Some of these inputs have normal spaces, others have tabs | ||
| }, | ||
| { | ||
| description: "no input with assume yes", | ||
| input: "", | ||
| verbosity: DebugLevel, | ||
| isValid: true, | ||
| assumeYes: true, | ||
| }, | ||
| } | ||
@@ -670,2 +678,3 @@ | ||
| Verbosity: tt.verbosity, | ||
| AssumeYes: tt.assumeYes, | ||
| } | ||
@@ -672,0 +681,0 @@ |
@@ -5,2 +5,3 @@ package print | ||
| "bufio" | ||
| "bytes" | ||
| "encoding/json" | ||
@@ -55,2 +56,3 @@ "errors" | ||
| Cmd *cobra.Command | ||
| AssumeYes bool | ||
| Verbosity Level | ||
@@ -139,2 +141,6 @@ } | ||
| func (p *Printer) PromptForConfirmation(prompt string) error { | ||
| if p.AssumeYes { | ||
| p.Warn("Auto-confirming prompt: %q\n", prompt) | ||
| return nil | ||
| } | ||
| question := fmt.Sprintf("%s [y/N] ", prompt) | ||
@@ -163,2 +169,6 @@ reader := bufio.NewReader(p.Cmd.InOrStdin()) | ||
| func (p *Printer) PromptForEnter(prompt string) error { | ||
| if p.AssumeYes { | ||
| p.Warn("Auto-confirming prompt: %q", prompt) | ||
| return nil | ||
| } | ||
| reader := bufio.NewReader(p.Cmd.InOrStdin()) | ||
@@ -254,6 +264,11 @@ p.Cmd.PrintErr(prompt) | ||
| case JSONOutputFormat: | ||
| details, err := json.MarshalIndent(output, "", " ") | ||
| buffer := &bytes.Buffer{} | ||
| encoder := json.NewEncoder(buffer) | ||
| encoder.SetEscapeHTML(false) | ||
| encoder.SetIndent("", " ") | ||
| err := encoder.Encode(output) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal json: %w", err) | ||
| } | ||
| details := buffer.Bytes() | ||
| p.Outputln(string(details)) | ||
@@ -260,0 +275,0 @@ |
Sorry, the diff of this file is too big to display