From f9a3ee901a6362fc8445314b58e9f40c42729b3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Wed, 27 Nov 2024 10:10:48 +0100 Subject: [PATCH 01/15] onboard public ip commands --- docs/stackit_beta.md | 1 + docs/stackit_beta_public-ip.md | 39 +++ docs/stackit_beta_public-ip_associate.md | 40 +++ docs/stackit_beta_public-ip_create.md | 47 ++++ docs/stackit_beta_public-ip_delete.md | 41 +++ docs/stackit_beta_public-ip_describe.md | 42 +++ docs/stackit_beta_public-ip_disassociate.md | 39 +++ docs/stackit_beta_public-ip_list.md | 50 ++++ docs/stackit_beta_public-ip_update.md | 47 ++++ internal/cmd/beta/beta.go | 2 + .../cmd/beta/public-ip/associate/associate.go | 128 +++++++++ .../public-ip/associate/associate_test.go | 257 ++++++++++++++++++ internal/cmd/beta/public-ip/create/create.go | 165 +++++++++++ .../cmd/beta/public-ip/create/create_test.go | 215 +++++++++++++++ internal/cmd/beta/public-ip/delete/delete.go | 111 ++++++++ .../cmd/beta/public-ip/delete/delete_test.go | 218 +++++++++++++++ .../cmd/beta/public-ip/describe/describe.go | 145 ++++++++++ .../beta/public-ip/describe/describe_test.go | 218 +++++++++++++++ .../public-ip/disassociate/disassociate.go | 115 ++++++++ .../disassociate/disassociate_test.go | 219 +++++++++++++++ internal/cmd/beta/public-ip/list/list.go | 177 ++++++++++++ internal/cmd/beta/public-ip/list/list_test.go | 204 ++++++++++++++ internal/cmd/beta/public-ip/public-ip.go | 38 +++ internal/cmd/beta/public-ip/update/update.go | 172 ++++++++++++ .../cmd/beta/public-ip/update/update_test.go | 254 +++++++++++++++++ internal/pkg/services/iaas/utils/utils.go | 9 + .../pkg/services/iaas/utils/utils_test.go | 55 ++++ 27 files changed, 3048 insertions(+) create mode 100644 docs/stackit_beta_public-ip.md create mode 100644 docs/stackit_beta_public-ip_associate.md create mode 100644 docs/stackit_beta_public-ip_create.md create mode 100644 docs/stackit_beta_public-ip_delete.md create mode 100644 docs/stackit_beta_public-ip_describe.md create mode 100644 docs/stackit_beta_public-ip_disassociate.md create mode 100644 docs/stackit_beta_public-ip_list.md create mode 100644 docs/stackit_beta_public-ip_update.md create mode 100644 internal/cmd/beta/public-ip/associate/associate.go create mode 100644 internal/cmd/beta/public-ip/associate/associate_test.go create mode 100644 internal/cmd/beta/public-ip/create/create.go create mode 100644 internal/cmd/beta/public-ip/create/create_test.go create mode 100644 internal/cmd/beta/public-ip/delete/delete.go create mode 100644 internal/cmd/beta/public-ip/delete/delete_test.go create mode 100644 internal/cmd/beta/public-ip/describe/describe.go create mode 100644 internal/cmd/beta/public-ip/describe/describe_test.go create mode 100644 internal/cmd/beta/public-ip/disassociate/disassociate.go create mode 100644 internal/cmd/beta/public-ip/disassociate/disassociate_test.go create mode 100644 internal/cmd/beta/public-ip/list/list.go create mode 100644 internal/cmd/beta/public-ip/list/list_test.go create mode 100644 internal/cmd/beta/public-ip/public-ip.go create mode 100644 internal/cmd/beta/public-ip/update/update.go create mode 100644 internal/cmd/beta/public-ip/update/update_test.go diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index 56453f281..36f1a82da 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -43,6 +43,7 @@ stackit beta [flags] * [stackit beta network](./stackit_beta_network.md) - Provides functionality for Network * [stackit beta network-area](./stackit_beta_network-area.md) - Provides functionality for STACKIT Network Area (SNA) * [stackit beta network-interface](./stackit_beta_network-interface.md) - Provides functionality for Network Interface +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP * [stackit beta server](./stackit_beta_server.md) - Provides functionality for Server * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex * [stackit beta volume](./stackit_beta_volume.md) - Provides functionality for Volume diff --git a/docs/stackit_beta_public-ip.md b/docs/stackit_beta_public-ip.md new file mode 100644 index 000000000..9ad499ac5 --- /dev/null +++ b/docs/stackit_beta_public-ip.md @@ -0,0 +1,39 @@ +## stackit beta public-ip + +Provides functionality for Public IP + +### Synopsis + +Provides functionality for Public IP. + +``` +stackit beta public-ip [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta public-ip" +``` + +### 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](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta public-ip associate](./stackit_beta_public-ip_associate.md) - Associates a public IP with a network interface or a virtual IP +* [stackit beta public-ip create](./stackit_beta_public-ip_create.md) - Creates a Public IP +* [stackit beta public-ip delete](./stackit_beta_public-ip_delete.md) - Deletes a Public IP +* [stackit beta public-ip describe](./stackit_beta_public-ip_describe.md) - Shows details of a public IP +* [stackit beta public-ip disassociate](./stackit_beta_public-ip_disassociate.md) - Disassociates a public IP from a network interface or a virtual IP +* [stackit beta public-ip list](./stackit_beta_public-ip_list.md) - Lists all public IPs of a project +* [stackit beta public-ip update](./stackit_beta_public-ip_update.md) - Updates a Public IP + diff --git a/docs/stackit_beta_public-ip_associate.md b/docs/stackit_beta_public-ip_associate.md new file mode 100644 index 000000000..862d77d7a --- /dev/null +++ b/docs/stackit_beta_public-ip_associate.md @@ -0,0 +1,40 @@ +## stackit beta public-ip associate + +Associates a public IP with a network interface or a virtual IP + +### Synopsis + +Associates a public IP with a network interface or a virtual IP. + +``` +stackit beta public-ip associate [flags] +``` + +### Examples + +``` + Associate public IP with ID "xxx" to a resource (network interface or virtual IP) with ID "yyy" + $ stackit beta public-ip associate xxx --associated-resource-id yyy +``` + +### Options + +``` + --associated-resource-id string Associates the public IP with a network interface or virtual IP (ID) + -h, --help Help for "stackit beta public-ip associate" +``` + +### 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 public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP + diff --git a/docs/stackit_beta_public-ip_create.md b/docs/stackit_beta_public-ip_create.md new file mode 100644 index 000000000..45b40ff46 --- /dev/null +++ b/docs/stackit_beta_public-ip_create.md @@ -0,0 +1,47 @@ +## stackit beta public-ip create + +Creates a Public IP + +### Synopsis + +Creates a Public IP. + +``` +stackit beta public-ip create [flags] +``` + +### Examples + +``` + Create a public IP + $ stackit beta public-ip create + + Create a public IP with associated resource with ID "xxx" + $ stackit beta public-ip create --associated-resource-id xxx + + Create a public IP with associated resource with ID "xxx" and labels + $ stackit beta public-ip create --associated-resource-id xxx --labels key=value,foo=bar +``` + +### Options + +``` + --associated-resource-id string Associates the public IP with a network interface or virtual IP (ID) + -h, --help Help for "stackit beta public-ip create" + --labels stringToString Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...' (default []) +``` + +### 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 public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP + diff --git a/docs/stackit_beta_public-ip_delete.md b/docs/stackit_beta_public-ip_delete.md new file mode 100644 index 000000000..bf3013edd --- /dev/null +++ b/docs/stackit_beta_public-ip_delete.md @@ -0,0 +1,41 @@ +## stackit beta public-ip delete + +Deletes a Public IP + +### Synopsis + +Deletes a Public IP. +If the public IP is still in use, the deletion will fail + + +``` +stackit beta public-ip delete [flags] +``` + +### Examples + +``` + Delete public IP with ID "xxx" + $ stackit beta public-ip delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta public-ip delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP + diff --git a/docs/stackit_beta_public-ip_describe.md b/docs/stackit_beta_public-ip_describe.md new file mode 100644 index 000000000..de26fcbf3 --- /dev/null +++ b/docs/stackit_beta_public-ip_describe.md @@ -0,0 +1,42 @@ +## stackit beta public-ip describe + +Shows details of a public IP + +### Synopsis + +Shows details of a public IP. + +``` +stackit beta public-ip describe [flags] +``` + +### Examples + +``` + Show details of a public IP with ID "xxx" + $ stackit beta public-ip describe xxx + + Show details of a public IP with ID "xxx" in JSON format + $ stackit beta public-ip describe xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta public-ip describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP + diff --git a/docs/stackit_beta_public-ip_disassociate.md b/docs/stackit_beta_public-ip_disassociate.md new file mode 100644 index 000000000..9ce4d5686 --- /dev/null +++ b/docs/stackit_beta_public-ip_disassociate.md @@ -0,0 +1,39 @@ +## stackit beta public-ip disassociate + +Disassociates a public IP from a network interface or a virtual IP + +### Synopsis + +Disassociates a public IP from a network interface or a virtual IP. + +``` +stackit beta public-ip disassociate [flags] +``` + +### Examples + +``` + Disassociate public IP with ID "xxx" from a resource (network interface or virtual IP) + $ stackit beta public-ip disassociate xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta public-ip disassociate" +``` + +### 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 public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP + diff --git a/docs/stackit_beta_public-ip_list.md b/docs/stackit_beta_public-ip_list.md new file mode 100644 index 000000000..9f521eaf1 --- /dev/null +++ b/docs/stackit_beta_public-ip_list.md @@ -0,0 +1,50 @@ +## stackit beta public-ip list + +Lists all public IPs of a project + +### Synopsis + +Lists all public IPs of a project. + +``` +stackit beta public-ip list [flags] +``` + +### Examples + +``` + Lists all public IPs + $ stackit beta public-ip list + + Lists all public IPs which contains the label xxx + $ stackit beta public-ip list --label-selector xxx + + Lists all public IPs in JSON format + $ stackit beta public-ip list --output-format json + + Lists up to 10 public IPs + $ stackit beta public-ip list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta public-ip list" + --label-selector string Filter by label + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP + diff --git a/docs/stackit_beta_public-ip_update.md b/docs/stackit_beta_public-ip_update.md new file mode 100644 index 000000000..5426bb22e --- /dev/null +++ b/docs/stackit_beta_public-ip_update.md @@ -0,0 +1,47 @@ +## stackit beta public-ip update + +Updates a Public IP + +### Synopsis + +Updates a Public IP. + +``` +stackit beta public-ip update [flags] +``` + +### Examples + +``` + Update public IP with ID "xxx" + $ stackit beta public-ip update xxx + + Update public IP with ID "xxx" with new associated resource with ID "yyy" + $ stackit beta public-ip update xxx --associated-resource-id yyy + + Update public IP with ID "xxx" with new labels + $ stackit beta public-ip update xxx --labels key=value,foo=bar +``` + +### Options + +``` + --associated-resource-id string Associates the public IP with a network interface or virtual IP (ID) + -h, --help Help for "stackit beta public-ip update" + --labels stringToString Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...' (default []) +``` + +### 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 public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP + diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index ab429fe4f..389c0d614 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network" networkArea "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-area" networkinterface "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-interface" + publicip "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/server" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/volume" @@ -46,4 +47,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(network.NewCmd(p)) cmd.AddCommand(volume.NewCmd(p)) cmd.AddCommand(networkinterface.NewCmd(p)) + cmd.AddCommand(publicip.NewCmd(p)) } diff --git a/internal/cmd/beta/public-ip/associate/associate.go b/internal/cmd/beta/public-ip/associate/associate.go new file mode 100644 index 000000000..695ecaf95 --- /dev/null +++ b/internal/cmd/beta/public-ip/associate/associate.go @@ -0,0 +1,128 @@ +package associate + +import ( + "context" + "fmt" + + "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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + + "github.com/spf13/cobra" +) + +const ( + publicIpIdArg = "PUBLIC_IP_ID" + + associatedResourceIdFlag = "associated-resource-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + PublicIpId string + AssociatedResourceId *string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "associate", + Short: "Associates a public IP with a network interface or a virtual IP", + Long: "Associates a public IP with a network interface or a virtual IP.", + Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Associate public IP with ID "xxx" to a resource (network interface or virtual IP) with ID "yyy"`, + `$ stackit beta public-ip associate xxx --associated-resource-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + if err != nil { + p.Debug(print.ErrorLevel, "get public IP: %v", err) + publicIpLabel = model.PublicIpId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to associate public IP %q with resource %v?", publicIpLabel, model.AssociatedResourceId) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("associate public IP: %w", err) + } + + p.Outputf("Associated public IP %q with resource %v.\n", publicIpLabel, resp.NetworkInterface) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), associatedResourceIdFlag, "Associates the public IP with a network interface or virtual IP (ID)") + + err := flags.MarkFlagsRequired(cmd, associatedResourceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + publicIpId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + AssociatedResourceId: flags.FlagToStringPointer(p, cmd, associatedResourceIdFlag), + PublicIpId: publicIpId, + } + + 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.ApiUpdatePublicIPRequest { + req := apiClient.UpdatePublicIP(ctx, model.ProjectId, model.PublicIpId) + + payload := iaas.UpdatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(model.AssociatedResourceId), + } + + return req.UpdatePublicIPPayload(payload) +} diff --git a/internal/cmd/beta/public-ip/associate/associate_test.go b/internal/cmd/beta/public-ip/associate/associate_test.go new file mode 100644 index 000000000..c2b3799ef --- /dev/null +++ b/internal/cmd/beta/public-ip/associate/associate_test.go @@ -0,0 +1,257 @@ +package associate + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/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 testPublicIpId = uuid.NewString() +var testAssociatedResourceId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testPublicIpId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + associatedResourceIdFlag: testAssociatedResourceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + PublicIpId: testPublicIpId, + AssociatedResourceId: utils.Ptr(testAssociatedResourceId), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.ApiUpdatePublicIPRequest { + request := testClient.UpdatePublicIP(testCtx, testProjectId, testPublicIpId) + request = request.UpdatePublicIPPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.UpdatePublicIPPayload)) iaas.UpdatePublicIPPayload { + payload := iaas.UpdatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "public ip id invalid 1", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "public ip id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "use associated resource id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[associatedResourceIdFlag] = testAssociatedResourceId + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AssociatedResourceId = utils.Ptr(testAssociatedResourceId) + }), + }, + { + description: "associated resource id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, associatedResourceIdFlag) + }), + isValid: false, + }, + { + description: "associated resource id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[associatedResourceIdFlag] = "" + }), + isValid: false, + }, + { + description: "associated resource id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[associatedResourceIdFlag] = "invalid-uuid" + }), + 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) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdatePublicIPRequest + }{ + { + 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), + cmp.AllowUnexported(iaas.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/public-ip/create/create.go b/internal/cmd/beta/public-ip/create/create.go new file mode 100644 index 000000000..d1f003408 --- /dev/null +++ b/internal/cmd/beta/public-ip/create/create.go @@ -0,0 +1,165 @@ +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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + associatedResourceIdFlag = "associated-resource-id" + labelFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + AssociatedResourceId *string + Labels *map[string]string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a Public IP", + Long: "Creates a Public IP.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a public IP`, + `$ stackit beta public-ip create`, + ), + examples.NewExample( + `Create a public IP with associated resource with ID "xxx"`, + `$ stackit beta public-ip create --associated-resource-id xxx`, + ), + examples.NewExample( + `Create a public IP with associated resource with ID "xxx" and labels`, + `$ stackit beta public-ip create --associated-resource-id xxx --labels key=value,foo=bar`, + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p, cmd) + if err != nil { + p.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a public IP for project %q?", projectLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create public IP: %w", err) + } + + return outputResult(p, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), associatedResourceIdFlag, "Associates the public IP with a network interface or virtual IP (ID)") + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...'") +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + AssociatedResourceId: flags.FlagToStringPointer(p, cmd, associatedResourceIdFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + } + + 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.ApiCreatePublicIPRequest { + req := apiClient.CreatePublicIP(ctx, model.ProjectId) + + var labelsMap *map[string]interface{} + if model.Labels != nil && len(*model.Labels) > 0 { + // convert map[string]string to map[string]interface{} + labelsMap = utils.Ptr(map[string]interface{}{}) + for k, v := range *model.Labels { + (*labelsMap)[k] = v + } + } + + payload := iaas.CreatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(model.AssociatedResourceId), + Labels: labelsMap, + } + + return req.CreatePublicIPPayload(payload) +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, publicIp *iaas.PublicIp) error { + switch model.OutputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(publicIp, "", " ") + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(publicIp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + p.Outputf("Created public IP for project %q.\nPublic IP ID: %s\n", projectLabel, *publicIp.Id) + return nil + } +} diff --git a/internal/cmd/beta/public-ip/create/create_test.go b/internal/cmd/beta/public-ip/create/create_test.go new file mode 100644 index 000000000..7411c0a9f --- /dev/null +++ b/internal/cmd/beta/public-ip/create/create_test.go @@ -0,0 +1,215 @@ +package create + +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 testAssociatedResourceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + associatedResourceIdFlag: testAssociatedResourceId, + labelFlag: "key=value", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + AssociatedResourceId: utils.Ptr(testAssociatedResourceId), + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiCreatePublicIPRequest)) iaas.ApiCreatePublicIPRequest { + request := testClient.CreatePublicIP(testCtx, testProjectId) + request = request.CreatePublicIPPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.CreatePublicIPPayload)) iaas.CreatePublicIPPayload { + payload := iaas.CreatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), + Labels: utils.Ptr(map[string]interface{}{ + "key": "value", + }), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +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: "required only", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, associatedResourceIdFlag) + delete(flagValues, labelFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AssociatedResourceId = nil + model.Labels = nil + }), + }, + { + description: "valid with associated resource id", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AssociatedResourceId = utils.Ptr(testAssociatedResourceId) + model.Labels = nil + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + 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 flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiCreatePublicIPRequest + }{ + { + 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), + cmp.AllowUnexported(iaas.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/public-ip/delete/delete.go b/internal/cmd/beta/public-ip/delete/delete.go new file mode 100644 index 000000000..1b46ead44 --- /dev/null +++ b/internal/cmd/beta/public-ip/delete/delete.go @@ -0,0 +1,111 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + publicIpIdArg = "PUBLIC_IP_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + PublicIpId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Deletes a Public IP", + Long: fmt.Sprintf("%s\n%s\n", + "Deletes a Public IP.", + "If the public IP is still in use, the deletion will fail", + ), + Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete public IP with ID "xxx"`, + "$ stackit beta public-ip delete xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + if err != nil { + p.Debug(print.ErrorLevel, "get public IP: %v", err) + publicIpLabel = model.PublicIpId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete public IP %q?", publicIpLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete public IP: %w", err) + } + + p.Info("Deleted public IP %q\n", model.PublicIpId) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + publicIpId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + PublicIpId: publicIpId, + } + + 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.ApiDeletePublicIPRequest { + return apiClient.DeletePublicIP(ctx, model.ProjectId, model.PublicIpId) +} diff --git a/internal/cmd/beta/public-ip/delete/delete_test.go b/internal/cmd/beta/public-ip/delete/delete_test.go new file mode 100644 index 000000000..1acd8b016 --- /dev/null +++ b/internal/cmd/beta/public-ip/delete/delete_test.go @@ -0,0 +1,218 @@ +package delete + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{} +var testPublicIpId = uuid.NewString() +var testProjectId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testPublicIpId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + 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, + }, + PublicIpId: testPublicIpId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiDeletePublicIPRequest)) iaas.ApiDeletePublicIPRequest { + request := testClient.DeletePublicIP(testCtx, testProjectId, testPublicIpId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "public ip id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "public ip id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiDeletePublicIPRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/public-ip/describe/describe.go b/internal/cmd/beta/public-ip/describe/describe.go new file mode 100644 index 000000000..51d514069 --- /dev/null +++ b/internal/cmd/beta/public-ip/describe/describe.go @@ -0,0 +1,145 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "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/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + + "github.com/spf13/cobra" +) + +const ( + publicIpIdArg = "PUBLIC_IP_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + PublicIpId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Shows details of a public IP", + Long: "Shows details of a public IP.", + Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Show details of a public IP with ID "xxx"`, + "$ stackit beta public-ip describe xxx", + ), + examples.NewExample( + `Show details of a public IP with ID "xxx" in JSON format`, + "$ stackit beta public-ip describe xxx --output-format json", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read public IP: %w", err) + } + + return outputResult(p, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + publicIpId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + PublicIpId: publicIpId, + } + + 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.ApiGetPublicIPRequest { + return apiClient.GetPublicIP(ctx, model.ProjectId, model.PublicIpId) +} + +func outputResult(p *print.Printer, outputFormat string, publicIp *iaas.PublicIp) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(publicIp, "", " ") + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(publicIp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.AddRow("ID", *publicIp.Id) + table.AddSeparator() + table.AddRow("IP ADDRESS", *publicIp.Ip) + table.AddSeparator() + table.AddRow("ASSOCIATED TO", *publicIp.NetworkInterface) + table.AddSeparator() + + if publicIp.Labels != nil && len(*publicIp.Labels) > 0 { + labels := []string{} + for key, value := range *publicIp.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + table.AddRow("LABELS", strings.Join(labels, "\n")) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + } +} diff --git a/internal/cmd/beta/public-ip/describe/describe_test.go b/internal/cmd/beta/public-ip/describe/describe_test.go new file mode 100644 index 000000000..9754de721 --- /dev/null +++ b/internal/cmd/beta/public-ip/describe/describe_test.go @@ -0,0 +1,218 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/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 testPublicIpId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testPublicIpId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + PublicIpId: testPublicIpId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiGetPublicIPRequest)) iaas.ApiGetPublicIPRequest { + request := testClient.GetPublicIP(testCtx, testProjectId, testPublicIpId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "public ip id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "public ip id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiGetPublicIPRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/public-ip/disassociate/disassociate.go b/internal/cmd/beta/public-ip/disassociate/disassociate.go new file mode 100644 index 000000000..408b1958e --- /dev/null +++ b/internal/cmd/beta/public-ip/disassociate/disassociate.go @@ -0,0 +1,115 @@ +package disassociate + +import ( + "context" + "fmt" + + "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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + + "github.com/spf13/cobra" +) + +const ( + publicIpIdArg = "PUBLIC_IP_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + PublicIpId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "disassociate", + Short: "Disassociates a public IP from a network interface or a virtual IP", + Long: "Disassociates a public IP from a network interface or a virtual IP.", + Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Disassociate public IP with ID "xxx" from a resource (network interface or virtual IP)`, + `$ stackit beta public-ip disassociate xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + if err != nil { + p.Debug(print.ErrorLevel, "get public IP: %v", err) + publicIpLabel = model.PublicIpId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to disassociate public IP %q from the associated resource?", publicIpLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("disassociate public IP: %w", err) + } + + p.Outputf("Disassociated public IP %q from the associated resource.\n", publicIpLabel) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + publicIpId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + PublicIpId: publicIpId, + } + + 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.ApiUpdatePublicIPRequest { + req := apiClient.UpdatePublicIP(ctx, model.ProjectId, model.PublicIpId) + + payload := iaas.UpdatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(nil), + } + + return req.UpdatePublicIPPayload(payload) +} diff --git a/internal/cmd/beta/public-ip/disassociate/disassociate_test.go b/internal/cmd/beta/public-ip/disassociate/disassociate_test.go new file mode 100644 index 000000000..e4478b0c7 --- /dev/null +++ b/internal/cmd/beta/public-ip/disassociate/disassociate_test.go @@ -0,0 +1,219 @@ +package disassociate + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "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 testPublicIpId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testPublicIpId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + PublicIpId: testPublicIpId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.ApiUpdatePublicIPRequest { + request := testClient.UpdatePublicIP(testCtx, testProjectId, testPublicIpId) + request = request.UpdatePublicIPPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.UpdatePublicIPPayload)) iaas.UpdatePublicIPPayload { + payload := iaas.UpdatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(nil), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "public ip id invalid 1", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "public ip id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdatePublicIPRequest + }{ + { + 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), + cmp.AllowUnexported(iaas.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/public-ip/list/list.go b/internal/cmd/beta/public-ip/list/list.go new file mode 100644 index 000000000..03900cc51 --- /dev/null +++ b/internal/cmd/beta/public-ip/list/list.go @@ -0,0 +1,177 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + + "github.com/spf13/cobra" +) + +const ( + limitFlag = "limit" + labelSelectorFlag = "label-selector" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 + LabelSelector *string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all public IPs of a project", + Long: "Lists all public IPs of a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Lists all public IPs`, + "$ stackit beta public-ip list", + ), + examples.NewExample( + `Lists all public IPs which contains the label xxx`, + "$ stackit beta public-ip list --label-selector xxx", + ), + examples.NewExample( + `Lists all public IPs in JSON format`, + "$ stackit beta public-ip list --output-format json", + ), + examples.NewExample( + `Lists up to 10 public IPs`, + "$ stackit beta public-ip list --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list public IPs: %w", err) + } + + if resp.Items == nil || len(*resp.Items) == 0 { + projectLabel, err := projectname.GetProjectName(ctx, p, cmd) + if err != nil { + p.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + p.Info("No public IPs found for project %q\n", projectLabel) + 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, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().String(labelSelectorFlag, "", "Filter by label") +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), + } + + 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.ApiListPublicIPsRequest { + req := apiClient.ListPublicIPs(ctx, model.ProjectId) + if model.LabelSelector != nil { + req = req.LabelSelector(*model.LabelSelector) + } + + return req +} + +func outputResult(p *print.Printer, outputFormat string, publicIps []iaas.PublicIp) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(publicIps, "", " ") + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(publicIps, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "IP ADDRESS", "USED BY") + + for _, publicIp := range publicIps { + table.AddRow(*publicIp.Id, *publicIp.Ip, *publicIp.NetworkInterface) + table.AddSeparator() + } + + p.Outputln(table.Render()) + return nil + } +} diff --git a/internal/cmd/beta/public-ip/list/list_test.go b/internal/cmd/beta/public-ip/list/list_test.go new file mode 100644 index 000000000..b7f39f61d --- /dev/null +++ b/internal/cmd/beta/public-ip/list/list_test.go @@ -0,0 +1,204 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/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 testLabelSelector = "label" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + limitFlag: "10", + labelSelectorFlag: testLabelSelector, + } + 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)), + LabelSelector: utils.Ptr(testLabelSelector), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiListPublicIPsRequest)) iaas.ApiListPublicIPsRequest { + request := testClient.ListPublicIPs(testCtx, testProjectId) + request = request.LabelSelector(testLabelSelector) + 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: "no flag 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: "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, + }, + { + description: "label selector empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[labelSelectorFlag] = "" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.LabelSelector = utils.Ptr("") + }), + }, + } + + 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.ApiListPublicIPsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/public-ip/public-ip.go b/internal/cmd/beta/public-ip/public-ip.go new file mode 100644 index 000000000..7005d00c0 --- /dev/null +++ b/internal/cmd/beta/public-ip/public-ip.go @@ -0,0 +1,38 @@ +package publicip + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip/associate" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip/disassociate" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/public-ip/update" + "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: "public-ip", + Short: "Provides functionality for Public IP", + Long: "Provides functionality for Public IP.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, p) + return cmd +} + +func addSubcommands(cmd *cobra.Command, p *print.Printer) { + cmd.AddCommand(create.NewCmd(p)) + cmd.AddCommand(delete.NewCmd(p)) + cmd.AddCommand(describe.NewCmd(p)) + cmd.AddCommand(list.NewCmd(p)) + cmd.AddCommand(update.NewCmd(p)) + cmd.AddCommand(associate.NewCmd(p)) + cmd.AddCommand(disassociate.NewCmd(p)) +} diff --git a/internal/cmd/beta/public-ip/update/update.go b/internal/cmd/beta/public-ip/update/update.go new file mode 100644 index 000000000..fa4db7c98 --- /dev/null +++ b/internal/cmd/beta/public-ip/update/update.go @@ -0,0 +1,172 @@ +package update + +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/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + publicIpIdArg = "PUBLIC_IP_ID" + + associatedResourceIdFlag = "associated-resource-id" + labelFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + PublicIpId string + AssociatedResourceId *string + Labels *map[string]string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates a Public IP", + Long: "Updates a Public IP.", + Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update public IP with ID "xxx"`, + `$ stackit beta public-ip update xxx`, + ), + examples.NewExample( + `Update public IP with ID "xxx" with new associated resource with ID "yyy"`, + `$ stackit beta public-ip update xxx --associated-resource-id yyy`, + ), + examples.NewExample( + `Update public IP with ID "xxx" with new labels`, + `$ stackit beta public-ip update xxx --labels key=value,foo=bar`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + if err != nil { + p.Debug(print.ErrorLevel, "get public IP: %v", err) + publicIpLabel = model.PublicIpId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update public IP %q?", publicIpLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update public IP: %w", err) + } + + return outputResult(p, model, publicIpLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), associatedResourceIdFlag, "Associates the public IP with a network interface or virtual IP (ID)") + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...'") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + publicIpId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + PublicIpId: publicIpId, + AssociatedResourceId: flags.FlagToStringPointer(p, cmd, associatedResourceIdFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + } + + 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.ApiUpdatePublicIPRequest { + req := apiClient.UpdatePublicIP(ctx, model.ProjectId, model.PublicIpId) + + var labelsMap *map[string]interface{} + if model.Labels != nil && len(*model.Labels) > 0 { + // convert map[string]string to map[string]interface{} + labelsMap = utils.Ptr(map[string]interface{}{}) + for k, v := range *model.Labels { + (*labelsMap)[k] = v + } + } + + payload := iaas.UpdatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(model.AssociatedResourceId), + Labels: labelsMap, + } + + return req.UpdatePublicIPPayload(payload) +} + +func outputResult(p *print.Printer, model *inputModel, publicIpLabel string, publicIp *iaas.PublicIp) error { + switch model.OutputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(publicIp, "", " ") + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(publicIp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal public IP: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + p.Outputf("Updated public IP %q.\n", publicIpLabel) + return nil + } +} diff --git a/internal/cmd/beta/public-ip/update/update_test.go b/internal/cmd/beta/public-ip/update/update_test.go new file mode 100644 index 000000000..17937f46a --- /dev/null +++ b/internal/cmd/beta/public-ip/update/update_test.go @@ -0,0 +1,254 @@ +package update + +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 testAssociatedResourceId = uuid.NewString() +var testPublicIpId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testPublicIpId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + associatedResourceIdFlag: testAssociatedResourceId, + labelFlag: "key=value", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + PublicIpId: testPublicIpId, + AssociatedResourceId: utils.Ptr(testAssociatedResourceId), + Labels: utils.Ptr(map[string]string{ + "key": "value", + }), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.ApiUpdatePublicIPRequest { + request := testClient.UpdatePublicIP(testCtx, testProjectId, testPublicIpId) + request = request.UpdatePublicIPPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.UpdatePublicIPPayload)) iaas.UpdatePublicIPPayload { + payload := iaas.UpdatePublicIPPayload{ + NetworkInterface: iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), + Labels: utils.Ptr(map[string]interface{}{ + "key": "value", + }), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "public ip id invalid 1", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "public ip id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "use associated resource id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[associatedResourceIdFlag] = testAssociatedResourceId + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AssociatedResourceId = utils.Ptr(testAssociatedResourceId) + }), + }, + { + description: "use labels", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[labelFlag] = "key=value" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Labels = &map[string]string{ + "key": "value", + } + }), + }, + } + + 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) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdatePublicIPRequest + }{ + { + 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), + cmp.AllowUnexported(iaas.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index 9e7d79510..64efe5234 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -8,6 +8,7 @@ import ( ) type IaaSClient interface { + GetPublicIPExecute(ctx context.Context, projectId, publicIpId string) (*iaas.PublicIp, error) GetVolumeExecute(ctx context.Context, projectId, volumeId string) (*iaas.Volume, error) GetNetworkExecute(ctx context.Context, projectId, networkId string) (*iaas.Network, error) GetNetworkAreaExecute(ctx context.Context, organizationId, areaId string) (*iaas.NetworkArea, error) @@ -15,6 +16,14 @@ type IaaSClient interface { GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error) } +func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (string, error) { + resp, err := apiClient.GetPublicIPExecute(ctx, projectId, publicIpId) + if err != nil { + return "", fmt.Errorf("get public ip: %w", err) + } + return *resp.Ip, nil +} + func GetVolumeName(ctx context.Context, apiClient IaaSClient, projectId, volumeId string) (string, error) { resp, err := apiClient.GetVolumeExecute(ctx, projectId, volumeId) if err != nil { diff --git a/internal/pkg/services/iaas/utils/utils_test.go b/internal/pkg/services/iaas/utils/utils_test.go index 9c0e41ac1..4571fbbd7 100644 --- a/internal/pkg/services/iaas/utils/utils_test.go +++ b/internal/pkg/services/iaas/utils/utils_test.go @@ -11,6 +11,8 @@ import ( ) type IaaSClientMocked struct { + GetPublicIpFails bool + GetPublicIpResp *iaas.PublicIp GetVolumeFails bool GetVolumeResp *iaas.Volume GetNetworkFails bool @@ -23,6 +25,13 @@ type IaaSClientMocked struct { GetNetworkAreaRangeResp *iaas.NetworkRange } +func (m *IaaSClientMocked) GetPublicIPExecute(_ context.Context, _, _ string) (*iaas.PublicIp, error) { + if m.GetPublicIpFails { + return nil, fmt.Errorf("could not get public ip") + } + return m.GetPublicIpResp, nil +} + func (m *IaaSClientMocked) GetVolumeExecute(_ context.Context, _, _ string) (*iaas.Volume, error) { if m.GetVolumeFails { return nil, fmt.Errorf("could not get volume") @@ -58,6 +67,52 @@ func (m *IaaSClientMocked) GetNetworkAreaRangeExecute(_ context.Context, _, _, _ return m.GetNetworkAreaRangeResp, nil } +func TestGetPublicIp(t *testing.T) { + type args struct { + getPublicIpFails bool + getPublicIpResp *iaas.PublicIp + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "base", + args: args{ + getPublicIpResp: &iaas.PublicIp{ + Ip: utils.Ptr("1.2.3.4"), + }, + }, + want: "1.2.3.4", + }, + { + name: "get public ip fails", + args: args{ + getPublicIpFails: true, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &IaaSClientMocked{ + GetPublicIpFails: tt.args.getPublicIpFails, + GetPublicIpResp: tt.args.getPublicIpResp, + } + got, err := GetPublicIP(context.Background(), m, "", "") + if (err != nil) != tt.wantErr { + t.Errorf("GetPublicIP() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetPublicIP() = %v, want %v", got, tt.want) + } + }) + } +} + func TestGetVolumeName(t *testing.T) { type args struct { getInstanceFails bool From f181a7afec485d6ef67554d9ca38bfe141a841b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Wed, 27 Nov 2024 11:33:12 +0100 Subject: [PATCH 02/15] changes in commands to edit descriptions and avoid nil pointer exceptions --- docs/stackit_beta_public-ip.md | 8 ++++---- docs/stackit_beta_public-ip_associate.md | 4 ++-- docs/stackit_beta_public-ip_describe.md | 4 ++-- docs/stackit_beta_public-ip_disassociate.md | 4 ++-- docs/stackit_beta_public-ip_list.md | 4 ++-- internal/cmd/beta/public-ip/associate/associate.go | 6 +++--- internal/cmd/beta/public-ip/describe/describe.go | 12 ++++++++---- .../cmd/beta/public-ip/disassociate/disassociate.go | 4 ++-- internal/cmd/beta/public-ip/list/list.go | 10 +++++++--- 9 files changed, 32 insertions(+), 24 deletions(-) diff --git a/docs/stackit_beta_public-ip.md b/docs/stackit_beta_public-ip.md index 9ad499ac5..1afea1a63 100644 --- a/docs/stackit_beta_public-ip.md +++ b/docs/stackit_beta_public-ip.md @@ -29,11 +29,11 @@ stackit beta public-ip [flags] ### SEE ALSO * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands -* [stackit beta public-ip associate](./stackit_beta_public-ip_associate.md) - Associates a public IP with a network interface or a virtual IP +* [stackit beta public-ip associate](./stackit_beta_public-ip_associate.md) - Associates a Public IP with a network interface or a virtual IP * [stackit beta public-ip create](./stackit_beta_public-ip_create.md) - Creates a Public IP * [stackit beta public-ip delete](./stackit_beta_public-ip_delete.md) - Deletes a Public IP -* [stackit beta public-ip describe](./stackit_beta_public-ip_describe.md) - Shows details of a public IP -* [stackit beta public-ip disassociate](./stackit_beta_public-ip_disassociate.md) - Disassociates a public IP from a network interface or a virtual IP -* [stackit beta public-ip list](./stackit_beta_public-ip_list.md) - Lists all public IPs of a project +* [stackit beta public-ip describe](./stackit_beta_public-ip_describe.md) - Shows details of a Public IP +* [stackit beta public-ip disassociate](./stackit_beta_public-ip_disassociate.md) - Disassociates a Public IP from a network interface or a virtual IP +* [stackit beta public-ip list](./stackit_beta_public-ip_list.md) - Lists all Public IPs of a project * [stackit beta public-ip update](./stackit_beta_public-ip_update.md) - Updates a Public IP diff --git a/docs/stackit_beta_public-ip_associate.md b/docs/stackit_beta_public-ip_associate.md index 862d77d7a..db6c15a6f 100644 --- a/docs/stackit_beta_public-ip_associate.md +++ b/docs/stackit_beta_public-ip_associate.md @@ -1,10 +1,10 @@ ## stackit beta public-ip associate -Associates a public IP with a network interface or a virtual IP +Associates a Public IP with a network interface or a virtual IP ### Synopsis -Associates a public IP with a network interface or a virtual IP. +Associates a Public IP with a network interface or a virtual IP. ``` stackit beta public-ip associate [flags] diff --git a/docs/stackit_beta_public-ip_describe.md b/docs/stackit_beta_public-ip_describe.md index de26fcbf3..029ab034e 100644 --- a/docs/stackit_beta_public-ip_describe.md +++ b/docs/stackit_beta_public-ip_describe.md @@ -1,10 +1,10 @@ ## stackit beta public-ip describe -Shows details of a public IP +Shows details of a Public IP ### Synopsis -Shows details of a public IP. +Shows details of a Public IP. ``` stackit beta public-ip describe [flags] diff --git a/docs/stackit_beta_public-ip_disassociate.md b/docs/stackit_beta_public-ip_disassociate.md index 9ce4d5686..28b9f9b35 100644 --- a/docs/stackit_beta_public-ip_disassociate.md +++ b/docs/stackit_beta_public-ip_disassociate.md @@ -1,10 +1,10 @@ ## stackit beta public-ip disassociate -Disassociates a public IP from a network interface or a virtual IP +Disassociates a Public IP from a network interface or a virtual IP ### Synopsis -Disassociates a public IP from a network interface or a virtual IP. +Disassociates a Public IP from a network interface or a virtual IP. ``` stackit beta public-ip disassociate [flags] diff --git a/docs/stackit_beta_public-ip_list.md b/docs/stackit_beta_public-ip_list.md index 9f521eaf1..007a7bc57 100644 --- a/docs/stackit_beta_public-ip_list.md +++ b/docs/stackit_beta_public-ip_list.md @@ -1,10 +1,10 @@ ## stackit beta public-ip list -Lists all public IPs of a project +Lists all Public IPs of a project ### Synopsis -Lists all public IPs of a project. +Lists all Public IPs of a project. ``` stackit beta public-ip list [flags] diff --git a/internal/cmd/beta/public-ip/associate/associate.go b/internal/cmd/beta/public-ip/associate/associate.go index 695ecaf95..a932e34b0 100644 --- a/internal/cmd/beta/public-ip/associate/associate.go +++ b/internal/cmd/beta/public-ip/associate/associate.go @@ -33,8 +33,8 @@ type inputModel struct { func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "associate", - Short: "Associates a public IP with a network interface or a virtual IP", - Long: "Associates a public IP with a network interface or a virtual IP.", + Short: "Associates a Public IP with a network interface or a virtual IP", + Long: "Associates a Public IP with a network interface or a virtual IP.", Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( @@ -62,7 +62,7 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to associate public IP %q with resource %v?", publicIpLabel, model.AssociatedResourceId) + prompt := fmt.Sprintf("Are you sure you want to associate public IP %q with resource %v?", publicIpLabel, *model.AssociatedResourceId) err = p.PromptForConfirmation(prompt) if err != nil { return err diff --git a/internal/cmd/beta/public-ip/describe/describe.go b/internal/cmd/beta/public-ip/describe/describe.go index 51d514069..9e47ab890 100644 --- a/internal/cmd/beta/public-ip/describe/describe.go +++ b/internal/cmd/beta/public-ip/describe/describe.go @@ -33,8 +33,8 @@ type inputModel struct { func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "describe", - Short: "Shows details of a public IP", - Long: "Shows details of a public IP.", + Short: "Shows details of a Public IP", + Long: "Shows details of a Public IP.", Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( @@ -125,8 +125,12 @@ func outputResult(p *print.Printer, outputFormat string, publicIp *iaas.PublicIp table.AddSeparator() table.AddRow("IP ADDRESS", *publicIp.Ip) table.AddSeparator() - table.AddRow("ASSOCIATED TO", *publicIp.NetworkInterface) - table.AddSeparator() + + if publicIp.NetworkInterface != nil { + networkInterfaceId := *publicIp.GetNetworkInterface() + table.AddRow("ASSOCIATED TO", networkInterfaceId) + table.AddSeparator() + } if publicIp.Labels != nil && len(*publicIp.Labels) > 0 { labels := []string{} diff --git a/internal/cmd/beta/public-ip/disassociate/disassociate.go b/internal/cmd/beta/public-ip/disassociate/disassociate.go index 408b1958e..f55596f68 100644 --- a/internal/cmd/beta/public-ip/disassociate/disassociate.go +++ b/internal/cmd/beta/public-ip/disassociate/disassociate.go @@ -29,8 +29,8 @@ type inputModel struct { func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "disassociate", - Short: "Disassociates a public IP from a network interface or a virtual IP", - Long: "Disassociates a public IP from a network interface or a virtual IP.", + Short: "Disassociates a Public IP from a network interface or a virtual IP", + Long: "Disassociates a Public IP from a network interface or a virtual IP.", Args: args.SingleArg(publicIpIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( diff --git a/internal/cmd/beta/public-ip/list/list.go b/internal/cmd/beta/public-ip/list/list.go index 03900cc51..4ab82d197 100644 --- a/internal/cmd/beta/public-ip/list/list.go +++ b/internal/cmd/beta/public-ip/list/list.go @@ -34,8 +34,8 @@ type inputModel struct { func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "list", - Short: "Lists all public IPs of a project", - Long: "Lists all public IPs of a project.", + Short: "Lists all Public IPs of a project", + Long: "Lists all Public IPs of a project.", Args: args.NoArgs, Example: examples.Build( examples.NewExample( @@ -167,7 +167,11 @@ func outputResult(p *print.Printer, outputFormat string, publicIps []iaas.Public table.SetHeader("ID", "IP ADDRESS", "USED BY") for _, publicIp := range publicIps { - table.AddRow(*publicIp.Id, *publicIp.Ip, *publicIp.NetworkInterface) + networkInterfaceId := "" + if publicIp.NetworkInterface != nil { + networkInterfaceId = *publicIp.GetNetworkInterface() + } + table.AddRow(*publicIp.Id, *publicIp.Ip, networkInterfaceId) table.AddSeparator() } From a0e16955aff367c983f06c25688722becc5a8863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Wed, 27 Nov 2024 11:40:45 +0100 Subject: [PATCH 03/15] fix utils --- internal/pkg/services/iaas/utils/utils.go | 1 + internal/pkg/services/iaas/utils/utils_test.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index 76ec3db02..0da1b98eb 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -23,6 +23,7 @@ func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpI return "", fmt.Errorf("get public ip: %w", err) } return *resp.Ip, nil +} func GetServerName(ctx context.Context, apiClient IaaSClient, projectId, serverId string) (string, error) { resp, err := apiClient.GetServerExecute(ctx, projectId, serverId) diff --git a/internal/pkg/services/iaas/utils/utils_test.go b/internal/pkg/services/iaas/utils/utils_test.go index f9c7283ff..2d54658d4 100644 --- a/internal/pkg/services/iaas/utils/utils_test.go +++ b/internal/pkg/services/iaas/utils/utils_test.go @@ -32,6 +32,7 @@ func (m *IaaSClientMocked) GetPublicIPExecute(_ context.Context, _, _ string) (* return nil, fmt.Errorf("could not get public ip") } return m.GetPublicIpResp, nil +} func (m *IaaSClientMocked) GetServerExecute(_ context.Context, _, _ string) (*iaas.Server, error) { if m.GetServerFails { @@ -74,7 +75,7 @@ func (m *IaaSClientMocked) GetNetworkAreaRangeExecute(_ context.Context, _, _, _ } return m.GetNetworkAreaRangeResp, nil } - + func TestGetPublicIp(t *testing.T) { type args struct { getPublicIpFails bool @@ -120,7 +121,7 @@ func TestGetPublicIp(t *testing.T) { }) } } - + func TestGetServerName(t *testing.T) { type args struct { getInstanceFails bool From 37cdd50ae65689a6dbc66f77fea72fa0659986a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Wed, 27 Nov 2024 14:47:10 +0100 Subject: [PATCH 04/15] add public ip to read me --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e15f0687b..888f2e03d 100644 --- a/README.md +++ b/README.md @@ -65,28 +65,28 @@ Help is available for any command by specifying the special flag `--help` (or si Below you can find a list of the STACKIT services already available in the CLI (along with their respective command names) and the ones that are currently planned to be integrated. -| Service | CLI Commands | Status | -| ---------------------------------- |----------------------------------------------------------------| ------------------------- | -| Observability | `observability` | :white_check_mark: | -| Infrastructure as a Service (IaaS) | `beta network-area`
`beta network`
`beta volume`
`beta network-interface`| :white_check_mark: (beta) | -| Authorization | `project`, `organization` | :white_check_mark: | -| DNS | `dns` | :white_check_mark: | -| Kubernetes Engine (SKE) | `ske` | :white_check_mark: | -| Load Balancer | `load-balancer` | :white_check_mark: | -| LogMe | `logme` | :white_check_mark: | -| MariaDB | `mariadb` | :white_check_mark: | -| MongoDB Flex | `mongodbflex` | :white_check_mark: | -| Object Storage | `object-storage` | :white_check_mark: | -| OpenSearch | `opensearch` | :white_check_mark: | -| PostgreSQL Flex | `postgresflex` | :white_check_mark: | -| RabbitMQ | `rabbitmq` | :white_check_mark: | -| Redis | `redis` | :white_check_mark: | -| Resource Manager | `project` | :white_check_mark: | -| Secrets Manager | `secrets-manager` | :white_check_mark: | -| Server Backup Management | `beta server backup` | :white_check_mark: (beta) | -| Server Command (Run Command) | `beta server command` | :white_check_mark: (beta) | -| Service Account | `service-account` | :white_check_mark: | -| SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | +| Service | CLI Commands | Status | +| ---------------------------------- |----------------------------------------------------------------------------------------------------------------------| ------------------------- | +| Observability | `observability` | :white_check_mark: | +| Infrastructure as a Service (IaaS) | `beta network-area`
`beta network`
`beta volume`
`beta network-interface`
`beta public-ip` | :white_check_mark: (beta) | +| Authorization | `project`, `organization` | :white_check_mark: | +| DNS | `dns` | :white_check_mark: | +| Kubernetes Engine (SKE) | `ske` | :white_check_mark: | +| Load Balancer | `load-balancer` | :white_check_mark: | +| LogMe | `logme` | :white_check_mark: | +| MariaDB | `mariadb` | :white_check_mark: | +| MongoDB Flex | `mongodbflex` | :white_check_mark: | +| Object Storage | `object-storage` | :white_check_mark: | +| OpenSearch | `opensearch` | :white_check_mark: | +| PostgreSQL Flex | `postgresflex` | :white_check_mark: | +| RabbitMQ | `rabbitmq` | :white_check_mark: | +| Redis | `redis` | :white_check_mark: | +| Resource Manager | `project` | :white_check_mark: | +| Secrets Manager | `secrets-manager` | :white_check_mark: | +| Server Backup Management | `beta server backup` | :white_check_mark: (beta) | +| Server Command (Run Command) | `beta server command` | :white_check_mark: (beta) | +| Service Account | `service-account` | :white_check_mark: | +| SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | ## Authentication From 7b3f54dd88fd16a0b9879384ae42b773cae2210c Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:30:55 +0100 Subject: [PATCH 05/15] Update internal/cmd/beta/public-ip/create/create.go Co-authored-by: Alexander Dahmen --- internal/cmd/beta/public-ip/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/public-ip/create/create.go b/internal/cmd/beta/public-ip/create/create.go index d1f003408..a2f6565c3 100644 --- a/internal/cmd/beta/public-ip/create/create.go +++ b/internal/cmd/beta/public-ip/create/create.go @@ -42,7 +42,7 @@ func NewCmd(p *print.Printer) *cobra.Command { `$ stackit beta public-ip create`, ), examples.NewExample( - `Create a public IP with associated resource with ID "xxx"`, + `Create a public IP with associated resource ID "xxx"`, `$ stackit beta public-ip create --associated-resource-id xxx`, ), examples.NewExample( From e445d3cbbf4f99dcf447e7fad13b0b368a764d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Thu, 28 Nov 2024 15:34:01 +0100 Subject: [PATCH 06/15] update examples --- docs/stackit_beta_public-ip_create.md | 4 ++-- docs/stackit_beta_public-ip_update.md | 2 +- internal/cmd/beta/public-ip/create/create.go | 2 +- internal/cmd/beta/public-ip/update/update.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/stackit_beta_public-ip_create.md b/docs/stackit_beta_public-ip_create.md index 45b40ff46..a566ac76e 100644 --- a/docs/stackit_beta_public-ip_create.md +++ b/docs/stackit_beta_public-ip_create.md @@ -16,10 +16,10 @@ stackit beta public-ip create [flags] Create a public IP $ stackit beta public-ip create - Create a public IP with associated resource with ID "xxx" + Create a public IP with associated resource ID "xxx" $ stackit beta public-ip create --associated-resource-id xxx - Create a public IP with associated resource with ID "xxx" and labels + Create a public IP with associated resource ID "xxx" and labels $ stackit beta public-ip create --associated-resource-id xxx --labels key=value,foo=bar ``` diff --git a/docs/stackit_beta_public-ip_update.md b/docs/stackit_beta_public-ip_update.md index 5426bb22e..cc62e28ac 100644 --- a/docs/stackit_beta_public-ip_update.md +++ b/docs/stackit_beta_public-ip_update.md @@ -16,7 +16,7 @@ stackit beta public-ip update [flags] Update public IP with ID "xxx" $ stackit beta public-ip update xxx - Update public IP with ID "xxx" with new associated resource with ID "yyy" + Update public IP with ID "xxx" with new associated resource ID "yyy" $ stackit beta public-ip update xxx --associated-resource-id yyy Update public IP with ID "xxx" with new labels diff --git a/internal/cmd/beta/public-ip/create/create.go b/internal/cmd/beta/public-ip/create/create.go index a2f6565c3..1f2bbe18c 100644 --- a/internal/cmd/beta/public-ip/create/create.go +++ b/internal/cmd/beta/public-ip/create/create.go @@ -46,7 +46,7 @@ func NewCmd(p *print.Printer) *cobra.Command { `$ stackit beta public-ip create --associated-resource-id xxx`, ), examples.NewExample( - `Create a public IP with associated resource with ID "xxx" and labels`, + `Create a public IP with associated resource ID "xxx" and labels`, `$ stackit beta public-ip create --associated-resource-id xxx --labels key=value,foo=bar`, ), ), diff --git a/internal/cmd/beta/public-ip/update/update.go b/internal/cmd/beta/public-ip/update/update.go index fa4db7c98..89e4764a0 100644 --- a/internal/cmd/beta/public-ip/update/update.go +++ b/internal/cmd/beta/public-ip/update/update.go @@ -46,7 +46,7 @@ func NewCmd(p *print.Printer) *cobra.Command { `$ stackit beta public-ip update xxx`, ), examples.NewExample( - `Update public IP with ID "xxx" with new associated resource with ID "yyy"`, + `Update public IP with ID "xxx" with new associated resource ID "yyy"`, `$ stackit beta public-ip update xxx --associated-resource-id yyy`, ), examples.NewExample( From 211fd43f54b727dcae7d8fc8e3ebccdcf086cb8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Thu, 28 Nov 2024 15:38:33 +0100 Subject: [PATCH 07/15] remove associated resource id from update command --- docs/stackit_beta_public-ip_update.md | 8 ++----- internal/cmd/beta/public-ip/update/update.go | 23 ++++++------------- .../cmd/beta/public-ip/update/update_test.go | 21 +++-------------- 3 files changed, 12 insertions(+), 40 deletions(-) diff --git a/docs/stackit_beta_public-ip_update.md b/docs/stackit_beta_public-ip_update.md index cc62e28ac..f5147ef0f 100644 --- a/docs/stackit_beta_public-ip_update.md +++ b/docs/stackit_beta_public-ip_update.md @@ -16,9 +16,6 @@ stackit beta public-ip update [flags] Update public IP with ID "xxx" $ stackit beta public-ip update xxx - Update public IP with ID "xxx" with new associated resource ID "yyy" - $ stackit beta public-ip update xxx --associated-resource-id yyy - Update public IP with ID "xxx" with new labels $ stackit beta public-ip update xxx --labels key=value,foo=bar ``` @@ -26,9 +23,8 @@ stackit beta public-ip update [flags] ### Options ``` - --associated-resource-id string Associates the public IP with a network interface or virtual IP (ID) - -h, --help Help for "stackit beta public-ip update" - --labels stringToString Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...' (default []) + -h, --help Help for "stackit beta public-ip update" + --labels stringToString Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...' (default []) ``` ### Options inherited from parent commands diff --git a/internal/cmd/beta/public-ip/update/update.go b/internal/cmd/beta/public-ip/update/update.go index 89e4764a0..b4f79caac 100644 --- a/internal/cmd/beta/public-ip/update/update.go +++ b/internal/cmd/beta/public-ip/update/update.go @@ -23,15 +23,13 @@ import ( const ( publicIpIdArg = "PUBLIC_IP_ID" - associatedResourceIdFlag = "associated-resource-id" - labelFlag = "labels" + labelFlag = "labels" ) type inputModel struct { *globalflags.GlobalFlagModel - PublicIpId string - AssociatedResourceId *string - Labels *map[string]string + PublicIpId string + Labels *map[string]string } func NewCmd(p *print.Printer) *cobra.Command { @@ -45,10 +43,6 @@ func NewCmd(p *print.Printer) *cobra.Command { `Update public IP with ID "xxx"`, `$ stackit beta public-ip update xxx`, ), - examples.NewExample( - `Update public IP with ID "xxx" with new associated resource ID "yyy"`, - `$ stackit beta public-ip update xxx --associated-resource-id yyy`, - ), examples.NewExample( `Update public IP with ID "xxx" with new labels`, `$ stackit beta public-ip update xxx --labels key=value,foo=bar`, @@ -96,7 +90,6 @@ func NewCmd(p *print.Printer) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.UUIDFlag(), associatedResourceIdFlag, "Associates the public IP with a network interface or virtual IP (ID)") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...'") } @@ -109,10 +102,9 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } model := inputModel{ - GlobalFlagModel: globalFlags, - PublicIpId: publicIpId, - AssociatedResourceId: flags.FlagToStringPointer(p, cmd, associatedResourceIdFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + GlobalFlagModel: globalFlags, + PublicIpId: publicIpId, + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), } if p.IsVerbosityDebug() { @@ -140,8 +132,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } payload := iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(model.AssociatedResourceId), - Labels: labelsMap, + Labels: labelsMap, } return req.UpdatePublicIPPayload(payload) diff --git a/internal/cmd/beta/public-ip/update/update_test.go b/internal/cmd/beta/public-ip/update/update_test.go index 17937f46a..ba9f1774a 100644 --- a/internal/cmd/beta/public-ip/update/update_test.go +++ b/internal/cmd/beta/public-ip/update/update_test.go @@ -22,7 +22,6 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &iaas.APIClient{} var testProjectId = uuid.NewString() -var testAssociatedResourceId = uuid.NewString() var testPublicIpId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -37,9 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - associatedResourceIdFlag: testAssociatedResourceId, - labelFlag: "key=value", + projectIdFlag: testProjectId, + labelFlag: "key=value", } for _, mod := range mods { mod(flagValues) @@ -53,8 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - PublicIpId: testPublicIpId, - AssociatedResourceId: utils.Ptr(testAssociatedResourceId), + PublicIpId: testPublicIpId, Labels: utils.Ptr(map[string]string{ "key": "value", }), @@ -76,7 +73,6 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.A func fixturePayload(mods ...func(payload *iaas.UpdatePublicIPPayload)) iaas.UpdatePublicIPPayload { payload := iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), Labels: utils.Ptr(map[string]interface{}{ "key": "value", }), @@ -144,17 +140,6 @@ func TestParseInput(t *testing.T) { flagValues: fixtureFlagValues(), isValid: false, }, - { - description: "use associated resource id", - argValues: fixtureArgValues(), - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[associatedResourceIdFlag] = testAssociatedResourceId - }), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.AssociatedResourceId = utils.Ptr(testAssociatedResourceId) - }), - }, { description: "use labels", argValues: fixtureArgValues(), From f577f45eb448d09166748679537eab98bf3a51fb Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:41:00 +0100 Subject: [PATCH 08/15] Update internal/cmd/beta/public-ip/associate/associate.go Co-authored-by: Alexander Dahmen --- internal/cmd/beta/public-ip/associate/associate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/public-ip/associate/associate.go b/internal/cmd/beta/public-ip/associate/associate.go index a932e34b0..20832f4a6 100644 --- a/internal/cmd/beta/public-ip/associate/associate.go +++ b/internal/cmd/beta/public-ip/associate/associate.go @@ -76,7 +76,7 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("associate public IP: %w", err) } - p.Outputf("Associated public IP %q with resource %v.\n", publicIpLabel, resp.NetworkInterface) + p.Outputf("Associated public IP %q with resource %v.\n", publicIpLabel, *resp.GetNetworkInterface()) return nil }, } From 035d4e0b721d092fea32b28ada2001c78ebfbb67 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:41:05 +0100 Subject: [PATCH 09/15] Update internal/cmd/beta/public-ip/delete/delete.go Co-authored-by: Alexander Dahmen --- internal/cmd/beta/public-ip/delete/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/public-ip/delete/delete.go b/internal/cmd/beta/public-ip/delete/delete.go index 1b46ead44..8a977b0ca 100644 --- a/internal/cmd/beta/public-ip/delete/delete.go +++ b/internal/cmd/beta/public-ip/delete/delete.go @@ -60,7 +60,7 @@ func NewCmd(p *print.Printer) *cobra.Command { } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete public IP %q?", publicIpLabel) + prompt := fmt.Sprintf("Are you sure you want to delete public IP %q? (This cannot be undone)", publicIpLabel) err = p.PromptForConfirmation(prompt) if err != nil { return err From d6081659b938b37ade90e44cde5b7b19a26cd263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Thu, 28 Nov 2024 15:57:28 +0100 Subject: [PATCH 10/15] add associated resource id information in the disassociate command --- internal/cmd/beta/public-ip/associate/associate.go | 2 +- internal/cmd/beta/public-ip/delete/delete.go | 2 +- .../cmd/beta/public-ip/disassociate/disassociate.go | 6 +++--- internal/cmd/beta/public-ip/update/update.go | 2 +- internal/pkg/services/iaas/utils/utils.go | 10 +++++++--- internal/pkg/services/iaas/utils/utils_test.go | 5 +++-- 6 files changed, 16 insertions(+), 11 deletions(-) diff --git a/internal/cmd/beta/public-ip/associate/associate.go b/internal/cmd/beta/public-ip/associate/associate.go index 20832f4a6..9e82108e9 100644 --- a/internal/cmd/beta/public-ip/associate/associate.go +++ b/internal/cmd/beta/public-ip/associate/associate.go @@ -55,7 +55,7 @@ func NewCmd(p *print.Printer) *cobra.Command { return err } - publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) if err != nil { p.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId diff --git a/internal/cmd/beta/public-ip/delete/delete.go b/internal/cmd/beta/public-ip/delete/delete.go index 8a977b0ca..28e625e23 100644 --- a/internal/cmd/beta/public-ip/delete/delete.go +++ b/internal/cmd/beta/public-ip/delete/delete.go @@ -53,7 +53,7 @@ func NewCmd(p *print.Printer) *cobra.Command { return err } - publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) if err != nil { p.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId diff --git a/internal/cmd/beta/public-ip/disassociate/disassociate.go b/internal/cmd/beta/public-ip/disassociate/disassociate.go index f55596f68..fe47bcdb0 100644 --- a/internal/cmd/beta/public-ip/disassociate/disassociate.go +++ b/internal/cmd/beta/public-ip/disassociate/disassociate.go @@ -51,14 +51,14 @@ func NewCmd(p *print.Printer) *cobra.Command { return err } - publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, associatedResourceId, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) if err != nil { p.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId } if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disassociate public IP %q from the associated resource?", publicIpLabel) + prompt := fmt.Sprintf("Are you sure you want to disassociate public IP %q from the associated resource %q?", publicIpLabel, associatedResourceId) err = p.PromptForConfirmation(prompt) if err != nil { return err @@ -72,7 +72,7 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("disassociate public IP: %w", err) } - p.Outputf("Disassociated public IP %q from the associated resource.\n", publicIpLabel) + p.Outputf("Disassociated public IP %q from the associated resource %q.\n", publicIpLabel, associatedResourceId) return nil }, } diff --git a/internal/cmd/beta/public-ip/update/update.go b/internal/cmd/beta/public-ip/update/update.go index b4f79caac..d930ab19e 100644 --- a/internal/cmd/beta/public-ip/update/update.go +++ b/internal/cmd/beta/public-ip/update/update.go @@ -61,7 +61,7 @@ func NewCmd(p *print.Printer) *cobra.Command { return err } - publicIpLabel, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) if err != nil { p.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index 0da1b98eb..fc24ec4bc 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -17,12 +17,16 @@ type IaaSClient interface { GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error) } -func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (string, error) { +func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (string, string, error) { resp, err := apiClient.GetPublicIPExecute(ctx, projectId, publicIpId) if err != nil { - return "", fmt.Errorf("get public ip: %w", err) + return "", "", fmt.Errorf("get public ip: %w", err) } - return *resp.Ip, nil + associatedResourceId := "" + if resp.GetNetworkInterface() != nil { + associatedResourceId = *resp.GetNetworkInterface() + } + return *resp.Ip, associatedResourceId, nil } func GetServerName(ctx context.Context, apiClient IaaSClient, projectId, serverId string) (string, error) { diff --git a/internal/pkg/services/iaas/utils/utils_test.go b/internal/pkg/services/iaas/utils/utils_test.go index 2d54658d4..408cc7404 100644 --- a/internal/pkg/services/iaas/utils/utils_test.go +++ b/internal/pkg/services/iaas/utils/utils_test.go @@ -91,7 +91,8 @@ func TestGetPublicIp(t *testing.T) { name: "base", args: args{ getPublicIpResp: &iaas.PublicIp{ - Ip: utils.Ptr("1.2.3.4"), + Ip: utils.Ptr("1.2.3.4"), + NetworkInterface: iaas.NewNullableString(utils.Ptr("1.2.3.4")), }, }, want: "1.2.3.4", @@ -110,7 +111,7 @@ func TestGetPublicIp(t *testing.T) { GetPublicIpFails: tt.args.getPublicIpFails, GetPublicIpResp: tt.args.getPublicIpResp, } - got, err := GetPublicIP(context.Background(), m, "", "") + got, _, err := GetPublicIP(context.Background(), m, "", "") if (err != nil) != tt.wantErr { t.Errorf("GetPublicIP() error = %v, wantErr %v", err, tt.wantErr) return From c564047572d45e328e8c3cff4928a9befbc6d0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Thu, 28 Nov 2024 16:01:48 +0100 Subject: [PATCH 11/15] fix linter issues --- internal/pkg/services/iaas/utils/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index fc24ec4bc..8b2c96d85 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -17,7 +17,7 @@ type IaaSClient interface { GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error) } -func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (string, string, error) { +func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (ip, associatedResource string, err error) { resp, err := apiClient.GetPublicIPExecute(ctx, projectId, publicIpId) if err != nil { return "", "", fmt.Errorf("get public ip: %w", err) From 873a4ae0b6c5a675e9132dfc945296c1fb8052ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Thu, 28 Nov 2024 16:08:59 +0100 Subject: [PATCH 12/15] remove obsolete unit tests --- .../cmd/beta/public-ip/associate/associate_test.go | 11 ----------- internal/cmd/beta/public-ip/update/update_test.go | 13 ------------- 2 files changed, 24 deletions(-) diff --git a/internal/cmd/beta/public-ip/associate/associate_test.go b/internal/cmd/beta/public-ip/associate/associate_test.go index c2b3799ef..99b51c3e6 100644 --- a/internal/cmd/beta/public-ip/associate/associate_test.go +++ b/internal/cmd/beta/public-ip/associate/associate_test.go @@ -136,17 +136,6 @@ func TestParseInput(t *testing.T) { flagValues: fixtureFlagValues(), isValid: false, }, - { - description: "use associated resource id", - argValues: fixtureArgValues(), - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[associatedResourceIdFlag] = testAssociatedResourceId - }), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.AssociatedResourceId = utils.Ptr(testAssociatedResourceId) - }), - }, { description: "associated resource id missing", argValues: fixtureArgValues(), diff --git a/internal/cmd/beta/public-ip/update/update_test.go b/internal/cmd/beta/public-ip/update/update_test.go index ba9f1774a..9590a858a 100644 --- a/internal/cmd/beta/public-ip/update/update_test.go +++ b/internal/cmd/beta/public-ip/update/update_test.go @@ -140,19 +140,6 @@ func TestParseInput(t *testing.T) { flagValues: fixtureFlagValues(), isValid: false, }, - { - description: "use labels", - argValues: fixtureArgValues(), - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[labelFlag] = "key=value" - }), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = &map[string]string{ - "key": "value", - } - }), - }, } for _, tt := range tests { From 395720cd4d84b9aef6cb44592316d5b4f0b5a9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Thu, 28 Nov 2024 16:58:17 +0100 Subject: [PATCH 13/15] add empty update check --- internal/cmd/beta/public-ip/update/update.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/cmd/beta/public-ip/update/update.go b/internal/cmd/beta/public-ip/update/update.go index d930ab19e..ff7899e89 100644 --- a/internal/cmd/beta/public-ip/update/update.go +++ b/internal/cmd/beta/public-ip/update/update.go @@ -9,6 +9,7 @@ import ( "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" @@ -101,10 +102,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu return nil, &cliErr.ProjectIdError{} } + labels := flags.FlagToStringToStringPointer(p, cmd, labelFlag) + + if labels == nil { + return nil, &errors.EmptyUpdateError{} + } + model := inputModel{ GlobalFlagModel: globalFlags, PublicIpId: publicIpId, - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + Labels: labels, } if p.IsVerbosityDebug() { From e354d24e55628d71632784c2eb268677dd912575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 3 Dec 2024 12:28:14 +0100 Subject: [PATCH 14/15] change public ip util to avoid NPE --- internal/pkg/services/iaas/utils/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index 8b2c96d85..fecc0872c 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -23,8 +23,8 @@ func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpI return "", "", fmt.Errorf("get public ip: %w", err) } associatedResourceId := "" - if resp.GetNetworkInterface() != nil { - associatedResourceId = *resp.GetNetworkInterface() + if resp.NetworkInterface != nil { + associatedResourceId = *resp.NetworkInterface.Get() } return *resp.Ip, associatedResourceId, nil } From d586e93a2989866afeea2714cecdb82f0d64eddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 3 Dec 2024 13:07:53 +0100 Subject: [PATCH 15/15] adapt documentation --- docs/stackit_beta_public-ip.md | 4 ++-- docs/stackit_beta_public-ip_associate.md | 2 +- docs/stackit_beta_public-ip_create.md | 2 +- docs/stackit_beta_public-ip_delete.md | 2 +- docs/stackit_beta_public-ip_describe.md | 2 +- docs/stackit_beta_public-ip_disassociate.md | 2 +- docs/stackit_beta_public-ip_list.md | 2 +- docs/stackit_beta_public-ip_update.md | 2 +- internal/cmd/beta/public-ip/public-ip.go | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/stackit_beta_public-ip.md b/docs/stackit_beta_public-ip.md index 1afea1a63..8d4ff0737 100644 --- a/docs/stackit_beta_public-ip.md +++ b/docs/stackit_beta_public-ip.md @@ -1,10 +1,10 @@ ## stackit beta public-ip -Provides functionality for Public IP +Provides functionality for public IPs ### Synopsis -Provides functionality for Public IP. +Provides functionality for public IPs. ``` stackit beta public-ip [flags] diff --git a/docs/stackit_beta_public-ip_associate.md b/docs/stackit_beta_public-ip_associate.md index db6c15a6f..7a7cf8788 100644 --- a/docs/stackit_beta_public-ip_associate.md +++ b/docs/stackit_beta_public-ip_associate.md @@ -36,5 +36,5 @@ stackit beta public-ip associate [flags] ### SEE ALSO -* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs diff --git a/docs/stackit_beta_public-ip_create.md b/docs/stackit_beta_public-ip_create.md index a566ac76e..03399d78e 100644 --- a/docs/stackit_beta_public-ip_create.md +++ b/docs/stackit_beta_public-ip_create.md @@ -43,5 +43,5 @@ stackit beta public-ip create [flags] ### SEE ALSO -* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs diff --git a/docs/stackit_beta_public-ip_delete.md b/docs/stackit_beta_public-ip_delete.md index bf3013edd..ef5fb53c0 100644 --- a/docs/stackit_beta_public-ip_delete.md +++ b/docs/stackit_beta_public-ip_delete.md @@ -37,5 +37,5 @@ stackit beta public-ip delete [flags] ### SEE ALSO -* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs diff --git a/docs/stackit_beta_public-ip_describe.md b/docs/stackit_beta_public-ip_describe.md index 029ab034e..4a0e85986 100644 --- a/docs/stackit_beta_public-ip_describe.md +++ b/docs/stackit_beta_public-ip_describe.md @@ -38,5 +38,5 @@ stackit beta public-ip describe [flags] ### SEE ALSO -* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs diff --git a/docs/stackit_beta_public-ip_disassociate.md b/docs/stackit_beta_public-ip_disassociate.md index 28b9f9b35..65c7cf8ac 100644 --- a/docs/stackit_beta_public-ip_disassociate.md +++ b/docs/stackit_beta_public-ip_disassociate.md @@ -35,5 +35,5 @@ stackit beta public-ip disassociate [flags] ### SEE ALSO -* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs diff --git a/docs/stackit_beta_public-ip_list.md b/docs/stackit_beta_public-ip_list.md index 007a7bc57..7e237c8c8 100644 --- a/docs/stackit_beta_public-ip_list.md +++ b/docs/stackit_beta_public-ip_list.md @@ -46,5 +46,5 @@ stackit beta public-ip list [flags] ### SEE ALSO -* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs diff --git a/docs/stackit_beta_public-ip_update.md b/docs/stackit_beta_public-ip_update.md index f5147ef0f..37c8ef7ed 100644 --- a/docs/stackit_beta_public-ip_update.md +++ b/docs/stackit_beta_public-ip_update.md @@ -39,5 +39,5 @@ stackit beta public-ip update [flags] ### SEE ALSO -* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for Public IP +* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs diff --git a/internal/cmd/beta/public-ip/public-ip.go b/internal/cmd/beta/public-ip/public-ip.go index 7005d00c0..5c5c8f410 100644 --- a/internal/cmd/beta/public-ip/public-ip.go +++ b/internal/cmd/beta/public-ip/public-ip.go @@ -18,8 +18,8 @@ import ( func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "public-ip", - Short: "Provides functionality for Public IP", - Long: "Provides functionality for Public IP.", + Short: "Provides functionality for public IPs", + Long: "Provides functionality for public IPs.", Args: args.NoArgs, Run: utils.CmdHelp, }