🚀 Big News:Socket Has Acquired Secure Annex.Learn More
Socket
Book a DemoSign in
Socket

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli

Package Overview
Dependencies
Versions
178
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mygithub.libinneed.workers.dev/stackitcloud/stackit-cli - go Package Compare versions

Comparing version
v0.50.1-0.20251219154613-c7dada1356cd
to
v0.51.0
+43
docs/stackit_beta_edge-cloud_instance_create.md
## 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...)
}
+1
-1

@@ -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 }}

@@ -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

@@ -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