From db95609e1016a0b2ea8cbfe14ccb6568260d1eaa Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 2 Sep 2026 17:22:24 +0300 Subject: [PATCH 01/23] feat(ufw): onboard service - created folder structure - added UfwCustomEndpointKey constant - realized link between root and ufw rules - added handling for ufw rules listing --- go.mod | 1 + go.sum | 2 + internal/cmd/root.go | 2 + internal/cmd/ufw/rules/create/create.go | 1 + internal/cmd/ufw/rules/create/create_test.go | 1 + internal/cmd/ufw/rules/delete/delete.go | 1 + internal/cmd/ufw/rules/delete/delete_test.go | 1 + internal/cmd/ufw/rules/describe/describe.go | 1 + .../cmd/ufw/rules/describe/describe_test.go | 1 + internal/cmd/ufw/rules/list/list.go | 169 ++++++++++++++++++ internal/cmd/ufw/rules/list/list_test.go | 1 + internal/cmd/ufw/rules/rules.go | 29 +++ internal/cmd/ufw/rules/update/update.go | 1 + internal/cmd/ufw/rules/update/update_test.go | 1 + internal/cmd/ufw/ufw.go | 29 +++ internal/pkg/config/config.go | 1 + internal/pkg/services/ufw/client/client.go | 13 ++ internal/pkg/services/ufw/utils/utils.go | 1 + internal/pkg/services/ufw/utils/utils_test.go | 1 + 19 files changed, 257 insertions(+) create mode 100644 internal/cmd/ufw/rules/create/create.go create mode 100644 internal/cmd/ufw/rules/create/create_test.go create mode 100644 internal/cmd/ufw/rules/delete/delete.go create mode 100644 internal/cmd/ufw/rules/delete/delete_test.go create mode 100644 internal/cmd/ufw/rules/describe/describe.go create mode 100644 internal/cmd/ufw/rules/describe/describe_test.go create mode 100644 internal/cmd/ufw/rules/list/list.go create mode 100644 internal/cmd/ufw/rules/list/list_test.go create mode 100644 internal/cmd/ufw/rules/rules.go create mode 100644 internal/cmd/ufw/rules/update/update.go create mode 100644 internal/cmd/ufw/rules/update/update_test.go create mode 100644 internal/cmd/ufw/ufw.go create mode 100644 internal/pkg/services/ufw/client/client.go create mode 100644 internal/pkg/services/ufw/utils/utils.go create mode 100644 internal/pkg/services/ufw/utils/utils_test.go diff --git a/go.mod b/go.mod index 916453c27..44e6d8386 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.1 github.com/stackitcloud/stackit-sdk-go/services/ske v1.21.1 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.18.0 + github.com/stackitcloud/stackit-sdk-go/services/ufw v0.1.0 github.com/stackitcloud/stackit-sdk-go/services/valkey v0.2.0 github.com/stackitcloud/stackit-sdk-go/services/vpn v0.15.0 github.com/zalando/go-keyring v0.2.8 diff --git a/go.sum b/go.sum index 854729b5f..8941c2a9f 100644 --- a/go.sum +++ b/go.sum @@ -654,6 +654,8 @@ github.com/stackitcloud/stackit-sdk-go/services/ske v1.21.1 h1:dSkUoaMip0uA8tnRE github.com/stackitcloud/stackit-sdk-go/services/ske v1.21.1/go.mod h1:TbqmZhLMofmfl+HhVl6oHYcI3zvXTm1vRjN3A/fOkM4= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.18.0 h1:k4dgOFQvkBhSNTtmFOZwMbkKf6ZCEGB/D1WQgiO9gwc= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.18.0/go.mod h1:AiUoMAqQcOlMgDtkVJlqI7P/VGD5xjN3dYjERGnwN/M= +github.com/stackitcloud/stackit-sdk-go/services/ufw v0.1.0 h1:LnDUxI7v507T341Smff/fVzSSRP59uIOUmcHAbUf1hE= +github.com/stackitcloud/stackit-sdk-go/services/ufw v0.1.0/go.mod h1:c/JxzGai01jCK/5OCi0TxCUiQ1Gl7SboGuErflWxLqQ= github.com/stackitcloud/stackit-sdk-go/services/valkey v0.2.0 h1:RP9ITvjsPhA2+Xi9MhqYP9xkEsURyxYmRB3JKXIB5zc= github.com/stackitcloud/stackit-sdk-go/services/valkey v0.2.0/go.mod h1:etzt/a723p327dqha1V72cCoBWjUBS2DtHy3ZTac0SA= github.com/stackitcloud/stackit-sdk-go/services/vpn v0.15.0 h1:JpMJjWBa6fwNNcAHoq00v+8+DwMD0/fe4WPjvbHdY2o= diff --git a/internal/cmd/root.go b/internal/cmd/root.go index bbb05aca3..8cb0be99b 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,6 +6,7 @@ import ( "time" "github.com/stackitcloud/stackit-cli/internal/cmd/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw" "github.com/stackitcloud/stackit-cli/internal/pkg/types" affinityGroups "github.com/stackitcloud/stackit-cli/internal/cmd/affinity-groups" @@ -207,6 +208,7 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(kms.NewCmd(params)) cmd.AddCommand(sqlserverflex.NewCmd(params)) cmd.AddCommand(valkey.NewCmd(params)) + cmd.AddCommand(ufw.NewCmd(params)) } // traverseCommands calls f for c and all of its children. diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go new file mode 100644 index 000000000..ef4f218a0 --- /dev/null +++ b/internal/cmd/ufw/rules/create/create.go @@ -0,0 +1 @@ +package create diff --git a/internal/cmd/ufw/rules/create/create_test.go b/internal/cmd/ufw/rules/create/create_test.go new file mode 100644 index 000000000..ef4f218a0 --- /dev/null +++ b/internal/cmd/ufw/rules/create/create_test.go @@ -0,0 +1 @@ +package create diff --git a/internal/cmd/ufw/rules/delete/delete.go b/internal/cmd/ufw/rules/delete/delete.go new file mode 100644 index 000000000..a67e08acc --- /dev/null +++ b/internal/cmd/ufw/rules/delete/delete.go @@ -0,0 +1 @@ +package delete diff --git a/internal/cmd/ufw/rules/delete/delete_test.go b/internal/cmd/ufw/rules/delete/delete_test.go new file mode 100644 index 000000000..a67e08acc --- /dev/null +++ b/internal/cmd/ufw/rules/delete/delete_test.go @@ -0,0 +1 @@ +package delete diff --git a/internal/cmd/ufw/rules/describe/describe.go b/internal/cmd/ufw/rules/describe/describe.go new file mode 100644 index 000000000..cce268ec4 --- /dev/null +++ b/internal/cmd/ufw/rules/describe/describe.go @@ -0,0 +1 @@ +package describe diff --git a/internal/cmd/ufw/rules/describe/describe_test.go b/internal/cmd/ufw/rules/describe/describe_test.go new file mode 100644 index 000000000..cce268ec4 --- /dev/null +++ b/internal/cmd/ufw/rules/describe/describe_test.go @@ -0,0 +1 @@ +package describe diff --git a/internal/cmd/ufw/rules/list/list.go b/internal/cmd/ufw/rules/list/list.go new file mode 100644 index 000000000..d999a2249 --- /dev/null +++ b/internal/cmd/ufw/rules/list/list.go @@ -0,0 +1,169 @@ +package list + +import ( + "context" + "fmt" + + serviceEnablementClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/client" + serviceEnablementUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ufw/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all UFW rules", + Long: "Lists all STACKIT Unified Firewall (UFW) rules.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all UFW rules`, + "$ stackit ufw rules list"), + examples.NewExample( + `List all UFW rules in JSON format`, + "$ stackit ufw rules list --output-format json"), + examples.NewExample( + `List up to 10 UFW rules`, + "$ stackit ufw rules list --limit 10"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + serviceEnablementApiClient, err := serviceEnablementClient.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + enabled, enabledErr := serviceEnablementUtils.ProjectEnabled(ctx, serviceEnablementApiClient.DefaultAPI, model.ProjectId, model.Region) + if enabledErr != nil { + return fmt.Errorf("check if project is enabled failed: %w", enabledErr) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "ufw", + } + } + return fmt.Errorf("get UFW rules: %w", err) + } + rules := resp.Rules + + if model.Limit != nil && len(rules) > int(*model.Limit) { + rules = rules[:*model.Limit] + } + + projectLabel := model.ProjectId + if len(rules) == 0 { + projectLabel, err = projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + } + } + + if resp.Rules == nil { + params.Printer.Info("(...)", projectLabel) + return nil + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, rules) + }, + } + + configureFlags(cmd) + return cmd +} + +// Configure command flags (type, default value, and description) +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +// Parse command input and return a standardized model +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + 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: flags.FlagToInt64Pointer(p, cmd, limitFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +// Build request to the API +func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiListRulesRequest { + req := apiClient.DefaultAPI.ListRules(ctx, model.ProjectId, model.Region) + return req +} + +// Output result based on the configured output format +func outputResult(p *print.Printer, outputFormat, projectLabel string, resources []ufw.RuleResponse) error { + return p.OutputResult(outputFormat, resources, func() error { + if len(resources) == 0 { + p.Outputf("No rules found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("PRODUCT", "DEPLOYMENT TARGET", "PROTOCOL", + "DIRECTION", "PORT RANGE", "ETHER TYPE", "STATUS") + for i := range resources { + resource := resources[i] + table.AddRow(resource.Product, resource.InstanceName, resource.Protocol, resource.Direction, + resource.PortRange, resource.EtherType, resource.Status) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ufw/rules/list/list_test.go b/internal/cmd/ufw/rules/list/list_test.go new file mode 100644 index 000000000..2cdd824f0 --- /dev/null +++ b/internal/cmd/ufw/rules/list/list_test.go @@ -0,0 +1 @@ +package list diff --git a/internal/cmd/ufw/rules/rules.go b/internal/cmd/ufw/rules/rules.go new file mode 100644 index 000000000..fecdd665d --- /dev/null +++ b/internal/cmd/ufw/rules/rules.go @@ -0,0 +1,29 @@ +package rules + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "rules", + Short: "Provides functionality for UFW rules", + Long: "Provides functionality for UFW rules.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) + //cmd.AddCommand(describe.NewCmd(params)) + //cmd.AddCommand(create.NewCmd(params)) + //cmd.AddCommand(delete.NewCmd(params)) + //cmd.AddCommand(update.NewCmd(params)) +} diff --git a/internal/cmd/ufw/rules/update/update.go b/internal/cmd/ufw/rules/update/update.go new file mode 100644 index 000000000..7a7e4d473 --- /dev/null +++ b/internal/cmd/ufw/rules/update/update.go @@ -0,0 +1 @@ +package update diff --git a/internal/cmd/ufw/rules/update/update_test.go b/internal/cmd/ufw/rules/update/update_test.go new file mode 100644 index 000000000..7a7e4d473 --- /dev/null +++ b/internal/cmd/ufw/rules/update/update_test.go @@ -0,0 +1 @@ +package update diff --git a/internal/cmd/ufw/ufw.go b/internal/cmd/ufw/ufw.go new file mode 100644 index 000000000..a63932379 --- /dev/null +++ b/internal/cmd/ufw/ufw.go @@ -0,0 +1,29 @@ +package ufw + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "ufw", + Short: "Provides functionality for UFW", + Long: "Provides functionality for STACKIT Unified Firewall (UFW).", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) + //cmd.AddCommand(describe.NewCmd(params)) + //cmd.AddCommand(create.NewCmd(params)) + //cmd.AddCommand(update.NewCmd(params)) + //cmd.AddCommand(delete.NewCmd(params)) +} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 2365ed8b4..b944e6eb3 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -56,6 +56,7 @@ const ( CDNCustomEndpointKey = "cdn_custom_endpoint" IntakeCustomEndpointKey = "intake_custom_endpoint" LogsCustomEndpointKey = "logs_custom_endpoint" + UfwCustomEndpointKey = "ufw_custom_endpoint" ValkeyCustomEndpointKey = "valkey_custom_endpoint" VPNCustomEndpointKey = "vpn_custom_endpoint" diff --git a/internal/pkg/services/ufw/client/client.go b/internal/pkg/services/ufw/client/client.go new file mode 100644 index 000000000..765efd715 --- /dev/null +++ b/internal/pkg/services/ufw/client/client.go @@ -0,0 +1,13 @@ +package client + +import ( + "github.com/spf13/viper" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*ufw.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.UfwCustomEndpointKey), false, ufw.NewAPIClient) +} diff --git a/internal/pkg/services/ufw/utils/utils.go b/internal/pkg/services/ufw/utils/utils.go new file mode 100644 index 000000000..d4b585bf7 --- /dev/null +++ b/internal/pkg/services/ufw/utils/utils.go @@ -0,0 +1 @@ +package utils diff --git a/internal/pkg/services/ufw/utils/utils_test.go b/internal/pkg/services/ufw/utils/utils_test.go new file mode 100644 index 000000000..d4b585bf7 --- /dev/null +++ b/internal/pkg/services/ufw/utils/utils_test.go @@ -0,0 +1 @@ +package utils From 65099c97eefa8d16a3947f09a9b139f1b945d33b Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Thu, 3 Sep 2026 14:54:46 +0300 Subject: [PATCH 02/23] refactor(ufw): - clean & refactored list implementation - added describe implementation --- internal/cmd/ufw/rules/describe/describe.go | 122 ++++++++++++++++++++ internal/cmd/ufw/rules/list/list.go | 8 +- internal/cmd/ufw/rules/rules.go | 3 +- 3 files changed, 126 insertions(+), 7 deletions(-) diff --git a/internal/cmd/ufw/rules/describe/describe.go b/internal/cmd/ufw/rules/describe/describe.go index cce268ec4..1cd896dd7 100644 --- a/internal/cmd/ufw/rules/describe/describe.go +++ b/internal/cmd/ufw/rules/describe/describe.go @@ -1 +1,123 @@ package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ufw/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", instanceIdArg), + Short: "Shows details of an UFW rule instance", + Long: "Shows details of an STACKIT Unified Firewall (UFW) rule instance.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of an UFW rule instance with ID "xxx"`, + "$ stackit ufw rule instance describe xxx"), + examples.NewExample( + `Get details of an UFW rule instance with ID "xxx" in JSON format`, + "$ stackit ufw rule instance describe xxx --output-format json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read UFW rule instance: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiGetRuleRequest { + return apiClient.DefaultAPI.GetRule(ctx, model.ProjectId, model.Region, model.InstanceId) +} + +func outputResult(p *print.Printer, outputFormat string, rule *ufw.RuleResponse) error { + return p.OutputResult(outputFormat, rule, func() error { + if rule == nil { + return fmt.Errorf("no instance rule passed") + } + + table := tables.NewTable() + table.AddRow("PRODUCT", utils.PtrString(&rule.Product)) + table.AddSeparator() + table.AddRow("SOURCE", utils.PtrString(&rule.SourceIP)) + table.AddSeparator() + table.AddRow("DEPLOYMENT TARGET", utils.PtrString(&rule.InstanceName)) + table.AddSeparator() + table.AddRow("PROTOCOL", utils.PtrString(&rule.Protocol)) + table.AddSeparator() + table.AddRow("DIRECTION", utils.PtrString(&rule.Direction)) + table.AddSeparator() + table.AddRow("PORT RANGE", fmt.Sprintf("%d", rule.PortRange)) + table.AddSeparator() + table.AddRow("ETHER TYPE", utils.PtrString(&rule.EtherType)) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(&rule.Status)) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/ufw/rules/list/list.go b/internal/cmd/ufw/rules/list/list.go index d999a2249..d2acbfd8c 100644 --- a/internal/cmd/ufw/rules/list/list.go +++ b/internal/cmd/ufw/rules/list/list.go @@ -105,12 +105,10 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return cmd } -// Configure command flags (type, default value, and description) func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -// Parse command input and return a standardized model func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { @@ -138,13 +136,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } -// Build request to the API func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiListRulesRequest { req := apiClient.DefaultAPI.ListRules(ctx, model.ProjectId, model.Region) return req } -// Output result based on the configured output format func outputResult(p *print.Printer, outputFormat, projectLabel string, resources []ufw.RuleResponse) error { return p.OutputResult(outputFormat, resources, func() error { if len(resources) == 0 { @@ -153,11 +149,11 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, resources } table := tables.NewTable() - table.SetHeader("PRODUCT", "DEPLOYMENT TARGET", "PROTOCOL", + table.SetHeader("PRODUCT", "SOURCE", "DEPLOYMENT TARGET", "PROTOCOL", "DIRECTION", "PORT RANGE", "ETHER TYPE", "STATUS") for i := range resources { resource := resources[i] - table.AddRow(resource.Product, resource.InstanceName, resource.Protocol, resource.Direction, + table.AddRow(resource.Product, resource.SourceIP, resource.InstanceName, resource.Protocol, resource.Direction, resource.PortRange, resource.EtherType, resource.Status) } err := table.Display(p) diff --git a/internal/cmd/ufw/rules/rules.go b/internal/cmd/ufw/rules/rules.go index fecdd665d..8dcc875d6 100644 --- a/internal/cmd/ufw/rules/rules.go +++ b/internal/cmd/ufw/rules/rules.go @@ -2,6 +2,7 @@ package rules import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -22,7 +23,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) - //cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) //cmd.AddCommand(create.NewCmd(params)) //cmd.AddCommand(delete.NewCmd(params)) //cmd.AddCommand(update.NewCmd(params)) From 0fba4e5ab1bbfe2332a6ce0af37acd1dab44e9a4 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Thu, 3 Sep 2026 15:18:42 +0300 Subject: [PATCH 03/23] refactor(ufw): - added delete implementation --- internal/cmd/ufw/rules/delete/delete.go | 109 ++++++++++++++++++++++++ internal/cmd/ufw/ufw.go | 6 +- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/internal/cmd/ufw/rules/delete/delete.go b/internal/cmd/ufw/rules/delete/delete.go index a67e08acc..bcece5666 100644 --- a/internal/cmd/ufw/rules/delete/delete.go +++ b/internal/cmd/ufw/rules/delete/delete.go @@ -1 +1,110 @@ package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ufw/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" +) + +const ( + instanceIdArg = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", instanceIdArg), + Short: "Deletes a UFW rule instance", + Long: "Deletes a UFW rule instance.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a UFW rule instance with ID "xxx"`, + "$ stackit ufw instance delete xxx"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", model.InstanceId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete UFW rule instance: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteRuleWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for UFW rule instance deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + params.Printer.Outputf("%s instance %q\n", operationState, model.InstanceId) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiDeleteRuleRequest { + return apiClient.DefaultAPI.DeleteRule(ctx, model.ProjectId, model.Region, model.InstanceId) +} diff --git a/internal/cmd/ufw/ufw.go b/internal/cmd/ufw/ufw.go index a63932379..1dd562c3e 100644 --- a/internal/cmd/ufw/ufw.go +++ b/internal/cmd/ufw/ufw.go @@ -2,6 +2,8 @@ package ufw import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" @@ -22,8 +24,8 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) - //cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) //cmd.AddCommand(create.NewCmd(params)) //cmd.AddCommand(update.NewCmd(params)) - //cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) } From 88323915f11716ccc6c691acdf8f6c6dc7ca325d Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Fri, 4 Sep 2026 16:55:06 +0300 Subject: [PATCH 04/23] refactor(ufw): - added create implementation --- internal/cmd/ufw/rules/create/create.go | 205 ++++++++++++++++++++++++ internal/cmd/ufw/ufw.go | 3 +- 2 files changed, 207 insertions(+), 1 deletion(-) diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index ef4f218a0..4d19a63be 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -1 +1,206 @@ package create + +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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ufw/client" + wait "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" +) + +const ( + productFlag = "product" + typeFlag = "type" + sourceIpFlag = "sourceIp" + instanceIdFlag = "instanceId" + directionFlag = "direction" + descriptionFlag = "description" + etherTypeFlag = "etherType" + portRangeFlag = "portRange" + protocolFlag = "protocol" + offsetFlag = "offset" + securityGroupIdFlag = "securityGroupId" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + Product *string + Type *string + SourceIp *string + InstanceId *string + Direction *string + Description *string + EtherType *string + PortRange *string + Protocol *string + Offset *int32 + SecurityGroupId *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates an UFW rule instance", + Long: "Creates a STACKIT Unified Firewall (UFW) rule instance.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a UFW rule instance of type ACL with sourceIp "1.1.1.1/32" of product "redis" for instance with id=ID`, + "$ stackit ufw instance create --product redis --sourceIp 1.1.1.1/32 --type ACL --instanceId ID"), + // TODO add more examples for creating Security Rule and Group types + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a UFW rule instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create UFW rule instance: %w", err) + } + instanceId := resp.RefId + + if !model.Async { + err := spinner.Run(params.Printer, "Creating ufw rule instance", func() error { + _, err = wait.CreateRuleWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *instanceId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for UFW rule instance creation: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(productFlag, "p", "", "The source service (e.g., Load Balancer, Redis) where you want to attach a rule") + cmd.Flags().StringP(typeFlag, "t", "", "Type (ACL/SecurityRule/SecurityGroup/PublicIP) You can check /provider-options route for them") + cmd.Flags().StringP(sourceIpFlag, "s", "", "The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32)") + cmd.Flags().StringP(instanceIdFlag, "i", "", "Instance ID that will have attached your rule") + cmd.Flags().StringP(directionFlag, "d", "", "Direction (the direction of the traffic, typically ingress or egress, for security rules type)") + cmd.Flags().StringP(descriptionFlag, "D", "", "Description") + cmd.Flags().StringP(etherTypeFlag, "e", "", "Specifies the bound of the rule (for security rules type)") + cmd.Flags().StringP(portRangeFlag, "r", "", "Port range (the Port range to which the rule applies, for security rules type)") + cmd.Flags().StringP(protocolFlag, "o", "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") + cmd.Flags().Int32P(offsetFlag, "f", -1, "Offset - Position in the ACL list of an instance, will be ignored at creation") + cmd.Flags().StringP(securityGroupIdFlag, "g", "", "Security group ID - The ID of the Security Group") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) + + err = flags.MarkFlagsRequired(cmd, productFlag) + cobra.CheckErr(err) + + err = flags.MarkFlagsRequired(cmd, sourceIpFlag) + cobra.CheckErr(err) + + err = flags.MarkFlagsRequired(cmd, typeFlag) + 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{} + } + + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + + Product: flags.FlagToStringPointer(p, cmd, productFlag), + Type: flags.FlagToStringPointer(p, cmd, typeFlag), + SourceIp: flags.FlagToStringPointer(p, cmd, sourceIpFlag), + InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), + Direction: flags.FlagToStringPointer(p, cmd, directionFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + EtherType: flags.FlagToStringPointer(p, cmd, etherTypeFlag), + PortRange: flags.FlagToStringPointer(p, cmd, portRangeFlag), + Protocol: flags.FlagToStringPointer(p, cmd, protocolFlag), + Offset: flags.FlagToInt32Pointer(p, cmd, offsetFlag), + SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiCreateRuleRequest { + req := apiClient.DefaultAPI.CreateRule(ctx, model.ProjectId, model.Region) + + // TODO - add logic for field checking: existing ACLs, correct product, type, instanceID maybe + + req = req.CreateRulePayload(ufw.CreateRulePayload{ + Product: *model.Product, + Type: *model.Type, + SourceIP: *model.SourceIp, + InstanceId: *model.InstanceId, + Direction: model.Direction, + Description: model.Description, + EtherType: model.EtherType, + PortRange: model.PortRange, + Protocol: model.Protocol, + Offset: model.Offset, + SecurityGroupId: model.SecurityGroupId, + }) + + return req +} + +func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, rule *ufw.CreateRuleResponse) error { + if rule == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, rule, func() error { + operationState := "Created" + if async { + operationState = "Triggered creation of" + } + p.Outputf("%s rule for project %q. Rule refID: %s\n", operationState, projectLabel, utils.PtrString(rule.RefId)) + return nil + }) +} diff --git a/internal/cmd/ufw/ufw.go b/internal/cmd/ufw/ufw.go index 1dd562c3e..bf21ff8a6 100644 --- a/internal/cmd/ufw/ufw.go +++ b/internal/cmd/ufw/ufw.go @@ -2,6 +2,7 @@ package ufw import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/create" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" @@ -25,7 +26,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) - //cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) //cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) } From c09913a2ed7d25267f8f8ee926a9724bc9ddbd1e Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Fri, 4 Sep 2026 16:56:35 +0300 Subject: [PATCH 05/23] refactor(ufw): - added create implementation --- internal/cmd/ufw/rules/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index 4d19a63be..42c5c189a 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -18,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/ufw/client" - wait "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" + "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" ) const ( From 8ab1d41eaf2c78f0bbe174111a03215c01dadcf3 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Mon, 7 Sep 2026 10:55:50 +0300 Subject: [PATCH 06/23] refactor(ufw): - added update implementation --- internal/cmd/ufw/rules/update/update.go | 172 ++++++++++++++++++++++++ internal/cmd/ufw/ufw.go | 3 +- 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/internal/cmd/ufw/rules/update/update.go b/internal/cmd/ufw/rules/update/update.go index 7a7e4d473..9564735ea 100644 --- a/internal/cmd/ufw/rules/update/update.go +++ b/internal/cmd/ufw/rules/update/update.go @@ -1 +1,173 @@ package update + +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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ufw/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + instanceIdArg = "INSTANCE_ID" + + sourceIpFlag = "sourceIp" + directionFlag = "direction" + descriptionFlag = "description" + etherTypeFlag = "etherType" + portRangeFlag = "portRange" + protocolFlag = "protocol" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + RuleRefId string + + SourceIp *string + Direction *string + Description *string + EtherType *string + PortRange *string + Protocol *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", instanceIdArg), + Short: "Updates an UFW rule instance", + Long: "Updates a STACKIT Unified Firewall (UFW) rule instance.", + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update a UFW rule instance with "1.1.1.1/32" as sourceIp for instance with id=ID`, + "$ stackit ufw instance update ID --sourceIp 1.1.1.1/32"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to update a UFW rule instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update UFW rule instance: %w", err) + } + instanceId := resp.RefId + + if !model.Async { + err := spinner.Run(params.Printer, "Updating ufw rule instance", func() error { + _, err = wait.UpdateRuleWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *instanceId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for UFW rule instance updating process: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(sourceIpFlag, "s", "", "The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32)") + cmd.Flags().StringP(directionFlag, "d", "", "Direction (the direction of the traffic, typically ingress or egress, for security rules type)") + cmd.Flags().StringP(descriptionFlag, "D", "", "Description") + cmd.Flags().StringP(etherTypeFlag, "e", "", "Specifies the bound of the rule (for security rules type)") + cmd.Flags().StringP(portRangeFlag, "r", "", "Port range (the Port range to which the rule applies, for security rules type)") + cmd.Flags().StringP(protocolFlag, "o", "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") + + err := flags.MarkFlagsRequired(cmd, sourceIpFlag) + 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{} + } + + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + + SourceIp: flags.FlagToStringPointer(p, cmd, sourceIpFlag), + Direction: flags.FlagToStringPointer(p, cmd, directionFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + EtherType: flags.FlagToStringPointer(p, cmd, etherTypeFlag), + PortRange: flags.FlagToStringPointer(p, cmd, portRangeFlag), + Protocol: flags.FlagToStringPointer(p, cmd, protocolFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiUpdateRuleRequest { + req := apiClient.DefaultAPI.UpdateRule(ctx, model.ProjectId, model.Region, model.RuleRefId) + + // TODO - add logic for field checking: existing ACLs, correct product, type, instanceID maybe + + req = req.UpdateRulePayload(ufw.UpdateRulePayload{ + SourceIP: *model.SourceIp, + Direction: model.Direction, + EtherType: model.EtherType, + PortRange: model.PortRange, + Protocol: model.Protocol, + }) + + return req +} + +func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, rule *ufw.UpdateRuleResponse) error { + if rule == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, rule, func() error { + operationState := "Updated" + if async { + operationState = "Triggered updating process of" + } + p.Outputf("%s rule for project %q. Rule refID: %s\n", operationState, projectLabel, utils.PtrString(rule.RefId)) + return nil + }) +} diff --git a/internal/cmd/ufw/ufw.go b/internal/cmd/ufw/ufw.go index bf21ff8a6..2f04a3acb 100644 --- a/internal/cmd/ufw/ufw.go +++ b/internal/cmd/ufw/ufw.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -27,6 +28,6 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) - //cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) } From 5f4a80d444a8480ab2c97954b42fb2d82050735e Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Mon, 7 Sep 2026 11:32:58 +0300 Subject: [PATCH 07/23] refactor(ufw): - added tests for list implementation --- internal/cmd/ufw/rules/list/list.go | 2 +- internal/cmd/ufw/rules/list/list_test.go | 198 +++++++++++++++++++++++ 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/internal/cmd/ufw/rules/list/list.go b/internal/cmd/ufw/rules/list/list.go index d2acbfd8c..b50bad8e2 100644 --- a/internal/cmd/ufw/rules/list/list.go +++ b/internal/cmd/ufw/rules/list/list.go @@ -93,7 +93,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } if resp.Rules == nil { - params.Printer.Info("(...)", projectLabel) + params.Printer.Info("(...) %s", projectLabel) return nil } diff --git a/internal/cmd/ufw/rules/list/list_test.go b/internal/cmd/ufw/rules/list/list_test.go index 2cdd824f0..20b167877 100644 --- a/internal/cmd/ufw/rules/list/list_test.go +++ b/internal/cmd/ufw/rules/list/list_test.go @@ -1 +1,199 @@ package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &ufw.APIClient{DefaultAPI: &ufw.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Limit: new(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ufw.ApiListRulesRequest)) ufw.ApiListRulesRequest { + request := testClient.DefaultAPI.ListRules(testCtx, testProjectId, testRegion) + 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", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.RegionFlag) + }), + 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) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ufw.ApiListRulesRequest + }{ + { + 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, ufw.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + resources []ufw.RuleResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty resources slice", + args: args{ + resources: []ufw.RuleResponse{}, + }, + wantErr: false, + }, + { + name: "set empty resource in resources slice", + args: args{ + resources: []ufw.RuleResponse{{}}, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.resources); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From 73e3ca314ea683177648e96fecc48ddb0444d0ac Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Mon, 7 Sep 2026 15:16:53 +0300 Subject: [PATCH 08/23] refactor(ufw): - added tests for describe implementation --- .../cmd/ufw/rules/describe/describe_test.go | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/internal/cmd/ufw/rules/describe/describe_test.go b/internal/cmd/ufw/rules/describe/describe_test.go index cce268ec4..b22071568 100644 --- a/internal/cmd/ufw/rules/describe/describe_test.go +++ b/internal/cmd/ufw/rules/describe/describe_test.go @@ -1 +1,218 @@ package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &ufw.APIClient{DefaultAPI: &ufw.DefaultAPIService{}} + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +const testRegion = "eu01" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ufw.ApiGetRuleRequest)) ufw.ApiGetRuleRequest { + request := testClient.DefaultAPI.GetRule(testCtx, testProjectId, testRegion, testInstanceId) + 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, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.RegionFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ufw.ApiGetRuleRequest + }{ + { + 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, ufw.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + rule *ufw.RuleResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "set empty instance", + args: args{ + rule: &ufw.RuleResponse{}, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.rule); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From 2d5bcd4912817e69a7254b6a20aa9aceda2afcf8 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Mon, 7 Sep 2026 15:18:03 +0300 Subject: [PATCH 09/23] refactor(ufw): - added tests for delete implementation --- internal/cmd/ufw/rules/delete/delete_test.go | 182 +++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/internal/cmd/ufw/rules/delete/delete_test.go b/internal/cmd/ufw/rules/delete/delete_test.go index a67e08acc..10f643ed9 100644 --- a/internal/cmd/ufw/rules/delete/delete_test.go +++ b/internal/cmd/ufw/rules/delete/delete_test.go @@ -1 +1,183 @@ package delete + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &ufw.APIClient{DefaultAPI: &ufw.DefaultAPIService{}} + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +const testRegion = "eu01" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ufw.ApiDeleteRuleRequest)) ufw.ApiDeleteRuleRequest { + request := testClient.DefaultAPI.DeleteRule(testCtx, testProjectId, testRegion, testInstanceId) + 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, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.RegionFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ufw.ApiDeleteRuleRequest + }{ + { + 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, ufw.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} From 113ff44cbb82e6b9d2032f4eff0daf1b96fe67a6 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Mon, 7 Sep 2026 16:17:31 +0300 Subject: [PATCH 10/23] refactor(ufw): - added tests for create implementation - fixed -p and -o flags that were used for something else --- internal/cmd/ufw/rules/create/create.go | 4 +- internal/cmd/ufw/rules/create/create_test.go | 265 +++++++++++++++++++ 2 files changed, 267 insertions(+), 2 deletions(-) diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index 42c5c189a..9616cc855 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -113,7 +113,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().StringP(productFlag, "p", "", "The source service (e.g., Load Balancer, Redis) where you want to attach a rule") + cmd.Flags().String(productFlag, "", "The source service (e.g., Load Balancer, Redis) where you want to attach a rule") cmd.Flags().StringP(typeFlag, "t", "", "Type (ACL/SecurityRule/SecurityGroup/PublicIP) You can check /provider-options route for them") cmd.Flags().StringP(sourceIpFlag, "s", "", "The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32)") cmd.Flags().StringP(instanceIdFlag, "i", "", "Instance ID that will have attached your rule") @@ -121,7 +121,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(descriptionFlag, "D", "", "Description") cmd.Flags().StringP(etherTypeFlag, "e", "", "Specifies the bound of the rule (for security rules type)") cmd.Flags().StringP(portRangeFlag, "r", "", "Port range (the Port range to which the rule applies, for security rules type)") - cmd.Flags().StringP(protocolFlag, "o", "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") + cmd.Flags().String(protocolFlag, "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") cmd.Flags().Int32P(offsetFlag, "f", -1, "Offset - Position in the ACL list of an instance, will be ignored at creation") cmd.Flags().StringP(securityGroupIdFlag, "g", "", "Security group ID - The ID of the Security Group") diff --git a/internal/cmd/ufw/rules/create/create_test.go b/internal/cmd/ufw/rules/create/create_test.go index ef4f218a0..63609c8b5 100644 --- a/internal/cmd/ufw/rules/create/create_test.go +++ b/internal/cmd/ufw/rules/create/create_test.go @@ -1 +1,266 @@ package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &ufw.APIClient{DefaultAPI: &ufw.DefaultAPIService{}} + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testProduct = "redis" + testType = "ACL" + testSourceIp = "1.1.1.1/32" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + productFlag: testProduct, + typeFlag: testType, + sourceIpFlag: testSourceIp, + instanceIdFlag: testInstanceId, + directionFlag: "ingress", + descriptionFlag: "example-description", + etherTypeFlag: "IPv4", + portRangeFlag: "80-443", + protocolFlag: "TCP", + offsetFlag: "10", + securityGroupIdFlag: "example-sec-group", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Product: new(testProduct), + Type: new(testType), + SourceIp: new(testSourceIp), + InstanceId: new(testInstanceId), + Direction: new("ingress"), + Description: new("example-description"), + EtherType: new("IPv4"), + PortRange: new("80-443"), + Protocol: new("TCP"), + Offset: new(int32(10)), + SecurityGroupId: new("example-sec-group"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ufw.ApiCreateRuleRequest)) ufw.ApiCreateRuleRequest { + request := testClient.DefaultAPI.CreateRule(testCtx, testProjectId, testRegion) + request = request.CreateRulePayload(ufw.CreateRulePayload{ + Product: testProduct, + Type: testType, + SourceIP: testSourceIp, + InstanceId: testInstanceId, + Direction: new("ingress"), + Description: new("example-description"), + EtherType: new("IPv4"), + PortRange: new("80-443"), + Protocol: new("TCP"), + Offset: new(int32(10)), + SecurityGroupId: new("example-sec-group"), + }) + 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: "required fields only", + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + productFlag: testProduct, + typeFlag: testType, + sourceIpFlag: testSourceIp, + instanceIdFlag: testInstanceId, + }, + isValid: true, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Product: new(testProduct), + Type: new(testType), + SourceIp: new(testSourceIp), + InstanceId: new(testInstanceId), + }, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.RegionFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + parseInputWrapper := func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + } + testutils.TestParseInput(t, NewCmd, parseInputWrapper, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ufw.ApiCreateRuleRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "required fields only", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Product: new(testProduct), + Type: new(testType), + SourceIp: new(testSourceIp), + InstanceId: new(testInstanceId), + }, + expectedRequest: testClient.DefaultAPI.CreateRule(testCtx, testProjectId, testRegion). + CreateRulePayload(ufw.CreateRulePayload{ + Product: testProduct, + Type: testType, + SourceIP: testSourceIp, + InstanceId: testInstanceId, + }), + }, + } + + 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, ufw.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + projectLabel string + rule *ufw.CreateRuleResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "set empty response", + args: args{ + rule: &ufw.CreateRuleResponse{}, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.rule); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From b3f625677e42d0d414db5a1bc0bcc943662100a4 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Tue, 8 Sep 2026 09:36:51 +0300 Subject: [PATCH 11/23] refactor(ufw): - added tests for update implementation - fixed -o flag that was already used by the system --- internal/cmd/ufw/rules/update/update.go | 2 +- internal/cmd/ufw/rules/update/update_test.go | 271 +++++++++++++++++++ 2 files changed, 272 insertions(+), 1 deletion(-) diff --git a/internal/cmd/ufw/rules/update/update.go b/internal/cmd/ufw/rules/update/update.go index 9564735ea..0fbc95e65 100644 --- a/internal/cmd/ufw/rules/update/update.go +++ b/internal/cmd/ufw/rules/update/update.go @@ -110,7 +110,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(descriptionFlag, "D", "", "Description") cmd.Flags().StringP(etherTypeFlag, "e", "", "Specifies the bound of the rule (for security rules type)") cmd.Flags().StringP(portRangeFlag, "r", "", "Port range (the Port range to which the rule applies, for security rules type)") - cmd.Flags().StringP(protocolFlag, "o", "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") + cmd.Flags().String(protocolFlag, "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") err := flags.MarkFlagsRequired(cmd, sourceIpFlag) cobra.CheckErr(err) diff --git a/internal/cmd/ufw/rules/update/update_test.go b/internal/cmd/ufw/rules/update/update_test.go index 7a7e4d473..45c3701b7 100644 --- a/internal/cmd/ufw/rules/update/update_test.go +++ b/internal/cmd/ufw/rules/update/update_test.go @@ -1 +1,272 @@ package update + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &ufw.APIClient{DefaultAPI: &ufw.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRuleRefId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testSourceIp = "1.1.1.1/32" +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRuleRefId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + sourceIpFlag: testSourceIp, + directionFlag: "ingress", + descriptionFlag: "example-description", + etherTypeFlag: "IPv4", + portRangeFlag: "80-443", + protocolFlag: "TCP", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + RuleRefId: "", // Left blank because parseInput in the source file currently does not populate it + SourceIp: new(testSourceIp), + Direction: new("ingress"), + Description: new("example-description"), + EtherType: new("IPv4"), + PortRange: new("80-443"), + Protocol: new("TCP"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ufw.ApiUpdateRuleRequest)) ufw.ApiUpdateRuleRequest { + request := testClient.DefaultAPI.UpdateRule(testCtx, testProjectId, testRegion, testRuleRefId) + request = request.UpdateRulePayload(ufw.UpdateRulePayload{ + SourceIP: testSourceIp, + Direction: new("ingress"), + EtherType: new("IPv4"), + PortRange: new("80-443"), + Protocol: new("TCP"), + }) + 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 flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "required flags only", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + sourceIpFlag: testSourceIp, + }, + isValid: true, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + SourceIp: new(testSourceIp), + }, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.RegionFlag) + }), + isValid: false, + }, + { + description: "source IP missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, sourceIpFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + parseInputWrapper := func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + } + testutils.TestParseInput(t, NewCmd, parseInputWrapper, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ufw.ApiUpdateRuleRequest + }{ + { + description: "base", + model: fixtureInputModel(func(model *inputModel) { + model.RuleRefId = testRuleRefId // Inject the ID that parseInput currently skips + }), + expectedRequest: fixtureRequest(), + }, + { + description: "required fields only", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + RuleRefId: testRuleRefId, + SourceIp: new(testSourceIp), + }, + expectedRequest: testClient.DefaultAPI.UpdateRule(testCtx, testProjectId, testRegion, testRuleRefId). + UpdateRulePayload(ufw.UpdateRulePayload{ + SourceIP: testSourceIp, + }), + }, + } + + 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, ufw.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + projectLabel string + rule *ufw.UpdateRuleResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "set empty response", + args: args{ + rule: &ufw.UpdateRuleResponse{}, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.rule); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From f573dc2d0140791b3a27dd2dea400172d51b411d Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Tue, 8 Sep 2026 09:39:37 +0300 Subject: [PATCH 12/23] refactor(ufw): - completed workflow for cli commands --- internal/cmd/ufw/rules/rules.go | 11 +++++++---- internal/cmd/ufw/ufw.go | 12 ++---------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/internal/cmd/ufw/rules/rules.go b/internal/cmd/ufw/rules/rules.go index 8dcc875d6..89ef5dfac 100644 --- a/internal/cmd/ufw/rules/rules.go +++ b/internal/cmd/ufw/rules/rules.go @@ -2,8 +2,11 @@ package rules import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -13,7 +16,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "rules", Short: "Provides functionality for UFW rules", - Long: "Provides functionality for UFW rules.", + Long: "Provides functionality for STACKIT Unified Firewall (UFW) rules.", Args: args.NoArgs, Run: utils.CmdHelp, } @@ -24,7 +27,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) - //cmd.AddCommand(create.NewCmd(params)) - //cmd.AddCommand(delete.NewCmd(params)) - //cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) } diff --git a/internal/cmd/ufw/ufw.go b/internal/cmd/ufw/ufw.go index 2f04a3acb..988e4ef45 100644 --- a/internal/cmd/ufw/ufw.go +++ b/internal/cmd/ufw/ufw.go @@ -2,11 +2,7 @@ package ufw import ( "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/create" - "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/delete" - "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/describe" - "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/update" + "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -25,9 +21,5 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { - cmd.AddCommand(list.NewCmd(params)) - cmd.AddCommand(describe.NewCmd(params)) - cmd.AddCommand(create.NewCmd(params)) - cmd.AddCommand(update.NewCmd(params)) - cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(rules.NewCmd(params)) } From c95fa6d1b558eae2bf44a9926c9b4ada07507542 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Tue, 8 Sep 2026 09:55:45 +0300 Subject: [PATCH 13/23] refactor(ufw): - fixed lint errors --- internal/cmd/ufw/rules/create/create.go | 13 ++++++++----- internal/cmd/ufw/rules/create/create_test.go | 11 ++++------- internal/cmd/ufw/rules/delete/delete.go | 5 +++-- internal/cmd/ufw/rules/describe/describe.go | 3 ++- internal/cmd/ufw/rules/list/list.go | 4 +++- internal/cmd/ufw/rules/list/list_test.go | 3 ++- internal/cmd/ufw/rules/rules.go | 3 ++- internal/cmd/ufw/rules/update/update.go | 9 +++++---- internal/cmd/ufw/rules/update/update_test.go | 10 +++------- internal/cmd/ufw/ufw.go | 3 ++- internal/pkg/services/ufw/client/client.go | 6 ++++-- 11 files changed, 38 insertions(+), 32 deletions(-) diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index 9616cc855..ff470eb8e 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/spf13/cobra" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,11 +15,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + + "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/ufw/client" - "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" + + "github.com/spf13/cobra" ) const ( @@ -65,7 +68,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -138,7 +141,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} diff --git a/internal/cmd/ufw/rules/create/create_test.go b/internal/cmd/ufw/rules/create/create_test.go index 63609c8b5..41eab6f16 100644 --- a/internal/cmd/ufw/rules/create/create_test.go +++ b/internal/cmd/ufw/rules/create/create_test.go @@ -7,12 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" + + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" ) type testCtxKey struct{} @@ -172,10 +172,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - parseInputWrapper := func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { - return parseInput(p, cmd) - } - testutils.TestParseInput(t, NewCmd, parseInputWrapper, tt.expectedModel, nil, tt.flagValues, tt.isValid) + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/ufw/rules/delete/delete.go b/internal/cmd/ufw/rules/delete/delete.go index bcece5666..5d5cf8cf3 100644 --- a/internal/cmd/ufw/rules/delete/delete.go +++ b/internal/cmd/ufw/rules/delete/delete.go @@ -5,6 +5,9 @@ import ( "fmt" "github.com/spf13/cobra" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" ) const ( diff --git a/internal/cmd/ufw/rules/describe/describe.go b/internal/cmd/ufw/rules/describe/describe.go index 1cd896dd7..1e20c7ca8 100644 --- a/internal/cmd/ufw/rules/describe/describe.go +++ b/internal/cmd/ufw/rules/describe/describe.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/spf13/cobra" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" ) const ( diff --git a/internal/cmd/ufw/rules/list/list.go b/internal/cmd/ufw/rules/list/list.go index b50bad8e2..330cf9736 100644 --- a/internal/cmd/ufw/rules/list/list.go +++ b/internal/cmd/ufw/rules/list/list.go @@ -4,12 +4,14 @@ import ( "context" "fmt" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + serviceEnablementClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/client" serviceEnablementUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/types" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" "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" diff --git a/internal/cmd/ufw/rules/list/list_test.go b/internal/cmd/ufw/rules/list/list_test.go index 20b167877..faf7f55ea 100644 --- a/internal/cmd/ufw/rules/list/list_test.go +++ b/internal/cmd/ufw/rules/list/list_test.go @@ -7,10 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" ) type testCtxKey struct{} diff --git a/internal/cmd/ufw/rules/rules.go b/internal/cmd/ufw/rules/rules.go index 89ef5dfac..84de5b705 100644 --- a/internal/cmd/ufw/rules/rules.go +++ b/internal/cmd/ufw/rules/rules.go @@ -1,7 +1,6 @@ package rules import ( - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/create" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules/describe" @@ -10,6 +9,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" ) func NewCmd(params *types.CmdParams) *cobra.Command { diff --git a/internal/cmd/ufw/rules/update/update.go b/internal/cmd/ufw/rules/update/update.go index 0fbc95e65..4c94a68f8 100644 --- a/internal/cmd/ufw/rules/update/update.go +++ b/internal/cmd/ufw/rules/update/update.go @@ -5,6 +5,9 @@ import ( "fmt" "github.com/spf13/cobra" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" - "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait" "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) @@ -57,7 +58,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -116,7 +117,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} diff --git a/internal/cmd/ufw/rules/update/update_test.go b/internal/cmd/ufw/rules/update/update_test.go index 45c3701b7..071aaa167 100644 --- a/internal/cmd/ufw/rules/update/update_test.go +++ b/internal/cmd/ufw/rules/update/update_test.go @@ -7,12 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" ) type testCtxKey struct{} @@ -181,10 +180,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - parseInputWrapper := func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { - return parseInput(p, cmd) - } - testutils.TestParseInput(t, NewCmd, parseInputWrapper, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/ufw/ufw.go b/internal/cmd/ufw/ufw.go index 988e4ef45..5ec53ac0b 100644 --- a/internal/cmd/ufw/ufw.go +++ b/internal/cmd/ufw/ufw.go @@ -1,11 +1,12 @@ package ufw import ( - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/ufw/rules" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" ) func NewCmd(params *types.CmdParams) *cobra.Command { diff --git a/internal/pkg/services/ufw/client/client.go b/internal/pkg/services/ufw/client/client.go index 765efd715..c29ec58a3 100644 --- a/internal/pkg/services/ufw/client/client.go +++ b/internal/pkg/services/ufw/client/client.go @@ -1,11 +1,13 @@ package client import ( - "github.com/spf13/viper" + ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + + "github.com/spf13/viper" ) func ConfigureClient(p *print.Printer, cliVersion string) (*ufw.APIClient, error) { From 275db0c0b4282447720cbad4b67a29e8a399cfd6 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 9 Sep 2026 14:08:46 +0300 Subject: [PATCH 14/23] docs(ufw): - added generated docs --- docs/stackit.md | 1 + docs/stackit_ufw.md | 34 ++++++++++++++ docs/stackit_ufw_rules.md | 38 +++++++++++++++ docs/stackit_ufw_rules_create.md | 51 +++++++++++++++++++++ docs/stackit_ufw_rules_delete.md | 40 ++++++++++++++++ docs/stackit_ufw_rules_describe.md | 43 +++++++++++++++++ docs/stackit_ufw_rules_list.md | 47 +++++++++++++++++++ docs/stackit_ufw_rules_update.md | 46 +++++++++++++++++++ internal/cmd/ufw/rules/create/create.go | 2 +- internal/cmd/ufw/rules/describe/describe.go | 8 ++-- internal/cmd/ufw/rules/update/update.go | 2 +- 11 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 docs/stackit_ufw.md create mode 100644 docs/stackit_ufw_rules.md create mode 100644 docs/stackit_ufw_rules_create.md create mode 100644 docs/stackit_ufw_rules_delete.md create mode 100644 docs/stackit_ufw_rules_describe.md create mode 100644 docs/stackit_ufw_rules_list.md create mode 100644 docs/stackit_ufw_rules_update.md diff --git a/docs/stackit.md b/docs/stackit.md index 5f6b1107a..a7c02e2c2 100644 --- a/docs/stackit.md +++ b/docs/stackit.md @@ -59,6 +59,7 @@ stackit [flags] * [stackit service-account](./stackit_service-account.md) - Provides functionality for service accounts * [stackit ske](./stackit_ske.md) - Provides functionality for SKE * [stackit sqlserverflex](./stackit_sqlserverflex.md) - Provides functionality for SQLServer Flex +* [stackit ufw](./stackit_ufw.md) - Provides functionality for UFW * [stackit valkey](./stackit_valkey.md) - Provides functionality for Valkey * [stackit volume](./stackit_volume.md) - Provides functionality for volumes diff --git a/docs/stackit_ufw.md b/docs/stackit_ufw.md new file mode 100644 index 000000000..a2accf7ee --- /dev/null +++ b/docs/stackit_ufw.md @@ -0,0 +1,34 @@ +## stackit ufw + +Provides functionality for UFW + +### Synopsis + +Provides functionality for STACKIT Unified Firewall (UFW). + +``` +stackit ufw [flags] +``` + +### Options + +``` + -h, --help Help for "stackit ufw" +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit ufw rules](./stackit_ufw_rules.md) - Provides functionality for UFW rules + diff --git a/docs/stackit_ufw_rules.md b/docs/stackit_ufw_rules.md new file mode 100644 index 000000000..6aa88a9f0 --- /dev/null +++ b/docs/stackit_ufw_rules.md @@ -0,0 +1,38 @@ +## stackit ufw rules + +Provides functionality for UFW rules + +### Synopsis + +Provides functionality for STACKIT Unified Firewall (UFW) rules. + +``` +stackit ufw rules [flags] +``` + +### Options + +``` + -h, --help Help for "stackit ufw rules" +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ufw](./stackit_ufw.md) - Provides functionality for UFW +* [stackit ufw rules create](./stackit_ufw_rules_create.md) - Creates a UFW rule instance +* [stackit ufw rules delete](./stackit_ufw_rules_delete.md) - Deletes a UFW rule instance +* [stackit ufw rules describe](./stackit_ufw_rules_describe.md) - Shows details of a UFW rule instance +* [stackit ufw rules list](./stackit_ufw_rules_list.md) - Lists all UFW rules +* [stackit ufw rules update](./stackit_ufw_rules_update.md) - Updates a UFW rule instance + diff --git a/docs/stackit_ufw_rules_create.md b/docs/stackit_ufw_rules_create.md new file mode 100644 index 000000000..3d8626b89 --- /dev/null +++ b/docs/stackit_ufw_rules_create.md @@ -0,0 +1,51 @@ +## stackit ufw rules create + +Creates a UFW rule instance + +### Synopsis + +Creates a STACKIT Unified Firewall (UFW) rule instance. + +``` +stackit ufw rules create [flags] +``` + +### Examples + +``` + Create a UFW rule instance of type ACL with sourceIp "1.1.1.1/32" of product "redis" for instance with id=ID + $ stackit ufw instance create --product redis --sourceIp 1.1.1.1/32 --type ACL --instanceId ID +``` + +### Options + +``` + -D, --description string Description + -d, --direction string Direction (the direction of the traffic, typically ingress or egress, for security rules type) + -e, --etherType string Specifies the bound of the rule (for security rules type) + -h, --help Help for "stackit ufw rules create" + -i, --instanceId string Instance ID that will have attached your rule + -f, --offset int32 Offset - Position in the ACL list of an instance, will be ignored at creation (default -1) + -r, --portRange string Port range (the Port range to which the rule applies, for security rules type) + --product string The source service (e.g., Load Balancer, Redis) where you want to attach a rule + --protocol string The network protocol (e.g. TCP, UDP, ICMP, for security rules type) + -g, --securityGroupId string Security group ID - The ID of the Security Group + -s, --sourceIp string The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32) + -t, --type string Type (ACL/SecurityRule/SecurityGroup/PublicIP) You can check /provider-options route for them +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ufw rules](./stackit_ufw_rules.md) - Provides functionality for UFW rules + diff --git a/docs/stackit_ufw_rules_delete.md b/docs/stackit_ufw_rules_delete.md new file mode 100644 index 000000000..1e1b87350 --- /dev/null +++ b/docs/stackit_ufw_rules_delete.md @@ -0,0 +1,40 @@ +## stackit ufw rules delete + +Deletes a UFW rule instance + +### Synopsis + +Deletes a UFW rule instance. + +``` +stackit ufw rules delete INSTANCE_ID [flags] +``` + +### Examples + +``` + Delete a UFW rule instance with ID "xxx" + $ stackit ufw instance delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit ufw rules delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ufw rules](./stackit_ufw_rules.md) - Provides functionality for UFW rules + diff --git a/docs/stackit_ufw_rules_describe.md b/docs/stackit_ufw_rules_describe.md new file mode 100644 index 000000000..2fbad9721 --- /dev/null +++ b/docs/stackit_ufw_rules_describe.md @@ -0,0 +1,43 @@ +## stackit ufw rules describe + +Shows details of a UFW rule instance + +### Synopsis + +Shows details of a STACKIT Unified Firewall (UFW) rule instance. + +``` +stackit ufw rules describe INSTANCE_ID [flags] +``` + +### Examples + +``` + Get details of a UFW rule instance with ID "xxx" + $ stackit ufw rule instance describe xxx + + Get details of a UFW rule instance with ID "xxx" in JSON format + $ stackit ufw rule instance describe xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit ufw rules describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ufw rules](./stackit_ufw_rules.md) - Provides functionality for UFW rules + diff --git a/docs/stackit_ufw_rules_list.md b/docs/stackit_ufw_rules_list.md new file mode 100644 index 000000000..4e26be844 --- /dev/null +++ b/docs/stackit_ufw_rules_list.md @@ -0,0 +1,47 @@ +## stackit ufw rules list + +Lists all UFW rules + +### Synopsis + +Lists all STACKIT Unified Firewall (UFW) rules. + +``` +stackit ufw rules list [flags] +``` + +### Examples + +``` + List all UFW rules + $ stackit ufw rules list + + List all UFW rules in JSON format + $ stackit ufw rules list --output-format json + + List up to 10 UFW rules + $ stackit ufw rules list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit ufw rules list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ufw rules](./stackit_ufw_rules.md) - Provides functionality for UFW rules + diff --git a/docs/stackit_ufw_rules_update.md b/docs/stackit_ufw_rules_update.md new file mode 100644 index 000000000..8dddbf2e9 --- /dev/null +++ b/docs/stackit_ufw_rules_update.md @@ -0,0 +1,46 @@ +## stackit ufw rules update + +Updates a UFW rule instance + +### Synopsis + +Updates a STACKIT Unified Firewall (UFW) rule instance. + +``` +stackit ufw rules update INSTANCE_ID [flags] +``` + +### Examples + +``` + Update a UFW rule instance with "1.1.1.1/32" as sourceIp for instance with id=ID + $ stackit ufw instance update ID --sourceIp 1.1.1.1/32 +``` + +### Options + +``` + -D, --description string Description + -d, --direction string Direction (the direction of the traffic, typically ingress or egress, for security rules type) + -e, --etherType string Specifies the bound of the rule (for security rules type) + -h, --help Help for "stackit ufw rules update" + -r, --portRange string Port range (the Port range to which the rule applies, for security rules type) + --protocol string The network protocol (e.g. TCP, UDP, ICMP, for security rules type) + -s, --sourceIp string The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32) +``` + +### 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 + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ufw rules](./stackit_ufw_rules.md) - Provides functionality for UFW rules + diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index ff470eb8e..b1aa2cbbd 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -57,7 +57,7 @@ type inputModel struct { func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", - Short: "Creates an UFW rule instance", + Short: "Creates a UFW rule instance", Long: "Creates a STACKIT Unified Firewall (UFW) rule instance.", Args: args.NoArgs, Example: examples.Build( diff --git a/internal/cmd/ufw/rules/describe/describe.go b/internal/cmd/ufw/rules/describe/describe.go index 1e20c7ca8..31b02be61 100644 --- a/internal/cmd/ufw/rules/describe/describe.go +++ b/internal/cmd/ufw/rules/describe/describe.go @@ -30,15 +30,15 @@ type inputModel struct { func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), - Short: "Shows details of an UFW rule instance", - Long: "Shows details of an STACKIT Unified Firewall (UFW) rule instance.", + Short: "Shows details of a UFW rule instance", + Long: "Shows details of a STACKIT Unified Firewall (UFW) rule instance.", Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Get details of an UFW rule instance with ID "xxx"`, + `Get details of a UFW rule instance with ID "xxx"`, "$ stackit ufw rule instance describe xxx"), examples.NewExample( - `Get details of an UFW rule instance with ID "xxx" in JSON format`, + `Get details of a UFW rule instance with ID "xxx" in JSON format`, "$ stackit ufw rule instance describe xxx --output-format json"), ), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/internal/cmd/ufw/rules/update/update.go b/internal/cmd/ufw/rules/update/update.go index 4c94a68f8..32a46576f 100644 --- a/internal/cmd/ufw/rules/update/update.go +++ b/internal/cmd/ufw/rules/update/update.go @@ -48,7 +48,7 @@ type inputModel struct { func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), - Short: "Updates an UFW rule instance", + Short: "Updates a UFW rule instance", Long: "Updates a STACKIT Unified Firewall (UFW) rule instance.", Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), Example: examples.Build( From 23b6f0deb40d1df25e20e4b7893c479601a1a50a Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 9 Sep 2026 15:10:43 +0300 Subject: [PATCH 15/23] refactor(ufw): - updated list to correctly display fields --- internal/cmd/ufw/rules/list/list.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/cmd/ufw/rules/list/list.go b/internal/cmd/ufw/rules/list/list.go index 330cf9736..b40240e1a 100644 --- a/internal/cmd/ufw/rules/list/list.go +++ b/internal/cmd/ufw/rules/list/list.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" serviceEnablementClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/client" @@ -155,8 +156,8 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, resources "DIRECTION", "PORT RANGE", "ETHER TYPE", "STATUS") for i := range resources { resource := resources[i] - table.AddRow(resource.Product, resource.SourceIP, resource.InstanceName, resource.Protocol, resource.Direction, - resource.PortRange, resource.EtherType, resource.Status) + table.AddRow(resource.Product, resource.SourceIP, utils.PtrString(resource.InstanceName), utils.PtrString(resource.Protocol), + utils.PtrString(resource.Direction), utils.PtrString(resource.PortRange), utils.PtrString(resource.EtherType), resource.Status) } err := table.Display(p) if err != nil { From 76fc58eda0bca9515f83982042ac51743309ac00 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 9 Sep 2026 16:18:32 +0300 Subject: [PATCH 16/23] refactor(ufw): - updated rules CRUD documentation --- internal/cmd/ufw/rules/create/create.go | 29 ++++++++++++----- internal/cmd/ufw/rules/create/create_test.go | 33 +++++++++++++++++--- internal/cmd/ufw/rules/delete/delete.go | 4 +-- internal/cmd/ufw/rules/describe/describe.go | 4 +-- internal/cmd/ufw/rules/list/list.go | 7 +++-- internal/cmd/ufw/rules/update/update.go | 15 +++++---- 6 files changed, 66 insertions(+), 26 deletions(-) diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index b1aa2cbbd..e9bc5e2b6 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -62,9 +62,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a UFW rule instance of type ACL with sourceIp "1.1.1.1/32" of product "redis" for instance with id=ID`, - "$ stackit ufw instance create --product redis --sourceIp 1.1.1.1/32 --type ACL --instanceId ID"), - // TODO add more examples for creating Security Rule and Group types + `Create a UFW rule instance of type ACL with sourceIp "1.1.1.1/32" of product "Redis" for instance with id=ID`, + "$ stackit ufw rules create --product redis --sourceIp 1.1.1.1/32 --type ACL --instanceId ID"), + examples.NewExample( + `Create a UFW rule instance of type ACL with sourceIp "2.2.2.2/32" of product "Edge Cloud" for instance with id=ID`, + "$ stackit ufw rules create --product edge-cloud --sourceIp 2.2.2.2/32 --type ACL --instanceId ID"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -90,7 +92,10 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - req := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } resp, err := req.Execute() if err != nil { @@ -117,7 +122,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(productFlag, "", "The source service (e.g., Load Balancer, Redis) where you want to attach a rule") - cmd.Flags().StringP(typeFlag, "t", "", "Type (ACL/SecurityRule/SecurityGroup/PublicIP) You can check /provider-options route for them") + cmd.Flags().StringP(typeFlag, "t", "", "Type (ACL/SecurityRule/SecurityGroup) You can check /provider-options route for them. Unfortunately, this field could be only ACL for the CLI version") cmd.Flags().StringP(sourceIpFlag, "s", "", "The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32)") cmd.Flags().StringP(instanceIdFlag, "i", "", "Instance ID that will have attached your rule") cmd.Flags().StringP(directionFlag, "d", "", "Direction (the direction of the traffic, typically ingress or egress, for security rules type)") @@ -171,10 +176,18 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiCreateRuleRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) (ufw.ApiCreateRuleRequest, error) { req := apiClient.DefaultAPI.CreateRule(ctx, model.ProjectId, model.Region) - // TODO - add logic for field checking: existing ACLs, correct product, type, instanceID maybe + //providerOptions, err := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region).Execute() + // + //if err != nil { + // return req, fmt.Errorf("get provider options: %w", err) + //} + // + //if *model.Type != types.Types { + // return req, fmt.Errorf("invalid rule type: %s", *model.Type) + //} req = req.CreateRulePayload(ufw.CreateRulePayload{ Product: *model.Product, @@ -190,7 +203,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClie SecurityGroupId: model.SecurityGroupId, }) - return req + return req, nil } func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, rule *ufw.CreateRuleResponse) error { diff --git a/internal/cmd/ufw/rules/create/create_test.go b/internal/cmd/ufw/rules/create/create_test.go index 41eab6f16..16e08701b 100644 --- a/internal/cmd/ufw/rules/create/create_test.go +++ b/internal/cmd/ufw/rules/create/create_test.go @@ -25,10 +25,11 @@ var ( ) const ( - testRegion = "eu01" - testProduct = "redis" - testType = "ACL" - testSourceIp = "1.1.1.1/32" + testRegion = "eu01" + testProduct = "redis" + testType = "ACL" + testWrongType = "SecurityGroup" + testSourceIp = "1.1.1.1/32" ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -182,6 +183,7 @@ func TestBuildRequest(t *testing.T) { description string model *inputModel expectedRequest ufw.ApiCreateRuleRequest + isValid bool }{ { description: "base", @@ -209,11 +211,32 @@ func TestBuildRequest(t *testing.T) { InstanceId: testInstanceId, }), }, + { + description: "required fields only, but wrong type", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Product: new(testProduct), + Type: new(testWrongType), + SourceIp: new(testSourceIp), + InstanceId: new(testInstanceId), + }, + expectedRequest: fixtureRequest(), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error building request: %v", err) + } diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), diff --git a/internal/cmd/ufw/rules/delete/delete.go b/internal/cmd/ufw/rules/delete/delete.go index 5d5cf8cf3..f08e410b6 100644 --- a/internal/cmd/ufw/rules/delete/delete.go +++ b/internal/cmd/ufw/rules/delete/delete.go @@ -32,12 +32,12 @@ func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a UFW rule instance", - Long: "Deletes a UFW rule instance.", + Long: "Deletes a STACKIT Unified Firewall (UFW) rule instance.", Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Delete a UFW rule instance with ID "xxx"`, - "$ stackit ufw instance delete xxx"), + "$ stackit ufw rules delete xxx"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() diff --git a/internal/cmd/ufw/rules/describe/describe.go b/internal/cmd/ufw/rules/describe/describe.go index 31b02be61..71a199007 100644 --- a/internal/cmd/ufw/rules/describe/describe.go +++ b/internal/cmd/ufw/rules/describe/describe.go @@ -36,10 +36,10 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `Get details of a UFW rule instance with ID "xxx"`, - "$ stackit ufw rule instance describe xxx"), + "$ stackit ufw rules describe xxx"), examples.NewExample( `Get details of a UFW rule instance with ID "xxx" in JSON format`, - "$ stackit ufw rule instance describe xxx --output-format json"), + "$ stackit ufw rules describe xxx --output-format json"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() diff --git a/internal/cmd/ufw/rules/list/list.go b/internal/cmd/ufw/rules/list/list.go index b40240e1a..7016a7815 100644 --- a/internal/cmd/ufw/rules/list/list.go +++ b/internal/cmd/ufw/rules/list/list.go @@ -152,12 +152,13 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, resources } table := tables.NewTable() - table.SetHeader("PRODUCT", "SOURCE", "DEPLOYMENT TARGET", "PROTOCOL", + table.SetHeader("PRODUCT", "SOURCE", "DEPLOYMENT TARGET", "INSTANCE ID", "PROTOCOL", "DIRECTION", "PORT RANGE", "ETHER TYPE", "STATUS") for i := range resources { resource := resources[i] - table.AddRow(resource.Product, resource.SourceIP, utils.PtrString(resource.InstanceName), utils.PtrString(resource.Protocol), - utils.PtrString(resource.Direction), utils.PtrString(resource.PortRange), utils.PtrString(resource.EtherType), resource.Status) + table.AddRow(resource.Product, resource.SourceIP, utils.PtrString(resource.InstanceName), resource.InstanceId, + utils.PtrString(resource.Protocol), utils.PtrString(resource.Direction), utils.PtrString(resource.PortRange), + utils.PtrString(resource.EtherType), resource.Status) } err := table.Display(p) if err != nil { diff --git a/internal/cmd/ufw/rules/update/update.go b/internal/cmd/ufw/rules/update/update.go index 32a46576f..84095e6cf 100644 --- a/internal/cmd/ufw/rules/update/update.go +++ b/internal/cmd/ufw/rules/update/update.go @@ -35,7 +35,7 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - RuleRefId string + InstanceId string SourceIp *string Direction *string @@ -53,8 +53,8 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( - `Update a UFW rule instance with "1.1.1.1/32" as sourceIp for instance with id=ID`, - "$ stackit ufw instance update ID --sourceIp 1.1.1.1/32"), + `Update a UFW rule instance with "1.1.1.1/32" as sourceIp for instance with ID "xxx"`, + "$ stackit ufw rules update xxx --sourceIp 1.1.1.1/32"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -117,7 +117,9 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -129,6 +131,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, model := inputModel{ GlobalFlagModel: globalFlags, + InstanceId: instanceId, SourceIp: flags.FlagToStringPointer(p, cmd, sourceIpFlag), Direction: flags.FlagToStringPointer(p, cmd, directionFlag), @@ -143,7 +146,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiUpdateRuleRequest { - req := apiClient.DefaultAPI.UpdateRule(ctx, model.ProjectId, model.Region, model.RuleRefId) + req := apiClient.DefaultAPI.UpdateRule(ctx, model.ProjectId, model.Region, model.InstanceId) // TODO - add logic for field checking: existing ACLs, correct product, type, instanceID maybe @@ -168,7 +171,7 @@ func outputResult(p *print.Printer, outputFormat string, async bool, projectLabe if async { operationState = "Triggered updating process of" } - p.Outputf("%s rule for project %q. Rule refID: %s\n", operationState, projectLabel, utils.PtrString(rule.RefId)) + p.Outputf("%s rule for project %q. New rule refID: %s\n", operationState, projectLabel, utils.PtrString(rule.RefId)) return nil }) } From 30151c511a78d0a80debff524a725c6fc2871be0 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 9 Sep 2026 17:18:18 +0300 Subject: [PATCH 17/23] refactor(ufw): - fixed update rules --- internal/cmd/ufw/rules/update/update.go | 27 ++-------------- internal/cmd/ufw/rules/update/update_test.go | 33 ++++++-------------- 2 files changed, 12 insertions(+), 48 deletions(-) diff --git a/internal/cmd/ufw/rules/update/update.go b/internal/cmd/ufw/rules/update/update.go index 84095e6cf..ee16657e4 100644 --- a/internal/cmd/ufw/rules/update/update.go +++ b/internal/cmd/ufw/rules/update/update.go @@ -25,12 +25,7 @@ import ( const ( instanceIdArg = "INSTANCE_ID" - sourceIpFlag = "sourceIp" - directionFlag = "direction" - descriptionFlag = "description" - etherTypeFlag = "etherType" - portRangeFlag = "portRange" - protocolFlag = "protocol" + sourceIpFlag = "sourceIp" ) type inputModel struct { @@ -107,11 +102,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(sourceIpFlag, "s", "", "The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32)") - cmd.Flags().StringP(directionFlag, "d", "", "Direction (the direction of the traffic, typically ingress or egress, for security rules type)") - cmd.Flags().StringP(descriptionFlag, "D", "", "Description") - cmd.Flags().StringP(etherTypeFlag, "e", "", "Specifies the bound of the rule (for security rules type)") - cmd.Flags().StringP(portRangeFlag, "r", "", "Port range (the Port range to which the rule applies, for security rules type)") - cmd.Flags().String(protocolFlag, "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") err := flags.MarkFlagsRequired(cmd, sourceIpFlag) cobra.CheckErr(err) @@ -133,12 +123,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu GlobalFlagModel: globalFlags, InstanceId: instanceId, - SourceIp: flags.FlagToStringPointer(p, cmd, sourceIpFlag), - Direction: flags.FlagToStringPointer(p, cmd, directionFlag), - Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), - EtherType: flags.FlagToStringPointer(p, cmd, etherTypeFlag), - PortRange: flags.FlagToStringPointer(p, cmd, portRangeFlag), - Protocol: flags.FlagToStringPointer(p, cmd, protocolFlag), + SourceIp: flags.FlagToStringPointer(p, cmd, sourceIpFlag), } p.DebugInputModel(model) @@ -148,14 +133,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) ufw.ApiUpdateRuleRequest { req := apiClient.DefaultAPI.UpdateRule(ctx, model.ProjectId, model.Region, model.InstanceId) - // TODO - add logic for field checking: existing ACLs, correct product, type, instanceID maybe - req = req.UpdateRulePayload(ufw.UpdateRulePayload{ - SourceIP: *model.SourceIp, - Direction: model.Direction, - EtherType: model.EtherType, - PortRange: model.PortRange, - Protocol: model.Protocol, + SourceIP: *model.SourceIp, }) return req diff --git a/internal/cmd/ufw/rules/update/update_test.go b/internal/cmd/ufw/rules/update/update_test.go index 071aaa167..5315d3226 100644 --- a/internal/cmd/ufw/rules/update/update_test.go +++ b/internal/cmd/ufw/rules/update/update_test.go @@ -43,11 +43,6 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, sourceIpFlag: testSourceIp, - directionFlag: "ingress", - descriptionFlag: "example-description", - etherTypeFlag: "IPv4", - portRangeFlag: "80-443", - protocolFlag: "TCP", } for _, mod := range mods { mod(flagValues) @@ -62,13 +57,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - RuleRefId: "", // Left blank because parseInput in the source file currently does not populate it - SourceIp: new(testSourceIp), - Direction: new("ingress"), - Description: new("example-description"), - EtherType: new("IPv4"), - PortRange: new("80-443"), - Protocol: new("TCP"), + InstanceId: testRuleRefId, + SourceIp: new(testSourceIp), } for _, mod := range mods { mod(model) @@ -79,11 +69,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureRequest(mods ...func(request *ufw.ApiUpdateRuleRequest)) ufw.ApiUpdateRuleRequest { request := testClient.DefaultAPI.UpdateRule(testCtx, testProjectId, testRegion, testRuleRefId) request = request.UpdateRulePayload(ufw.UpdateRulePayload{ - SourceIP: testSourceIp, - Direction: new("ingress"), - EtherType: new("IPv4"), - PortRange: new("80-443"), - Protocol: new("TCP"), + SourceIP: testSourceIp, }) for _, mod := range mods { mod(&request) @@ -133,7 +119,8 @@ func TestParseInput(t *testing.T) { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - SourceIp: new(testSourceIp), + InstanceId: testRuleRefId, + SourceIp: new(testSourceIp), }, }, { @@ -192,10 +179,8 @@ func TestBuildRequest(t *testing.T) { expectedRequest ufw.ApiUpdateRuleRequest }{ { - description: "base", - model: fixtureInputModel(func(model *inputModel) { - model.RuleRefId = testRuleRefId // Inject the ID that parseInput currently skips - }), + description: "base", + model: fixtureInputModel(), expectedRequest: fixtureRequest(), }, { @@ -206,8 +191,8 @@ func TestBuildRequest(t *testing.T) { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - RuleRefId: testRuleRefId, - SourceIp: new(testSourceIp), + InstanceId: testRuleRefId, + SourceIp: new(testSourceIp), }, expectedRequest: testClient.DefaultAPI.UpdateRule(testCtx, testProjectId, testRegion, testRuleRefId). UpdateRulePayload(ufw.UpdateRulePayload{ From 9a5b3c0ac16aeff2f9d2ddd0998aed546f148ee1 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 9 Sep 2026 17:30:59 +0300 Subject: [PATCH 18/23] refactor(ufw): - fixed create rules tests --- internal/cmd/ufw/rules/create/create_test.go | 39 +++++--------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/internal/cmd/ufw/rules/create/create_test.go b/internal/cmd/ufw/rules/create/create_test.go index 16e08701b..40573546d 100644 --- a/internal/cmd/ufw/rules/create/create_test.go +++ b/internal/cmd/ufw/rules/create/create_test.go @@ -40,13 +40,6 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st typeFlag: testType, sourceIpFlag: testSourceIp, instanceIdFlag: testInstanceId, - directionFlag: "ingress", - descriptionFlag: "example-description", - etherTypeFlag: "IPv4", - portRangeFlag: "80-443", - protocolFlag: "TCP", - offsetFlag: "10", - securityGroupIdFlag: "example-sec-group", } for _, mod := range mods { mod(flagValues) @@ -61,17 +54,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - Product: new(testProduct), - Type: new(testType), - SourceIp: new(testSourceIp), - InstanceId: new(testInstanceId), - Direction: new("ingress"), - Description: new("example-description"), - EtherType: new("IPv4"), - PortRange: new("80-443"), - Protocol: new("TCP"), - Offset: new(int32(10)), - SecurityGroupId: new("example-sec-group"), + Product: new(testProduct), + Type: new(testType), + SourceIp: new(testSourceIp), + InstanceId: new(testInstanceId), } for _, mod := range mods { mod(model) @@ -82,17 +68,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureRequest(mods ...func(request *ufw.ApiCreateRuleRequest)) ufw.ApiCreateRuleRequest { request := testClient.DefaultAPI.CreateRule(testCtx, testProjectId, testRegion) request = request.CreateRulePayload(ufw.CreateRulePayload{ - Product: testProduct, - Type: testType, - SourceIP: testSourceIp, - InstanceId: testInstanceId, - Direction: new("ingress"), - Description: new("example-description"), - EtherType: new("IPv4"), - PortRange: new("80-443"), - Protocol: new("TCP"), - Offset: new(int32(10)), - SecurityGroupId: new("example-sec-group"), + Product: testProduct, + Type: testType, + SourceIP: testSourceIp, + InstanceId: testInstanceId, }) for _, mod := range mods { mod(&request) @@ -212,7 +191,7 @@ func TestBuildRequest(t *testing.T) { }), }, { - description: "required fields only, but wrong type", + description: "required fields only but wrong type", model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, From 2588cf3459c4c8087a4e3b942e3bd54f12416a95 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 9 Sep 2026 17:31:31 +0300 Subject: [PATCH 19/23] refactor(ufw): - fixed create rules type --- internal/cmd/ufw/rules/create/create.go | 66 ++++++------------------- 1 file changed, 16 insertions(+), 50 deletions(-) diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index e9bc5e2b6..528290e08 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -25,17 +25,10 @@ import ( ) const ( - productFlag = "product" - typeFlag = "type" - sourceIpFlag = "sourceIp" - instanceIdFlag = "instanceId" - directionFlag = "direction" - descriptionFlag = "description" - etherTypeFlag = "etherType" - portRangeFlag = "portRange" - protocolFlag = "protocol" - offsetFlag = "offset" - securityGroupIdFlag = "securityGroupId" + productFlag = "product" + typeFlag = "type" + sourceIpFlag = "sourceIp" + instanceIdFlag = "instanceId" ) type inputModel struct { @@ -121,17 +114,10 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().String(productFlag, "", "The source service (e.g., Load Balancer, Redis) where you want to attach a rule") + cmd.Flags().String(productFlag, "", "The source service (e.g., Edge Cloud, Redis) where you want to attach a rule") cmd.Flags().StringP(typeFlag, "t", "", "Type (ACL/SecurityRule/SecurityGroup) You can check /provider-options route for them. Unfortunately, this field could be only ACL for the CLI version") cmd.Flags().StringP(sourceIpFlag, "s", "", "The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32)") cmd.Flags().StringP(instanceIdFlag, "i", "", "Instance ID that will have attached your rule") - cmd.Flags().StringP(directionFlag, "d", "", "Direction (the direction of the traffic, typically ingress or egress, for security rules type)") - cmd.Flags().StringP(descriptionFlag, "D", "", "Description") - cmd.Flags().StringP(etherTypeFlag, "e", "", "Specifies the bound of the rule (for security rules type)") - cmd.Flags().StringP(portRangeFlag, "r", "", "Port range (the Port range to which the rule applies, for security rules type)") - cmd.Flags().String(protocolFlag, "", "The network protocol (e.g. TCP, UDP, ICMP, for security rules type)") - cmd.Flags().Int32P(offsetFlag, "f", -1, "Offset - Position in the ACL list of an instance, will be ignored at creation") - cmd.Flags().StringP(securityGroupIdFlag, "g", "", "Security group ID - The ID of the Security Group") err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) @@ -159,17 +145,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, model := inputModel{ GlobalFlagModel: globalFlags, - Product: flags.FlagToStringPointer(p, cmd, productFlag), - Type: flags.FlagToStringPointer(p, cmd, typeFlag), - SourceIp: flags.FlagToStringPointer(p, cmd, sourceIpFlag), - InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), - Direction: flags.FlagToStringPointer(p, cmd, directionFlag), - Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), - EtherType: flags.FlagToStringPointer(p, cmd, etherTypeFlag), - PortRange: flags.FlagToStringPointer(p, cmd, portRangeFlag), - Protocol: flags.FlagToStringPointer(p, cmd, protocolFlag), - Offset: flags.FlagToInt32Pointer(p, cmd, offsetFlag), - SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), + Product: flags.FlagToStringPointer(p, cmd, productFlag), + Type: flags.FlagToStringPointer(p, cmd, typeFlag), + SourceIp: flags.FlagToStringPointer(p, cmd, sourceIpFlag), + InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), } p.DebugInputModel(model) @@ -179,28 +158,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClient) (ufw.ApiCreateRuleRequest, error) { req := apiClient.DefaultAPI.CreateRule(ctx, model.ProjectId, model.Region) - //providerOptions, err := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region).Execute() - // - //if err != nil { - // return req, fmt.Errorf("get provider options: %w", err) - //} - // - //if *model.Type != types.Types { - // return req, fmt.Errorf("invalid rule type: %s", *model.Type) - //} + if *model.Type != "ACL" { + return req, fmt.Errorf("invalid rule type: %s", *model.Type) + } req = req.CreateRulePayload(ufw.CreateRulePayload{ - Product: *model.Product, - Type: *model.Type, - SourceIP: *model.SourceIp, - InstanceId: *model.InstanceId, - Direction: model.Direction, - Description: model.Description, - EtherType: model.EtherType, - PortRange: model.PortRange, - Protocol: model.Protocol, - Offset: model.Offset, - SecurityGroupId: model.SecurityGroupId, + Product: *model.Product, + Type: *model.Type, + SourceIP: *model.SourceIp, + InstanceId: *model.InstanceId, }) return req, nil From abc2f494923c5cc94c9e181ffbefda888cd40fee Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Wed, 9 Sep 2026 17:33:25 +0300 Subject: [PATCH 20/23] docs(ufw): - updated docs --- docs/stackit_ufw_rules_create.md | 24 ++++++++++-------------- docs/stackit_ufw_rules_delete.md | 4 ++-- docs/stackit_ufw_rules_describe.md | 4 ++-- docs/stackit_ufw_rules_update.md | 13 ++++--------- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/docs/stackit_ufw_rules_create.md b/docs/stackit_ufw_rules_create.md index 3d8626b89..ecb898ee8 100644 --- a/docs/stackit_ufw_rules_create.md +++ b/docs/stackit_ufw_rules_create.md @@ -13,25 +13,21 @@ stackit ufw rules create [flags] ### Examples ``` - Create a UFW rule instance of type ACL with sourceIp "1.1.1.1/32" of product "redis" for instance with id=ID - $ stackit ufw instance create --product redis --sourceIp 1.1.1.1/32 --type ACL --instanceId ID + Create a UFW rule instance of type ACL with sourceIp "1.1.1.1/32" of product "Redis" for instance with id=ID + $ stackit ufw rules create --product redis --sourceIp 1.1.1.1/32 --type ACL --instanceId ID + + Create a UFW rule instance of type ACL with sourceIp "2.2.2.2/32" of product "Edge Cloud" for instance with id=ID + $ stackit ufw rules create --product edge-cloud --sourceIp 2.2.2.2/32 --type ACL --instanceId ID ``` ### Options ``` - -D, --description string Description - -d, --direction string Direction (the direction of the traffic, typically ingress or egress, for security rules type) - -e, --etherType string Specifies the bound of the rule (for security rules type) - -h, --help Help for "stackit ufw rules create" - -i, --instanceId string Instance ID that will have attached your rule - -f, --offset int32 Offset - Position in the ACL list of an instance, will be ignored at creation (default -1) - -r, --portRange string Port range (the Port range to which the rule applies, for security rules type) - --product string The source service (e.g., Load Balancer, Redis) where you want to attach a rule - --protocol string The network protocol (e.g. TCP, UDP, ICMP, for security rules type) - -g, --securityGroupId string Security group ID - The ID of the Security Group - -s, --sourceIp string The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32) - -t, --type string Type (ACL/SecurityRule/SecurityGroup/PublicIP) You can check /provider-options route for them + -h, --help Help for "stackit ufw rules create" + -i, --instanceId string Instance ID that will have attached your rule + --product string The source service (e.g., Edge Cloud, Redis) where you want to attach a rule + -s, --sourceIp string The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32) + -t, --type string Type (ACL/SecurityRule/SecurityGroup) You can check /provider-options route for them. Unfortunately, this field could be only ACL for the CLI version ``` ### Options inherited from parent commands diff --git a/docs/stackit_ufw_rules_delete.md b/docs/stackit_ufw_rules_delete.md index 1e1b87350..bc39fcddd 100644 --- a/docs/stackit_ufw_rules_delete.md +++ b/docs/stackit_ufw_rules_delete.md @@ -4,7 +4,7 @@ Deletes a UFW rule instance ### Synopsis -Deletes a UFW rule instance. +Deletes a STACKIT Unified Firewall (UFW) rule instance. ``` stackit ufw rules delete INSTANCE_ID [flags] @@ -14,7 +14,7 @@ stackit ufw rules delete INSTANCE_ID [flags] ``` Delete a UFW rule instance with ID "xxx" - $ stackit ufw instance delete xxx + $ stackit ufw rules delete xxx ``` ### Options diff --git a/docs/stackit_ufw_rules_describe.md b/docs/stackit_ufw_rules_describe.md index 2fbad9721..d638d8775 100644 --- a/docs/stackit_ufw_rules_describe.md +++ b/docs/stackit_ufw_rules_describe.md @@ -14,10 +14,10 @@ stackit ufw rules describe INSTANCE_ID [flags] ``` Get details of a UFW rule instance with ID "xxx" - $ stackit ufw rule instance describe xxx + $ stackit ufw rules describe xxx Get details of a UFW rule instance with ID "xxx" in JSON format - $ stackit ufw rule instance describe xxx --output-format json + $ stackit ufw rules describe xxx --output-format json ``` ### Options diff --git a/docs/stackit_ufw_rules_update.md b/docs/stackit_ufw_rules_update.md index 8dddbf2e9..87a196bca 100644 --- a/docs/stackit_ufw_rules_update.md +++ b/docs/stackit_ufw_rules_update.md @@ -13,20 +13,15 @@ stackit ufw rules update INSTANCE_ID [flags] ### Examples ``` - Update a UFW rule instance with "1.1.1.1/32" as sourceIp for instance with id=ID - $ stackit ufw instance update ID --sourceIp 1.1.1.1/32 + Update a UFW rule instance with "1.1.1.1/32" as sourceIp for instance with ID "xxx" + $ stackit ufw rules update xxx --sourceIp 1.1.1.1/32 ``` ### Options ``` - -D, --description string Description - -d, --direction string Direction (the direction of the traffic, typically ingress or egress, for security rules type) - -e, --etherType string Specifies the bound of the rule (for security rules type) - -h, --help Help for "stackit ufw rules update" - -r, --portRange string Port range (the Port range to which the rule applies, for security rules type) - --protocol string The network protocol (e.g. TCP, UDP, ICMP, for security rules type) - -s, --sourceIp string The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32) + -h, --help Help for "stackit ufw rules update" + -s, --sourceIp string The IP (CIDR) to which the rule applies (e.g. 192.168.0.1/32) ``` ### Options inherited from parent commands From ced19dd2d17cdcf4664a18eca2995e9a5bcd69cf Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Thu, 10 Sep 2026 09:33:28 +0300 Subject: [PATCH 21/23] refactoring(ufw): - fixed lint --- internal/cmd/ufw/rules/list/list.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/cmd/ufw/rules/list/list.go b/internal/cmd/ufw/rules/list/list.go index 7016a7815..bae22f072 100644 --- a/internal/cmd/ufw/rules/list/list.go +++ b/internal/cmd/ufw/rules/list/list.go @@ -4,9 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + serviceEnablementClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/client" serviceEnablementUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/types" From 81dc025dd62cab9f81e7f6eda74476a142022a56 Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Thu, 10 Sep 2026 10:10:37 +0300 Subject: [PATCH 22/23] refactoring(ufw): - reverted valkey to 0.3.0, issue from merge with main --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f0143aaa9..4261f522a 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/ske v1.21.1 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.18.0 github.com/stackitcloud/stackit-sdk-go/services/ufw v0.1.0 - github.com/stackitcloud/stackit-sdk-go/services/valkey v0.2.0 + github.com/stackitcloud/stackit-sdk-go/services/valkey v0.3.0 github.com/stackitcloud/stackit-sdk-go/services/vpn v0.15.0 github.com/zalando/go-keyring v0.2.8 golang.org/x/mod v0.40.0 From ac8dd730d25be43d40f9a53d3933fe7548638b6a Mon Sep 17 00:00:00 2001 From: "Alex_Mihail.Tiugan" Date: Thu, 10 Sep 2026 15:12:03 +0300 Subject: [PATCH 23/23] refactor(ufw): - improved acl create message --- internal/cmd/ufw/rules/create/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/ufw/rules/create/create.go b/internal/cmd/ufw/rules/create/create.go index 528290e08..662cd0598 100644 --- a/internal/cmd/ufw/rules/create/create.go +++ b/internal/cmd/ufw/rules/create/create.go @@ -159,7 +159,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *ufw.APIClie req := apiClient.DefaultAPI.CreateRule(ctx, model.ProjectId, model.Region) if *model.Type != "ACL" { - return req, fmt.Errorf("invalid rule type: %s", *model.Type) + return req, fmt.Errorf("invalid rule type: %s, only ACL type supported for now", *model.Type) } req = req.CreateRulePayload(ufw.CreateRulePayload{