diff --git a/README.md b/README.md
index 638180559..2bac5e5b5 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 public-ip`
`beta key-pair` | :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`
`beta security-group`
`beta key-pair` | :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 acc708dd4..4ab26fe12 100644
--- a/docs/stackit_beta.md
+++ b/docs/stackit_beta.md
@@ -45,6 +45,7 @@ stackit beta [flags]
* [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 security-group](./stackit_beta_security-group.md) - Provides functionality for security groups
* [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_security-group.md b/docs/stackit_beta_security-group.md
new file mode 100644
index 000000000..fcc28bdd7
--- /dev/null
+++ b/docs/stackit_beta_security-group.md
@@ -0,0 +1,33 @@
+## stackit beta security-group
+
+Provides functionality for security groups
+
+### Synopsis
+
+Provides functionality for security groups.
+
+```
+stackit beta security-group [flags]
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta security-group"
+```
+
+### 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 security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules
+
diff --git a/docs/stackit_beta_security-group_rule.md b/docs/stackit_beta_security-group_rule.md
new file mode 100644
index 000000000..a680f5bfc
--- /dev/null
+++ b/docs/stackit_beta_security-group_rule.md
@@ -0,0 +1,36 @@
+## stackit beta security-group rule
+
+Provides functionality for security group rules
+
+### Synopsis
+
+Provides functionality for security group rules.
+
+```
+stackit beta security-group rule [flags]
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta security-group rule"
+```
+
+### 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 security-group](./stackit_beta_security-group.md) - Provides functionality for security groups
+* [stackit beta security-group rule create](./stackit_beta_security-group_rule_create.md) - Creates a security group rule
+* [stackit beta security-group rule delete](./stackit_beta_security-group_rule_delete.md) - Deletes a security group rule
+* [stackit beta security-group rule describe](./stackit_beta_security-group_rule_describe.md) - Shows details of a security group rule
+* [stackit beta security-group rule list](./stackit_beta_security-group_rule_list.md) - Lists all security group rules in a security group of a project
+
diff --git a/docs/stackit_beta_security-group_rule_create.md b/docs/stackit_beta_security-group_rule_create.md
new file mode 100644
index 000000000..7fba6fe31
--- /dev/null
+++ b/docs/stackit_beta_security-group_rule_create.md
@@ -0,0 +1,60 @@
+## stackit beta security-group rule create
+
+Creates a security group rule
+
+### Synopsis
+
+Creates a security group rule.
+
+```
+stackit beta security-group rule create [flags]
+```
+
+### Examples
+
+```
+ Create a security group rule for security group with ID "xxx" with direction "ingress"
+ $ stackit beta security-group rule create --security-group-id xxx --direction ingress
+
+ Create a security group rule for security group with ID "xxx" with direction "egress", protocol "icmp" and icmp parameters
+ $ stackit beta security-group rule create --security-group-id xxx --direction egress --protocol-name icmp --icmp-parameter-code 0 --icmp-parameter-type 8
+
+ Create a security group rule for security group with ID "xxx" with direction "ingress", protocol "tcp" and port range values
+ $ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-name tcp --port-range-max 24 --port-range-min 22
+
+ Create a security group rule for security group with ID "xxx" with direction "ingress" and protocol number 1
+ $ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-number 1
+```
+
+### Options
+
+```
+ --description string The rule description
+ --direction string The direction of the traffic which the rule should match. The possible values are: "ingress", "egress"
+ --ether-type string The ethertype which the rule should match
+ -h, --help Help for "stackit beta security-group rule create"
+ --icmp-parameter-code int ICMP code. Can be set if the protocol is ICMP
+ --icmp-parameter-type int ICMP type. Can be set if the protocol is ICMP
+ --ip-range string The remote IP range which the rule should match
+ --port-range-max int The maximum port number. Should be greater or equal to the minimum. This should only be provided if the protocol is not ICMP
+ --port-range-min int The minimum port number. Should be less or equal to the maximum. This should only be provided if the protocol is not ICMP
+ --protocol-name string The protocol name which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided
+ --protocol-number int The protocol number which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided
+ --remote-security-group-id string The remote security group which the rule should match
+ --security-group-id string The security group 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 security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules
+
diff --git a/docs/stackit_beta_security-group_rule_delete.md b/docs/stackit_beta_security-group_rule_delete.md
new file mode 100644
index 000000000..fd56a81d6
--- /dev/null
+++ b/docs/stackit_beta_security-group_rule_delete.md
@@ -0,0 +1,42 @@
+## stackit beta security-group rule delete
+
+Deletes a security group rule
+
+### Synopsis
+
+Deletes a security group rule.
+If the security group rule is still in use, the deletion will fail
+
+
+```
+stackit beta security-group rule delete [flags]
+```
+
+### Examples
+
+```
+ Delete security group rule with ID "xxx" in security group with ID "yyy"
+ $ stackit beta security-group rule delete xxx --security-group-id yyy
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta security-group rule delete"
+ --security-group-id string The security group 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 security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules
+
diff --git a/docs/stackit_beta_security-group_rule_describe.md b/docs/stackit_beta_security-group_rule_describe.md
new file mode 100644
index 000000000..eaa29cc08
--- /dev/null
+++ b/docs/stackit_beta_security-group_rule_describe.md
@@ -0,0 +1,43 @@
+## stackit beta security-group rule describe
+
+Shows details of a security group rule
+
+### Synopsis
+
+Shows details of a security group rule.
+
+```
+stackit beta security-group rule describe [flags]
+```
+
+### Examples
+
+```
+ Show details of a security group rule with ID "xxx" in security group with ID "yyy"
+ $ stackit beta security-group rule describe xxx --security-group-id yyy
+
+ Show details of a security group rule with ID "xxx" in security group with ID "yyy" in JSON format
+ $ stackit beta security-group rule describe xxx --security-group-id yyy --output-format json
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta security-group rule describe"
+ --security-group-id string The security group 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 security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules
+
diff --git a/docs/stackit_beta_security-group_rule_list.md b/docs/stackit_beta_security-group_rule_list.md
new file mode 100644
index 000000000..02fef7466
--- /dev/null
+++ b/docs/stackit_beta_security-group_rule_list.md
@@ -0,0 +1,47 @@
+## stackit beta security-group rule list
+
+Lists all security group rules in a security group of a project
+
+### Synopsis
+
+Lists all security group rules in a security group of a project.
+
+```
+stackit beta security-group rule list [flags]
+```
+
+### Examples
+
+```
+ Lists all security group rules in security group with ID "xxx"
+ $ stackit beta security-group rule list --security-group-id xxx
+
+ Lists all security group rules in security group with ID "xxx" in JSON format
+ $ stackit beta security-group rule list --security-group-id xxx --output-format json
+
+ Lists up to 10 security group rules in security group with ID "xxx"
+ $ stackit beta security-group rule list --security-group-id xxx --limit 10
+```
+
+### Options
+
+```
+ -h, --help Help for "stackit beta security-group rule list"
+ --limit int Maximum number of entries to list
+ --security-group-id string The security group 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 security-group rule](./stackit_beta_security-group_rule.md) - Provides functionality for security group rules
+
diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go
index 975704383..0ec43d0f2 100644
--- a/internal/cmd/beta/beta.go
+++ b/internal/cmd/beta/beta.go
@@ -8,6 +8,7 @@ import (
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"
+ securitygroup "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group"
"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"
@@ -49,5 +50,6 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(volume.NewCmd(p))
cmd.AddCommand(networkinterface.NewCmd(p))
cmd.AddCommand(publicip.NewCmd(p))
+ cmd.AddCommand(securitygroup.NewCmd(p))
cmd.AddCommand(keypair.NewCmd(p))
}
diff --git a/internal/cmd/beta/security-group/rule/create/create.go b/internal/cmd/beta/security-group/rule/create/create.go
new file mode 100644
index 000000000..04b49745e
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/create/create.go
@@ -0,0 +1,246 @@
+package create
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/goccy/go-yaml"
+ "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"
+ iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+
+ "github.com/spf13/cobra"
+)
+
+const (
+ securityGroupIdFlag = "security-group-id"
+ directionFlag = "direction"
+ descriptionFlag = "description"
+ etherTypeFlag = "ether-type"
+ icmpParameterCodeFlag = "icmp-parameter-code"
+ icmpParameterTypeFlag = "icmp-parameter-type"
+ ipRangeFlag = "ip-range"
+ portRangeMaxFlag = "port-range-max"
+ portRangeMinFlag = "port-range-min"
+ remoteSecurityGroupIdFlag = "remote-security-group-id"
+ protocolNumberFlag = "protocol-number"
+ protocolNameFlag = "protocol-name"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ SecurityGroupId string
+ Direction *string
+ Description *string
+ EtherType *string
+ IcmpParameterCode *int64
+ IcmpParameterType *int64
+ IpRange *string
+ PortRangeMax *int64
+ PortRangeMin *int64
+ RemoteSecurityGroupId *string
+ ProtocolNumber *int64
+ ProtocolName *string
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "create",
+ Short: "Creates a security group rule",
+ Long: "Creates a security group rule.",
+ Args: args.NoArgs,
+ Example: examples.Build(
+ examples.NewExample(
+ `Create a security group rule for security group with ID "xxx" with direction "ingress"`,
+ `$ stackit beta security-group rule create --security-group-id xxx --direction ingress`,
+ ),
+ examples.NewExample(
+ `Create a security group rule for security group with ID "xxx" with direction "egress", protocol "icmp" and icmp parameters`,
+ `$ stackit beta security-group rule create --security-group-id xxx --direction egress --protocol-name icmp --icmp-parameter-code 0 --icmp-parameter-type 8`,
+ ),
+ examples.NewExample(
+ `Create a security group rule for security group with ID "xxx" with direction "ingress", protocol "tcp" and port range values`,
+ `$ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-name tcp --port-range-max 24 --port-range-min 22`,
+ ),
+ examples.NewExample(
+ `Create a security group rule for security group with ID "xxx" with direction "ingress" and protocol number 1 `,
+ `$ stackit beta security-group rule create --security-group-id xxx --direction ingress --protocol-number 1`,
+ ),
+ ),
+ 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
+ }
+
+ securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.SecurityGroupId)
+ if err != nil {
+ p.Debug(print.ErrorLevel, "get security group name: %v", err)
+ securityGroupLabel = model.SecurityGroupId
+ }
+
+ if !model.AssumeYes {
+ prompt := fmt.Sprintf("Are you sure you want to create a security group rule for security group %q for project %q?", securityGroupLabel, projectLabel)
+ err = 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 security group rule : %w", err)
+ }
+
+ return outputResult(p, model, projectLabel, securityGroupLabel, resp)
+ },
+ }
+ configureFlags(cmd)
+ return cmd
+}
+
+func configureFlags(cmd *cobra.Command) {
+ cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`)
+ cmd.Flags().String(directionFlag, "", `The direction of the traffic which the rule should match. The possible values are: "ingress", "egress"`)
+ cmd.Flags().String(descriptionFlag, "", `The rule description`)
+ cmd.Flags().String(etherTypeFlag, "", `The ethertype which the rule should match`)
+ cmd.Flags().Int64(icmpParameterCodeFlag, 0, `ICMP code. Can be set if the protocol is ICMP`)
+ cmd.Flags().Int64(icmpParameterTypeFlag, 0, `ICMP type. Can be set if the protocol is ICMP`)
+ cmd.Flags().String(ipRangeFlag, "", `The remote IP range which the rule should match`)
+ cmd.Flags().Int64(portRangeMaxFlag, 0, `The maximum port number. Should be greater or equal to the minimum. This should only be provided if the protocol is not ICMP`)
+ cmd.Flags().Int64(portRangeMinFlag, 0, `The minimum port number. Should be less or equal to the maximum. This should only be provided if the protocol is not ICMP`)
+ cmd.Flags().Var(flags.UUIDFlag(), remoteSecurityGroupIdFlag, `The remote security group which the rule should match`)
+ cmd.Flags().Int64(protocolNumberFlag, 0, `The protocol number which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided`)
+ cmd.Flags().String(protocolNameFlag, "", `The protocol name which the rule should match. If a protocol is to be defined, either "protocol-name" or "protocol-number" must be provided`)
+
+ err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag, directionFlag)
+ cmd.MarkFlagsMutuallyExclusive(protocolNumberFlag, protocolNameFlag)
+ cobra.CheckErr(err)
+}
+
+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,
+ SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag),
+ Direction: flags.FlagToStringPointer(p, cmd, directionFlag),
+ Description: flags.FlagToStringPointer(p, cmd, descriptionFlag),
+ EtherType: flags.FlagToStringPointer(p, cmd, etherTypeFlag),
+ IcmpParameterCode: flags.FlagToInt64Pointer(p, cmd, icmpParameterCodeFlag),
+ IcmpParameterType: flags.FlagToInt64Pointer(p, cmd, icmpParameterTypeFlag),
+ IpRange: flags.FlagToStringPointer(p, cmd, ipRangeFlag),
+ PortRangeMax: flags.FlagToInt64Pointer(p, cmd, portRangeMaxFlag),
+ PortRangeMin: flags.FlagToInt64Pointer(p, cmd, portRangeMinFlag),
+ RemoteSecurityGroupId: flags.FlagToStringPointer(p, cmd, remoteSecurityGroupIdFlag),
+ ProtocolNumber: flags.FlagToInt64Pointer(p, cmd, protocolNumberFlag),
+ ProtocolName: flags.FlagToStringPointer(p, cmd, protocolNameFlag),
+ }
+
+ 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.ApiCreateSecurityGroupRuleRequest {
+ req := apiClient.CreateSecurityGroupRule(ctx, model.ProjectId, model.SecurityGroupId)
+ icmpParameters := &iaas.ICMPParameters{}
+ portRange := &iaas.PortRange{}
+ protocol := &iaas.CreateProtocol{}
+
+ payload := iaas.CreateSecurityGroupRulePayload{
+ Direction: model.Direction,
+ Description: model.Description,
+ Ethertype: model.EtherType,
+ IpRange: model.IpRange,
+ RemoteSecurityGroupId: model.RemoteSecurityGroupId,
+ }
+
+ if model.IcmpParameterCode != nil || model.IcmpParameterType != nil {
+ icmpParameters.Code = model.IcmpParameterCode
+ icmpParameters.Type = model.IcmpParameterType
+
+ payload.IcmpParameters = icmpParameters
+ }
+
+ if model.PortRangeMax != nil || model.PortRangeMin != nil {
+ portRange.Max = model.PortRangeMax
+ portRange.Min = model.PortRangeMin
+
+ payload.PortRange = portRange
+ }
+
+ if model.ProtocolNumber != nil || model.ProtocolName != nil {
+ protocol.Int64 = model.ProtocolNumber
+ protocol.String = model.ProtocolName
+
+ payload.Protocol = protocol
+ }
+
+ if model.RemoteSecurityGroupId == nil {
+ payload.RemoteSecurityGroupId = nil
+ }
+
+ return req.CreateSecurityGroupRulePayload(payload)
+}
+
+func outputResult(p *print.Printer, model *inputModel, projectLabel, securityGroupName string, securityGroupRule *iaas.SecurityGroupRule) error {
+ switch model.OutputFormat {
+ case print.JSONOutputFormat:
+ details, err := json.MarshalIndent(securityGroupRule, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal security group rule: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ case print.YAMLOutputFormat:
+ details, err := yaml.MarshalWithOptions(securityGroupRule, yaml.IndentSequence(true))
+ if err != nil {
+ return fmt.Errorf("marshal security group rule: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ default:
+ operationState := "Created"
+ if model.Async {
+ operationState = "Triggered creation of"
+ }
+ p.Outputf("%s security group rule for security group %q in project %q.\nSecurity group rule ID: %s\n", operationState, securityGroupName, projectLabel, *securityGroupRule.Id)
+ return nil
+ }
+}
diff --git a/internal/cmd/beta/security-group/rule/create/create_test.go b/internal/cmd/beta/security-group/rule/create/create_test.go
new file mode 100644
index 000000000..5d0fd2098
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/create/create_test.go
@@ -0,0 +1,335 @@
+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 testSecurityGroupId = uuid.NewString()
+var testRemoteSecurityGroupId = uuid.NewString()
+
+func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
+ flagValues := map[string]string{
+ projectIdFlag: testProjectId,
+ securityGroupIdFlag: testSecurityGroupId,
+ directionFlag: "ingress",
+ descriptionFlag: "example-description",
+ etherTypeFlag: "ether",
+ icmpParameterCodeFlag: "0",
+ icmpParameterTypeFlag: "8",
+ ipRangeFlag: "10.1.2.3",
+ portRangeMaxFlag: "24",
+ portRangeMinFlag: "22",
+ remoteSecurityGroupIdFlag: testRemoteSecurityGroupId,
+ protocolNumberFlag: "1",
+ protocolNameFlag: "icmp",
+ }
+ 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,
+ },
+ SecurityGroupId: testSecurityGroupId,
+ Direction: utils.Ptr("ingress"),
+ Description: utils.Ptr("example-description"),
+ EtherType: utils.Ptr("ether"),
+ IcmpParameterCode: utils.Ptr(int64(0)),
+ IcmpParameterType: utils.Ptr(int64(8)),
+ IpRange: utils.Ptr("10.1.2.3"),
+ PortRangeMax: utils.Ptr(int64(24)),
+ PortRangeMin: utils.Ptr(int64(22)),
+ RemoteSecurityGroupId: utils.Ptr(testRemoteSecurityGroupId),
+ ProtocolNumber: utils.Ptr(int64(1)),
+ ProtocolName: utils.Ptr("icmp"),
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRuleRequest)) iaas.ApiCreateSecurityGroupRuleRequest {
+ request := testClient.CreateSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId)
+ request = request.CreateSecurityGroupRulePayload(fixturePayload())
+ for _, mod := range mods {
+ mod(&request)
+ }
+ return request
+}
+
+func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRuleRequest)) iaas.ApiCreateSecurityGroupRuleRequest {
+ request := testClient.CreateSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId)
+ request = request.CreateSecurityGroupRulePayload(iaas.CreateSecurityGroupRulePayload{
+ Direction: utils.Ptr("ingress"),
+ })
+ for _, mod := range mods {
+ mod(&request)
+ }
+ return request
+}
+
+func fixturePayload(mods ...func(payload *iaas.CreateSecurityGroupRulePayload)) iaas.CreateSecurityGroupRulePayload {
+ payload := iaas.CreateSecurityGroupRulePayload{
+ Direction: utils.Ptr("ingress"),
+ Description: utils.Ptr("example-description"),
+ Ethertype: utils.Ptr("ether"),
+ IcmpParameters: &iaas.ICMPParameters{
+ Code: utils.Ptr(int64(0)),
+ Type: utils.Ptr(int64(8)),
+ },
+ IpRange: utils.Ptr("10.1.2.3"),
+ PortRange: &iaas.PortRange{
+ Max: utils.Ptr(int64(24)),
+ Min: utils.Ptr(int64(22)),
+ },
+ Protocol: &iaas.CreateProtocol{
+ Int64: utils.Ptr(int64(1)),
+ String: utils.Ptr("icmp"),
+ },
+ RemoteSecurityGroupId: utils.Ptr(testRemoteSecurityGroupId),
+ }
+ 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(func(flagValues map[string]string) {
+ delete(flagValues, portRangeMaxFlag)
+ delete(flagValues, portRangeMinFlag)
+ delete(flagValues, protocolNumberFlag)
+ }),
+ isValid: true,
+ expectedModel: fixtureInputModel(func(model *inputModel) {
+ model.PortRangeMax = nil
+ model.PortRangeMin = nil
+ model.ProtocolNumber = nil
+ }),
+ },
+ {
+ description: "required only",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, descriptionFlag)
+ delete(flagValues, etherTypeFlag)
+ delete(flagValues, icmpParameterCodeFlag)
+ delete(flagValues, icmpParameterTypeFlag)
+ delete(flagValues, ipRangeFlag)
+ delete(flagValues, portRangeMaxFlag)
+ delete(flagValues, portRangeMinFlag)
+ delete(flagValues, remoteSecurityGroupIdFlag)
+ delete(flagValues, protocolNumberFlag)
+ delete(flagValues, protocolNameFlag)
+ }),
+ isValid: true,
+ expectedModel: fixtureInputModel(func(model *inputModel) {
+ model.Description = nil
+ model.EtherType = nil
+ model.IcmpParameterCode = nil
+ model.IcmpParameterType = nil
+ model.IpRange = nil
+ model.PortRangeMax = nil
+ model.PortRangeMin = nil
+ model.RemoteSecurityGroupId = nil
+ model.ProtocolNumber = nil
+ model.ProtocolName = nil
+ }),
+ },
+ {
+ description: "direction missing",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, directionFlag)
+ delete(flagValues, protocolNumberFlag)
+ delete(flagValues, protocolNameFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "protocol is not icmp and port range values are provided",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[protocolNameFlag] = "not-icmp"
+ delete(flagValues, icmpParameterCodeFlag)
+ delete(flagValues, icmpParameterTypeFlag)
+ delete(flagValues, protocolNumberFlag)
+ }),
+ isValid: true,
+ expectedModel: fixtureInputModel(func(model *inputModel) {
+ model.IcmpParameterCode = nil
+ model.IcmpParameterType = nil
+ model.ProtocolName = utils.Ptr("not-icmp")
+ model.ProtocolNumber = 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,
+ },
+ {
+ description: "security group id missing",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, securityGroupIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 1",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 2",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = "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.ValidateFlagGroups()
+ if err != nil {
+ if !tt.isValid {
+ return
+ }
+ t.Fatalf("error validating flag groups: %v", err)
+ }
+ err = cmd.ValidateRequiredFlags()
+ if err != nil {
+ if !tt.isValid {
+ return
+ }
+ t.Fatalf("error validating flags: %v", err)
+ }
+
+ model, err := parseInput(p, cmd)
+ if err != nil {
+ if !tt.isValid {
+ return
+ }
+ t.Fatalf("error parsing 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) {
+ var tests = []struct {
+ description string
+ model *inputModel
+ expectedRequest iaas.ApiCreateSecurityGroupRuleRequest
+ }{
+ {
+ description: "base",
+ model: fixtureInputModel(),
+ expectedRequest: fixtureRequest(),
+ },
+ {
+ description: "only direction and security group id in payload",
+ model: &inputModel{
+ GlobalFlagModel: &globalflags.GlobalFlagModel{
+ ProjectId: testProjectId,
+ Verbosity: globalflags.VerbosityDefault,
+ },
+ Direction: utils.Ptr("ingress"),
+ SecurityGroupId: testSecurityGroupId,
+ },
+ expectedRequest: fixtureRequiredRequest(),
+ },
+ }
+ 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/security-group/rule/delete/delete.go b/internal/cmd/beta/security-group/rule/delete/delete.go
new file mode 100644
index 000000000..b28ade08e
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/delete/delete.go
@@ -0,0 +1,130 @@
+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/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 (
+ securityGroupRuleIdArg = "SECURITY_GROUP_RULE_ID"
+
+ securityGroupIdFlag = "security-group-id"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ SecurityGroupRuleId string
+ SecurityGroupId *string
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "delete",
+ Short: "Deletes a security group rule",
+ Long: fmt.Sprintf("%s\n%s\n",
+ "Deletes a security group rule.",
+ "If the security group rule is still in use, the deletion will fail",
+ ),
+ Args: args.SingleArg(securityGroupRuleIdArg, utils.ValidateUUID),
+ Example: examples.Build(
+ examples.NewExample(
+ `Delete security group rule with ID "xxx" in security group with ID "yyy"`,
+ "$ stackit beta security-group rule delete xxx --security-group-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
+ }
+
+ securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, *model.SecurityGroupId)
+ if err != nil {
+ p.Debug(print.ErrorLevel, "get security group name: %v", err)
+ securityGroupLabel = *model.SecurityGroupId
+ }
+
+ securityGroupRuleLabel, err := iaasUtils.GetSecurityGroupRuleName(ctx, apiClient, model.ProjectId, model.SecurityGroupRuleId, *model.SecurityGroupId)
+ if err != nil {
+ p.Debug(print.ErrorLevel, "get security group rule name: %v", err)
+ securityGroupRuleLabel = model.SecurityGroupRuleId
+ }
+
+ if !model.AssumeYes {
+ prompt := fmt.Sprintf("Are you sure you want to delete security group rule %q from security group %q?", securityGroupRuleLabel, securityGroupLabel)
+ 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 security group rule: %w", err)
+ }
+
+ p.Info("Deleted security group rule %q from security group %q\n", securityGroupRuleLabel, securityGroupLabel)
+ return nil
+ },
+ }
+ configureFlags(cmd)
+ return cmd
+}
+
+func configureFlags(cmd *cobra.Command) {
+ cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`)
+
+ err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag)
+ cobra.CheckErr(err)
+}
+
+func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
+ securityGroupRuleId := inputArgs[0]
+
+ globalFlags := globalflags.Parse(p, cmd)
+ if globalFlags.ProjectId == "" {
+ return nil, &errors.ProjectIdError{}
+ }
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ SecurityGroupRuleId: securityGroupRuleId,
+ SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag),
+ }
+
+ 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.ApiDeleteSecurityGroupRuleRequest {
+ return apiClient.DeleteSecurityGroupRule(ctx, model.ProjectId, *model.SecurityGroupId, model.SecurityGroupRuleId)
+}
diff --git a/internal/cmd/beta/security-group/rule/delete/delete_test.go b/internal/cmd/beta/security-group/rule/delete/delete_test.go
new file mode 100644
index 000000000..e8d36d7f6
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/delete/delete_test.go
@@ -0,0 +1,235 @@
+package delete
+
+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 testSecurityGroupId = uuid.NewString()
+var testSecurityGroupRuleId = uuid.NewString()
+
+func fixtureArgValues(mods ...func(argValues []string)) []string {
+ argValues := []string{
+ testSecurityGroupRuleId,
+ }
+ 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,
+ securityGroupIdFlag: testSecurityGroupId,
+ }
+ 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,
+ },
+ SecurityGroupId: utils.Ptr(testSecurityGroupId),
+ SecurityGroupRuleId: testSecurityGroupRuleId,
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiDeleteSecurityGroupRuleRequest)) iaas.ApiDeleteSecurityGroupRuleRequest {
+ request := testClient.DeleteSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId, testSecurityGroupRuleId)
+ for _, mod := range mods {
+ mod(&request)
+ }
+ return request
+}
+
+func TestParseInput(t *testing.T) {
+ tests := []struct {
+ description string
+ argValues []string
+ flagValues map[string]string
+ aclValues []string
+ isValid bool
+ expectedModel *inputModel
+ }{
+ {
+ description: "base",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(),
+ isValid: true,
+ expectedModel: fixtureInputModel(),
+ },
+ {
+ description: "no values",
+ 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: "security group id missing",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, securityGroupIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 1",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 2",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = "invalid-uuid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group rule id invalid 1",
+ argValues: []string{""},
+ flagValues: fixtureFlagValues(),
+ isValid: false,
+ },
+ {
+ description: "security group rule 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 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.ApiDeleteSecurityGroupRuleRequest
+ }{
+ {
+ 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/security-group/rule/describe/describe.go b/internal/cmd/beta/security-group/rule/describe/describe.go
new file mode 100644
index 000000000..93f7df60c
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/describe/describe.go
@@ -0,0 +1,186 @@
+package describe
+
+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-cli/internal/pkg/utils"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+
+ "github.com/spf13/cobra"
+)
+
+const (
+ securityGroupRuleIdArg = "SECURITY_GROUP_RULE_ID"
+
+ securityGroupIdFlag = "security-group-id"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ SecurityGroupRuleId string
+ SecurityGroupId *string
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "describe",
+ Short: "Shows details of a security group rule",
+ Long: "Shows details of a security group rule.",
+ Args: args.SingleArg(securityGroupRuleIdArg, utils.ValidateUUID),
+ Example: examples.Build(
+ examples.NewExample(
+ `Show details of a security group rule with ID "xxx" in security group with ID "yyy"`,
+ "$ stackit beta security-group rule describe xxx --security-group-id yyy",
+ ),
+ examples.NewExample(
+ `Show details of a security group rule with ID "xxx" in security group with ID "yyy" in JSON format`,
+ "$ stackit beta security-group rule describe xxx --security-group-id yyy --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 security group rule: %w", err)
+ }
+
+ return outputResult(p, model.OutputFormat, resp)
+ },
+ }
+ configureFlags(cmd)
+ return cmd
+}
+
+func configureFlags(cmd *cobra.Command) {
+ cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`)
+
+ err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag)
+ cobra.CheckErr(err)
+}
+
+func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
+ securityGroupRuleId := inputArgs[0]
+
+ globalFlags := globalflags.Parse(p, cmd)
+ if globalFlags.ProjectId == "" {
+ return nil, &errors.ProjectIdError{}
+ }
+
+ model := inputModel{
+ GlobalFlagModel: globalFlags,
+ SecurityGroupRuleId: securityGroupRuleId,
+ SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag),
+ }
+
+ 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.ApiGetSecurityGroupRuleRequest {
+ return apiClient.GetSecurityGroupRule(ctx, model.ProjectId, *model.SecurityGroupId, model.SecurityGroupRuleId)
+}
+
+func outputResult(p *print.Printer, outputFormat string, securityGroupRule *iaas.SecurityGroupRule) error {
+ switch outputFormat {
+ case print.JSONOutputFormat:
+ details, err := json.MarshalIndent(securityGroupRule, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal security group rule: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ case print.YAMLOutputFormat:
+ details, err := yaml.MarshalWithOptions(securityGroupRule, yaml.IndentSequence(true))
+ if err != nil {
+ return fmt.Errorf("marshal security group rule: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ default:
+ table := tables.NewTable()
+ table.AddRow("ID", *securityGroupRule.Id)
+ table.AddSeparator()
+
+ if securityGroupRule.Protocol != nil {
+ if securityGroupRule.Protocol.Name != nil {
+ table.AddRow("PROTOCOL NAME", *securityGroupRule.Protocol.Name)
+ table.AddSeparator()
+ }
+
+ if securityGroupRule.Protocol.Number != nil {
+ table.AddRow("PROTOCOL NUMBER", *securityGroupRule.Protocol.Number)
+ table.AddSeparator()
+ }
+ }
+
+ table.AddRow("DIRECTION", *securityGroupRule.Direction)
+ table.AddSeparator()
+
+ if securityGroupRule.PortRange != nil {
+ if securityGroupRule.PortRange.Min != nil {
+ table.AddRow("START PORT", *securityGroupRule.PortRange.Min)
+ table.AddSeparator()
+ }
+
+ if securityGroupRule.PortRange.Max != nil {
+ table.AddRow("END PORT", *securityGroupRule.PortRange.Max)
+ table.AddSeparator()
+ }
+ }
+
+ if securityGroupRule.Ethertype != nil {
+ table.AddRow("ETHER TYPE", *securityGroupRule.Ethertype)
+ table.AddSeparator()
+ }
+
+ if securityGroupRule.IpRange != nil {
+ table.AddRow("IP RANGE", *securityGroupRule.IpRange)
+ table.AddSeparator()
+ }
+
+ if securityGroupRule.RemoteSecurityGroupId != nil {
+ table.AddRow("REMOTE SECURITY GROUP", *securityGroupRule.RemoteSecurityGroupId)
+ table.AddSeparator()
+ }
+
+ err := table.Display(p)
+ if err != nil {
+ return fmt.Errorf("render table: %w", err)
+ }
+ return nil
+ }
+}
diff --git a/internal/cmd/beta/security-group/rule/describe/describe_test.go b/internal/cmd/beta/security-group/rule/describe/describe_test.go
new file mode 100644
index 000000000..6463ef1fc
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/describe/describe_test.go
@@ -0,0 +1,246 @@
+package describe
+
+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 testSecurityGroupId = uuid.NewString()
+var testSecurityGroupRuleId = uuid.NewString()
+
+func fixtureArgValues(mods ...func(argValues []string)) []string {
+ argValues := []string{
+ testSecurityGroupRuleId,
+ }
+ 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,
+ securityGroupIdFlag: testSecurityGroupId,
+ }
+ 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,
+ },
+ SecurityGroupId: utils.Ptr(testSecurityGroupId),
+ SecurityGroupRuleId: testSecurityGroupRuleId,
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiGetSecurityGroupRuleRequest)) iaas.ApiGetSecurityGroupRuleRequest {
+ request := testClient.GetSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId, testSecurityGroupRuleId)
+ 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: "security group id missing",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, securityGroupIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 1",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 2",
+ argValues: fixtureArgValues(),
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = "invalid-uuid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group rule id invalid 1",
+ argValues: []string{""},
+ flagValues: fixtureFlagValues(),
+ isValid: false,
+ },
+ {
+ description: "security group rule 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.ApiGetSecurityGroupRuleRequest
+ }{
+ {
+ 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/security-group/rule/list/list.go b/internal/cmd/beta/security-group/rule/list/list.go
new file mode 100644
index 000000000..3675dec91
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/list/list.go
@@ -0,0 +1,191 @@
+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"
+ iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/tables"
+ "github.com/stackitcloud/stackit-sdk-go/services/iaas"
+
+ "github.com/spf13/cobra"
+)
+
+const (
+ limitFlag = "limit"
+
+ securityGroupIdFlag = "security-group-id"
+)
+
+type inputModel struct {
+ *globalflags.GlobalFlagModel
+ Limit *int64
+ SecurityGroupId *string
+}
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list",
+ Short: "Lists all security group rules in a security group of a project",
+ Long: "Lists all security group rules in a security group of a project.",
+ Args: args.NoArgs,
+ Example: examples.Build(
+ examples.NewExample(
+ `Lists all security group rules in security group with ID "xxx"`,
+ "$ stackit beta security-group rule list --security-group-id xxx",
+ ),
+ examples.NewExample(
+ `Lists all security group rules in security group with ID "xxx" in JSON format`,
+ "$ stackit beta security-group rule list --security-group-id xxx --output-format json",
+ ),
+ examples.NewExample(
+ `Lists up to 10 security group rules in security group with ID "xxx"`,
+ "$ stackit beta security-group rule list --security-group-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 security group rules: %w", err)
+ }
+
+ if resp.Items == nil || len(*resp.Items) == 0 {
+ securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, *model.SecurityGroupId)
+ if err != nil {
+ p.Debug(print.ErrorLevel, "get security group name: %v", err)
+ securityGroupLabel = *model.SecurityGroupId
+ }
+
+ 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 rules found in security group %q for project %q\n", securityGroupLabel, 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().Var(flags.UUIDFlag(), securityGroupIdFlag, `The security group ID`)
+
+ err := flags.MarkFlagsRequired(cmd, securityGroupIdFlag)
+ 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,
+ SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag),
+ }
+
+ 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.ApiListSecurityGroupRulesRequest {
+ return apiClient.ListSecurityGroupRules(ctx, model.ProjectId, *model.SecurityGroupId)
+}
+
+func outputResult(p *print.Printer, outputFormat string, securityGroupRules []iaas.SecurityGroupRule) error {
+ switch outputFormat {
+ case print.JSONOutputFormat:
+ details, err := json.MarshalIndent(securityGroupRules, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal security group rules: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ case print.YAMLOutputFormat:
+ details, err := yaml.MarshalWithOptions(securityGroupRules, yaml.IndentSequence(true))
+ if err != nil {
+ return fmt.Errorf("marshal security group rules: %w", err)
+ }
+ p.Outputln(string(details))
+
+ return nil
+ default:
+ table := tables.NewTable()
+ table.SetHeader("ID", "ETHER TYPE", "DIRECTION", "PROTOCOL")
+
+ for _, securityGroupRule := range securityGroupRules {
+ etherType := ""
+ if securityGroupRule.Ethertype != nil {
+ etherType = *securityGroupRule.Ethertype
+ }
+
+ protocolName := ""
+ if securityGroupRule.Protocol != nil {
+ if securityGroupRule.Protocol.Name != nil {
+ protocolName = *securityGroupRule.Protocol.Name
+ }
+ }
+
+ table.AddRow(*securityGroupRule.Id, etherType, *securityGroupRule.Direction, protocolName)
+ table.AddSeparator()
+ }
+
+ p.Outputln(table.Render())
+ return nil
+ }
+}
diff --git a/internal/cmd/beta/security-group/rule/list/list_test.go b/internal/cmd/beta/security-group/rule/list/list_test.go
new file mode 100644
index 000000000..016039ed7
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/list/list_test.go
@@ -0,0 +1,214 @@
+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 testSecurityGroupId = uuid.NewString()
+
+func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
+ flagValues := map[string]string{
+ projectIdFlag: testProjectId,
+ limitFlag: "10",
+ securityGroupIdFlag: testSecurityGroupId,
+ }
+ 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)),
+ SecurityGroupId: utils.Ptr(testSecurityGroupId),
+ }
+ for _, mod := range mods {
+ mod(model)
+ }
+ return model
+}
+
+func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupRulesRequest)) iaas.ApiListSecurityGroupRulesRequest {
+ request := testClient.ListSecurityGroupRules(testCtx, testProjectId, testSecurityGroupId)
+ 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: "security group id missing",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ delete(flagValues, securityGroupIdFlag)
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 1",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = ""
+ }),
+ isValid: false,
+ },
+ {
+ description: "security group id invalid 2",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[securityGroupIdFlag] = "invalid-uuid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "limit invalid",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[limitFlag] = "invalid"
+ }),
+ isValid: false,
+ },
+ {
+ description: "limit invalid 2",
+ flagValues: fixtureFlagValues(func(flagValues map[string]string) {
+ flagValues[limitFlag] = "0"
+ }),
+ isValid: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ p := print.NewPrinter()
+ cmd := NewCmd(p)
+ err := globalflags.Configure(cmd.Flags())
+ if err != nil {
+ t.Fatalf("configure global flags: %v", err)
+ }
+
+ for flag, value := range tt.flagValues {
+ err := cmd.Flags().Set(flag, value)
+ if err != nil {
+ if !tt.isValid {
+ return
+ }
+ t.Fatalf("setting flag --%s=%s: %v", flag, value, err)
+ }
+ }
+
+ err = cmd.ValidateRequiredFlags()
+ if err != nil {
+ if !tt.isValid {
+ return
+ }
+ t.Fatalf("error validating flags: %v", err)
+ }
+
+ model, err := parseInput(p, cmd)
+ if err != nil {
+ if !tt.isValid {
+ return
+ }
+ t.Fatalf("error parsing input: %v", err)
+ }
+
+ if !tt.isValid {
+ t.Fatalf("did not fail on invalid input")
+ }
+ diff := cmp.Diff(model, tt.expectedModel)
+ if diff != "" {
+ t.Fatalf("Data does not match: %s", diff)
+ }
+ })
+ }
+}
+
+func TestBuildRequest(t *testing.T) {
+ tests := []struct {
+ description string
+ model *inputModel
+ expectedRequest iaas.ApiListSecurityGroupRulesRequest
+ }{
+ {
+ 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/security-group/rule/security_group_rule.go b/internal/cmd/beta/security-group/rule/security_group_rule.go
new file mode 100644
index 000000000..26b3443f5
--- /dev/null
+++ b/internal/cmd/beta/security-group/rule/security_group_rule.go
@@ -0,0 +1,32 @@
+package rule
+
+import (
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/create"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/delete"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/describe"
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule/list"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/args"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/print"
+ "github.com/stackitcloud/stackit-cli/internal/pkg/utils"
+
+ "github.com/spf13/cobra"
+)
+
+func NewCmd(p *print.Printer) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "rule",
+ Short: "Provides functionality for security group rules",
+ Long: "Provides functionality for security group rules.",
+ 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))
+}
diff --git a/internal/cmd/beta/security-group/security_group.go b/internal/cmd/beta/security-group/security_group.go
new file mode 100644
index 000000000..53f380d90
--- /dev/null
+++ b/internal/cmd/beta/security-group/security_group.go
@@ -0,0 +1,26 @@
+package securitygroup
+
+import (
+ "github.com/stackitcloud/stackit-cli/internal/cmd/beta/security-group/rule"
+ "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: "security-group",
+ Short: "Provides functionality for security groups",
+ Long: "Provides functionality for security groups.",
+ Args: args.NoArgs,
+ Run: utils.CmdHelp,
+ }
+ addSubcommands(cmd, p)
+ return cmd
+}
+
+func addSubcommands(cmd *cobra.Command, p *print.Printer) {
+ cmd.AddCommand(rule.NewCmd(p))
+}
diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go
index fecc0872c..e7a455688 100644
--- a/internal/pkg/services/iaas/utils/utils.go
+++ b/internal/pkg/services/iaas/utils/utils.go
@@ -8,6 +8,8 @@ import (
)
type IaaSClient interface {
+ GetSecurityGroupRuleExecute(ctx context.Context, projectId, securityGroupRuleId, securityGroupId string) (*iaas.SecurityGroupRule, error)
+ GetSecurityGroupExecute(ctx context.Context, projectId, securityGroupId string) (*iaas.SecurityGroup, error)
GetPublicIPExecute(ctx context.Context, projectId, publicIpId string) (*iaas.PublicIp, error)
GetServerExecute(ctx context.Context, projectId, serverId string) (*iaas.Server, error)
GetVolumeExecute(ctx context.Context, projectId, volumeId string) (*iaas.Volume, error)
@@ -17,6 +19,23 @@ type IaaSClient interface {
GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error)
}
+func GetSecurityGroupRuleName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupRuleId, securityGroupId string) (string, error) {
+ resp, err := apiClient.GetSecurityGroupRuleExecute(ctx, projectId, securityGroupRuleId, securityGroupId)
+ if err != nil {
+ return "", fmt.Errorf("get security group rule: %w", err)
+ }
+ securityGroupRuleName := *resp.Ethertype + ", " + *resp.Direction
+ return securityGroupRuleName, nil
+}
+
+func GetSecurityGroupName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupId string) (string, error) {
+ resp, err := apiClient.GetSecurityGroupExecute(ctx, projectId, securityGroupId)
+ if err != nil {
+ return "", fmt.Errorf("get security group: %w", err)
+ }
+ return *resp.Name, nil
+}
+
func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (ip, associatedResource string, err error) {
resp, err := apiClient.GetPublicIPExecute(ctx, projectId, publicIpId)
if err != nil {
diff --git a/internal/pkg/services/iaas/utils/utils_test.go b/internal/pkg/services/iaas/utils/utils_test.go
index bc0d94299..c7e75a683 100644
--- a/internal/pkg/services/iaas/utils/utils_test.go
+++ b/internal/pkg/services/iaas/utils/utils_test.go
@@ -11,20 +11,38 @@ import (
)
type IaaSClientMocked struct {
- GetPublicIpFails bool
- GetPublicIpResp *iaas.PublicIp
- GetServerFails bool
- GetServerResp *iaas.Server
- GetVolumeFails bool
- GetVolumeResp *iaas.Volume
- GetNetworkFails bool
- GetNetworkResp *iaas.Network
- GetNetworkAreaFails bool
- GetNetworkAreaResp *iaas.NetworkArea
- GetAttachedProjectsFails bool
- GetAttachedProjectsResp *iaas.ProjectListResponse
- GetNetworkAreaRangeFails bool
- GetNetworkAreaRangeResp *iaas.NetworkRange
+ GetSecurityGroupRuleFails bool
+ GetSecurityGroupRuleResp *iaas.SecurityGroupRule
+ GetSecurityGroupFails bool
+ GetSecurityGroupResp *iaas.SecurityGroup
+ GetPublicIpFails bool
+ GetPublicIpResp *iaas.PublicIp
+ GetServerFails bool
+ GetServerResp *iaas.Server
+ GetVolumeFails bool
+ GetVolumeResp *iaas.Volume
+ GetNetworkFails bool
+ GetNetworkResp *iaas.Network
+ GetNetworkAreaFails bool
+ GetNetworkAreaResp *iaas.NetworkArea
+ GetAttachedProjectsFails bool
+ GetAttachedProjectsResp *iaas.ProjectListResponse
+ GetNetworkAreaRangeFails bool
+ GetNetworkAreaRangeResp *iaas.NetworkRange
+}
+
+func (m *IaaSClientMocked) GetSecurityGroupRuleExecute(_ context.Context, _, _, _ string) (*iaas.SecurityGroupRule, error) {
+ if m.GetSecurityGroupRuleFails {
+ return nil, fmt.Errorf("could not get security group rule")
+ }
+ return m.GetSecurityGroupRuleResp, nil
+}
+
+func (m *IaaSClientMocked) GetSecurityGroupExecute(_ context.Context, _, _ string) (*iaas.SecurityGroup, error) {
+ if m.GetSecurityGroupFails {
+ return nil, fmt.Errorf("could not get security group")
+ }
+ return m.GetSecurityGroupResp, nil
}
func (m *IaaSClientMocked) GetPublicIPExecute(_ context.Context, _, _ string) (*iaas.PublicIp, error) {
@@ -76,6 +94,99 @@ func (m *IaaSClientMocked) GetNetworkAreaRangeExecute(_ context.Context, _, _, _
return m.GetNetworkAreaRangeResp, nil
}
+func TestGetSecurityGroupRuleName(t *testing.T) {
+ type args struct {
+ getInstanceFails bool
+ getInstanceResp *iaas.SecurityGroupRule
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantErr bool
+ }{
+ {
+ name: "base",
+ args: args{
+ getInstanceResp: &iaas.SecurityGroupRule{
+ Ethertype: utils.Ptr("IPv6"),
+ Direction: utils.Ptr("ingress"),
+ },
+ },
+ want: "IPv6, ingress",
+ },
+ {
+ name: "get security group rule fails",
+ args: args{
+ getInstanceFails: true,
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &IaaSClientMocked{
+ GetSecurityGroupRuleFails: tt.args.getInstanceFails,
+ GetSecurityGroupRuleResp: tt.args.getInstanceResp,
+ }
+ got, err := GetSecurityGroupRuleName(context.Background(), m, "", "", "")
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetSecurityGroupRuleName() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("GetSecurityGroupRuleName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestGetSecurityGroupName(t *testing.T) {
+ type args struct {
+ getInstanceFails bool
+ getInstanceResp *iaas.SecurityGroup
+ }
+ tests := []struct {
+ name string
+ args args
+ want string
+ wantErr bool
+ }{
+ {
+ name: "base",
+ args: args{
+ getInstanceResp: &iaas.SecurityGroup{
+ Name: utils.Ptr("test"),
+ },
+ },
+ want: "test",
+ },
+ {
+ name: "get security group fails",
+ args: args{
+ getInstanceFails: true,
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &IaaSClientMocked{
+ GetSecurityGroupFails: tt.args.getInstanceFails,
+ GetSecurityGroupResp: tt.args.getInstanceResp,
+ }
+ got, err := GetSecurityGroupName(context.Background(), m, "", "")
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetSecurityGroupName() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("GetSecurityGroupName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
func TestGetPublicIp(t *testing.T) {
type args struct {
getPublicIpFails bool