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