mygithub.libinneed.workers.dev/stackitcloud/stackit-cli
Advanced tools
| ## stackit beta server network-interface attach | ||
| Attaches a network interface to a server | ||
| ### Synopsis | ||
| Attaches a network interface to a server. | ||
| ``` | ||
| stackit beta server network-interface attach [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Attach a network interface with ID "xxx" to a server with ID "yyy" | ||
| $ stackit beta server network-interface attach --network-interface-id xxx --server-id yyy | ||
| Create a network interface for network with ID "xxx" and attach it to a server with ID "yyy" | ||
| $ stackit beta server network-interface attach --network-id xxx --server-id yyy --create | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -b, --create If this is set a network interface will be created. (default false) | ||
| -h, --help Help for "stackit beta server network-interface attach" | ||
| --network-id string Network ID | ||
| --network-interface-id string Network Interface ID | ||
| --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server network-interface](./stackit_beta_server_network-interface.md) - Allows attaching/detaching network interfaces to servers | ||
| ## stackit beta server network-interface detach | ||
| Detaches a network interface from a server | ||
| ### Synopsis | ||
| Detaches a network interface from a server. | ||
| ``` | ||
| stackit beta server network-interface detach [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Detach a network interface with ID "xxx" from a server with ID "yyy" | ||
| $ stackit beta server network-interface detach --network-interface-id xxx --server-id yyy | ||
| Detach and delete all network interfaces for network with ID "xxx" and detach them from a server with ID "yyy" | ||
| $ stackit beta server network-interface detach --network-id xxx --server-id yyy --delete | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -b, --delete If this is set all network interfaces will be deleted. (default false) | ||
| -h, --help Help for "stackit beta server network-interface detach" | ||
| --network-id string Network ID | ||
| --network-interface-id string Network Interface ID | ||
| --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server network-interface](./stackit_beta_server_network-interface.md) - Allows attaching/detaching network interfaces to servers | ||
| ## stackit beta server network-interface list | ||
| Lists all attached network interfaces of a server | ||
| ### Synopsis | ||
| Lists all attached network interfaces of a server. | ||
| ``` | ||
| stackit beta server network-interface list [flags] | ||
| ``` | ||
| ### Examples | ||
| ``` | ||
| Lists all attached network interfaces of server with ID "xxx" | ||
| $ stackit beta server network-interface list --server-id xxx | ||
| Lists all attached network interfaces of server with ID "xxx" in JSON format | ||
| $ stackit beta server network-interface list --server-id xxx --output-format json | ||
| Lists up to 10 attached network interfaces of server with ID "xxx" | ||
| $ stackit beta server network-interface list --server-id xxx --limit 10 | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server network-interface list" | ||
| --limit int Maximum number of entries to list | ||
| --server-id string Server ID | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server network-interface](./stackit_beta_server_network-interface.md) - Allows attaching/detaching network interfaces to servers | ||
| ## stackit beta server network-interface | ||
| Allows attaching/detaching network interfaces to servers | ||
| ### Synopsis | ||
| Allows attaching/detaching network interfaces to servers. | ||
| ``` | ||
| stackit beta server network-interface [flags] | ||
| ``` | ||
| ### Options | ||
| ``` | ||
| -h, --help Help for "stackit beta server network-interface" | ||
| ``` | ||
| ### Options inherited from parent commands | ||
| ``` | ||
| -y, --assume-yes If set, skips all confirmation prompts | ||
| --async If set, runs the command asynchronously | ||
| -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] | ||
| -p, --project-id string Project ID | ||
| --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") | ||
| ``` | ||
| ### SEE ALSO | ||
| * [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers | ||
| * [stackit beta server network-interface attach](./stackit_beta_server_network-interface_attach.md) - Attaches a network interface to a server | ||
| * [stackit beta server network-interface detach](./stackit_beta_server_network-interface_detach.md) - Detaches a network interface from a server | ||
| * [stackit beta server network-interface list](./stackit_beta_server_network-interface_list.md) - Lists all attached network interfaces of a server | ||
| package attach | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testNicId = uuid.NewString() | ||
| var testNetworkId = uuid.NewString() | ||
| // contains nic id | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| networkInterfaceIdFlag: testNicId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| ServerId: utils.Ptr(testServerId), | ||
| NicId: utils.Ptr(testNicId), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequestAttach(mods ...func(request *iaas.ApiAddNicToServerRequest)) iaas.ApiAddNicToServerRequest { | ||
| request := testClient.AddNicToServer(testCtx, testProjectId, testServerId, testNicId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixtureRequestCreateAndAttach(mods ...func(request *iaas.ApiAddNetworkToServerRequest)) iaas.ApiAddNetworkToServerRequest { | ||
| request := testClient.AddNetworkToServer(testCtx, testProjectId, testServerId, testNetworkId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, serverIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // only create | ||
| { | ||
| description: "provided flags invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[createFlag] = "true" | ||
| delete(flagValues, networkInterfaceIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // only network id | ||
| { | ||
| description: "provided flags invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, networkInterfaceIdFlag) | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // create and nic id | ||
| { | ||
| description: "provided flags invalid 3", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[createFlag] = "true" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // create and network id (valid) | ||
| { | ||
| description: "provided flags valid", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[createFlag] = "true" | ||
| delete(flagValues, networkInterfaceIdFlag) | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Create = utils.Ptr(true) | ||
| model.NicId = nil | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| }), | ||
| }, | ||
| // create, nic id and network id | ||
| { | ||
| description: "provided flags invalid 4", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[createFlag] = "true" | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: false, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Create = utils.Ptr(true) | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| }), | ||
| }, | ||
| // network id and nic id | ||
| { | ||
| description: "provided flags invalid 5", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: false, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| }), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateFlagGroups() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flag groups: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequestAttach(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiAddNicToServerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequestAttach(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequestAttach(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequestCreateAndAttach(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiAddNetworkToServerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.NicId = nil | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| model.Create = utils.Ptr(true) | ||
| }), | ||
| expectedRequest: fixtureRequestCreateAndAttach(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequestCreateAndAttach(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package attach | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| 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/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| serverIdFlag = "server-id" | ||
| networkInterfaceIdFlag = "network-interface-id" | ||
| createFlag = "create" | ||
| networkIdFlag = "network-id" | ||
| defaultCreateFlag = false | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId *string | ||
| NicId *string | ||
| NetworkId *string | ||
| Create *bool | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "attach", | ||
| Short: "Attaches a network interface to a server", | ||
| Long: "Attaches a network interface to a server.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Attach a network interface with ID "xxx" to a server with ID "yyy"`, | ||
| `$ stackit beta server network-interface attach --network-interface-id xxx --server-id yyy`, | ||
| ), | ||
| examples.NewExample( | ||
| `Create a network interface for network with ID "xxx" and attach it to a server with ID "yyy"`, | ||
| `$ stackit beta server network-interface attach --network-id xxx --server-id yyy --create`, | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = *model.ServerId | ||
| } | ||
| // if the create flag is provided a network interface will be created and attached | ||
| if model.Create != nil && *model.Create { | ||
| networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, *model.NetworkId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get network name: %v", err) | ||
| networkLabel = *model.NetworkId | ||
| } | ||
| 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 = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequestCreateAndAttach(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("create and attach network interface: %w", err) | ||
| } | ||
| p.Info("Created a network interface for network %q and attached it to server %q\n", networkLabel, serverLabel) | ||
| return nil | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to attach network interface %q to server %q?", *model.NicId, serverLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequestAttach(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("attach network interface: %w", err) | ||
| } | ||
| p.Info("Attached network interface %q to server %q\n", *model.NicId, serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), serverIdFlag, "Server ID") | ||
| cmd.Flags().Var(flags.UUIDFlag(), networkInterfaceIdFlag, "Network Interface ID") | ||
| cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID") | ||
| cmd.Flags().BoolP(createFlag, "b", defaultCreateFlag, "If this is set a network interface will be created. (default false)") | ||
| cmd.MarkFlagsRequiredTogether(createFlag, networkIdFlag) | ||
| cmd.MarkFlagsMutuallyExclusive(createFlag, networkInterfaceIdFlag) | ||
| cmd.MarkFlagsMutuallyExclusive(networkIdFlag, networkInterfaceIdFlag) | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| // if create is not provided then network-interface-id is needed | ||
| networkInterfaceId := flags.FlagToStringPointer(p, cmd, networkInterfaceIdFlag) | ||
| create := flags.FlagToBoolPointer(p, cmd, createFlag) | ||
| if create == nil && networkInterfaceId == nil { | ||
| return nil, &cliErr.ServerNicAttachMissingNicIdError{Cmd: cmd} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), | ||
| NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), | ||
| NicId: networkInterfaceId, | ||
| Create: create, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequestAttach(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddNicToServerRequest { | ||
| return apiClient.AddNicToServer(ctx, model.ProjectId, *model.ServerId, *model.NicId) | ||
| } | ||
| func buildRequestCreateAndAttach(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddNetworkToServerRequest { | ||
| return apiClient.AddNetworkToServer(ctx, model.ProjectId, *model.ServerId, *model.NetworkId) | ||
| } |
| package detach | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| var testNicId = uuid.NewString() | ||
| var testNetworkId = uuid.NewString() | ||
| // contains nic id | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| serverIdFlag: testServerId, | ||
| networkInterfaceIdFlag: testNicId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| ServerId: utils.Ptr(testServerId), | ||
| NicId: utils.Ptr(testNicId), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequestDetach(mods ...func(request *iaas.ApiRemoveNicFromServerRequest)) iaas.ApiRemoveNicFromServerRequest { | ||
| request := testClient.RemoveNicFromServer(testCtx, testProjectId, testServerId, testNicId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func fixtureRequestDetachAndDelete(mods ...func(request *iaas.ApiRemoveNetworkFromServerRequest)) iaas.ApiRemoveNetworkFromServerRequest { | ||
| request := testClient.RemoveNetworkFromServer(testCtx, testProjectId, testServerId, testNetworkId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, serverIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // only delete | ||
| { | ||
| description: "provided flags invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[deleteFlag] = "true" | ||
| delete(flagValues, networkInterfaceIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // only network id | ||
| { | ||
| description: "provided flags invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, networkInterfaceIdFlag) | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // delete and nic id | ||
| { | ||
| description: "provided flags invalid 3", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[deleteFlag] = "true" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| // delete and network id (valid) | ||
| { | ||
| description: "provided flags valid", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[deleteFlag] = "true" | ||
| delete(flagValues, networkInterfaceIdFlag) | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Delete = utils.Ptr(true) | ||
| model.NicId = nil | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| }), | ||
| }, | ||
| // delete, nic id and network id | ||
| { | ||
| description: "provided flags invalid 4", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[deleteFlag] = "true" | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: false, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.Delete = utils.Ptr(true) | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| }), | ||
| }, | ||
| // network id and nic id | ||
| { | ||
| description: "provided flags invalid 5", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[networkIdFlag] = testNetworkId | ||
| }), | ||
| isValid: false, | ||
| expectedModel: fixtureInputModel(func(model *inputModel) { | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| }), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateFlagGroups() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flag groups: %v", err) | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequestDetach(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiRemoveNicFromServerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequestDetach(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequestDetach(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequestDetachAndDelete(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiRemoveNetworkFromServerRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(func(model *inputModel) { | ||
| model.NicId = nil | ||
| model.NetworkId = utils.Ptr(testNetworkId) | ||
| model.Delete = utils.Ptr(true) | ||
| }), | ||
| expectedRequest: fixtureRequestDetachAndDelete(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequestDetachAndDelete(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package detach | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "github.com/spf13/cobra" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| 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/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| const ( | ||
| serverIdFlag = "server-id" | ||
| networkInterfaceIdFlag = "network-interface-id" | ||
| networkIdFlag = "network-id" | ||
| deleteFlag = "delete" | ||
| defaultDeleteFlag = false | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId *string | ||
| NicId *string | ||
| NetworkId *string | ||
| Delete *bool | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "detach", | ||
| Short: "Detaches a network interface from a server", | ||
| Long: "Detaches a network interface from a server.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Detach a network interface with ID "xxx" from a server with ID "yyy"`, | ||
| `$ stackit beta server network-interface detach --network-interface-id xxx --server-id yyy`, | ||
| ), | ||
| examples.NewExample( | ||
| `Detach and delete all network interfaces for network with ID "xxx" and detach them from a server with ID "yyy"`, | ||
| `$ stackit beta server network-interface detach --network-id xxx --server-id yyy --delete`, | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = *model.ServerId | ||
| } | ||
| // if the delete flag is provided a network interface is detached and deleted | ||
| if model.Delete != nil && *model.Delete { | ||
| networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, *model.NetworkId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get network name: %v", err) | ||
| networkLabel = *model.NetworkId | ||
| } | ||
| 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 = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequestDetachAndDelete(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("detach and delete network interfaces: %w", err) | ||
| } | ||
| p.Info("Detached and deleted all network interfaces of network %q from server %q\n", networkLabel, serverLabel) | ||
| return nil | ||
| } | ||
| if !model.AssumeYes { | ||
| prompt := fmt.Sprintf("Are you sure you want to detach network interface %q from server %q?", *model.NicId, serverLabel) | ||
| err = p.PromptForConfirmation(prompt) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
| // Call API | ||
| req := buildRequestDetach(ctx, model, apiClient) | ||
| err = req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("detach network interface: %w", err) | ||
| } | ||
| p.Info("Detached network interface %q from server %q\n", *model.NicId, serverLabel) | ||
| return nil | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), serverIdFlag, "Server ID") | ||
| cmd.Flags().Var(flags.UUIDFlag(), networkInterfaceIdFlag, "Network Interface ID") | ||
| cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID") | ||
| cmd.Flags().BoolP(deleteFlag, "b", defaultDeleteFlag, "If this is set all network interfaces will be deleted. (default false)") | ||
| cmd.MarkFlagsRequiredTogether(deleteFlag, networkIdFlag) | ||
| cmd.MarkFlagsMutuallyExclusive(deleteFlag, networkInterfaceIdFlag) | ||
| cmd.MarkFlagsMutuallyExclusive(networkIdFlag, networkInterfaceIdFlag) | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| // if delete is not provided then network-interface-id is needed | ||
| networkInterfaceId := flags.FlagToStringPointer(p, cmd, networkInterfaceIdFlag) | ||
| deleteValue := flags.FlagToBoolPointer(p, cmd, deleteFlag) | ||
| if deleteValue == nil && networkInterfaceId == nil { | ||
| return nil, &cliErr.ServerNicDetachMissingNicIdError{Cmd: cmd} | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), | ||
| NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), | ||
| NicId: networkInterfaceId, | ||
| Delete: deleteValue, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequestDetach(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveNicFromServerRequest { | ||
| return apiClient.RemoveNicFromServer(ctx, model.ProjectId, *model.ServerId, *model.NicId) | ||
| } | ||
| func buildRequestDetachAndDelete(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveNetworkFromServerRequest { | ||
| return apiClient.RemoveNetworkFromServer(ctx, model.ProjectId, *model.ServerId, *model.NetworkId) | ||
| } |
| package list | ||
| // TODO: hier sind wir | ||
| import ( | ||
| "context" | ||
| "testing" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/google/go-cmp/cmp" | ||
| "github.com/google/go-cmp/cmp/cmpopts" | ||
| "github.com/google/uuid" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| ) | ||
| var projectIdFlag = globalflags.ProjectIdFlag | ||
| type testCtxKey struct{} | ||
| var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") | ||
| var testClient = &iaas.APIClient{} | ||
| var testProjectId = uuid.NewString() | ||
| var testServerId = uuid.NewString() | ||
| func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { | ||
| flagValues := map[string]string{ | ||
| projectIdFlag: testProjectId, | ||
| limitFlag: "10", | ||
| serverIdFlag: testServerId, | ||
| } | ||
| for _, mod := range mods { | ||
| mod(flagValues) | ||
| } | ||
| return flagValues | ||
| } | ||
| func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { | ||
| model := &inputModel{ | ||
| GlobalFlagModel: &globalflags.GlobalFlagModel{ | ||
| Verbosity: globalflags.VerbosityDefault, | ||
| ProjectId: testProjectId, | ||
| }, | ||
| Limit: utils.Ptr(int64(10)), | ||
| ServerId: utils.Ptr(testServerId), | ||
| } | ||
| for _, mod := range mods { | ||
| mod(model) | ||
| } | ||
| return model | ||
| } | ||
| func fixtureRequest(mods ...func(request *iaas.ApiListServerNicsRequest)) iaas.ApiListServerNicsRequest { | ||
| request := testClient.ListServerNics(testCtx, testProjectId, testServerId) | ||
| for _, mod := range mods { | ||
| mod(&request) | ||
| } | ||
| return request | ||
| } | ||
| func TestParseInput(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| flagValues map[string]string | ||
| isValid bool | ||
| expectedModel *inputModel | ||
| }{ | ||
| { | ||
| description: "base", | ||
| flagValues: fixtureFlagValues(), | ||
| isValid: true, | ||
| expectedModel: fixtureInputModel(), | ||
| }, | ||
| { | ||
| description: "no values", | ||
| flagValues: map[string]string{}, | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, projectIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "project id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[projectIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id missing", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| delete(flagValues, serverIdFlag) | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 1", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "server id invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[serverIdFlag] = "invalid-uuid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "invalid" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| { | ||
| description: "limit invalid 2", | ||
| flagValues: fixtureFlagValues(func(flagValues map[string]string) { | ||
| flagValues[limitFlag] = "0" | ||
| }), | ||
| isValid: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| p := print.NewPrinter() | ||
| cmd := NewCmd(p) | ||
| err := globalflags.Configure(cmd.Flags()) | ||
| if err != nil { | ||
| t.Fatalf("configure global flags: %v", err) | ||
| } | ||
| for flag, value := range tt.flagValues { | ||
| err := cmd.Flags().Set(flag, value) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("setting flag --%s=%s: %v", flag, value, err) | ||
| } | ||
| } | ||
| err = cmd.ValidateRequiredFlags() | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error validating flags: %v", err) | ||
| } | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| if !tt.isValid { | ||
| return | ||
| } | ||
| t.Fatalf("error parsing input: %v", err) | ||
| } | ||
| if !tt.isValid { | ||
| t.Fatalf("did not fail on invalid input") | ||
| } | ||
| diff := cmp.Diff(model, tt.expectedModel) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
| func TestBuildRequest(t *testing.T) { | ||
| tests := []struct { | ||
| description string | ||
| model *inputModel | ||
| expectedRequest iaas.ApiListServerNicsRequest | ||
| }{ | ||
| { | ||
| description: "base", | ||
| model: fixtureInputModel(), | ||
| expectedRequest: fixtureRequest(), | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.description, func(t *testing.T) { | ||
| request := buildRequest(testCtx, tt.model, testClient) | ||
| diff := cmp.Diff(request, tt.expectedRequest, | ||
| cmp.AllowUnexported(tt.expectedRequest), | ||
| cmpopts.EquateComparable(testCtx), | ||
| ) | ||
| if diff != "" { | ||
| t.Fatalf("Data does not match: %s", diff) | ||
| } | ||
| }) | ||
| } | ||
| } |
| package list | ||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "github.com/goccy/go-yaml" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/errors" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/examples" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/flags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" | ||
| iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/tables" | ||
| "github.com/stackitcloud/stackit-sdk-go/services/iaas" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| const ( | ||
| serverIdFlag = "server-id" | ||
| limitFlag = "limit" | ||
| ) | ||
| type inputModel struct { | ||
| *globalflags.GlobalFlagModel | ||
| ServerId *string | ||
| Limit *int64 | ||
| } | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists all attached network interfaces of a server", | ||
| Long: "Lists all attached network interfaces of a server.", | ||
| Args: args.NoArgs, | ||
| Example: examples.Build( | ||
| examples.NewExample( | ||
| `Lists all attached network interfaces of server with ID "xxx"`, | ||
| "$ stackit beta server network-interface list --server-id xxx", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists all attached network interfaces of server with ID "xxx" in JSON format`, | ||
| "$ stackit beta server network-interface list --server-id xxx --output-format json", | ||
| ), | ||
| examples.NewExample( | ||
| `Lists up to 10 attached network interfaces of server with ID "xxx"`, | ||
| "$ stackit beta server network-interface list --server-id xxx --limit 10", | ||
| ), | ||
| ), | ||
| RunE: func(cmd *cobra.Command, _ []string) error { | ||
| ctx := context.Background() | ||
| model, err := parseInput(p, cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Configure API client | ||
| apiClient, err := client.ConfigureClient(p) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| // Call API | ||
| req := buildRequest(ctx, model, apiClient) | ||
| resp, err := req.Execute() | ||
| if err != nil { | ||
| return fmt.Errorf("list attached network interfaces: %w", err) | ||
| } | ||
| if resp.Items == nil || len(*resp.Items) == 0 { | ||
| serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "get server name: %v", err) | ||
| serverLabel = *model.ServerId | ||
| } | ||
| p.Info("No attached network interfaces found for server %q\n", serverLabel) | ||
| return nil | ||
| } | ||
| // Truncate output | ||
| items := *resp.Items | ||
| if model.Limit != nil && len(items) > int(*model.Limit) { | ||
| items = items[:*model.Limit] | ||
| } | ||
| return outputResult(p, model.OutputFormat, *model.ServerId, items) | ||
| }, | ||
| } | ||
| configureFlags(cmd) | ||
| return cmd | ||
| } | ||
| func configureFlags(cmd *cobra.Command) { | ||
| cmd.Flags().Var(flags.UUIDFlag(), serverIdFlag, "Server ID") | ||
| cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") | ||
| err := flags.MarkFlagsRequired(cmd, serverIdFlag) | ||
| cobra.CheckErr(err) | ||
| } | ||
| func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { | ||
| globalFlags := globalflags.Parse(p, cmd) | ||
| if globalFlags.ProjectId == "" { | ||
| return nil, &errors.ProjectIdError{} | ||
| } | ||
| limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) | ||
| if limit != nil && *limit < 1 { | ||
| return nil, &errors.FlagValidationError{ | ||
| Flag: limitFlag, | ||
| Details: "must be greater than 0", | ||
| } | ||
| } | ||
| model := inputModel{ | ||
| GlobalFlagModel: globalFlags, | ||
| ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), | ||
| Limit: limit, | ||
| } | ||
| if p.IsVerbosityDebug() { | ||
| modelStr, err := print.BuildDebugStrFromInputModel(model) | ||
| if err != nil { | ||
| p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) | ||
| } else { | ||
| p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) | ||
| } | ||
| } | ||
| return &model, nil | ||
| } | ||
| func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListServerNicsRequest { | ||
| return apiClient.ListServerNics(ctx, model.ProjectId, *model.ServerId) | ||
| } | ||
| func outputResult(p *print.Printer, outputFormat, serverId string, serverNics []iaas.NIC) error { | ||
| switch outputFormat { | ||
| case print.JSONOutputFormat: | ||
| details, err := json.MarshalIndent(serverNics, "", " ") | ||
| if err != nil { | ||
| return fmt.Errorf("marshal server network interfaces: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| case print.YAMLOutputFormat: | ||
| details, err := yaml.MarshalWithOptions(serverNics, yaml.IndentSequence(true)) | ||
| if err != nil { | ||
| return fmt.Errorf("marshal server network interfaces: %w", err) | ||
| } | ||
| p.Outputln(string(details)) | ||
| return nil | ||
| default: | ||
| table := tables.NewTable() | ||
| table.SetHeader("NIC ID", "SERVER ID") | ||
| for i := range serverNics { | ||
| nic := serverNics[i] | ||
| table.AddRow(*nic.Id, serverId) | ||
| } | ||
| table.EnableAutoMergeOnColumns(2) | ||
| p.Outputln(table.Render()) | ||
| return nil | ||
| } | ||
| } |
| package networkinterface | ||
| import ( | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/network-interface/attach" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/network-interface/detach" | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/network-interface/list" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/args" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/print" | ||
| "github.com/stackitcloud/stackit-cli/internal/pkg/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
| func NewCmd(p *print.Printer) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "network-interface", | ||
| Short: "Allows attaching/detaching network interfaces to servers", | ||
| Long: "Allows attaching/detaching network interfaces to servers.", | ||
| Args: args.NoArgs, | ||
| Run: utils.CmdHelp, | ||
| } | ||
| addSubcommands(cmd, p) | ||
| return cmd | ||
| } | ||
| func addSubcommands(cmd *cobra.Command, p *print.Printer) { | ||
| cmd.AddCommand(attach.NewCmd(p)) | ||
| cmd.AddCommand(list.NewCmd(p)) | ||
| cmd.AddCommand(detach.NewCmd(p)) | ||
| } |
@@ -16,5 +16,5 @@ name: Renovate | ||
| - name: Self-hosted Renovate | ||
| uses: renovatebot/github-action@v41.0.0 | ||
| uses: renovatebot/github-action@v41.0.5 | ||
| with: | ||
| configurationFile: .github/renovate.json | ||
| token: ${{ secrets.RENOVATE_TOKEN }} |
@@ -38,2 +38,3 @@ ## stackit beta server | ||
| * [stackit beta server list](./stackit_beta_server_list.md) - Lists all servers of a project | ||
| * [stackit beta server network-interface](./stackit_beta_server_network-interface.md) - Allows attaching/detaching network interfaces to servers | ||
| * [stackit beta server public-ip](./stackit_beta_server_public-ip.md) - Allows attaching/detaching public IPs to servers | ||
@@ -40,0 +41,0 @@ * [stackit beta server update](./stackit_beta_server_update.md) - Updates a server |
+2
-2
@@ -7,3 +7,3 @@ module github.com/stackitcloud/stackit-cli | ||
| github.com/fatih/color v1.18.0 | ||
| github.com/goccy/go-yaml v1.15.6 | ||
| github.com/goccy/go-yaml v1.15.7 | ||
| github.com/golang-jwt/jwt/v5 v5.2.1 | ||
@@ -31,3 +31,3 @@ github.com/google/go-cmp v0.6.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.3.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.4.0 | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.0 | ||
@@ -34,0 +34,0 @@ github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v0.8.0 |
+4
-4
@@ -29,4 +29,4 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= | ||
| github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= | ||
| github.com/goccy/go-yaml v1.15.6 h1:gy5kf1yjMia3/c3wWD+u1z3lU5XlhpT8FZGaLJU9cOA= | ||
| github.com/goccy/go-yaml v1.15.6/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= | ||
| github.com/goccy/go-yaml v1.15.7 h1:L7XuKpd/A66X4w/dlk08lVfiIADdy79a1AzRoIefC98= | ||
| github.com/goccy/go-yaml v1.15.7/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= | ||
| github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= | ||
@@ -160,4 +160,4 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.5.0/go.mod h1:Wpqj80yGruCNYGr2yxqhRaLLj4gPSkhJqZyWRXUh/QU= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.3.0 h1:6IZBX9fyza9Eln3FHGHquvLNXQslk+dtkQp41G9+7+Y= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.3.0/go.mod h1:zyg0hpiNdZLRbelkJb2KDf9OHQKLqqcTpePQ1qHL5dE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.4.0 h1:K5fVTcJxjOVwJBa3kiWRsYNAq+I3jAYdU1U+f6no5lE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v0.4.0/go.mod h1:zyg0hpiNdZLRbelkJb2KDf9OHQKLqqcTpePQ1qHL5dE= | ||
| github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.0 h1:ssEywzCS8IdRtzyxweLUKBG5GFbgwjNWJh++wGqigJM= | ||
@@ -164,0 +164,0 @@ github.com/stackitcloud/stackit-sdk-go/services/ske v0.20.0/go.mod h1:A4+9KslxCA31JvxnT+O/GC67eAOdw+iqhBzewZZaCD0= |
@@ -10,2 +10,3 @@ package server | ||
| "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/list" | ||
| networkinterface "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/network-interface" | ||
| publicip "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/public-ip" | ||
@@ -43,2 +44,3 @@ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server/update" | ||
| cmd.AddCommand(volume.NewCmd(p)) | ||
| cmd.AddCommand(networkinterface.NewCmd(p)) | ||
| } |
@@ -149,4 +149,24 @@ package errors | ||
| IAAS_SERVER_MISSING_IMAGE_OR_VOLUME_FLAGS = `Either "image-id" or "boot-volume-source-type" and "boot-volume-source-id" flags must be provided.` | ||
| IAAS_SERVER_NIC_ATTACH_MISSING_NIC_ID = `The "network-interface-id" flag must be provided if the "create" flag is not provided.` | ||
| IAAS_SERVER_NIC_DETACH_MISSING_NIC_ID = `The "network-interface-id" flag must be provided if the "delete" flag is not provided.` | ||
| ) | ||
| type ServerNicAttachMissingNicIdError struct { | ||
| Cmd *cobra.Command | ||
| } | ||
| func (e *ServerNicAttachMissingNicIdError) Error() string { | ||
| return IAAS_SERVER_NIC_ATTACH_MISSING_NIC_ID | ||
| } | ||
| type ServerNicDetachMissingNicIdError struct { | ||
| Cmd *cobra.Command | ||
| } | ||
| func (e *ServerNicDetachMissingNicIdError) Error() string { | ||
| return IAAS_SERVER_NIC_DETACH_MISSING_NIC_ID | ||
| } | ||
| type ServerCreateMissingVolumeIdError struct { | ||
@@ -153,0 +173,0 @@ Cmd *cobra.Command |
@@ -81,2 +81,24 @@ package print | ||
| func fixtureHTTPRequestUnescaped(mods ...func(req *http.Request)) *http.Request { | ||
| testBody, err := json.Marshal(map[string]string{"key": "value"}) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| request, err := http.NewRequest("GET", "http://example.com/v2/projects?limit=50&member=User.Name%40stackit.cloud", bytes.NewReader(testBody)) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| request.Header.Set("Content-Type", "application/json") | ||
| request.Header.Set("Accept", "application/json") | ||
| request.Header.Set("Content-Length", "15") | ||
| for _, mod := range mods { | ||
| mod(request) | ||
| } | ||
| return request | ||
| } | ||
| func fixtureHTTPResponse(mods ...func(resp *http.Response)) *http.Response { | ||
@@ -406,2 +428,12 @@ testBody, err := json.Marshal(map[string]string{"key": "value"}) | ||
| }, | ||
| { | ||
| description: "unescaped test", | ||
| inputReq: fixtureHTTPRequestUnescaped(), | ||
| expected: []string{ | ||
| "request to http://example.com/v2/projects?limit=50&member=User.Name@stackit.cloud: GET HTTP/1.1", | ||
| "request headers: [Accept: application/json, Content-Length: 15, Content-Type: application/json]", | ||
| "request body: [key: value]", | ||
| }, | ||
| isValid: true, | ||
| }, | ||
| } | ||
@@ -408,0 +440,0 @@ |
@@ -9,2 +9,3 @@ package print | ||
| "net/http" | ||
| "net/url" | ||
| "slices" | ||
@@ -145,4 +146,10 @@ "sort" | ||
| status := fmt.Sprintf("request to %s: %s %s", req.URL, req.Method, req.Proto) | ||
| // unescape url in order to get rid of e.g. %40 | ||
| unescapedURL, err := url.PathUnescape(req.URL.String()) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unescape request url: %w", err) | ||
| } | ||
| status := fmt.Sprintf("request to %s: %s %s", unescapedURL, req.Method, req.Proto) | ||
| headersMap := buildHeaderMap(req.Header, includeHeaders) | ||
@@ -152,3 +159,2 @@ headers := fmt.Sprintf("request headers: %v", BuildDebugStrFromMap(headersMap)) | ||
| var save io.ReadCloser | ||
| var err error | ||
@@ -190,4 +196,11 @@ save, req.Body, err = drainBody(req.Body) | ||
| status := fmt.Sprintf("response from %s: %s %s", resp.Request.URL, resp.Proto, resp.Status) | ||
| var err error | ||
| // unescape url in order to get rid of e.g. %40 | ||
| unescapedURL, err := url.PathUnescape(resp.Request.URL.String()) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unescape response url: %w", err) | ||
| } | ||
| status := fmt.Sprintf("response from %s: %s %s", unescapedURL, resp.Proto, resp.Status) | ||
| headersMap := buildHeaderMap(resp.Header, includeHeaders) | ||
@@ -197,3 +210,2 @@ headers := fmt.Sprintf("response headers: %v", BuildDebugStrFromMap(headersMap)) | ||
| var save io.ReadCloser | ||
| var err error | ||
@@ -200,0 +212,0 @@ save, resp.Body, err = drainBody(resp.Body) |