diff --git a/README.md b/README.md
index eaae8e299..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`
`beta server` | :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
diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md
index edc19472c..abc498479 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 networks
* [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 interfaces
+* [stackit beta public-ip](./stackit_beta_public-ip.md) - Provides functionality for public IPs
* [stackit beta server](./stackit_beta_server.md) - Provides functionality for servers
* [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex
* [stackit beta volume](./stackit_beta_volume.md) - Provides functionality for volumes
diff --git a/docs/stackit_beta_public-ip.md b/docs/stackit_beta_public-ip.md
new file mode 100644
index 000000000..8d4ff0737
--- /dev/null
+++ b/docs/stackit_beta_public-ip.md
@@ -0,0 +1,39 @@
+## stackit beta public-ip
+
+Provides functionality for public IPs
+
+### Synopsis
+
+Provides functionality for public IPs.
+
+```
+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..7a7cf8788
--- /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 IPs
+
diff --git a/docs/stackit_beta_public-ip_create.md b/docs/stackit_beta_public-ip_create.md
new file mode 100644
index 000000000..03399d78e
--- /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 ID "xxx"
+ $ stackit beta public-ip create --associated-resource-id xxx
+
+ 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
+```
+
+### 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 IPs
+
diff --git a/docs/stackit_beta_public-ip_delete.md b/docs/stackit_beta_public-ip_delete.md
new file mode 100644
index 000000000..ef5fb53c0
--- /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 IPs
+
diff --git a/docs/stackit_beta_public-ip_describe.md b/docs/stackit_beta_public-ip_describe.md
new file mode 100644
index 000000000..4a0e85986
--- /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 IPs
+
diff --git a/docs/stackit_beta_public-ip_disassociate.md b/docs/stackit_beta_public-ip_disassociate.md
new file mode 100644
index 000000000..65c7cf8ac
--- /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 IPs
+
diff --git a/docs/stackit_beta_public-ip_list.md b/docs/stackit_beta_public-ip_list.md
new file mode 100644
index 000000000..7e237c8c8
--- /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 IPs
+
diff --git a/docs/stackit_beta_public-ip_update.md b/docs/stackit_beta_public-ip_update.md
new file mode 100644
index 000000000..37c8ef7ed
--- /dev/null
+++ b/docs/stackit_beta_public-ip_update.md
@@ -0,0 +1,43 @@
+## 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 labels
+ $ stackit beta public-ip update xxx --labels key=value,foo=bar
+```
+
+### Options
+
+```
+ -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 IPs
+
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..9e82108e9
--- /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.GetNetworkInterface())
+ 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..99b51c3e6
--- /dev/null
+++ b/internal/cmd/beta/public-ip/associate/associate_test.go
@@ -0,0 +1,246 @@
+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: "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..1f2bbe18c
--- /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 ID "xxx"`,
+ `$ stackit beta public-ip create --associated-resource-id xxx`,
+ ),
+ examples.NewExample(
+ `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`,
+ ),
+ ),
+ 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..28e625e23
--- /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? (This cannot be undone)", 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..9e47ab890
--- /dev/null
+++ b/internal/cmd/beta/public-ip/describe/describe.go
@@ -0,0 +1,149 @@
+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()
+
+ if publicIp.NetworkInterface != nil {
+ networkInterfaceId := *publicIp.GetNetworkInterface()
+ table.AddRow("ASSOCIATED TO", networkInterfaceId)
+ 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..fe47bcdb0
--- /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, 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 %q?", publicIpLabel, associatedResourceId)
+ 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 %q.\n", publicIpLabel, associatedResourceId)
+ 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..4ab82d197
--- /dev/null
+++ b/internal/cmd/beta/public-ip/list/list.go
@@ -0,0 +1,181 @@
+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 {
+ networkInterfaceId := ""
+ if publicIp.NetworkInterface != nil {
+ networkInterfaceId = *publicIp.GetNetworkInterface()
+ }
+ table.AddRow(*publicIp.Id, *publicIp.Ip, networkInterfaceId)
+ 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..5c5c8f410
--- /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 IPs",
+ Long: "Provides functionality for public IPs.",
+ 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..ff7899e89
--- /dev/null
+++ b/internal/cmd/beta/public-ip/update/update.go
@@ -0,0 +1,170 @@
+package update
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/goccy/go-yaml"
+
+ "github.com/spf13/cobra"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/args"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
+ 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"
+
+ labelFlag = "labels"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ PublicIpId 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 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().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{}
+ }
+
+ labels := flags.FlagToStringToStringPointer(p, cmd, labelFlag)
+
+ if labels == nil {
+ return nil, &errors.EmptyUpdateError{}
+ }
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ PublicIpId: publicIpId,
+ Labels: labels,
+ }
+
+ 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{
+ 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..9590a858a
--- /dev/null
+++ b/internal/cmd/beta/public-ip/update/update_test.go
@@ -0,0 +1,226 @@
+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 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,
+ 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,
+ 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{
+ 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,
+ },
+ }
+
+ 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)
+ }
+ })
+ }
+}