From a03188630157be43c97f13f5260d631e04fcf556 Mon Sep 17 00:00:00 2001 From: Jonas Schlecht Date: Mon, 24 Aug 2026 14:20:51 +0200 Subject: [PATCH 1/2] feat(albwaf): onboard albwaf config Relates to STACKITCLI-444 --- docs/stackit_beta_alb-waf.md | 1 + docs/stackit_beta_alb-waf_config.md | 37 +++ docs/stackit_beta_alb-waf_config_create.md | 47 +++ docs/stackit_beta_alb-waf_config_delete.md | 40 +++ docs/stackit_beta_alb-waf_config_describe.md | 40 +++ docs/stackit_beta_alb-waf_config_list.md | 44 +++ internal/cmd/beta/alb-waf/alb-waf.go | 2 + internal/cmd/beta/alb-waf/config/config.go | 32 ++ .../cmd/beta/alb-waf/config/create/create.go | 137 +++++++++ .../beta/alb-waf/config/create/create_test.go | 273 ++++++++++++++++++ .../cmd/beta/alb-waf/config/delete/delete.go | 99 +++++++ .../beta/alb-waf/config/delete/delete_test.go | 150 ++++++++++ .../beta/alb-waf/config/describe/describe.go | 160 ++++++++++ .../alb-waf/config/describe/describe_test.go | 228 +++++++++++++++ internal/cmd/beta/alb-waf/config/list/list.go | 193 +++++++++++++ .../cmd/beta/alb-waf/config/list/list_test.go | 187 ++++++++++++ 16 files changed, 1670 insertions(+) create mode 100644 docs/stackit_beta_alb-waf_config.md create mode 100644 docs/stackit_beta_alb-waf_config_create.md create mode 100644 docs/stackit_beta_alb-waf_config_delete.md create mode 100644 docs/stackit_beta_alb-waf_config_describe.md create mode 100644 docs/stackit_beta_alb-waf_config_list.md create mode 100644 internal/cmd/beta/alb-waf/config/config.go create mode 100644 internal/cmd/beta/alb-waf/config/create/create.go create mode 100644 internal/cmd/beta/alb-waf/config/create/create_test.go create mode 100644 internal/cmd/beta/alb-waf/config/delete/delete.go create mode 100644 internal/cmd/beta/alb-waf/config/delete/delete_test.go create mode 100644 internal/cmd/beta/alb-waf/config/describe/describe.go create mode 100644 internal/cmd/beta/alb-waf/config/describe/describe_test.go create mode 100644 internal/cmd/beta/alb-waf/config/list/list.go create mode 100644 internal/cmd/beta/alb-waf/config/list/list_test.go diff --git a/docs/stackit_beta_alb-waf.md b/docs/stackit_beta_alb-waf.md index c2c6b4c40..fcf5e77e4 100644 --- a/docs/stackit_beta_alb-waf.md +++ b/docs/stackit_beta_alb-waf.md @@ -30,6 +30,7 @@ stackit beta alb-waf [flags] ### SEE ALSO * [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta alb-waf config](./stackit_beta_alb-waf_config.md) - Provides functionality for WAF configurations of the ALB WAF * [stackit beta alb-waf custom-rule-group](./stackit_beta_alb-waf_custom-rule-group.md) - Provides functionality for custom rule groups of the ALB WAF * [stackit beta alb-waf managed-rule-set](./stackit_beta_alb-waf_managed-rule-set.md) - Provides functionality for managed rule sets of the ALB WAF diff --git a/docs/stackit_beta_alb-waf_config.md b/docs/stackit_beta_alb-waf_config.md new file mode 100644 index 000000000..112680322 --- /dev/null +++ b/docs/stackit_beta_alb-waf_config.md @@ -0,0 +1,37 @@ +## stackit beta alb-waf config + +Provides functionality for WAF configurations of the ALB WAF + +### Synopsis + +Provides functionality for Web Application Firewall (WAF) configurations for application loadbalancers. + +``` +stackit beta alb-waf config [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf config" +``` + +### 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 beta alb-waf](./stackit_beta_alb-waf.md) - Manages the Web Application Firewall (WAF) for application loadbalancers +* [stackit beta alb-waf config create](./stackit_beta_alb-waf_config_create.md) - Creates an ALB WAF configuration +* [stackit beta alb-waf config delete](./stackit_beta_alb-waf_config_delete.md) - Deletes an ALB WAF configuration +* [stackit beta alb-waf config describe](./stackit_beta_alb-waf_config_describe.md) - Shows details of an ALB WAF configuration +* [stackit beta alb-waf config list](./stackit_beta_alb-waf_config_list.md) - Lists all ALB WAF configurations + diff --git a/docs/stackit_beta_alb-waf_config_create.md b/docs/stackit_beta_alb-waf_config_create.md new file mode 100644 index 000000000..68cdaec87 --- /dev/null +++ b/docs/stackit_beta_alb-waf_config_create.md @@ -0,0 +1,47 @@ +## stackit beta alb-waf config create + +Creates an ALB WAF configuration + +### Synopsis + +Creates a STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configuration. + +``` +stackit beta alb-waf config create [flags] +``` + +### Examples + +``` + Create an ALB WAF configuration with name "my-waf-config" + $ stackit beta alb-waf config create --name my-waf-config + + Create an ALB WAF configuration with a managed rule set, a custom rule group and labels + $ stackit beta alb-waf config create --name my-waf-config --managed-rule-set-name my-managed-rule-set --custom-rule-group-name my-custom-rule-group --labels key1=value1,key2=value2 +``` + +### Options + +``` + --custom-rule-group-name string Name of the custom rule group configuration to attach to the WAF + -h, --help Help for "stackit beta alb-waf config create" + --labels stringToString Labels are key-value string pairs which can be attached to the WAF configuration. E.g. '--labels key1=value1,key2=value2,...' (default []) + --managed-rule-set-name string Name of the managed rule set configuration to attach to the WAF + --name string Name of the WAF configuration +``` + +### 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 beta alb-waf config](./stackit_beta_alb-waf_config.md) - Provides functionality for WAF configurations of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_config_delete.md b/docs/stackit_beta_alb-waf_config_delete.md new file mode 100644 index 000000000..e21d25b8a --- /dev/null +++ b/docs/stackit_beta_alb-waf_config_delete.md @@ -0,0 +1,40 @@ +## stackit beta alb-waf config delete + +Deletes an ALB WAF configuration + +### Synopsis + +Deletes a STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configuration. + +``` +stackit beta alb-waf config delete NAME [flags] +``` + +### Examples + +``` + Delete an ALB WAF configuration with name "my-waf-config" + $ stackit beta alb-waf config delete my-waf-config +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf config 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 beta alb-waf config](./stackit_beta_alb-waf_config.md) - Provides functionality for WAF configurations of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_config_describe.md b/docs/stackit_beta_alb-waf_config_describe.md new file mode 100644 index 000000000..f4ce32bd5 --- /dev/null +++ b/docs/stackit_beta_alb-waf_config_describe.md @@ -0,0 +1,40 @@ +## stackit beta alb-waf config describe + +Shows details of an ALB WAF configuration + +### Synopsis + +Shows details of a STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configuration. + +``` +stackit beta alb-waf config describe NAME [flags] +``` + +### Examples + +``` + Show details of an ALB WAF configuration with name "my-waf-config" + $ stackit beta alb-waf config describe my-waf-config +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf config 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 beta alb-waf config](./stackit_beta_alb-waf_config.md) - Provides functionality for WAF configurations of the ALB WAF + diff --git a/docs/stackit_beta_alb-waf_config_list.md b/docs/stackit_beta_alb-waf_config_list.md new file mode 100644 index 000000000..1827c66aa --- /dev/null +++ b/docs/stackit_beta_alb-waf_config_list.md @@ -0,0 +1,44 @@ +## stackit beta alb-waf config list + +Lists all ALB WAF configurations + +### Synopsis + +Lists all STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configurations. + +``` +stackit beta alb-waf config list [flags] +``` + +### Examples + +``` + List all ALB WAF configurations + $ stackit beta alb-waf config list + + List the first 10 ALB WAF configurations + $ stackit beta alb-waf config list --limit=10 +``` + +### Options + +``` + -h, --help Help for "stackit beta alb-waf config list" + --limit int Limit the output to the first n elements +``` + +### 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 beta alb-waf config](./stackit_beta_alb-waf_config.md) - Provides functionality for WAF configurations of the ALB WAF + diff --git a/internal/cmd/beta/alb-waf/alb-waf.go b/internal/cmd/beta/alb-waf/alb-waf.go index 6de57a31b..89a010957 100644 --- a/internal/cmd/beta/alb-waf/alb-waf.go +++ b/internal/cmd/beta/alb-waf/alb-waf.go @@ -1,6 +1,7 @@ package albwaf import ( + config "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/config" customrulegroup "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/custom-rule-group" managedruleset "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/managed-rule-set" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -27,5 +28,6 @@ func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( managedruleset.NewCmd(params), customrulegroup.NewCmd(params), + config.NewCmd(params), ) } diff --git a/internal/cmd/beta/alb-waf/config/config.go b/internal/cmd/beta/alb-waf/config/config.go new file mode 100644 index 000000000..9a2ecbf45 --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/config.go @@ -0,0 +1,32 @@ +package config + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/config/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/config/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/config/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb-waf/config/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" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Provides functionality for WAF configurations of the ALB WAF", + Long: "Provides functionality for Web Application Firewall (WAF) configurations for application loadbalancers.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/beta/alb-waf/config/create/create.go b/internal/cmd/beta/alb-waf/config/create/create.go new file mode 100644 index 000000000..9c44cc75c --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/create/create.go @@ -0,0 +1,137 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "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/albwaf/client" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + nameFlag = "name" + managedRuleSetNameFlag = "managed-rule-set-name" + customRuleGroupNameFlag = "custom-rule-group-name" + labelsFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string + ManagedRuleSetName *string + CustomRuleGroupName *string + Labels *map[string]string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates an ALB WAF configuration", + Long: "Creates a STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configuration.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create an ALB WAF configuration with name "my-waf-config"`, + "$ stackit beta alb-waf config create --name my-waf-config"), + examples.NewExample( + `Create an ALB WAF configuration with a managed rule set, a custom rule group and labels`, + "$ stackit beta alb-waf config create --name my-waf-config --managed-rule-set-name my-managed-rule-set --custom-rule-group-name my-custom-rule-group --labels key1=value1,key2=value2"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + 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 an ALB WAF configuration for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create ALB WAF configuration: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(nameFlag, "", "Name of the WAF configuration") + cmd.Flags().String(managedRuleSetNameFlag, "", "Name of the managed rule set configuration to attach to the WAF") + cmd.Flags().String(customRuleGroupNameFlag, "", "Name of the custom rule group configuration to attach to the WAF") + cmd.Flags().StringToString(labelsFlag, nil, "Labels are key-value string pairs which can be attached to the WAF configuration. E.g. '--labels key1=value1,key2=value2,...'") + + err := flags.MarkFlagsRequired(cmd, nameFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + ManagedRuleSetName: flags.FlagToStringPointer(p, cmd, managedRuleSetNameFlag), + CustomRuleGroupName: flags.FlagToStringPointer(p, cmd, customRuleGroupNameFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) albwaf.ApiCreateWAFRequest { + req := apiClient.DefaultAPI.CreateWAF(ctx, model.ProjectId, model.Region) + payload := albwaf.CreateWAFPayload{ + Name: model.Name, + ManagedRuleSetName: model.ManagedRuleSetName, + CustomRuleGroupName: model.CustomRuleGroupName, + Labels: model.Labels, + } + return req.CreateWAFPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *albwaf.GetWAFResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("create WAF configuration response is empty") + } + p.Outputf("Created WAF configuration %q for project %q.\n", resp.Name, projectLabel) + return nil + }) +} diff --git a/internal/cmd/beta/alb-waf/config/create/create_test.go b/internal/cmd/beta/alb-waf/config/create/create_test.go new file mode 100644 index 000000000..bf0c31add --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/create/create_test.go @@ -0,0 +1,273 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/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/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testName = "my-waf-config" + testManagedRuleSetName = "my-managed-rule-set" + testCustomRuleGroupName = "my-custom-rule-group" + testLabelKey = "key1" + testLabelValue = "value1" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + nameFlag: testName, + managedRuleSetNameFlag: testManagedRuleSetName, + customRuleGroupNameFlag: testCustomRuleGroupName, + labelsFlag: testLabelKey + "=" + testLabelValue, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + Name: testName, + ManagedRuleSetName: utils.Ptr(testManagedRuleSetName), + CustomRuleGroupName: utils.Ptr(testCustomRuleGroupName), + Labels: utils.Ptr(map[string]string{testLabelKey: testLabelValue}), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *albwaf.ApiCreateWAFRequest)) albwaf.ApiCreateWAFRequest { + request := testClient.DefaultAPI.CreateWAF(testCtx, testProjectId, testRegion) + request = request.CreateWAFPayload(albwaf.CreateWAFPayload{ + Name: testName, + ManagedRuleSetName: utils.Ptr(testManagedRuleSetName), + CustomRuleGroupName: utils.Ptr(testCustomRuleGroupName), + Labels: utils.Ptr(map[string]string{testLabelKey: testLabelValue}), + }) + 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: "only required flags", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, managedRuleSetNameFlag) + delete(flagValues, customRuleGroupNameFlag) + delete(flagValues, labelsFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ManagedRuleSetName = nil + model.CustomRuleGroupName = nil + model.Labels = nil + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, 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: "name missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err = cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest albwaf.ApiCreateWAFRequest + }{ + { + 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, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + resp *albwaf.GetWAFResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "base", + args: args{ + projectLabel: "my-project", + resp: &albwaf.GetWAFResponse{ + Name: testName, + }, + }, + wantErr: false, + }, + { + name: "json output", + args: args{ + outputFormat: print.JSONOutputFormat, + projectLabel: "my-project", + resp: &albwaf.GetWAFResponse{ + Name: testName, + }, + }, + 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.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/alb-waf/config/delete/delete.go b/internal/cmd/beta/alb-waf/config/delete/delete.go new file mode 100644 index 000000000..9208def0d --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/delete/delete.go @@ -0,0 +1,99 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/albwaf/client" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + nameArg = "NAME" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", nameArg), + Short: "Deletes an ALB WAF configuration", + Long: "Deletes a STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configuration.", + Args: args.SingleArg(nameArg, nil), + Example: examples.Build( + examples.NewExample( + `Delete an ALB WAF configuration with name "my-waf-config"`, + "$ stackit beta alb-waf config delete my-waf-config", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + 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 delete the ALB WAF configuration %q for project %q?", model.Name, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete ALB WAF configuration: %w", err) + } + + params.Printer.Outputf("WAF configuration %q deleted.\n", model.Name) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + name := inputArgs[0] + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: name, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) albwaf.ApiDeleteWAFRequest { + return apiClient.DefaultAPI.DeleteWAF(ctx, model.ProjectId, model.Region, model.Name) +} diff --git a/internal/cmd/beta/alb-waf/config/delete/delete_test.go b/internal/cmd/beta/alb-waf/config/delete/delete_test.go new file mode 100644 index 000000000..67789d928 --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/delete/delete_test.go @@ -0,0 +1,150 @@ +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" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testRegion = "eu01" + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testName = "my-waf-config" +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testName, + } + for _, m := range mods { + m(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 _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + Name: testName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *albwaf.ApiDeleteWAFRequest)) albwaf.ApiDeleteWAFRequest { + request := testClient.DefaultAPI.DeleteWAF(testCtx, testProjectId, testRegion, testName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argsValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argsValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argsValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argsValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argsValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argsValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedResult albwaf.ApiDeleteWAFRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedResult: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedResult, + cmp.AllowUnexported(tt.expectedResult, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/alb-waf/config/describe/describe.go b/internal/cmd/beta/alb-waf/config/describe/describe.go new file mode 100644 index 000000000..b1ef7b0d2 --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/describe/describe.go @@ -0,0 +1,160 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "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/albwaf/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +const ( + nameArg = "NAME" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", nameArg), + Short: "Shows details of an ALB WAF configuration", + Long: "Shows details of a STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configuration.", + Args: args.SingleArg(nameArg, nil), + Example: examples.Build( + examples.NewExample( + `Show details of an ALB WAF configuration with name "my-waf-config"`, + `$ stackit beta alb-waf config describe my-waf-config`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read ALB WAF configuration: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + name := inputArgs[0] + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: name, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) albwaf.ApiGetWAFRequest { + return apiClient.DefaultAPI.GetWAF(ctx, model.ProjectId, model.Region, model.Name) +} + +func outputResult(p *print.Printer, outputFormat string, resp *albwaf.GetWAFResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("no WAF configuration found") + } + table := tables.NewTable() + table.SetTitle("WAF Configuration") + table.AddRow("NAME", resp.Name) + table.AddSeparator() + + managedRuleSet := "-" + if resp.ManagedRuleSetName != nil && *resp.ManagedRuleSetName != "" { + managedRuleSet = *resp.ManagedRuleSetName + } + table.AddRow("MANAGED RULE SET", managedRuleSet) + table.AddSeparator() + + customRuleGroup := "-" + if resp.CustomRuleGroupName != nil && *resp.CustomRuleGroupName != "" { + customRuleGroup = *resp.CustomRuleGroupName + } + table.AddRow("CUSTOM RULE GROUP", customRuleGroup) + table.AddSeparator() + + if resp.Labels != nil && len(*resp.Labels) > 0 { + table.AddRow("LABELS", formatLabels(*resp.Labels)) + table.AddSeparator() + } + + usageCount := "" + if resp.Usage != nil { + usageCount = utils.PtrString(resp.Usage.Count) + } + table.AddRow("USAGE COUNT", usageCount) + table.AddSeparator() + + if resp.Usage != nil && len(resp.Usage.Items) > 0 { + table.AddRow("USED BY", formatUsageItems(resp.Usage.Items)) + table.AddSeparator() + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} + +func formatLabels(labels map[string]string) string { + pairs := make([]string, 0, len(labels)) + for v, k := range labels { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(pairs, "\n") +} + +// formatUsageItems renders each WAF usage item as " (listeners: l1, l2, ...)", +// or just "" if no listener names are set, one per line. +func formatUsageItems(items []albwaf.WAFUsageItem) string { + lines := make([]string, 0, len(items)) + for i := range items { + item := &items[i] + if len(item.ListenerNames) > 0 { + lines = append(lines, fmt.Sprintf("%s (listeners: %s)", item.LoadBalancerName, strings.Join(item.ListenerNames, ", "))) + } else { + lines = append(lines, item.LoadBalancerName) + } + } + return strings.Join(lines, "\n") +} diff --git a/internal/cmd/beta/alb-waf/config/describe/describe_test.go b/internal/cmd/beta/alb-waf/config/describe/describe_test.go new file mode 100644 index 000000000..039693564 --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/describe/describe_test.go @@ -0,0 +1,228 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testRegion = "eu01" + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testName = "my-waf-config" +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testName, + } + for _, m := range mods { + m(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 _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + Name: testName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *albwaf.ApiGetWAFRequest)) albwaf.ApiGetWAFRequest { + request := testClient.DefaultAPI.GetWAF(testCtx, testProjectId, testRegion, testName) + 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: "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, + }, + } + + 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 albwaf.ApiGetWAFRequest + }{ + { + 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, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + resp *albwaf.GetWAFResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "base", + args: args{ + resp: &albwaf.GetWAFResponse{ + Name: testName, + }, + }, + wantErr: false, + }, + { + name: "json output", + args: args{ + outputFormat: print.JSONOutputFormat, + resp: &albwaf.GetWAFResponse{ + Name: testName, + }, + }, + wantErr: false, + }, + { + name: "with all optional fields", + args: args{ + resp: &albwaf.GetWAFResponse{ + Name: testName, + ManagedRuleSetName: utils.Ptr("my-managed-rule-set"), + CustomRuleGroupName: utils.Ptr("my-custom-rule-group"), + Labels: utils.Ptr(map[string]string{"key1": "value1", "key2": "value2"}), + Usage: &albwaf.WAFUsage{ + Count: utils.Ptr(int32(2)), + Items: []albwaf.WAFUsageItem{ + { + LoadBalancerName: "my-load-balancer", + ListenerNames: []string{"listener-1", "listener-2"}, + }, + { + LoadBalancerName: "other-load-balancer", + }, + }, + }, + }, + }, + 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.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/alb-waf/config/list/list.go b/internal/cmd/beta/alb-waf/config/list/list.go new file mode 100644 index 000000000..ea56a9a54 --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/list/list.go @@ -0,0 +1,193 @@ +package list + +import ( + "context" + "fmt" + "math" + "strconv" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/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" + "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/albwaf/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +const ( + limitFlag = "limit" + + // maxPageSize is the maximum number of items the API returns per page. + maxPageSize = 100 +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all ALB WAF configurations", + Long: "Lists all STACKIT Application Load Balancer (ALB) Web Application Firewall (WAF) configurations.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all ALB WAF configurations`, + `$ stackit beta alb-waf config list`, + ), + examples.NewExample( + `List the first 10 ALB WAF configurations`, + `$ stackit beta alb-waf config 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 + } + + // Configure API client + 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 + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + items, err := fetchWAFConfigurations(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("list ALB WAF configurations: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient, pageId string, pageSize int64) albwaf.ApiListWAFRequest { + req := apiClient.DefaultAPI.ListWAF(ctx, model.ProjectId, model.Region) + req = req.PageSize(strconv.FormatInt(pageSize, 10)) + if pageId != "" { + req = req.PageId(pageId) + } + return req +} + +func fetchWAFConfigurations(ctx context.Context, model *inputModel, apiClient *albwaf.APIClient) ([]albwaf.GetWAFResponse, error) { + var pageId string + items := make([]albwaf.GetWAFResponse, 0) + received := int64(0) + limit := int64(math.MaxInt64) + if model.Limit != nil { + limit = *model.Limit + } + // Replace with pagination function to be introduced in STACKITSDK-525 + for { + want := min(int64(maxPageSize), limit-received) + request := buildRequest(ctx, model, apiClient, pageId, want) + response, err := request.Execute() + if err != nil { + return nil, fmt.Errorf("list WAF configurations: %w", err) + } + items = append(items, response.Items...) + pageId = "" + if response.NextPageId != nil { + pageId = *response.NextPageId + } + received += want + if pageId == "" || received >= limit { + break + } + } + return items, nil +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []albwaf.GetWAFResponse) error { + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No WAF configurations found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("NAME", "MANAGED RULE SET", "CUSTOM RULE GROUP", "USED BY") + for i := range items { + item := &items[i] + + managedRuleSet := "-" + if item.ManagedRuleSetName != nil && *item.ManagedRuleSetName != "" { + managedRuleSet = *item.ManagedRuleSetName + } + + customRuleGroup := "-" + if item.CustomRuleGroupName != nil && *item.CustomRuleGroupName != "" { + customRuleGroup = *item.CustomRuleGroupName + } + + var usedBy int + if item.Usage != nil && item.Usage.Count != nil { + usedBy = int(*item.Usage.Count) + } + + table.AddRow( + item.Name, + managedRuleSet, + customRuleGroup, + usedBy, + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/beta/alb-waf/config/list/list_test.go b/internal/cmd/beta/alb-waf/config/list/list_test.go new file mode 100644 index 000000000..ba79a45fe --- /dev/null +++ b/internal/cmd/beta/alb-waf/config/list/list_test.go @@ -0,0 +1,187 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + albwaf "github.com/stackitcloud/stackit-sdk-go/services/albwaf/v1api" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &albwaf.APIClient{DefaultAPI: &albwaf.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testLimit int64 = 10 +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: strconv.Itoa(int(testLimit)), + } + 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: utils.Ptr(testLimit), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *albwaf.ApiListWAFRequest)) albwaf.ApiListWAFRequest { + request := testClient.DefaultAPI.ListWAF(testCtx, testProjectId, testRegion) + request = request.PageSize(strconv.FormatInt(maxPageSize, 10)) + 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: "limit invalid", + 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 albwaf.ApiListWAFRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient, "", maxPageSize) + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, albwaf.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + items []albwaf.GetWAFResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: "", + items: []albwaf.GetWAFResponse{}, + }, + wantErr: false, + }, + { + name: "output format json", + args: args{ + outputFormat: print.JSONOutputFormat, + items: []albwaf.GetWAFResponse{}, + }, + 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.items); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} From cc4fc262d0f5b7c81d0f7e4b9e8359cd33aeb2c4 Mon Sep 17 00:00:00 2001 From: Jonas Schlecht Date: Mon, 24 Aug 2026 15:25:16 +0200 Subject: [PATCH 2/2] fix(albwaf): use outputf instead of info for crg delete --- internal/cmd/beta/alb-waf/custom-rule-group/delete/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/alb-waf/custom-rule-group/delete/delete.go b/internal/cmd/beta/alb-waf/custom-rule-group/delete/delete.go index 3548ad5d5..3feba8c6c 100644 --- a/internal/cmd/beta/alb-waf/custom-rule-group/delete/delete.go +++ b/internal/cmd/beta/alb-waf/custom-rule-group/delete/delete.go @@ -74,7 +74,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("delete ALB WAF custom rule group: %w", err) } - params.Printer.Info("Custom rule group %q deleted.\n", model.Name) + params.Printer.Outputf("Custom rule group %q deleted.\n", model.Name) return nil }, }