diff --git a/README.md b/README.md
index df0cbe601..e15f0687b 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ Below you can find a list of the STACKIT services already available in the CLI (
| Service | CLI Commands | Status |
| ---------------------------------- |----------------------------------------------------------------| ------------------------- |
| Observability | `observability` | :white_check_mark: |
-| Infrastructure as a Service (IaaS) | `beta network-area`
`beta network`
`beta volume` | :white_check_mark: (beta) |
+| 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: |
diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md
index 8da8fef61..56453f281 100644
--- a/docs/stackit_beta.md
+++ b/docs/stackit_beta.md
@@ -42,6 +42,7 @@ stackit beta [flags]
* [stackit](./stackit.md) - Manage STACKIT resources using the command line
* [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 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_network-interface.md b/docs/stackit_beta_network-interface.md
new file mode 100644
index 000000000..1832e4ef9
--- /dev/null
+++ b/docs/stackit_beta_network-interface.md
@@ -0,0 +1,37 @@
+## stackit beta network-interface
+
+Provides functionality for Network Interface
+
+### Synopsis
+
+Provides functionality for Network Interface.
+
+```
+stackit beta network-interface [flags]
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta network-interface"
+```
+
+### Options inherited from parent commands
+
+```
+ -y, --assume-yes If set, skips all confirmation prompts
+ --async If set, runs the command asynchronously
+ -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
+ -p, --project-id string Project ID
+ --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
+```
+
+### SEE ALSO
+
+* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands
+* [stackit beta network-interface create](./stackit_beta_network-interface_create.md) - Creates a network interface
+* [stackit beta network-interface delete](./stackit_beta_network-interface_delete.md) - Deletes a network interface
+* [stackit beta network-interface describe](./stackit_beta_network-interface_describe.md) - Describes a network interface
+* [stackit beta network-interface list](./stackit_beta_network-interface_list.md) - Lists all network interfaces of a network
+* [stackit beta network-interface update](./stackit_beta_network-interface_update.md) - Updates a network interface
+
diff --git a/docs/stackit_beta_network-interface_create.md b/docs/stackit_beta_network-interface_create.md
new file mode 100644
index 000000000..a36ed217f
--- /dev/null
+++ b/docs/stackit_beta_network-interface_create.md
@@ -0,0 +1,50 @@
+## stackit beta network-interface create
+
+Creates a network interface
+
+### Synopsis
+
+Creates a network interface.
+
+```
+stackit beta network-interface create [flags]
+```
+
+### Examples
+
+```
+ Create a network interface for network with ID "xxx"
+ $ stackit beta network-interface create --network-id xxx
+
+ Create a network interface with allowed addresses, labels, a name, security groups and nic security enabled for network with ID "xxx"
+ $ stackit beta network-interface create --network-id xxx --allowed-addresses "1.1.1.1,8.8.8.8,9.9.9.9" --labels key=value,key2=value2 --name NAME --security-groups "UUID1,UUID2" --nic-security
+```
+
+### Options
+
+```
+ --allowed-addresses strings List of allowed IPs
+ -h, --help Help for "stackit beta network-interface create"
+ -i, --ipv4 string IPv4 address
+ -s, --ipv6 string IPv6 address
+ --labels stringToString Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...' (default [])
+ -n, --name string Network interface name
+ --network-id string Network ID
+ -b, --nic-security If this is set to false, then no security groups will apply to this network interface. (default true)
+ --security-groups strings List of security groups
+```
+
+### 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 network-interface](./stackit_beta_network-interface.md) - Provides functionality for Network Interface
+
diff --git a/docs/stackit_beta_network-interface_delete.md b/docs/stackit_beta_network-interface_delete.md
new file mode 100644
index 000000000..954a7b472
--- /dev/null
+++ b/docs/stackit_beta_network-interface_delete.md
@@ -0,0 +1,40 @@
+## stackit beta network-interface delete
+
+Deletes a network interface
+
+### Synopsis
+
+Deletes a network interface.
+
+```
+stackit beta network-interface delete [flags]
+```
+
+### Examples
+
+```
+ Delete network interface with nic id "xxx" and network ID "yyy"
+ $ stackit beta network-interface delete xxx --network-id yyy
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta network-interface delete"
+ --network-id string Network ID
+```
+
+### Options inherited from parent commands
+
+```
+ -y, --assume-yes If set, skips all confirmation prompts
+ --async If set, runs the command asynchronously
+ -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
+ -p, --project-id string Project ID
+ --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
+```
+
+### SEE ALSO
+
+* [stackit beta network-interface](./stackit_beta_network-interface.md) - Provides functionality for Network Interface
+
diff --git a/docs/stackit_beta_network-interface_describe.md b/docs/stackit_beta_network-interface_describe.md
new file mode 100644
index 000000000..a879d87aa
--- /dev/null
+++ b/docs/stackit_beta_network-interface_describe.md
@@ -0,0 +1,46 @@
+## stackit beta network-interface describe
+
+Describes a network interface
+
+### Synopsis
+
+Describes a network interface.
+
+```
+stackit beta network-interface describe [flags]
+```
+
+### Examples
+
+```
+ Describes network interface with nic id "xxx" and network ID "yyy"
+ $ stackit beta network-interface describe xxx --network-id yyy
+
+ Describes network interface with nic id "xxx" and network ID "yyy" in JSON format
+ $ stackit beta network-interface describe xxx --network-id yyy --output-format json
+
+ Describes network interface with nic id "xxx" and network ID "yyy" in yaml format
+ $ stackit beta network-interface describe xxx --network-id yyy --output-format yaml
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta network-interface describe"
+ --network-id string Network ID
+```
+
+### Options inherited from parent commands
+
+```
+ -y, --assume-yes If set, skips all confirmation prompts
+ --async If set, runs the command asynchronously
+ -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
+ -p, --project-id string Project ID
+ --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
+```
+
+### SEE ALSO
+
+* [stackit beta network-interface](./stackit_beta_network-interface.md) - Provides functionality for Network Interface
+
diff --git a/docs/stackit_beta_network-interface_list.md b/docs/stackit_beta_network-interface_list.md
new file mode 100644
index 000000000..04707adcb
--- /dev/null
+++ b/docs/stackit_beta_network-interface_list.md
@@ -0,0 +1,51 @@
+## stackit beta network-interface list
+
+Lists all network interfaces of a network
+
+### Synopsis
+
+Lists all network interfaces of a network.
+
+```
+stackit beta network-interface list [flags]
+```
+
+### Examples
+
+```
+ Lists all network interfaces with network ID "xxx"
+ $ stackit beta network-interface list --network-id xxx
+
+ Lists all network interfaces with network ID "xxx" which contains the label xxx
+ $ stackit beta network-interface list --network-id xxx --label-selector xxx
+
+ Lists all network interfaces with network ID "xxx" in JSON format
+ $ stackit beta network-interface list --network-id xxx --output-format json
+
+ Lists up to 10 network interfaces with network ID "xxx"
+ $ stackit beta network-interface list --network-id xxx --limit 10
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta network-interface list"
+ --label-selector string Filter by label
+ --limit int Maximum number of entries to list
+ --network-id string Network ID
+```
+
+### Options inherited from parent commands
+
+```
+ -y, --assume-yes If set, skips all confirmation prompts
+ --async If set, runs the command asynchronously
+ -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"]
+ -p, --project-id string Project ID
+ --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info")
+```
+
+### SEE ALSO
+
+* [stackit beta network-interface](./stackit_beta_network-interface.md) - Provides functionality for Network Interface
+
diff --git a/docs/stackit_beta_network-interface_update.md b/docs/stackit_beta_network-interface_update.md
new file mode 100644
index 000000000..506fb1ba4
--- /dev/null
+++ b/docs/stackit_beta_network-interface_update.md
@@ -0,0 +1,51 @@
+## stackit beta network-interface update
+
+Updates a network interface
+
+### Synopsis
+
+Updates a network interface.
+
+```
+stackit beta network-interface update [flags]
+```
+
+### Examples
+
+```
+ Updates a network interface with nic id "xxx" and network-id "yyy" to new allowed addresses "1.1.1.1,8.8.8.8,9.9.9.9" and new labels "key=value,key2=value2"
+ $ stackit beta network-interface update xxx --network-id yyy --allowed-addresses "1.1.1.1,8.8.8.8,9.9.9.9" --labels key=value,key2=value2
+
+ Updates a network interface with nic id "xxx" and network-id "yyy" with new name "nic-name-new"
+ $ stackit beta network-interface update xxx --network-id yyy --name nic-name-new
+
+ Updates a network interface with nic id "xxx" and network-id "yyy" to include the security group "zzz"
+ $ stackit beta network-interface update xxx --network-id yyy --security-groups zzz
+```
+
+### Options
+
+```
+ --allowed-addresses strings List of allowed IPs
+ -h, --help Help for "stackit beta network-interface update"
+ --labels stringToString Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...' (default [])
+ -n, --name string Network interface name
+ --network-id string Network ID
+ -b, --nic-security If this is set to false, then no security groups will apply to this network interface. (default true)
+ --security-groups strings List of security groups
+```
+
+### 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 network-interface](./stackit_beta_network-interface.md) - Provides functionality for Network Interface
+
diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go
index e1a16ccdb..ab429fe4f 100644
--- a/internal/cmd/beta/beta.go
+++ b/internal/cmd/beta/beta.go
@@ -5,6 +5,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"
"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"
@@ -44,4 +45,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(networkArea.NewCmd(p))
cmd.AddCommand(network.NewCmd(p))
cmd.AddCommand(volume.NewCmd(p))
+ cmd.AddCommand(networkinterface.NewCmd(p))
}
diff --git a/internal/cmd/beta/network-interface/create/create.go b/internal/cmd/beta/network-interface/create/create.go
new file mode 100644
index 000000000..0a1345f47
--- /dev/null
+++ b/internal/cmd/beta/network-interface/create/create.go
@@ -0,0 +1,251 @@
+package create
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "regexp"
+
+ "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"
+ "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 (
+ networkIdFlag = "network-id"
+ allowedAddressesFlag = "allowed-addresses"
+ ipv4Flag = "ipv4"
+ ipv6Flag = "ipv6"
+ labelFlag = "labels"
+ nameFlag = "name"
+ securityGroupsFlag = "security-groups"
+ nicSecurityFlag = "nic-security"
+
+ nameRegex = `^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`
+ maxNameLength = 63
+ securityGroupsRegex = `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`
+ securityGroupLength = 36
+ defaultNicSecurityFlag = true
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ NetworkId *string
+ AllowedAddresses *[]iaas.AllowedAddressesInner
+ Ipv4 *string
+ Ipv6 *string
+ Labels *map[string]string
+ Name *string // <= 63 characters + regex ^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$
+ NicSecurity *bool
+ SecurityGroups *[]string // = 36 characters + regex ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "create",
+ Short: "Creates a network interface",
+ Long: "Creates a network interface.",
+ Args: args.NoArgs,
+ Example: examples.Build(
+ examples.NewExample(
+ `Create a network interface for network with ID "xxx"`,
+ `$ stackit beta network-interface create --network-id xxx`,
+ ),
+ examples.NewExample(
+ `Create a network interface with allowed addresses, labels, a name, security groups and nic security enabled for network with ID "xxx"`,
+ `$ stackit beta network-interface create --network-id xxx --allowed-addresses "1.1.1.1,8.8.8.8,9.9.9.9" --labels key=value,key2=value2 --name NAME --security-groups "UUID1,UUID2" --nic-security`,
+ ),
+ ),
+ 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 network interface 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 network interface: %w", err)
+ }
+
+ return outputResult(p, model, resp)
+ },
+ }
+ configureFlags(cmd)
+ return cmd
+}
+
+func configureFlags(cmd *cobra.Command) {
+ cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID")
+ cmd.Flags().StringSlice(allowedAddressesFlag, nil, "List of allowed IPs")
+ cmd.Flags().StringP(ipv4Flag, "i", "", "IPv4 address")
+ cmd.Flags().StringP(ipv6Flag, "s", "", "IPv6 address")
+ cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...'")
+ cmd.Flags().StringP(nameFlag, "n", "", "Network interface name")
+ cmd.Flags().BoolP(nicSecurityFlag, "b", defaultNicSecurityFlag, "If this is set to false, then no security groups will apply to this network interface.")
+ cmd.Flags().StringSlice(securityGroupsFlag, nil, "List of security groups")
+
+ err := flags.MarkFlagsRequired(cmd, networkIdFlag)
+ cobra.CheckErr(err)
+}
+
+func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
+ globalFlags := globalflags.Parse(p, cmd)
+ if globalFlags.ProjectId == "" {
+ return nil, &errors.ProjectIdError{}
+ }
+
+ allowedAddresses := flags.FlagToStringSlicePointer(p, cmd, allowedAddressesFlag)
+ var allowedAddressesInner []iaas.AllowedAddressesInner
+ if allowedAddresses != nil && len(*allowedAddresses) > 0 {
+ allowedAddressesInner = make([]iaas.AllowedAddressesInner, len(*allowedAddresses))
+ for i, address := range *allowedAddresses {
+ allowedAddressesInner[i].String = &address
+ }
+ }
+
+ // check name length <= 63 and regex must apply
+ name := flags.FlagToStringPointer(p, cmd, nameFlag)
+ if name != nil {
+ if len(*name) > maxNameLength {
+ return nil, &errors.FlagValidationError{
+ Flag: nameFlag,
+ Details: fmt.Sprintf("name %s is too long (maximum length is %d characters)", *name, maxNameLength),
+ }
+ }
+ nameRegex := regexp.MustCompile(nameRegex)
+ if !nameRegex.MatchString(*name) {
+ return nil, &errors.FlagValidationError{
+ Flag: nameFlag,
+ Details: fmt.Sprintf("name %s didn't match the required regex expression %s", *name, nameRegex),
+ }
+ }
+ }
+
+ // check security groups size and regex
+ securityGroups := flags.FlagToStringSlicePointer(p, cmd, securityGroupsFlag)
+ if securityGroups != nil && len(*securityGroups) > 0 {
+ securityGroupsRegex := regexp.MustCompile(securityGroupsRegex)
+ // iterate over them
+ for _, value := range *securityGroups {
+ if len(value) != securityGroupLength {
+ return nil, &errors.FlagValidationError{
+ Flag: securityGroupsFlag,
+ Details: fmt.Sprintf("security groups uuid %s does not match (must be %d characters long)", value, securityGroupLength),
+ }
+ }
+ if !securityGroupsRegex.MatchString(value) {
+ return nil, &errors.FlagValidationError{
+ Flag: securityGroupsFlag,
+ Details: fmt.Sprintf("security groups uuid %s didn't match the required regex expression %s", value, securityGroupsRegex),
+ }
+ }
+ }
+ }
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag),
+ Ipv4: flags.FlagToStringPointer(p, cmd, ipv4Flag),
+ Ipv6: flags.FlagToStringPointer(p, cmd, ipv6Flag),
+ Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag),
+ Name: name,
+ NicSecurity: flags.FlagToBoolPointer(p, cmd, nicSecurityFlag),
+ SecurityGroups: securityGroups,
+ }
+
+ if allowedAddresses != nil {
+ model.AllowedAddresses = utils.Ptr(allowedAddressesInner)
+ }
+
+ 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.ApiCreateNicRequest {
+ req := apiClient.CreateNic(ctx, model.ProjectId, *model.NetworkId)
+
+ var labelsMap *map[string]interface{}
+ if model.Labels != nil && len(*model.Labels) > 0 {
+ // convert map[string]string to map[string]interface{}
+ convertedMap := make(map[string]interface{}, len(*model.Labels))
+ for k, v := range *model.Labels {
+ convertedMap[k] = v
+ }
+ labelsMap = &convertedMap
+ }
+
+ payload := iaas.CreateNicPayload{
+ AllowedAddresses: model.AllowedAddresses,
+ Ipv4: model.Ipv4,
+ Ipv6: model.Ipv6,
+ Labels: labelsMap,
+ Name: model.Name,
+ NicSecurity: model.NicSecurity,
+ SecurityGroups: model.SecurityGroups,
+ }
+ return req.CreateNicPayload(payload)
+}
+
+func outputResult(p *print.Printer, model *inputModel, nic *iaas.NIC) error {
+ switch model.OutputFormat {
+ case print.JSONOutputFormat:
+ details, err := json.MarshalIndent(nic, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal network interface: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ case print.YAMLOutputFormat:
+ details, err := yaml.MarshalWithOptions(nic, yaml.IndentSequence(true))
+ if err != nil {
+ return fmt.Errorf("marshal network interface: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ default:
+ p.Outputf("Created network interface for project %q.\nNIC ID: %s\n", model.ProjectId, *nic.Id)
+ return nil
+ }
+}
diff --git a/internal/cmd/beta/network-interface/create/create_test.go b/internal/cmd/beta/network-interface/create/create_test.go
new file mode 100644
index 000000000..2173cf1ea
--- /dev/null
+++ b/internal/cmd/beta/network-interface/create/create_test.go
@@ -0,0 +1,262 @@
+package create
+
+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"
+)
+
+type testCtxKey struct{}
+
+var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
+var testClient = &iaas.APIClient{}
+
+var projectIdFlag = globalflags.ProjectIdFlag
+var testProjectId = uuid.NewString()
+var testNetworkId = uuid.NewString()
+var testSecurityGroup = uuid.NewString()
+
+func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
+ flagValues := map[string]string{
+ projectIdFlag: testProjectId,
+ networkIdFlag: testNetworkId,
+ allowedAddressesFlag: "1.1.1.1,8.8.8.8,9.9.9.9",
+ ipv4Flag: "1.2.3.4",
+ ipv6Flag: "2001:0db8:85a3:08d3::0370:7344",
+ labelFlag: "key=value",
+ nameFlag: "testNic",
+ nicSecurityFlag: "true",
+ securityGroupsFlag: testSecurityGroup,
+ }
+ for _, mod := range mods {
+ mod(flagValues)
+ }
+ return flagValues
+}
+
+func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
+ var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")),
+ }
+ model := &inputModel{
+ GlobalFlagModel: &globalflags.GlobalFlagModel{
+ ProjectId: testProjectId,
+ Verbosity: globalflags.VerbosityDefault,
+ },
+ NetworkId: utils.Ptr(testNetworkId),
+ AllowedAddresses: utils.Ptr(allowedAddresses),
+ Ipv4: utils.Ptr("1.2.3.4"),
+ Ipv6: utils.Ptr("2001:0db8:85a3:08d3::0370:7344"),
+ Labels: utils.Ptr(map[string]string{
+ "key": "value",
+ }),
+ Name: utils.Ptr("testNic"),
+ NicSecurity: utils.Ptr(true),
+ SecurityGroups: utils.Ptr([]string{testSecurityGroup}),
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiCreateNicRequest)) iaas.ApiCreateNicRequest {
+ request := testClient.CreateNic(testCtx, testProjectId, testNetworkId)
+ request = request.CreateNicPayload(fixturePayload())
+ for _, mod := range mods {
+ mod(&request)
+ }
+ return request
+}
+
+func fixturePayload(mods ...func(payload *iaas.CreateNicPayload)) iaas.CreateNicPayload {
+ var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")),
+ }
+ payload := iaas.CreateNicPayload{
+ AllowedAddresses: utils.Ptr(allowedAddresses),
+ Ipv4: utils.Ptr("1.2.3.4"),
+ Ipv6: utils.Ptr("2001:0db8:85a3:08d3::0370:7344"),
+ Labels: utils.Ptr(map[string]interface{}{
+ "key": "value",
+ }),
+ Name: utils.Ptr("testNic"),
+ NicSecurity: utils.Ptr(true),
+ SecurityGroups: utils.Ptr([]string{testSecurityGroup}),
+ }
+ 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: "no values",
+ flagValues: map[string]string{},
+ isValid: false,
+ },
+ {
+ description: "network id missing",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, networkIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 1",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 2",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = "invalid-uuid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "allowed addresses missing",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, allowedAddressesFlag)
+ }),
+ expectedModel: fixtureInputModel(func(model *inputModel) {
+ model.AllowedAddresses = nil
+ }),
+ isValid: true,
+ },
+ {
+ description: "name to long",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[nameFlag] = "verylongstringwith66characterstotestthenameregexwithinthisunittest"
+ }),
+ isValid: false,
+ },
+ {
+ description: "name invalid",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[nameFlag] = "test?"
+ }),
+ isValid: false,
+ },
+ {
+ description: "name empty string invalid",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[nameFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group uuid to short",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupsFlag] = "d61a8564-c8dd-4ffb-bc15-143e7d0c85e"
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group uuid invalid",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupsFlag] = "d61a8564-c8dd-4ffb-bc15-143e7d0c85e?"
+ }),
+ 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.ApiCreateNicRequest
+ }{
+ {
+ 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/network-interface/delete/delete.go b/internal/cmd/beta/network-interface/delete/delete.go
new file mode 100644
index 000000000..72adc082a
--- /dev/null
+++ b/internal/cmd/beta/network-interface/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/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"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+)
+
+const (
+ nicIdArg = "NIC_ID"
+
+ networkIdFlag = "network-id"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ NetworkId *string
+ NicId string
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete",
+ Short: "Deletes a network interface",
+ Long: "Deletes a network interface.",
+ Args: args.SingleArg(nicIdArg, utils.ValidateUUID),
+ Example: examples.Build(
+ examples.NewExample(
+ `Delete network interface with nic id "xxx" and network ID "yyy"`,
+ `$ stackit beta network-interface delete xxx --network-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
+ }
+
+ if !model.AssumeYes {
+ prompt := fmt.Sprintf("Are you sure you want to delete the network interface %q? (This cannot be undone)", model.NicId)
+ 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 network interface: %w", err)
+ }
+
+ p.Info("Deleted network interface %q\n", model.NicId)
+
+ return nil
+ },
+ }
+ configureFlags(cmd)
+ return cmd
+}
+
+func configureFlags(cmd *cobra.Command) {
+ cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID")
+
+ err := flags.MarkFlagsRequired(cmd, networkIdFlag)
+ cobra.CheckErr(err)
+}
+
+func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
+ nicId := inputArgs[0]
+ globalFlags := globalflags.Parse(p, cmd)
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag),
+ NicId: nicId,
+ }
+
+ 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.ApiDeleteNicRequest {
+ req := apiClient.DeleteNic(ctx, model.ProjectId, *model.NetworkId, model.NicId)
+ return req
+}
diff --git a/internal/cmd/beta/network-interface/delete/delete_test.go b/internal/cmd/beta/network-interface/delete/delete_test.go
new file mode 100644
index 000000000..4b9fdd56b
--- /dev/null
+++ b/internal/cmd/beta/network-interface/delete/delete_test.go
@@ -0,0 +1,202 @@
+package delete
+
+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"
+)
+
+type testCtxKey struct{}
+
+var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
+var testClient = &iaas.APIClient{}
+
+var projectIdFlag = globalflags.ProjectIdFlag
+var testProjectId = uuid.NewString()
+var testNetworkId = uuid.NewString()
+var testNicId = uuid.NewString()
+
+func fixtureArgValues(mods ...func(argValues []string)) []string {
+ argValues := []string{
+ testNicId,
+ }
+ 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,
+ networkIdFlag: testNetworkId,
+ }
+ 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,
+ },
+ NetworkId: utils.Ptr(testNetworkId),
+ NicId: testNicId,
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiDeleteNicRequest)) iaas.ApiDeleteNicRequest {
+ request := testClient.DeleteNic(testCtx, testProjectId, testNetworkId, testNicId)
+ 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: fixtureArgValues(),
+ flagValues: map[string]string{},
+ isValid: false,
+ },
+ {
+ description: "network id missing",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, networkIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 1",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 2",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = "invalid-uuid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "nic argument missing",
+ argValues: []string{},
+ 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 parsing 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 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.ApiDeleteNicRequest
+ }{
+ {
+ 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/network-interface/describe/describe.go b/internal/cmd/beta/network-interface/describe/describe.go
new file mode 100644
index 000000000..45702c3df
--- /dev/null
+++ b/internal/cmd/beta/network-interface/describe/describe.go
@@ -0,0 +1,189 @@
+package describe
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "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"
+ "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"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/tables"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+)
+
+const (
+ nicIdArg = "NIC_ID"
+
+ networkIdFlag = "network-id"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ NetworkId *string
+ NicId string
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "describe",
+ Short: "Describes a network interface",
+ Long: "Describes a network interface.",
+ Args: args.SingleArg(nicIdArg, utils.ValidateUUID),
+ Example: examples.Build(
+ examples.NewExample(
+ `Describes network interface with nic id "xxx" and network ID "yyy"`,
+ `$ stackit beta network-interface describe xxx --network-id yyy`,
+ ),
+ examples.NewExample(
+ `Describes network interface with nic id "xxx" and network ID "yyy" in JSON format`,
+ `$ stackit beta network-interface describe xxx --network-id yyy --output-format json`,
+ ),
+ examples.NewExample(
+ `Describes network interface with nic id "xxx" and network ID "yyy" in yaml format`,
+ `$ stackit beta network-interface describe xxx --network-id yyy --output-format yaml`,
+ ),
+ ),
+ 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("describe network interface: %w", err)
+ }
+
+ return outputResult(p, model.OutputFormat, resp)
+ },
+ }
+ configureFlags(cmd)
+ return cmd
+}
+
+func configureFlags(cmd *cobra.Command) {
+ cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID")
+
+ err := flags.MarkFlagsRequired(cmd, networkIdFlag)
+ cobra.CheckErr(err)
+}
+
+func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
+ nicId := inputArgs[0]
+ globalFlags := globalflags.Parse(p, cmd)
+ if globalFlags.ProjectId == "" {
+ return nil, &errors.ProjectIdError{}
+ }
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag),
+ NicId: nicId,
+ }
+
+ 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.ApiGetNicRequest {
+ req := apiClient.GetNic(ctx, model.ProjectId, *model.NetworkId, model.NicId)
+ return req
+}
+
+func outputResult(p *print.Printer, outputFormat string, nic *iaas.NIC) error {
+ switch outputFormat {
+ case print.JSONOutputFormat:
+ details, err := json.MarshalIndent(nic, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal network interface: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ case print.YAMLOutputFormat:
+ details, err := yaml.MarshalWithOptions(nic, yaml.IndentSequence(true))
+ if err != nil {
+ return fmt.Errorf("marshal network interface: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ default:
+ table := tables.NewTable()
+ table.AddRow("ID", *nic.Id)
+ table.AddSeparator()
+ table.AddRow("NETWORK ID", *nic.NetworkId)
+ table.AddSeparator()
+ if nic.Name != nil {
+ table.AddRow("NAME", *nic.Name)
+ table.AddSeparator()
+ }
+ if nic.Ipv4 != nil {
+ table.AddRow("IPV4", *nic.Ipv4)
+ table.AddSeparator()
+ }
+ if nic.Ipv6 != nil {
+ table.AddRow("IPV6", *nic.Ipv6)
+ table.AddSeparator()
+ }
+ table.AddRow("MAC", *nic.Mac)
+ table.AddSeparator()
+ table.AddRow("NIC SECURITY", *nic.NicSecurity)
+ if nic.AllowedAddresses != nil && len(*nic.AllowedAddresses) > 0 {
+ allowedAddresses := []string{}
+ for _, value := range *nic.AllowedAddresses {
+ allowedAddresses = append(allowedAddresses, *value.String)
+ }
+ table.AddSeparator()
+ table.AddRow("ALLOWED ADDRESSES", strings.Join(allowedAddresses, "\n"))
+ }
+ if nic.Labels != nil && len(*nic.Labels) > 0 {
+ labels := []string{}
+ for key, value := range *nic.Labels {
+ labels = append(labels, fmt.Sprintf("%s: %s", key, value))
+ }
+ table.AddSeparator()
+ table.AddRow("LABELS", strings.Join(labels, "\n"))
+ }
+ table.AddSeparator()
+ table.AddRow("STATUS", *nic.Status)
+ table.AddSeparator()
+ table.AddRow("TYPE", *nic.Type)
+ if nic.SecurityGroups != nil && len(*nic.SecurityGroups) > 0 {
+ table.AddSeparator()
+ table.AddRow("SECURITY GROUPS", strings.Join(*nic.SecurityGroups, "\n"))
+ }
+
+ err := table.Display(p)
+ if err != nil {
+ return fmt.Errorf("render table: %w", err)
+ }
+ return nil
+ }
+}
diff --git a/internal/cmd/beta/network-interface/describe/describe_test.go b/internal/cmd/beta/network-interface/describe/describe_test.go
new file mode 100644
index 000000000..145a12cc8
--- /dev/null
+++ b/internal/cmd/beta/network-interface/describe/describe_test.go
@@ -0,0 +1,202 @@
+package describe
+
+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"
+)
+
+type testCtxKey struct{}
+
+var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
+var testClient = &iaas.APIClient{}
+
+var projectIdFlag = globalflags.ProjectIdFlag
+var testProjectId = uuid.NewString()
+var testNetworkId = uuid.NewString()
+var testNicId = uuid.NewString()
+
+func fixtureArgValues(mods ...func(argValues []string)) []string {
+ argValues := []string{
+ testNicId,
+ }
+ 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,
+ networkIdFlag: testNetworkId,
+ }
+ 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,
+ },
+ NetworkId: utils.Ptr(testNetworkId),
+ NicId: testNicId,
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiGetNicRequest)) iaas.ApiGetNicRequest {
+ request := testClient.GetNic(testCtx, testProjectId, testNetworkId, testNicId)
+ 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: fixtureArgValues(),
+ flagValues: map[string]string{},
+ isValid: false,
+ },
+ {
+ description: "network id missing",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, networkIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 1",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 2",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = "invalid-uuid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "nic argument missing",
+ argValues: []string{},
+ 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 parsing 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 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.ApiGetNicRequest
+ }{
+ {
+ 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/network-interface/list/list.go b/internal/cmd/beta/network-interface/list/list.go
new file mode 100644
index 000000000..fc8b4a8c5
--- /dev/null
+++ b/internal/cmd/beta/network-interface/list/list.go
@@ -0,0 +1,182 @@
+package list
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/goccy/go-yaml"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/args"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/examples"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/flags"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/print"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client"
+ "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"
+ networkIdFlag = "network-id"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ Limit *int64
+ LabelSelector *string
+ NetworkId *string
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list",
+ Short: "Lists all network interfaces of a network",
+ Long: "Lists all network interfaces of a network.",
+ Args: args.NoArgs,
+ Example: examples.Build(
+ examples.NewExample(
+ `Lists all network interfaces with network ID "xxx"`,
+ `$ stackit beta network-interface list --network-id xxx`,
+ ),
+ examples.NewExample(
+ `Lists all network interfaces with network ID "xxx" which contains the label xxx`,
+ `$ stackit beta network-interface list --network-id xxx --label-selector xxx`,
+ ),
+ examples.NewExample(
+ `Lists all network interfaces with network ID "xxx" in JSON format`,
+ `$ stackit beta network-interface list --network-id xxx --output-format json`,
+ ),
+ examples.NewExample(
+ `Lists up to 10 network interfaces with network ID "xxx"`,
+ `$ stackit beta network-interface list --network-id xxx --limit 10`,
+ ),
+ ),
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ ctx := context.Background()
+ model, err := parseInput(p, cmd)
+ if err != nil {
+ return err
+ }
+
+ // Configure API client
+ apiClient, err := client.ConfigureClient(p)
+ if err != nil {
+ return err
+ }
+
+ // Call API
+ req := buildRequest(ctx, model, apiClient)
+ resp, err := req.Execute()
+ if err != nil {
+ return fmt.Errorf("list network interfaces: %w", err)
+ }
+
+ if resp.Items == nil || len(*resp.Items) == 0 {
+ p.Info("No network interfaces found for network %d\n", model.NetworkId)
+ 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().Var(flags.UUIDFlag(), networkIdFlag, "Network ID")
+ cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
+ cmd.Flags().String(labelSelectorFlag, "", "Filter by label")
+
+ err := flags.MarkFlagsRequired(cmd, networkIdFlag)
+ cobra.CheckErr(err)
+}
+
+func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) {
+ globalFlags := globalflags.Parse(p, cmd)
+ if globalFlags.ProjectId == "" {
+ return nil, &errors.ProjectIdError{}
+ }
+
+ limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
+ if limit != nil && *limit < 1 {
+ return nil, &errors.FlagValidationError{
+ Flag: limitFlag,
+ Details: "must be greater than 0",
+ }
+ }
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ Limit: limit,
+ LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag),
+ NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag),
+ }
+
+ 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.ApiListNicsRequest {
+ req := apiClient.ListNics(ctx, model.ProjectId, *model.NetworkId)
+ if model.LabelSelector != nil {
+ req = req.LabelSelector(*model.LabelSelector)
+ }
+
+ return req
+}
+
+func outputResult(p *print.Printer, outputFormat string, nics []iaas.NIC) error {
+ switch outputFormat {
+ case print.JSONOutputFormat:
+ details, err := json.MarshalIndent(nics, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal nics: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ case print.YAMLOutputFormat:
+ details, err := yaml.MarshalWithOptions(nics, yaml.IndentSequence(true))
+ if err != nil {
+ return fmt.Errorf("marshal nics: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ default:
+ table := tables.NewTable()
+ table.SetHeader("ID", "NAME", "NIC SECURITY", "STATUS", "TYPE")
+
+ for _, nic := range nics {
+ name := ""
+ if nic.Name != nil {
+ name = *nic.Name
+ }
+ table.AddRow(*nic.Id, name, *nic.NicSecurity, *nic.Status, *nic.Type)
+ table.AddSeparator()
+ }
+
+ p.Outputln(table.Render())
+ return nil
+ }
+}
diff --git a/internal/cmd/beta/network-interface/list/list_test.go b/internal/cmd/beta/network-interface/list/list_test.go
new file mode 100644
index 000000000..3058072b0
--- /dev/null
+++ b/internal/cmd/beta/network-interface/list/list_test.go
@@ -0,0 +1,207 @@
+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 testNetworkId = uuid.NewString()
+var testLabelSelector = "label"
+
+func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
+ flagValues := map[string]string{
+ projectIdFlag: testProjectId,
+ networkIdFlag: testNetworkId,
+ 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),
+ NetworkId: utils.Ptr(testNetworkId),
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiListNicsRequest {
+ request := testClient.ListNics(testCtx, testProjectId, testNetworkId)
+ 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.ApiListNicsRequest
+ }{
+ {
+ 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/network-interface/network-interface.go b/internal/cmd/beta/network-interface/network-interface.go
new file mode 100644
index 000000000..b1d648226
--- /dev/null
+++ b/internal/cmd/beta/network-interface/network-interface.go
@@ -0,0 +1,33 @@
+package networkinterface
+
+import (
+ "github.com/spf13/cobra"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-interface/create"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-interface/delete"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-interface/describe"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-interface/list"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/network-interface/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"
+)
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "network-interface",
+ Short: "Provides functionality for Network Interface",
+ Long: "Provides functionality for Network Interface.",
+ 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(update.NewCmd(p))
+ cmd.AddCommand(describe.NewCmd(p))
+ cmd.AddCommand(list.NewCmd(p))
+}
diff --git a/internal/cmd/beta/network-interface/update/update.go b/internal/cmd/beta/network-interface/update/update.go
new file mode 100644
index 000000000..130a68a91
--- /dev/null
+++ b/internal/cmd/beta/network-interface/update/update.go
@@ -0,0 +1,243 @@
+package update
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "regexp"
+
+ "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"
+ "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"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+)
+
+const (
+ nicIdArg = "NIC_ID"
+
+ networkIdFlag = "network-id"
+ allowedAddressesFlag = "allowed-addresses"
+ labelFlag = "labels"
+ nameFlag = "name"
+ securityGroupsFlag = "security-groups"
+ nicSecurityFlag = "nic-security"
+
+ nameRegex = `^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`
+ maxNameLength = 63
+ securityGroupsRegex = `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`
+ securityGroupLength = 36
+ defaultNicSecurityFlag = true
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ NicId string
+ NetworkId *string
+ AllowedAddresses *[]iaas.AllowedAddressesInner
+ Labels *map[string]string
+ Name *string // <= 63 characters + regex ^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$
+ NicSecurity *bool
+ SecurityGroups *[]string // = 36 characters + regex ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "update",
+ Short: "Updates a network interface",
+ Long: "Updates a network interface.",
+ Args: args.SingleArg(nicIdArg, utils.ValidateUUID),
+ Example: examples.Build(
+ examples.NewExample(
+ `Updates a network interface with nic id "xxx" and network-id "yyy" to new allowed addresses "1.1.1.1,8.8.8.8,9.9.9.9" and new labels "key=value,key2=value2"`,
+ `$ stackit beta network-interface update xxx --network-id yyy --allowed-addresses "1.1.1.1,8.8.8.8,9.9.9.9" --labels key=value,key2=value2`,
+ ),
+ examples.NewExample(
+ `Updates a network interface with nic id "xxx" and network-id "yyy" with new name "nic-name-new"`,
+ `$ stackit beta network-interface update xxx --network-id yyy --name nic-name-new`,
+ ),
+ examples.NewExample(
+ `Updates a network interface with nic id "xxx" and network-id "yyy" to include the security group "zzz"`,
+ `$ stackit beta network-interface update xxx --network-id yyy --security-groups zzz`,
+ ),
+ ),
+ 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
+ }
+
+ if !model.AssumeYes {
+ prompt := fmt.Sprintf("Are you sure you want to update the network interface %q?", model.NicId)
+ 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 network interface: %w", err)
+ }
+
+ return outputResult(p, model, resp)
+ },
+ }
+ configureFlags(cmd)
+ return cmd
+}
+
+func configureFlags(cmd *cobra.Command) {
+ cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID")
+ cmd.Flags().StringSlice(allowedAddressesFlag, nil, "List of allowed IPs")
+ cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network-interface. E.g. '--labels key1=value1,key2=value2,...'")
+ cmd.Flags().StringP(nameFlag, "n", "", "Network interface name")
+ cmd.Flags().BoolP(nicSecurityFlag, "b", defaultNicSecurityFlag, "If this is set to false, then no security groups will apply to this network interface.")
+ cmd.Flags().StringSlice(securityGroupsFlag, nil, "List of security groups")
+
+ err := flags.MarkFlagsRequired(cmd, networkIdFlag)
+ cobra.CheckErr(err)
+}
+
+func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
+ nicId := inputArgs[0]
+ globalFlags := globalflags.Parse(p, cmd)
+ if globalFlags.ProjectId == "" {
+ return nil, &errors.ProjectIdError{}
+ }
+
+ allowedAddresses := flags.FlagToStringSlicePointer(p, cmd, allowedAddressesFlag)
+ var allowedAddressesInner []iaas.AllowedAddressesInner
+ if allowedAddresses != nil && len(*allowedAddresses) > 0 {
+ allowedAddressesInner = make([]iaas.AllowedAddressesInner, len(*allowedAddresses))
+ for i, address := range *allowedAddresses {
+ allowedAddressesInner[i].String = &address
+ }
+ }
+
+ // check name length and regex must apply
+ name := flags.FlagToStringPointer(p, cmd, nameFlag)
+ if name != nil {
+ if len(*name) > maxNameLength {
+ return nil, &errors.FlagValidationError{
+ Flag: nameFlag,
+ Details: fmt.Sprintf("name %s is too long (maximum length is %d characters)", *name, maxNameLength),
+ }
+ }
+ nameRegex := regexp.MustCompile(nameRegex)
+ if !nameRegex.MatchString(*name) {
+ return nil, &errors.FlagValidationError{
+ Flag: nameFlag,
+ Details: fmt.Sprintf("name %s didn't match the required regex expression %s", *name, nameRegex),
+ }
+ }
+ }
+
+ // check security groups size and regex
+ securityGroups := flags.FlagToStringSlicePointer(p, cmd, securityGroupsFlag)
+ if securityGroups != nil && len(*securityGroups) > 0 {
+ securityGroupsRegex := regexp.MustCompile(securityGroupsRegex)
+ // iterate over them
+ for _, value := range *securityGroups {
+ if len(value) != securityGroupLength {
+ return nil, &errors.FlagValidationError{
+ Flag: securityGroupsFlag,
+ Details: fmt.Sprintf("security groups uuid %s does not match (must be %d characters long)", value, securityGroupLength),
+ }
+ }
+ if !securityGroupsRegex.MatchString(value) {
+ return nil, &errors.FlagValidationError{
+ Flag: securityGroupsFlag,
+ Details: fmt.Sprintf("security groups uuid %s didn't match the required regex expression %s", value, securityGroupsRegex),
+ }
+ }
+ }
+ }
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ NicId: nicId,
+ NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag),
+ Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag),
+ Name: name,
+ NicSecurity: flags.FlagToBoolPointer(p, cmd, nicSecurityFlag),
+ SecurityGroups: securityGroups,
+ }
+
+ if allowedAddresses != nil {
+ model.AllowedAddresses = utils.Ptr(allowedAddressesInner)
+ }
+
+ 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.ApiUpdateNicRequest {
+ req := apiClient.UpdateNic(ctx, model.ProjectId, *model.NetworkId, model.NicId)
+
+ var labelsMap *map[string]interface{}
+ if model.Labels != nil && len(*model.Labels) > 0 {
+ // convert map[string]string to map[string]interface{}
+ convertedMap := make(map[string]interface{}, len(*model.Labels))
+ for k, v := range *model.Labels {
+ convertedMap[k] = v
+ }
+ labelsMap = &convertedMap
+ }
+
+ payload := iaas.UpdateNicPayload{
+ AllowedAddresses: model.AllowedAddresses,
+ Labels: labelsMap,
+ Name: model.Name,
+ NicSecurity: model.NicSecurity,
+ SecurityGroups: model.SecurityGroups,
+ }
+ return req.UpdateNicPayload(payload)
+}
+
+func outputResult(p *print.Printer, model *inputModel, nic *iaas.NIC) error {
+ switch model.OutputFormat {
+ case print.JSONOutputFormat:
+ details, err := json.MarshalIndent(nic, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal network interface: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ case print.YAMLOutputFormat:
+ details, err := yaml.MarshalWithOptions(nic, yaml.IndentSequence(true))
+ if err != nil {
+ return fmt.Errorf("marshal network interface: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ default:
+ p.Outputf("Updated network interface for project %q.\n", model.ProjectId)
+ return nil
+ }
+}
diff --git a/internal/cmd/beta/network-interface/update/update_test.go b/internal/cmd/beta/network-interface/update/update_test.go
new file mode 100644
index 000000000..00eeda71f
--- /dev/null
+++ b/internal/cmd/beta/network-interface/update/update_test.go
@@ -0,0 +1,293 @@
+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 testNetworkId = uuid.NewString()
+var testNicId = uuid.NewString()
+var testSecurityGroup = uuid.NewString()
+
+func fixtureArgValues(mods ...func(argValues []string)) []string {
+ argValues := []string{
+ testNicId,
+ }
+ 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,
+ networkIdFlag: testNetworkId,
+ allowedAddressesFlag: "1.1.1.1,8.8.8.8,9.9.9.9",
+ labelFlag: "key=value",
+ nameFlag: "testNic",
+ nicSecurityFlag: "true",
+ securityGroupsFlag: testSecurityGroup,
+ }
+ for _, mod := range mods {
+ mod(flagValues)
+ }
+ return flagValues
+}
+
+func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
+ var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")),
+ }
+ model := &inputModel{
+ GlobalFlagModel: &globalflags.GlobalFlagModel{
+ ProjectId: testProjectId,
+ Verbosity: globalflags.VerbosityDefault,
+ },
+ NetworkId: utils.Ptr(testNetworkId),
+ AllowedAddresses: utils.Ptr(allowedAddresses),
+ Labels: utils.Ptr(map[string]string{
+ "key": "value",
+ }),
+ Name: utils.Ptr("testNic"),
+ NicSecurity: utils.Ptr(true),
+ SecurityGroups: utils.Ptr([]string{testSecurityGroup}),
+ NicId: testNicId,
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiUpdateNicRequest)) iaas.ApiUpdateNicRequest {
+ request := testClient.UpdateNic(testCtx, testProjectId, testNetworkId, testNicId)
+ request = request.UpdateNicPayload(fixturePayload())
+ for _, mod := range mods {
+ mod(&request)
+ }
+ return request
+}
+
+func fixturePayload(mods ...func(payload *iaas.UpdateNicPayload)) iaas.UpdateNicPayload {
+ var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")),
+ iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")),
+ }
+ payload := iaas.UpdateNicPayload{
+ AllowedAddresses: utils.Ptr(allowedAddresses),
+ Labels: utils.Ptr(map[string]interface{}{
+ "key": "value",
+ }),
+ Name: utils.Ptr("testNic"),
+ NicSecurity: utils.Ptr(true),
+ SecurityGroups: utils.Ptr([]string{testSecurityGroup}),
+ }
+ 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: fixtureArgValues(),
+ flagValues: map[string]string{},
+ isValid: false,
+ },
+ {
+ description: "network id missing",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, networkIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 1",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "network id invalid 2",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[networkIdFlag] = "invalid-uuid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "allowed addresses missing",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, allowedAddressesFlag)
+ }),
+ expectedModel: fixtureInputModel(func(model *inputModel) {
+ model.AllowedAddresses = nil
+ }),
+ isValid: true,
+ },
+ {
+ description: "name to long",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[nameFlag] = "verylongstringwith66characterstotestthenameregexwithinthisunittest"
+ }),
+ isValid: false,
+ },
+ {
+ description: "name invalid",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[nameFlag] = "test?"
+ }),
+ isValid: false,
+ },
+ {
+ description: "name empty string invalid",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[nameFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group uuid to short",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupsFlag] = "d61a8564-c8dd-4ffb-bc15-143e7d0c85e"
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group uuid invalid",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupsFlag] = "d61a8564-c8dd-4ffb-bc15-143e7d0c85e?"
+ }),
+ isValid: false,
+ },
+ {
+ description: "nic argument missing",
+ argValues: []string{},
+ 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 parsing 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 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.ApiUpdateNicRequest
+ }{
+ {
+ 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)
+ }
+ })
+ }
+}