From 863828c1bc190e1207ee0cb39d5cef7e3e89f2fc Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 19 Nov 2025 17:38:12 +0100 Subject: [PATCH 01/49] feat(cdn): add cdn client, config, list command --- go.mod | 1 + go.sum | 2 + internal/cmd/beta/cdn/cdn.go | 25 ++ .../cmd/beta/cdn/distribution/distribution.go | 24 + .../cmd/beta/cdn/distribution/list/list.go | 173 +++++++ .../beta/cdn/distribution/list/list_test.go | 424 ++++++++++++++++++ internal/cmd/config/set/set.go | 4 + internal/cmd/config/unset/unset.go | 7 + internal/cmd/config/unset/unset_test.go | 13 + internal/pkg/config/config.go | 3 + internal/pkg/services/cdn/client/client.go | 13 + 11 files changed, 689 insertions(+) create mode 100644 internal/cmd/beta/cdn/cdn.go create mode 100644 internal/cmd/beta/cdn/distribution/distribution.go create mode 100644 internal/cmd/beta/cdn/distribution/list/list.go create mode 100644 internal/cmd/beta/cdn/distribution/list/list_test.go create mode 100644 internal/pkg/services/cdn/client/client.go diff --git a/go.mod b/go.mod index 9fc4511c3..c6736283d 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/core v0.19.0 github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 + github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 github.com/stackitcloud/stackit-sdk-go/services/git v0.9.0 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0 diff --git a/go.sum b/go.sum index 65d126b27..3830937c3 100644 --- a/go.sum +++ b/go.sum @@ -567,6 +567,8 @@ github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1 h1:DaJkEN/6l+AJEQ3Dr+ github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1/go.mod h1:SzA+UsSNv4D9IvNT7hwYPewgAvUgj5WXIU2tZ0XaMBI= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw= +github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 h1:Q+qIdejeMsYMkbtVoI9BpGlKGdSVFRBhH/zj44SP8TM= +github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0/go.mod h1:YGadfhuy8yoseczTxF7vN4t9ES2WxGQr0Pug14ii7y4= github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 h1:CnhAMLql0MNmAeq4roQKN8OpSKX4FSgTU6Eu6detB4I= github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1/go.mod h1:7Bx85knfNSBxulPdJUFuBePXNee3cO+sOTYnUG6M+iQ= github.com/stackitcloud/stackit-sdk-go/services/git v0.9.0 h1:zuoJnsLnjxdQcQbs7gUXYzrN0Ip5NXj+6LFBp1EO6cg= diff --git a/internal/cmd/beta/cdn/cdn.go b/internal/cmd/beta/cdn/cdn.go new file mode 100644 index 000000000..794c81bd2 --- /dev/null +++ b/internal/cmd/beta/cdn/cdn.go @@ -0,0 +1,25 @@ +package cdn + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "cdn", + Short: "Manage CDN resources", + Long: "Manage the lifecycle of CDN resources.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand(distribution.NewCommand(params)) +} diff --git a/internal/cmd/beta/cdn/distribution/distribution.go b/internal/cmd/beta/cdn/distribution/distribution.go new file mode 100644 index 000000000..72d48c7fe --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/distribution.go @@ -0,0 +1,24 @@ +package distribution + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCommand(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "distribution", + Short: "Manage CDN distributions", + Long: "Manage the lifecycle of CDN distributions.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/beta/cdn/distribution/list/list.go b/internal/cmd/beta/cdn/distribution/list/list.go new file mode 100644 index 000000000..d6b8aa46c --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/list/list.go @@ -0,0 +1,173 @@ +package list + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + SortBy string +} + +const ( + sortByFlag = "sort-by" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List CDN distributions", + Long: "List all CDN distributions in your account.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all CDN distributions`, + `$ stackit beta dns distribution list`, + ), + examples.NewExample( + `List all CDN distributions sorted by id`, + `$ stackit beta dns distribution list --sort-by=id`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() // should this be cancellable? + + 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 + } + + distributions, err := fetchDistributions(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("fetch distributions: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, distributions) + }, + } + + configureFlags(cmd) + return cmd +} + +var sortByFlagOptions = []string{"id", "created", "updated", "origin-url", "status"} + +func configureFlags(cmd *cobra.Command) { + // same default as apiClient + cmd.Flags().Var(flags.EnumFlag(false, "created", sortByFlagOptions...), sortByFlag, fmt.Sprintf("Sort entries by a specific field, one of %q", sortByFlagOptions)) +} + +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, + SortBy: flags.FlagWithDefaultToStringValue(p, cmd, sortByFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient, nextPageID cdn.ListDistributionsResponseGetNextPageIdentifierAttributeType) cdn.ApiListDistributionsRequest { + req := apiClient.ListDistributions(ctx, model.GlobalFlagModel.ProjectId) + req = req.SortBy(toAPISortBy(model.SortBy)) + req = req.PageSize(100) + if nextPageID != nil { + req = req.PageIdentifier(*nextPageID) + } + return req +} + +func toAPISortBy(sortBy string) string { + switch sortBy { + case "id": + return "id" + case "created": + return "createdAt" + case "updated": + return "updatedAt" + case "origin-url": + return "originUrl" + case "status": + return "status" + default: + panic("invalid sortBy value, programmer error") + } +} + +func outputResult(p *print.Printer, outputFormat string, distributions []cdn.Distribution) error { + if distributions == nil { + distributions = make([]cdn.Distribution, 0) // otherwise prints null in json output + } + return p.OutputResult(outputFormat, distributions, func() error { + if len(distributions) == 0 { + p.Outputln("No CDN distributions found") + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "REGIONS", "STATUS") + for i := range distributions { + d := &distributions[i] + regions := make([]string, 0, len(*d.Config.Regions)) + for _, r := range *d.Config.Regions { + regions = append(regions, string(r)) + } + joinedRegions := strings.Join(regions, ", ") + table.AddRow( + utils.PtrString(d.Id), + joinedRegions, + utils.PtrString(d.Status), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} + +func fetchDistributions(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) ([]cdn.Distribution, error) { + var nextPageID cdn.ListDistributionsResponseGetNextPageIdentifierAttributeType + var distributions []cdn.Distribution + for { + request := buildRequest(ctx, model, apiClient, nextPageID) + response, err := request.Execute() + if err != nil { + return nil, fmt.Errorf("list distributions: %w", err) + } + nextPageID = response.NextPageIdentifier + if response.Distributions != nil { + distributions = append(distributions, *response.Distributions...) + } + if nextPageID == nil { + break + } + } + return distributions, nil +} diff --git a/internal/cmd/beta/cdn/distribution/list/list_test.go b/internal/cmd/beta/cdn/distribution/list/list_test.go new file mode 100644 index 000000000..74ee9f654 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/list/list_test.go @@ -0,0 +1,424 @@ +package list + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" +) + +var testProjectId = uuid.NewString() +var testClient = &cdn.APIClient{} +var testCtx = context.WithValue(context.Background(), "foo", "foo") +var testNextPageID = "next-page-id-123" +var testTime = time.Now() +var testID = "dist-1" +var testStatus = cdn.DISTRIBUTIONSTATUS_ACTIVE + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func flagSortBy(sortBy string) func(m map[string]string) { + return func(m map[string]string) { + m[sortByFlag] = sortBy + } +} + +func flagProjectId(id *string) func(m map[string]string) { + return func(m map[string]string) { + if id == nil { + delete(m, globalflags.ProjectIdFlag) + } else { + m[globalflags.ProjectIdFlag] = *id + } + } +} + +func fixtureInputModel(mods ...func(m *inputModel)) *inputModel { + m := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + SortBy: "created", + } + for _, mod := range mods { + mod(m) + } + return m +} + +func inputSortBy(sortBy string) func(m *inputModel) { + return func(m *inputModel) { + m.SortBy = sortBy + } +} + +func fixtureRequest(mods ...func(r cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + r := testClient.ListDistributions(testCtx, testProjectId) + r = r.PageSize(100) + r = r.SortBy("createdAt") + for _, mod := range mods { + r = mod(r) + } + return r +} + +func requestSortBy(sortBy string) func(r cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + return func(r cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + return r.SortBy(sortBy) + } +} + +func requestNextPageID(nextPageID string) func(r cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + return func(r cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + return r.PageIdentifier(nextPageID) + } +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expected: fixtureInputModel(), + }, + { + description: "no project id", + flagValues: fixtureFlagValues(flagProjectId(nil)), + isValid: false, + }, + { + description: "sort by id", + flagValues: fixtureFlagValues(flagSortBy("id")), + isValid: true, + expected: fixtureInputModel(inputSortBy("id")), + }, + { + description: "sort by origin-url", + flagValues: fixtureFlagValues(flagSortBy("origin-url")), + isValid: true, + expected: fixtureInputModel(inputSortBy("origin-url")), + }, + { + description: "sort by status", + flagValues: fixtureFlagValues(flagSortBy("status")), + isValid: true, + expected: fixtureInputModel(inputSortBy("status")), + }, + { + description: "sort by created", + flagValues: fixtureFlagValues(flagSortBy("created")), + isValid: true, + expected: fixtureInputModel(inputSortBy("created")), + }, + { + description: "sort by updated", + flagValues: fixtureFlagValues(flagSortBy("updated")), + isValid: true, + expected: fixtureInputModel(inputSortBy("updated")), + }, + { + description: "invalid sort by", + flagValues: fixtureFlagValues(flagSortBy("invalid")), + isValid: false, + }, + { + description: "missing sort by uses default", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + delete(flagValues, sortByFlag) + }, + ), + isValid: true, + expected: fixtureInputModel(inputSortBy("created")), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + nextPageID *string + expected cdn.ApiListDistributionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expected: fixtureRequest(), + }, + { + description: "sort by updatedAt", + inputModel: fixtureInputModel(inputSortBy("updated")), + expected: fixtureRequest(requestSortBy("updatedAt")), + }, + { + description: "with next page id", + inputModel: fixtureInputModel(), + nextPageID: &testNextPageID, + expected: fixtureRequest(requestNextPageID(testNextPageID)), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + req := buildRequest(testCtx, tt.inputModel, testClient, tt.nextPageID) + diff := cmp.Diff(req, tt.expected, + cmp.AllowUnexported(tt.expected), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Errorf("buildRequest() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +type testResponse struct { + statusCode int + body cdn.ListDistributionsResponse +} + +func fixtureTestResponse(mods ...func(r *testResponse)) testResponse { + r := testResponse{ + statusCode: 200, + } + for _, mod := range mods { + mod(&r) + } + return r +} + +func responseStatus(statusCode int) func(r *testResponse) { + return func(r *testResponse) { + r.statusCode = statusCode + } +} + +func responseNextPageID(nextPageID *string) func(r *testResponse) { + return func(r *testResponse) { + r.body.NextPageIdentifier = nextPageID + } +} + +func responseDistributions(distributions ...cdn.Distribution) func(r *testResponse) { + return func(r *testResponse) { + r.body.Distributions = &distributions + } +} + +func fixtureDistribution(id string) cdn.Distribution { + return cdn.Distribution{ + Id: &id, + } +} + +func TestFetchDistributions(t *testing.T) { + tests := []struct { + description string + responses []testResponse + expected []cdn.Distribution + fails bool + }{ + { + description: "no distributions", + responses: []testResponse{ + fixtureTestResponse(), + }, + expected: nil, + }, + { + description: "single distribution, single page", + responses: []testResponse{ + fixtureTestResponse( + responseDistributions(fixtureDistribution("dist-1")), + ), + }, + expected: []cdn.Distribution{ + fixtureDistribution("dist-1"), + }, + }, + { + description: "multiple distributions, multiple pages", + responses: []testResponse{ + fixtureTestResponse( + responseNextPageID(&testNextPageID), + responseDistributions( + fixtureDistribution("dist-1"), + ), + ), + fixtureTestResponse( + responseDistributions( + fixtureDistribution("dist-2"), + ), + ), + }, + expected: []cdn.Distribution{ + fixtureDistribution("dist-1"), + fixtureDistribution("dist-2"), + }, + }, + { + description: "API error", + responses: []testResponse{ + fixtureTestResponse( + responseStatus(500), + ), + }, + fails: true, + }, + { + description: "API error on second page", + responses: []testResponse{ + fixtureTestResponse( + responseNextPageID(&testNextPageID), + responseDistributions( + fixtureDistribution("dist-1"), + ), + ), + fixtureTestResponse(responseStatus(500)), + }, + fails: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + callCount := 0 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := tt.responses[callCount] + callCount++ + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.statusCode) + bs, err := json.Marshal(resp.body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + _, err = w.Write(bs) + if err != nil { + t.Fatalf("write: %v", err) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + client, err := cdn.NewAPIClient( + sdkConfig.WithEndpoint(server.URL), + sdkConfig.WithoutAuthentication(), + ) + if err != nil { + t.Fatalf("failed to create test client: %v", err) + } + got, err := fetchDistributions(testCtx, fixtureInputModel(), client) + if err != nil { + if !tt.fails { + t.Fatalf("fetchDistributions() unexpected error: %v", err) + } + return + } + if callCount != len(tt.responses) { + t.Errorf("fetchDistributions() expected %d calls, got %d", len(tt.responses), callCount) + } + diff := cmp.Diff(got, tt.expected) + if diff != "" { + t.Errorf("fetchDistributions() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFormat string + distributions []cdn.Distribution + expected string + }{ + { + description: "no distributions", + outputFormat: "json", + distributions: []cdn.Distribution{}, + expected: `[] +`, + }, + { + description: "no distributions nil slice", + outputFormat: "json", + expected: `[] +`, + }, + { + description: "single distribution", + outputFormat: "table", + distributions: []cdn.Distribution{ + { + Id: &testID, + Config: &cdn.Config{ + Regions: &[]cdn.Region{ + cdn.REGION_EU, + cdn.REGION_AF, + }, + }, + Status: &testStatus, + }, + }, + expected: ` + ID │ REGIONS │ STATUS +────────┼─────────┼──────── + dist-1 │ EU, AF │ ACTIVE + +`, + }, + { + description: "no distributions, table format", + outputFormat: "table", + expected: "No CDN distributions found\n", + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + buffer := &bytes.Buffer{} + p.Cmd.SetOut(buffer) + if err := outputResult(p, tt.outputFormat, tt.distributions); err != nil { + t.Fatalf("outputResult: %v", err) + } + if buffer.String() != tt.expected { + t.Errorf("want:\n%s\ngot:\n%s", tt.expected, buffer.String()) + } + }) + } +} diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index e694e8901..93548fa10 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -47,6 +47,7 @@ const ( sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" iaasCustomEndpointFlag = "iaas-custom-endpoint" tokenCustomEndpointFlag = "token-custom-endpoint" + cdnCustomEndpointFlag = "cdn-custom-endpoint" ) type inputModel struct { @@ -161,6 +162,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sqlServerFlexCustomEndpointFlag, "", "SQLServer Flex API base URL, used in calls to this API") cmd.Flags().String(iaasCustomEndpointFlag, "", "IaaS API base URL, used in calls to this API") cmd.Flags().String(tokenCustomEndpointFlag, "", "Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication.") + cmd.Flags().String(cdnCustomEndpointFlag, "", "CDN API base URL, used in calls to this API") err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag)) cobra.CheckErr(err) @@ -219,6 +221,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.TokenCustomEndpointKey, cmd.Flags().Lookup(tokenCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.CDNCustomEndpointKey, cmd.Flags().Lookup(cdnCustomEndpointFlag)) + cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index 28ed935b9..33a2fa5c5 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -51,6 +51,7 @@ const ( sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" iaasCustomEndpointFlag = "iaas-custom-endpoint" tokenCustomEndpointFlag = "token-custom-endpoint" + cdnCustomEndpointFlag = "cdn-custom-endpoint" ) type inputModel struct { @@ -89,6 +90,7 @@ type inputModel struct { SQLServerFlexCustomEndpoint bool IaaSCustomEndpoint bool TokenCustomEndpoint bool + CDNCustomEndpoint bool } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -212,6 +214,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if model.TokenCustomEndpoint { viper.Set(config.TokenCustomEndpointKey, "") } + if model.CDNCustomEndpoint { + viper.Set(config.CDNCustomEndpointKey, "") + } err := config.Write() if err != nil { @@ -260,6 +265,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL") cmd.Flags().Bool(iaasCustomEndpointFlag, false, "IaaS API base URL. If unset, uses the default base URL") cmd.Flags().Bool(tokenCustomEndpointFlag, false, "Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication.") + cmd.Flags().Bool(cdnCustomEndpointFlag, false, "Custom CDN endpoint URL. If unset, uses the default base URL") } func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { @@ -299,6 +305,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag), IaaSCustomEndpoint: flags.FlagToBoolValue(p, cmd, iaasCustomEndpointFlag), TokenCustomEndpoint: flags.FlagToBoolValue(p, cmd, tokenCustomEndpointFlag), + CDNCustomEndpoint: flags.FlagToBoolValue(p, cmd, cdnCustomEndpointFlag), } p.DebugInputModel(model) diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index e48dff384..1a35587e9 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -44,6 +44,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool sqlServerFlexCustomEndpointFlag: true, iaasCustomEndpointFlag: true, tokenCustomEndpointFlag: true, + cdnCustomEndpointFlag: true, } for _, mod := range mods { mod(flagValues) @@ -84,6 +85,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { SQLServerFlexCustomEndpoint: true, IaaSCustomEndpoint: true, TokenCustomEndpoint: true, + CDNCustomEndpoint: true, } for _, mod := range mods { mod(model) @@ -140,6 +142,7 @@ func TestParseInput(t *testing.T) { model.SQLServerFlexCustomEndpoint = false model.IaaSCustomEndpoint = false model.TokenCustomEndpoint = false + model.CDNCustomEndpoint = false }), }, { @@ -302,6 +305,16 @@ func TestParseInput(t *testing.T) { model.TokenCustomEndpoint = false }), }, + { + description: "cdn custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[cdnCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.CDNCustomEndpoint = false + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 9a03ac3b8..0c91034f0 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -48,6 +48,7 @@ const ( IaaSCustomEndpointKey = "iaas_custom_endpoint" TokenCustomEndpointKey = "token_custom_endpoint" GitCustomEndpointKey = "git_custom_endpoint" + CDNCustomEndpointKey = "cdn_custom_endpoint" ProjectNameKey = "project_name" DefaultProfileName = "default" @@ -109,6 +110,7 @@ var ConfigKeys = []string{ TokenCustomEndpointKey, GitCustomEndpointKey, AlbCustomEndpoint, + CDNCustomEndpointKey, } var defaultConfigFolderPath string @@ -196,6 +198,7 @@ func setConfigDefaults() { viper.SetDefault(TokenCustomEndpointKey, "") viper.SetDefault(GitCustomEndpointKey, "") viper.SetDefault(AlbCustomEndpoint, "") + viper.SetDefault(CDNCustomEndpointKey, "") } func getConfigFilePath(configFolder string) string { diff --git a/internal/pkg/services/cdn/client/client.go b/internal/pkg/services/cdn/client/client.go new file mode 100644 index 000000000..afefb7a92 --- /dev/null +++ b/internal/pkg/services/cdn/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" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*cdn.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.CDNCustomEndpointKey), true, cdn.NewAPIClient) +} From f198a956b3f8ef78a63caa9d2e6d8a0a2b08863f Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 19 Nov 2025 17:42:36 +0100 Subject: [PATCH 02/49] fix(cdn): generate docs --- docs/stackit_config_set.md | 1 + docs/stackit_config_unset.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index 9b07e46b5..70382292f 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -31,6 +31,7 @@ stackit config set [flags] ``` --allowed-url-domain string Domain name, used for the verification of the URLs that are given in the custom identity provider endpoint and "STACKIT curl" command --authorization-custom-endpoint string Authorization API base URL, used in calls to this API + --cdn-custom-endpoint string CDN API base URL, used in calls to this API --dns-custom-endpoint string DNS API base URL, used in calls to this API -h, --help Help for "stackit config set" --iaas-custom-endpoint string IaaS API base URL, used in calls to this API diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index 9d4c83088..0e85f4e9f 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -29,6 +29,7 @@ stackit config unset [flags] --allowed-url-domain Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands. If unset, defaults to stackit.cloud --async Configuration option to run commands asynchronously --authorization-custom-endpoint Authorization API base URL. If unset, uses the default base URL + --cdn-custom-endpoint Custom CDN endpoint URL. If unset, uses the default base URL --dns-custom-endpoint DNS API base URL. If unset, uses the default base URL -h, --help Help for "stackit config unset" --iaas-custom-endpoint IaaS API base URL. If unset, uses the default base URL From d5f430419cc4794efaf43d38afec9fa067cd594e Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 19 Nov 2025 17:55:20 +0100 Subject: [PATCH 03/49] fix(cdn) fix linting issues --- internal/cmd/beta/cdn/distribution/list/list_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/list/list_test.go b/internal/cmd/beta/cdn/distribution/list/list_test.go index 74ee9f654..8ea66a844 100644 --- a/internal/cmd/beta/cdn/distribution/list/list_test.go +++ b/internal/cmd/beta/cdn/distribution/list/list_test.go @@ -7,7 +7,6 @@ import ( "net/http" "net/http/httptest" "testing" - "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -20,11 +19,12 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/cdn" ) +type testCtxKey struct{} + var testProjectId = uuid.NewString() var testClient = &cdn.APIClient{} -var testCtx = context.WithValue(context.Background(), "foo", "foo") +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testNextPageID = "next-page-id-123" -var testTime = time.Now() var testID = "dist-1" var testStatus = cdn.DISTRIBUTIONSTATUS_ACTIVE @@ -317,7 +317,7 @@ func TestFetchDistributions(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { callCount := 0 - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { resp := tt.responses[callCount] callCount++ w.Header().Set("Content-Type", "application/json") From 8c7129303baa0b7cfcaf052119bbbab21d90450d Mon Sep 17 00:00:00 2001 From: cgoetz-inovex Date: Wed, 19 Nov 2025 17:56:05 +0100 Subject: [PATCH 04/49] Update internal/cmd/beta/cdn/distribution/list/list.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruben Hönle --- internal/cmd/beta/cdn/distribution/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/cdn/distribution/list/list.go b/internal/cmd/beta/cdn/distribution/list/list.go index d6b8aa46c..549cab672 100644 --- a/internal/cmd/beta/cdn/distribution/list/list.go +++ b/internal/cmd/beta/cdn/distribution/list/list.go @@ -37,7 +37,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `List all CDN distributions`, - `$ stackit beta dns distribution list`, + `$ stackit beta cdn distribution list`, ), examples.NewExample( `List all CDN distributions sorted by id`, From 82f7d4909c136d14c1381b50afc3e8d3f958be2e Mon Sep 17 00:00:00 2001 From: cgoetz-inovex Date: Wed, 19 Nov 2025 17:56:12 +0100 Subject: [PATCH 05/49] Update internal/cmd/beta/cdn/distribution/list/list.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruben Hönle --- internal/cmd/beta/cdn/distribution/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/beta/cdn/distribution/list/list.go b/internal/cmd/beta/cdn/distribution/list/list.go index 549cab672..3c9cdd1ac 100644 --- a/internal/cmd/beta/cdn/distribution/list/list.go +++ b/internal/cmd/beta/cdn/distribution/list/list.go @@ -41,7 +41,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { ), examples.NewExample( `List all CDN distributions sorted by id`, - `$ stackit beta dns distribution list --sort-by=id`, + `$ stackit beta cdn distribution list --sort-by=id`, ), ), RunE: func(cmd *cobra.Command, args []string) error { From a87cd60540d1f83bea0619cc9a5479574700254c Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Thu, 20 Nov 2025 11:50:03 +0100 Subject: [PATCH 06/49] fix(cdn): rename sortBy params, use EnumSliceToStringSlice --- .../cmd/beta/cdn/distribution/list/list.go | 30 ++++--------------- .../beta/cdn/distribution/list/list_test.go | 24 +++++++++------ 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/list/list.go b/internal/cmd/beta/cdn/distribution/list/list.go index 3c9cdd1ac..22dc326dd 100644 --- a/internal/cmd/beta/cdn/distribution/list/list.go +++ b/internal/cmd/beta/cdn/distribution/list/list.go @@ -16,6 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/cdn" ) @@ -70,11 +71,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -var sortByFlagOptions = []string{"id", "created", "updated", "origin-url", "status"} +var sortByFlagOptions = []string{"id", "createdAt", "updatedAt", "originUrl", "status", "originUrlRelated"} func configureFlags(cmd *cobra.Command) { // same default as apiClient - cmd.Flags().Var(flags.EnumFlag(false, "created", sortByFlagOptions...), sortByFlag, fmt.Sprintf("Sort entries by a specific field, one of %q", sortByFlagOptions)) + cmd.Flags().Var(flags.EnumFlag(false, "createdAt", sortByFlagOptions...), sortByFlag, fmt.Sprintf("Sort entries by a specific field, one of %q", sortByFlagOptions)) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { @@ -94,7 +95,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient, nextPageID cdn.ListDistributionsResponseGetNextPageIdentifierAttributeType) cdn.ApiListDistributionsRequest { req := apiClient.ListDistributions(ctx, model.GlobalFlagModel.ProjectId) - req = req.SortBy(toAPISortBy(model.SortBy)) + req = req.SortBy(model.SortBy) req = req.PageSize(100) if nextPageID != nil { req = req.PageIdentifier(*nextPageID) @@ -102,23 +103,6 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClie return req } -func toAPISortBy(sortBy string) string { - switch sortBy { - case "id": - return "id" - case "created": - return "createdAt" - case "updated": - return "updatedAt" - case "origin-url": - return "originUrl" - case "status": - return "status" - default: - panic("invalid sortBy value, programmer error") - } -} - func outputResult(p *print.Printer, outputFormat string, distributions []cdn.Distribution) error { if distributions == nil { distributions = make([]cdn.Distribution, 0) // otherwise prints null in json output @@ -133,11 +117,7 @@ func outputResult(p *print.Printer, outputFormat string, distributions []cdn.Dis table.SetHeader("ID", "REGIONS", "STATUS") for i := range distributions { d := &distributions[i] - regions := make([]string, 0, len(*d.Config.Regions)) - for _, r := range *d.Config.Regions { - regions = append(regions, string(r)) - } - joinedRegions := strings.Join(regions, ", ") + joinedRegions := strings.Join(sdkUtils.EnumSliceToStringSlice(*d.Config.Regions), ", ") table.AddRow( utils.PtrString(d.Id), joinedRegions, diff --git a/internal/cmd/beta/cdn/distribution/list/list_test.go b/internal/cmd/beta/cdn/distribution/list/list_test.go index 8ea66a844..ca62685d9 100644 --- a/internal/cmd/beta/cdn/distribution/list/list_test.go +++ b/internal/cmd/beta/cdn/distribution/list/list_test.go @@ -60,7 +60,7 @@ func fixtureInputModel(mods ...func(m *inputModel)) *inputModel { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - SortBy: "created", + SortBy: "createdAt", } for _, mod := range mods { mod(m) @@ -123,9 +123,9 @@ func TestParseInput(t *testing.T) { }, { description: "sort by origin-url", - flagValues: fixtureFlagValues(flagSortBy("origin-url")), + flagValues: fixtureFlagValues(flagSortBy("originUrl")), isValid: true, - expected: fixtureInputModel(inputSortBy("origin-url")), + expected: fixtureInputModel(inputSortBy("originUrl")), }, { description: "sort by status", @@ -135,15 +135,21 @@ func TestParseInput(t *testing.T) { }, { description: "sort by created", - flagValues: fixtureFlagValues(flagSortBy("created")), + flagValues: fixtureFlagValues(flagSortBy("createdAt")), isValid: true, - expected: fixtureInputModel(inputSortBy("created")), + expected: fixtureInputModel(inputSortBy("createdAt")), }, { description: "sort by updated", - flagValues: fixtureFlagValues(flagSortBy("updated")), + flagValues: fixtureFlagValues(flagSortBy("updatedAt")), isValid: true, - expected: fixtureInputModel(inputSortBy("updated")), + expected: fixtureInputModel(inputSortBy("updatedAt")), + }, + { + description: "sort by originUrlRelated", + flagValues: fixtureFlagValues(flagSortBy("originUrlRelated")), + isValid: true, + expected: fixtureInputModel(inputSortBy("originUrlRelated")), }, { description: "invalid sort by", @@ -158,7 +164,7 @@ func TestParseInput(t *testing.T) { }, ), isValid: true, - expected: fixtureInputModel(inputSortBy("created")), + expected: fixtureInputModel(inputSortBy("createdAt")), }, } @@ -183,7 +189,7 @@ func TestBuildRequest(t *testing.T) { }, { description: "sort by updatedAt", - inputModel: fixtureInputModel(inputSortBy("updated")), + inputModel: fixtureInputModel(inputSortBy("updatedAt")), expected: fixtureRequest(requestSortBy("updatedAt")), }, { From 42d11961be00fcb4e5eba1890d3e7f90b1c85c7e Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Thu, 20 Nov 2025 16:01:23 +0100 Subject: [PATCH 07/49] fix(cdn): make `testNextPageID`, `testID` and `testStatus` constant --- .../beta/cdn/distribution/list/list_test.go | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/list/list_test.go b/internal/cmd/beta/cdn/distribution/list/list_test.go index ca62685d9..287be0a58 100644 --- a/internal/cmd/beta/cdn/distribution/list/list_test.go +++ b/internal/cmd/beta/cdn/distribution/list/list_test.go @@ -15,6 +15,7 @@ import ( "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/stackitcloud/stackit-cli/internal/pkg/utils" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/cdn" ) @@ -24,9 +25,12 @@ type testCtxKey struct{} var testProjectId = uuid.NewString() var testClient = &cdn.APIClient{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testNextPageID = "next-page-id-123" -var testID = "dist-1" -var testStatus = cdn.DISTRIBUTIONSTATUS_ACTIVE + +const ( + testNextPageID = "next-page-id-123" + testID = "dist-1" + testStatus = cdn.DISTRIBUTIONSTATUS_ACTIVE +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -195,7 +199,7 @@ func TestBuildRequest(t *testing.T) { { description: "with next page id", inputModel: fixtureInputModel(), - nextPageID: &testNextPageID, + nextPageID: utils.Ptr(testNextPageID), expected: fixtureRequest(requestNextPageID(testNextPageID)), }, } @@ -281,7 +285,7 @@ func TestFetchDistributions(t *testing.T) { description: "multiple distributions, multiple pages", responses: []testResponse{ fixtureTestResponse( - responseNextPageID(&testNextPageID), + responseNextPageID(utils.Ptr(testNextPageID)), responseDistributions( fixtureDistribution("dist-1"), ), @@ -310,7 +314,7 @@ func TestFetchDistributions(t *testing.T) { description: "API error on second page", responses: []testResponse{ fixtureTestResponse( - responseNextPageID(&testNextPageID), + responseNextPageID(utils.Ptr(testNextPageID)), responseDistributions( fixtureDistribution("dist-1"), ), @@ -389,14 +393,14 @@ func TestOutputResult(t *testing.T) { outputFormat: "table", distributions: []cdn.Distribution{ { - Id: &testID, + Id: utils.Ptr(testID), Config: &cdn.Config{ Regions: &[]cdn.Region{ cdn.REGION_EU, cdn.REGION_AF, }, }, - Status: &testStatus, + Status: utils.Ptr(testStatus), }, }, expected: ` From 0d7729aedf97906044f06ed6d1e8967e73131388 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Thu, 20 Nov 2025 16:08:25 +0100 Subject: [PATCH 08/49] fix(cdn): add cdn subcommand to beta, generate docs --- docs/stackit_beta.md | 1 + docs/stackit_beta_cdn.md | 34 +++++++++++++++++ docs/stackit_beta_cdn_distribution.md | 34 +++++++++++++++++ docs/stackit_beta_cdn_distribution_list.md | 44 ++++++++++++++++++++++ internal/cmd/beta/beta.go | 2 + 5 files changed, 115 insertions(+) create mode 100644 docs/stackit_beta_cdn.md create mode 100644 docs/stackit_beta_cdn_distribution.md create mode 100644 docs/stackit_beta_cdn_distribution_list.md diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index 23097f8ca..fce0031cb 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -42,6 +42,7 @@ stackit beta [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers +* [stackit beta cdn](./stackit_beta_cdn.md) - Manage CDN resources * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex diff --git a/docs/stackit_beta_cdn.md b/docs/stackit_beta_cdn.md new file mode 100644 index 000000000..b0a99f688 --- /dev/null +++ b/docs/stackit_beta_cdn.md @@ -0,0 +1,34 @@ +## stackit beta cdn + +Manage CDN resources + +### Synopsis + +Manage the lifecycle of CDN resources. + +``` +stackit beta cdn [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn" +``` + +### 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](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution.md b/docs/stackit_beta_cdn_distribution.md new file mode 100644 index 000000000..583780dca --- /dev/null +++ b/docs/stackit_beta_cdn_distribution.md @@ -0,0 +1,34 @@ +## stackit beta cdn distribution + +Manage CDN distributions + +### Synopsis + +Manage the lifecycle of CDN distributions. + +``` +stackit beta cdn distribution [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution" +``` + +### 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 cdn](./stackit_beta_cdn.md) - Manage CDN resources +* [stackit beta cdn distribution list](./stackit_beta_cdn_distribution_list.md) - List CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution_list.md b/docs/stackit_beta_cdn_distribution_list.md new file mode 100644 index 000000000..38873ae02 --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_list.md @@ -0,0 +1,44 @@ +## stackit beta cdn distribution list + +List CDN distributions + +### Synopsis + +List all CDN distributions in your account. + +``` +stackit beta cdn distribution list [flags] +``` + +### Examples + +``` + List all CDN distributions + $ stackit beta cdn distribution list + + List all CDN distributions sorted by id + $ stackit beta cdn distribution list --sort-by=id +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution list" + --sort-by string Sort entries by a specific field, one of ["id" "createdAt" "updatedAt" "originUrl" "status" "originUrlRelated"] (default "createdAt") +``` + +### 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 cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index b026da770..8b50b53ca 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" "github.com/stackitcloud/stackit-cli/internal/cmd/params" @@ -40,4 +41,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(sqlserverflex.NewCmd(params)) cmd.AddCommand(alb.NewCmd(params)) cmd.AddCommand(kms.NewCmd(params)) + cmd.AddCommand(cdn.NewCmd(params)) } From 7e50687cc94be75ba3774f66cd93093efefa27e4 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Mon, 1 Dec 2025 12:55:55 +0100 Subject: [PATCH 09/49] feat(cdn) add limit flag to distribution list --- .../cmd/beta/cdn/distribution/list/list.go | 32 ++++++++++++--- .../beta/cdn/distribution/list/list_test.go | 39 ++++++++++++++++++- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/list/list.go b/internal/cmd/beta/cdn/distribution/list/list.go index 22dc326dd..414a1d11f 100644 --- a/internal/cmd/beta/cdn/distribution/list/list.go +++ b/internal/cmd/beta/cdn/distribution/list/list.go @@ -3,6 +3,7 @@ package list import ( "context" "fmt" + "math" "strings" "github.com/spf13/cobra" @@ -23,10 +24,13 @@ import ( type inputModel struct { *globalflags.GlobalFlagModel SortBy string + Limit *int32 } const ( - sortByFlag = "sort-by" + sortByFlag = "sort-by" + limitFlag = "" + maxPageSize = int32(100) ) func NewCmd(params *params.CmdParams) *cobra.Command { @@ -76,6 +80,7 @@ var sortByFlagOptions = []string{"id", "createdAt", "updatedAt", "originUrl", "s func configureFlags(cmd *cobra.Command) { // same default as apiClient cmd.Flags().Var(flags.EnumFlag(false, "createdAt", sortByFlagOptions...), sortByFlag, fmt.Sprintf("Sort entries by a specific field, one of %q", sortByFlagOptions)) + cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { @@ -84,6 +89,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return nil, &errors.ProjectIdError{} } + limit := flags.FlagToInt32Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + model := inputModel{ GlobalFlagModel: globalFlags, SortBy: flags.FlagWithDefaultToStringValue(p, cmd, sortByFlag), @@ -93,10 +106,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient, nextPageID cdn.ListDistributionsResponseGetNextPageIdentifierAttributeType) cdn.ApiListDistributionsRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient, nextPageID cdn.ListDistributionsResponseGetNextPageIdentifierAttributeType, pageLimit int32) cdn.ApiListDistributionsRequest { req := apiClient.ListDistributions(ctx, model.GlobalFlagModel.ProjectId) req = req.SortBy(model.SortBy) - req = req.PageSize(100) + req = req.PageSize(pageLimit) if nextPageID != nil { req = req.PageIdentifier(*nextPageID) } @@ -135,17 +148,24 @@ func outputResult(p *print.Printer, outputFormat string, distributions []cdn.Dis func fetchDistributions(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) ([]cdn.Distribution, error) { var nextPageID cdn.ListDistributionsResponseGetNextPageIdentifierAttributeType var distributions []cdn.Distribution + received := int32(0) + limit := int32(math.MaxInt32) + if model.Limit != nil { + limit = utils.Min(limit, *model.Limit) + } for { - request := buildRequest(ctx, model, apiClient, nextPageID) + want := utils.Min(maxPageSize, limit-received) + request := buildRequest(ctx, model, apiClient, nextPageID, want) response, err := request.Execute() if err != nil { return nil, fmt.Errorf("list distributions: %w", err) } - nextPageID = response.NextPageIdentifier if response.Distributions != nil { distributions = append(distributions, *response.Distributions...) } - if nextPageID == nil { + nextPageID = response.NextPageIdentifier + received += want + if nextPageID == nil || received >= limit { break } } diff --git a/internal/cmd/beta/cdn/distribution/list/list_test.go b/internal/cmd/beta/cdn/distribution/list/list_test.go index 287be0a58..617e79bc5 100644 --- a/internal/cmd/beta/cdn/distribution/list/list_test.go +++ b/internal/cmd/beta/cdn/distribution/list/list_test.go @@ -4,8 +4,10 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "slices" "testing" "github.com/google/go-cmp/cmp" @@ -205,7 +207,7 @@ func TestBuildRequest(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - req := buildRequest(testCtx, tt.inputModel, testClient, tt.nextPageID) + req := buildRequest(testCtx, tt.inputModel, testClient, tt.nextPageID, maxPageSize) diff := cmp.Diff(req, tt.expected, cmp.AllowUnexported(tt.expected), cmpopts.EquateComparable(testCtx), @@ -256,9 +258,21 @@ func fixtureDistribution(id string) cdn.Distribution { } } +func fixtureDistributions(count int) []cdn.Distribution { + distributions := make([]cdn.Distribution, count) + for i := 0; i < count; i++ { + id := fmt.Sprintf("dist-%d", i+1) + distributions[i] = cdn.Distribution{ + Id: &id, + } + } + return distributions +} + func TestFetchDistributions(t *testing.T) { tests := []struct { description string + limit int responses []testResponse expected []cdn.Distribution fails bool @@ -323,6 +337,20 @@ func TestFetchDistributions(t *testing.T) { }, fails: true, }, + { + description: "limit across 2 pages", + limit: 110, + responses: []testResponse{ + fixtureTestResponse( + responseNextPageID(utils.Ptr(testNextPageID)), + responseDistributions(fixtureDistributions(100)...), + ), + fixtureTestResponse( + responseDistributions(fixtureDistributions(10)...), + ), + }, + expected: slices.Concat(fixtureDistributions(100), fixtureDistributions(10)), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { @@ -350,7 +378,14 @@ func TestFetchDistributions(t *testing.T) { if err != nil { t.Fatalf("failed to create test client: %v", err) } - got, err := fetchDistributions(testCtx, fixtureInputModel(), client) + var mods []func(m *inputModel) + if tt.limit > 0 { + mods = append(mods, func(m *inputModel) { + m.Limit = utils.Ptr(int32(tt.limit)) + }) + } + model := fixtureInputModel(mods...) + got, err := fetchDistributions(testCtx, model, client) if err != nil { if !tt.fails { t.Fatalf("fetchDistributions() unexpected error: %v", err) From 9e049a0cb1a21d103d2c3e41a750cd1f58a7e2cf Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Thu, 20 Nov 2025 15:58:16 +0100 Subject: [PATCH 10/49] feat(cdn): add distribution create cmd --- .../beta/cdn/distribution/create/create.go | 137 +++++++++ .../cdn/distribution/create/create_test.go | 273 ++++++++++++++++++ .../cmd/beta/cdn/distribution/distribution.go | 2 + 3 files changed, 412 insertions(+) create mode 100644 internal/cmd/beta/cdn/distribution/create/create.go create mode 100644 internal/cmd/beta/cdn/distribution/create/create_test.go diff --git a/internal/cmd/beta/cdn/distribution/create/create.go b/internal/cmd/beta/cdn/distribution/create/create.go new file mode 100644 index 000000000..03c472ccc --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/create/create.go @@ -0,0 +1,137 @@ +package create + +import ( + "context" + "fmt" + "net/url" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" +) + +const ( + regionsFlag = "regions" + originURLFlag = "origin-url" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Regions []cdn.Region + OriginURL string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a CDN distribution", + Long: "Create a CDN distribution for a given originUrl in multiple regions.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a distribution for regions EU and AF`, + `$ stackit beta cdn distribution create --regions=EU,AF --origin-url=https://example.com`, + ), + ), + 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 + } + + 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 + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a CDN distribution 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 CDN distribution: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.EnumSliceFlag(false, []string{}, sdkUtils.EnumSliceToStringSlice(cdn.AllowedRegionEnumValues)...), regionsFlag, fmt.Sprintf("Regions in which content should be cached, multiple of: %q", cdn.AllowedRegionEnumValues)) + cmd.Flags().String(originURLFlag, "", "The origin of the content that should be made available through the CDN. Note that the path and query parameters are ignored. Ports are allowed. If no protocol is provided, `https` is assumed. So `www.example.com:1234/somePath?q=123` is normalized to `https://www.example.com:1234`") + err := flags.MarkFlagsRequired(cmd, regionsFlag, originURLFlag) + 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{} + } + + regionStrings := flags.FlagToStringSliceValue(p, cmd, regionsFlag) + regions := make([]cdn.Region, 0, len(regionStrings)) + for _, regionStr := range regionStrings { + regions = append(regions, cdn.Region(regionStr)) + } + + originUrlString := flags.FlagToStringValue(p, cmd, originURLFlag) + _, err := url.Parse(originUrlString) + if err != nil { + return nil, fmt.Errorf("invalid originUrl: '%s' (%w)", originUrlString, err) + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Regions: regions, + OriginURL: originUrlString, + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) cdn.ApiCreateDistributionRequest { + req := apiClient.CreateDistribution(ctx, model.ProjectId) + payload := cdn.NewCreateDistributionPayload( + model.OriginURL, + model.Regions, + ) + return req.CreateDistributionPayload(*payload) +} + +func outputResult(p *print.Printer, outputFormat string, projectLabel string, resp *cdn.CreateDistributionResponse) error { + if resp == nil { + return fmt.Errorf("create distribution response is nil") + } + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created CDN distribution for %q. Id: %s\n", projectLabel, utils.PtrString(resp.Distribution.Id)) + return nil + }) +} diff --git a/internal/cmd/beta/cdn/distribution/create/create_test.go b/internal/cmd/beta/cdn/distribution/create/create_test.go new file mode 100644 index 000000000..eec31c51f --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/create/create_test.go @@ -0,0 +1,273 @@ +package create + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/stackitcloud/stackit-cli/internal/pkg/utils" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" + "k8s.io/utils/ptr" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &cdn.APIClient{} +var testProjectId = uuid.NewString() + +const testOriginURL = "https://example.com/somePath?foo=bar" +const testRegions = cdn.REGION_EU + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + originURLFlag: testOriginURL, + regionsFlag: string(testRegions), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func flagRegions(regions ...cdn.Region) func(flagValues map[string]string) { + return func(flagValues map[string]string) { + if len(regions) == 0 { + delete(flagValues, regionsFlag) + return + } + stringRegions := sdkUtils.EnumSliceToStringSlice(regions) + flagValues[regionsFlag] = strings.Join(stringRegions, ",") + } +} + +func flagOriginURL(originURL string) func(flagValues map[string]string) { + return func(flagValues map[string]string) { + if originURL == "" { + delete(flagValues, originURLFlag) + return + } + flagValues[originURLFlag] = originURL + } +} + +func flagProjectID(id *string) func(flagValues map[string]string) { + return func(flagValues map[string]string) { + if id == nil { + delete(flagValues, projectIdFlag) + return + } + flagValues[projectIdFlag] = *id + } +} + +func fixtureModel(mods ...func(m *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + Regions: []cdn.Region{testRegions}, + OriginURL: testOriginURL, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func modelRegions(regions ...cdn.Region) func(m *inputModel) { + return func(m *inputModel) { + m.Regions = regions + } +} + +func fixturePayload(mods ...func(p *cdn.CreateDistributionPayload)) cdn.CreateDistributionPayload { + p := *cdn.NewCreateDistributionPayload( + testOriginURL, + []cdn.Region{testRegions}, + ) + for _, mod := range mods { + mod(&p) + } + return p +} + +func payloadRegions(regions ...cdn.Region) func(p *cdn.CreateDistributionPayload) { + return func(p *cdn.CreateDistributionPayload) { + p.Regions = ®ions + } +} + +func fixtureRequest(mods ...func(p *cdn.CreateDistributionPayload)) cdn.ApiCreateDistributionRequest { + req := testClient.CreateDistribution(testCtx, testProjectId) + req = req.CreateDistributionPayload(fixturePayload(mods...)) + return req +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expected: fixtureModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(flagProjectID(nil)), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(flagProjectID(utils.Ptr(""))), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(flagProjectID(utils.Ptr("invalid-uuid"))), + isValid: false, + }, + { + description: "origin url missing", + flagValues: fixtureFlagValues(flagOriginURL("")), + isValid: false, + }, + { + description: "origin url invalid", + flagValues: fixtureFlagValues(flagOriginURL("://invalid-url")), + isValid: false, + }, + { + description: "regions missing", + flagValues: fixtureFlagValues(flagRegions()), + isValid: false, + }, + { + description: "multiple regions", + flagValues: fixtureFlagValues(flagRegions(cdn.REGION_EU, cdn.REGION_AF)), + isValid: true, + expected: fixtureModel(modelRegions(cdn.REGION_EU, cdn.REGION_AF)), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected cdn.ApiCreateDistributionRequest + }{ + { + description: "base", + model: fixtureModel(), + expected: fixtureRequest(), + }, + { + description: "multiple regions", + model: fixtureModel(modelRegions(cdn.REGION_AF, cdn.REGION_EU)), + expected: fixtureRequest(payloadRegions(cdn.REGION_AF, cdn.REGION_EU)), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expected, + cmp.AllowUnexported(tt.expected), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFormat string + response *cdn.CreateDistributionResponse + expected string + wantErr bool + }{ + { + description: "nil response", + outputFormat: "table", + response: nil, + wantErr: true, + }, + { + description: "table output", + outputFormat: "table", + response: &cdn.CreateDistributionResponse{ + Distribution: &cdn.Distribution{ + Id: ptr.To("dist-1234"), + }, + }, + expected: fmt.Sprintf("Created CDN distribution for %q. Id: dist-1234\n", testProjectId), + }, + { + description: "json output", + outputFormat: "json", + response: &cdn.CreateDistributionResponse{ + Distribution: &cdn.Distribution{ + Id: ptr.To("dist-1234"), + }, + }, + expected: `{ + "distribution": { + "config": null, + "createdAt": null, + "domains": null, + "id": "dist-1234", + "projectId": null, + "status": null, + "updatedAt": null + } +} +`, + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + buffer := &bytes.Buffer{} + p.Cmd.SetOut(buffer) + if err := outputResult(p, tt.outputFormat, testProjectId, tt.response); (err != nil) != tt.wantErr { + t.Fatalf("outputResult: %v", err) + } + if buffer.String() != tt.expected { + t.Errorf("want:\n%s\ngot:\n%s", tt.expected, buffer.String()) + } + }) + } +} diff --git a/internal/cmd/beta/cdn/distribution/distribution.go b/internal/cmd/beta/cdn/distribution/distribution.go index 72d48c7fe..c98bb705d 100644 --- a/internal/cmd/beta/cdn/distribution/distribution.go +++ b/internal/cmd/beta/cdn/distribution/distribution.go @@ -2,6 +2,7 @@ package distribution import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/list" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -21,4 +22,5 @@ func NewCommand(params *params.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) } From cbf7a9fc4bad28f16f24eaaa6a9b3f14976c98e3 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Thu, 20 Nov 2025 16:10:16 +0100 Subject: [PATCH 11/49] fix(cdn): generate distribution create docs --- docs/stackit_beta_cdn_distribution.md | 1 + docs/stackit_beta_cdn_distribution_create.md | 42 ++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 docs/stackit_beta_cdn_distribution_create.md diff --git a/docs/stackit_beta_cdn_distribution.md b/docs/stackit_beta_cdn_distribution.md index 583780dca..9365bcddd 100644 --- a/docs/stackit_beta_cdn_distribution.md +++ b/docs/stackit_beta_cdn_distribution.md @@ -30,5 +30,6 @@ stackit beta cdn distribution [flags] ### SEE ALSO * [stackit beta cdn](./stackit_beta_cdn.md) - Manage CDN resources +* [stackit beta cdn distribution create](./stackit_beta_cdn_distribution_create.md) - Create a CDN distribution * [stackit beta cdn distribution list](./stackit_beta_cdn_distribution_list.md) - List CDN distributions diff --git a/docs/stackit_beta_cdn_distribution_create.md b/docs/stackit_beta_cdn_distribution_create.md new file mode 100644 index 000000000..6c185bc3d --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_create.md @@ -0,0 +1,42 @@ +## stackit beta cdn distribution create + +Create a CDN distribution + +### Synopsis + +Create a CDN distribution for a given originUrl in multiple regions. + +``` +stackit beta cdn distribution create [flags] +``` + +### Examples + +``` + Create a distribution for regions EU and AF + $ stackit beta cdn distribution create --regions=EU,AF --origin-url=https://example.com +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution create" + --origin-url https The origin of the content that should be made available through the CDN. Note that the path and query parameters are ignored. Ports are allowed. If no protocol is provided, https is assumed. So `www.example.com:1234/somePath?q=123` is normalized to `https://www.example.com:1234` + --regions strings Regions in which content should be cached, multiple of: ["EU" "US" "AF" "SA" "ASIA"] (default []) +``` + +### 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 cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + From 7296ee033ff782129e52f7a9c1c6605494384fc6 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Fri, 21 Nov 2025 10:40:03 +0100 Subject: [PATCH 12/49] squash this --- .../beta/cdn/distribution/delete/delete.go | 42 +++++++++++++++++ .../cdn/distribution/describe/describe.go | 43 +++++++++++++++++ .../cmd/beta/cdn/distribution/distribution.go | 6 +++ .../beta/cdn/distribution/update/update.go | 46 +++++++++++++++++++ internal/cmd/beta/cdn/domain/create/create.go | 37 +++++++++++++++ .../cmd/beta/cdn/domain/describe/describe.go | 1 + internal/cmd/beta/cdn/domain/domain.go | 23 ++++++++++ 7 files changed, 198 insertions(+) create mode 100644 internal/cmd/beta/cdn/distribution/delete/delete.go create mode 100644 internal/cmd/beta/cdn/distribution/describe/describe.go create mode 100644 internal/cmd/beta/cdn/distribution/update/update.go create mode 100644 internal/cmd/beta/cdn/domain/create/create.go create mode 100644 internal/cmd/beta/cdn/domain/describe/describe.go create mode 100644 internal/cmd/beta/cdn/domain/domain.go diff --git a/internal/cmd/beta/cdn/distribution/delete/delete.go b/internal/cmd/beta/cdn/distribution/delete/delete.go new file mode 100644 index 000000000..b9c27174c --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/delete/delete.go @@ -0,0 +1,42 @@ +package delete + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +const distributionIDFlag = "distribution-id" + +type inputModel struct { + *globalflags.GlobalFlagModel + DistributionID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a CDN distribution", + Long: "Delete a CDN distribution by its ID.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete a CDN distribution with ID "xxx"`, + `$ stackit beta cdn distribution delete --distribution-id xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + + // TODO + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + //TODO +} diff --git a/internal/cmd/beta/cdn/distribution/describe/describe.go b/internal/cmd/beta/cdn/distribution/describe/describe.go new file mode 100644 index 000000000..aeedfe3ce --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/describe/describe.go @@ -0,0 +1,43 @@ +package describe + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +const distributionIDFlag = "distribution-id" + +type inputModel struct { + *globalflags.GlobalFlagModel + DistributionID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describe a CDN distribution", + Long: "Describe a CDN distribution by its ID.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Get details of a CDN distribution with ID "xxx"`, + `$ stackit beta cdn distribution describe --distribution-id xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + // TODO + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(distributionIDFlag, "", "The ID of the CDN distribution to describe") + err := cmd.MarkFlagRequired(distributionIDFlag) + cobra.CheckErr(err) +} diff --git a/internal/cmd/beta/cdn/distribution/distribution.go b/internal/cmd/beta/cdn/distribution/distribution.go index c98bb705d..12c4ffedd 100644 --- a/internal/cmd/beta/cdn/distribution/distribution.go +++ b/internal/cmd/beta/cdn/distribution/distribution.go @@ -2,6 +2,9 @@ package distribution import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/update" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/list" "github.com/stackitcloud/stackit-cli/internal/cmd/params" @@ -22,5 +25,8 @@ func NewCommand(params *params.CmdParams) *cobra.Command { func addSubcommands(cmd *cobra.Command, params *params.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/cmd/beta/cdn/distribution/update/update.go b/internal/cmd/beta/cdn/distribution/update/update.go new file mode 100644 index 000000000..df2eaa254 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/update/update.go @@ -0,0 +1,46 @@ +package update + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" +) + +const ( + distributionIDFlag = "distribution-id" + regionsFlag = "regions" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + DistributionID string + Regions []cdn.Region +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Update a CDN distribution", + Long: "Update a CDN distribution by its ID, allowing replacement of its regions.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Update a CDN distribution with ID "xxx" to be cached in reions "EU" and "AF"`, + `$ stackit beta cdn distribution update --distribution-id xxx --regions EU,AF`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + // TODO + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + //TODO +} diff --git a/internal/cmd/beta/cdn/domain/create/create.go b/internal/cmd/beta/cdn/domain/create/create.go new file mode 100644 index 000000000..ca0335673 --- /dev/null +++ b/internal/cmd/beta/cdn/domain/create/create.go @@ -0,0 +1,37 @@ +package create + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" +) + +const ( + flagDistributionID = "distribution-id" + flagName = "name" +) + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a CDN domain", + Long: "Create a new CDN domain associated with a CDN distribution.", + Args: cobra.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a CDN domain named "example.com" for distribution with ID "xxx"`, + `$ stackit beta cdn domain create --name example.com --distribution-id xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + // TODO + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + // TODO +} diff --git a/internal/cmd/beta/cdn/domain/describe/describe.go b/internal/cmd/beta/cdn/domain/describe/describe.go new file mode 100644 index 000000000..cce268ec4 --- /dev/null +++ b/internal/cmd/beta/cdn/domain/describe/describe.go @@ -0,0 +1 @@ +package describe diff --git a/internal/cmd/beta/cdn/domain/domain.go b/internal/cmd/beta/cdn/domain/domain.go new file mode 100644 index 000000000..5e1ae37ff --- /dev/null +++ b/internal/cmd/beta/cdn/domain/domain.go @@ -0,0 +1,23 @@ +package domain + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCommand(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "domain", + Short: "Manage CDN domains", + Long: "Manage the lifecycle of CDN domains.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { + +} From 1235792bb8eda78460bdf5c349807bfc93cf3a0e Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Mon, 1 Dec 2025 10:35:11 +0100 Subject: [PATCH 13/49] feat(cdn) implement cdn distribution create/delete/describe/update --- go.mod | 5 +- go.sum | 4 +- .../beta/cdn/distribution/create/create.go | 272 ++++++++++-- .../cdn/distribution/create/create_test.go | 395 ++++++++++++++--- .../beta/cdn/distribution/delete/delete.go | 66 ++- .../cdn/distribution/delete/delete_test.go | 130 ++++++ .../cdn/distribution/describe/describe.go | 187 +++++++- .../distribution/describe/describe_test.go | 406 ++++++++++++++++++ .../cmd/beta/cdn/distribution/distribution.go | 6 +- .../beta/cdn/distribution/update/update.go | 353 ++++++++++++++- .../cdn/distribution/update/update_test.go | 366 ++++++++++++++++ internal/pkg/flags/flag_to_value.go | 28 ++ internal/pkg/utils/strings.go | 13 + internal/pkg/utils/utils.go | 8 + 14 files changed, 2115 insertions(+), 124 deletions(-) create mode 100644 internal/cmd/beta/cdn/distribution/delete/delete_test.go create mode 100644 internal/cmd/beta/cdn/distribution/describe/describe_test.go create mode 100644 internal/cmd/beta/cdn/distribution/update/update_test.go diff --git a/go.mod b/go.mod index 8918edfbe..d01af7966 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/core v0.20.0 github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 - github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 + github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0 @@ -36,6 +36,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/ske v1.4.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 github.com/zalando/go-keyring v0.2.6 + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/mod v0.30.0 golang.org/x/oauth2 v0.33.0 golang.org/x/term v0.37.0 @@ -258,7 +259,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index ea8aa0408..9b1277165 100644 --- a/go.sum +++ b/go.sum @@ -567,8 +567,8 @@ github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1 h1:DaJkEN/6l+AJEQ3Dr+ github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1/go.mod h1:SzA+UsSNv4D9IvNT7hwYPewgAvUgj5WXIU2tZ0XaMBI= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw= -github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0 h1:Q+qIdejeMsYMkbtVoI9BpGlKGdSVFRBhH/zj44SP8TM= -github.com/stackitcloud/stackit-sdk-go/services/cdn v1.6.0/go.mod h1:YGadfhuy8yoseczTxF7vN4t9ES2WxGQr0Pug14ii7y4= +github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 h1:CiOlfCsCDwHP0kas7qyhfp5XtL2kVmn9e4wjtc3LO10= +github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1/go.mod h1:PyZ6g9JsGZZyeISAF+5E7L1lAlMnmbl2YbPj5Teu8to= github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 h1:CnhAMLql0MNmAeq4roQKN8OpSKX4FSgTU6Eu6detB4I= github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1/go.mod h1:7Bx85knfNSBxulPdJUFuBePXNee3cO+sOTYnUG6M+iQ= github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 h1:RgWfaWDY8ZGZp5gEBe/A1r7s5NCRuLiYuHhscH6Ej9U= diff --git a/internal/cmd/beta/cdn/distribution/create/create.go b/internal/cmd/beta/cdn/distribution/create/create.go index 03c472ccc..32cf99b10 100644 --- a/internal/cmd/beta/cdn/distribution/create/create.go +++ b/internal/cmd/beta/cdn/distribution/create/create.go @@ -3,7 +3,7 @@ package create import ( "context" "fmt" - "net/url" + "strings" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" @@ -21,34 +21,101 @@ import ( ) const ( - regionsFlag = "regions" - originURLFlag = "origin-url" + flagRegion = "regions" + flagHTTP = "http" + flagHTTPOriginURL = "http-origin-url" + flagHTTPGeofencing = "http-geofencing" + flagHTTPOriginRequestHeaders = "http-origin-request-header" + flagBucket = "bucket" + flagBucketURL = "bucket-url" + flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" + flagBucketRegion = "bucket-region" + flagBlockedCountries = "blocked-countries" + flagBlockedIPs = "blocked-ips" + flagDefaultCacheDuration = "default-cache-duration" + flagLoki = "loki" + flagLokiUsername = "loki-username" + flagLokiPushURL = "loki-push-url" + flagMonthlyLimitBytes = "monthly-limit-bytes" + flagOptimizer = "optimizer" ) +type httpInputModel struct { + OriginURL string + Geofencing *map[string][]string + OriginRequestHeaders *map[string]string +} + +type bucketInputModel struct { + URL string + AccessKeyID string + Password string + Region string +} + +type lokiInputModel struct { + Username string + Password string + PushURL string +} + type inputModel struct { *globalflags.GlobalFlagModel - Regions []cdn.Region - OriginURL string + Regions []cdn.Region + HTTP *httpInputModel + Bucket *bucketInputModel + BlockedCountries []string + BlockedIPs []string + DefaultCacheDuration string + MonthlyLimitBytes *int64 + Loki *lokiInputModel + Optimizer bool } func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: "create", - Short: "Create a CDN distribution", - Long: "Create a CDN distribution for a given originUrl in multiple regions.", - Args: args.NoArgs, + Use: "create", + Short: "Create a CDN distribution", + Long: "Create a CDN distribution for a given originUrl in multiple regions.", + Args: args.NoArgs, Example: examples.Build( - examples.NewExample( - `Create a distribution for regions EU and AF`, - `$ stackit beta cdn distribution create --regions=EU,AF --origin-url=https://example.com`, - ), + //TODO ), + PreRun: func(cmd *cobra.Command, args []string) { + // either flagHTTP or flagBucket must be set, depending on which we mark other flags as required + if flags.FlagToBoolValue(params.Printer, cmd, flagHTTP) { + err := cmd.MarkFlagRequired(flagHTTPOriginURL) + cobra.CheckErr(err) + } else { + err := flags.MarkFlagsRequired(cmd, flagBucketURL, flagBucketCredentialsAccessKeyID, flagBucketRegion) + cobra.CheckErr(err) + } + // if user uses loki, mark related flags as required + if flags.FlagToBoolValue(params.Printer, cmd, flagLoki) { + err := flags.MarkFlagsRequired(cmd, flagLokiUsername, flagLokiPushURL) + cobra.CheckErr(err) + } + }, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } + if model.Bucket != nil { + pw, err := params.Printer.PromptForPassword("enter your secret access key for the object storage bucket: ") + if err != nil { + return fmt.Errorf("reading secret access key: %w", err) + } + model.Bucket.Password = pw + } + if model.Loki != nil { + pw, err := params.Printer.PromptForPassword("enter your password for the loki log sink: ") + if err != nil { + return fmt.Errorf("reading loki password: %w", err) + } + model.Loki.Password = pw + } apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { @@ -84,9 +151,26 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().Var(flags.EnumSliceFlag(false, []string{}, sdkUtils.EnumSliceToStringSlice(cdn.AllowedRegionEnumValues)...), regionsFlag, fmt.Sprintf("Regions in which content should be cached, multiple of: %q", cdn.AllowedRegionEnumValues)) - cmd.Flags().String(originURLFlag, "", "The origin of the content that should be made available through the CDN. Note that the path and query parameters are ignored. Ports are allowed. If no protocol is provided, `https` is assumed. So `www.example.com:1234/somePath?q=123` is normalized to `https://www.example.com:1234`") - err := flags.MarkFlagsRequired(cmd, regionsFlag, originURLFlag) + cmd.Flags().Var(flags.EnumSliceFlag(false, []string{}, sdkUtils.EnumSliceToStringSlice(cdn.AllowedRegionEnumValues)...), flagRegion, fmt.Sprintf("Regions in which content should be cached, multiple of: %q", cdn.AllowedRegionEnumValues)) + cmd.Flags().Bool(flagHTTP, false, "Use HTTP backend") + cmd.Flags().String(flagHTTPOriginURL, "", "Origin URL for HTTP backend") + cmd.Flags().StringSlice(flagHTTPOriginRequestHeaders, []string{}, "Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers!") + cmd.Flags().StringArray(flagHTTPGeofencing, []string{}, "Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable.") + cmd.Flags().Bool(flagBucket, false, "Use Object Storage backend") + cmd.Flags().String(flagBucketURL, "", "Bucket URL for Object Storage backend") + cmd.Flags().String(flagBucketCredentialsAccessKeyID, "", "Access Key ID for Object Storage backend") + cmd.Flags().String(flagBucketRegion, "", "Region for Object Storage backend") + cmd.Flags().StringSlice(flagBlockedCountries, []string{}, "Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR')") + cmd.Flags().StringSlice(flagBlockedIPs, []string{}, "Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')") + cmd.Flags().String(flagDefaultCacheDuration, "", "ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)") + cmd.Flags().Bool(flagLoki, false, "Enable Loki log sink for the CDN distribution") + cmd.Flags().String(flagLokiUsername, "", "Username for log sink") + cmd.Flags().String(flagLokiPushURL, "", "Push URL for log sink") + cmd.Flags().Int64(flagMonthlyLimitBytes, 0, "Monthly limit in bytes for the CDN distribution") + cmd.Flags().Bool(flagOptimizer, false, "Enable optimizer for the CDN distribution (paid feature).") + cmd.MarkFlagsMutuallyExclusive(flagHTTP, flagBucket) + cmd.MarkFlagsOneRequired(flagHTTP, flagBucket) + err := flags.MarkFlagsRequired(cmd, flagRegion) cobra.CheckErr(err) } @@ -96,33 +180,171 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return nil, &errors.ProjectIdError{} } - regionStrings := flags.FlagToStringSliceValue(p, cmd, regionsFlag) + regionStrings := flags.FlagToStringSliceValue(p, cmd, flagRegion) regions := make([]cdn.Region, 0, len(regionStrings)) for _, regionStr := range regionStrings { regions = append(regions, cdn.Region(regionStr)) } - originUrlString := flags.FlagToStringValue(p, cmd, originURLFlag) - _, err := url.Parse(originUrlString) - if err != nil { - return nil, fmt.Errorf("invalid originUrl: '%s' (%w)", originUrlString, err) + var http *httpInputModel + if flags.FlagToBoolValue(p, cmd, flagHTTP) { + originURL := flags.FlagToStringValue(p, cmd, flagHTTPOriginURL) + + var geofencing *map[string][]string + geofencingInput := flags.FlagToStringArrayValue(p, cmd, flagHTTPGeofencing) + if geofencingInput != nil { + geofencing = parseGeofencing(p, geofencingInput) + } + + var originRequestHeaders *map[string]string + originRequestHeadersInput := flags.FlagToStringSliceValue(p, cmd, flagHTTPOriginRequestHeaders) + if originRequestHeadersInput != nil { + originRequestHeaders = parseOriginRequestHeaders(p, originRequestHeadersInput) + } + + http = &httpInputModel{ + OriginURL: originURL, + Geofencing: geofencing, + OriginRequestHeaders: originRequestHeaders, + } + } + + var bucket *bucketInputModel + if flags.FlagToBoolValue(p, cmd, flagBucket) { + bucketURL := flags.FlagToStringValue(p, cmd, flagBucketURL) + accessKeyID := flags.FlagToStringValue(p, cmd, flagBucketCredentialsAccessKeyID) + region := flags.FlagToStringValue(p, cmd, flagBucketRegion) + + bucket = &bucketInputModel{ + URL: bucketURL, + AccessKeyID: accessKeyID, + Password: "", + Region: region, + } + } + + blockedCountries := flags.FlagToStringSliceValue(p, cmd, flagBlockedCountries) + blockedIPs := flags.FlagToStringSliceValue(p, cmd, flagBlockedIPs) + cacheDuration := flags.FlagToStringValue(p, cmd, flagDefaultCacheDuration) + monthlyLimit := flags.FlagToInt64Pointer(p, cmd, flagMonthlyLimitBytes) + + var loki *lokiInputModel + if flags.FlagToBoolValue(p, cmd, flagLoki) { + loki = &lokiInputModel{ + Username: flags.FlagToStringValue(p, cmd, flagLokiUsername), + PushURL: flags.FlagToStringValue(p, cmd, flagLokiPushURL), + Password: "", + } } + optimizer := flags.FlagToBoolValue(p, cmd, flagOptimizer) + model := inputModel{ - GlobalFlagModel: globalFlags, - Regions: regions, - OriginURL: originUrlString, + GlobalFlagModel: globalFlags, + Regions: regions, + HTTP: http, + Bucket: bucket, + BlockedCountries: blockedCountries, + BlockedIPs: blockedIPs, + DefaultCacheDuration: cacheDuration, + MonthlyLimitBytes: monthlyLimit, + Loki: loki, + Optimizer: optimizer, } return &model, nil } +func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]string { + geofencing := make(map[string][]string) + for _, in := range geofencingInput { + firstSpace := strings.IndexRune(in, ' ') + if firstSpace == -1 { + p.Debug(print.ErrorLevel, "invalid geofencing entry (no space found): %q", in) + continue + } + urlPart := in[:firstSpace] + countriesPart := in[firstSpace+1:] + geofencing[urlPart] = nil + countries := strings.Split(countriesPart, ",") + for _, country := range countries { + country = strings.TrimSpace(country) + geofencing[urlPart] = append(geofencing[urlPart], country) + } + } + return &geofencing +} + +func parseOriginRequestHeaders(p *print.Printer, originRequestHeadersInput []string) *map[string]string { + originRequestHeaders := make(map[string]string) + for _, in := range originRequestHeadersInput { + parts := strings.Split(in, ":") + if len(parts) != 2 { + p.Debug(print.ErrorLevel, "invalid origin request header entry (no colon found): %q", in) + continue + } + originRequestHeaders[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + return &originRequestHeaders +} + func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) cdn.ApiCreateDistributionRequest { req := apiClient.CreateDistribution(ctx, model.ProjectId) + var backend cdn.CreateDistributionPayloadGetBackendArgType + if model.HTTP != nil { + backend = cdn.CreateDistributionPayloadGetBackendArgType{ + HttpBackendCreate: &cdn.HttpBackendCreate{ + Geofencing: model.HTTP.Geofencing, + OriginRequestHeaders: model.HTTP.OriginRequestHeaders, + OriginUrl: &model.HTTP.OriginURL, + Type: utils.Ptr("http"), + }, + } + } else { + backend = cdn.CreateDistributionPayloadGetBackendArgType{ + BucketBackendCreate: &cdn.BucketBackendCreate{ + BucketUrl: &model.Bucket.URL, + Credentials: cdn.NewBucketCredentials( + model.Bucket.AccessKeyID, + model.Bucket.Password, + ), + Region: &model.Bucket.Region, + Type: utils.Ptr("bucket"), + }, + } + } + payload := cdn.NewCreateDistributionPayload( - model.OriginURL, + backend, model.Regions, ) + if len(model.BlockedCountries) > 0 { + payload.BlockedCountries = &model.BlockedCountries + } + if len(model.BlockedIPs) > 0 { + payload.BlockedIps = &model.BlockedIPs + } + if model.DefaultCacheDuration != "" { + payload.DefaultCacheDuration = utils.Ptr(model.DefaultCacheDuration) + } + if model.Loki != nil { + payload.LogSink = &cdn.CreateDistributionPayloadGetLogSinkArgType{ + LokiLogSinkCreate: &cdn.LokiLogSinkCreate{ + Credentials: &cdn.LokiLogSinkCredentials{ + Password: &model.Loki.Password, + Username: &model.Loki.Username, + }, + PushUrl: &model.Loki.PushURL, + Type: utils.Ptr("loki"), + }, + } + } + payload.MonthlyLimitBytes = model.MonthlyLimitBytes + if model.Optimizer { + payload.Optimizer = &cdn.CreateDistributionPayloadGetOptimizerArgType{ + Enabled: utils.Ptr(true), + } + } return req.CreateDistributionPayload(*payload) } diff --git a/internal/cmd/beta/cdn/distribution/create/create_test.go b/internal/cmd/beta/cdn/distribution/create/create_test.go index eec31c51f..8f78edd49 100644 --- a/internal/cmd/beta/cdn/distribution/create/create_test.go +++ b/internal/cmd/beta/cdn/distribution/create/create_test.go @@ -28,49 +28,54 @@ var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &cdn.APIClient{} var testProjectId = uuid.NewString() -const testOriginURL = "https://example.com/somePath?foo=bar" const testRegions = cdn.REGION_EU func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ projectIdFlag: testProjectId, - originURLFlag: testOriginURL, - regionsFlag: string(testRegions), + flagRegion: string(testRegions), } + flagsHTTPBackend()(flagValues) for _, mod := range mods { mod(flagValues) } return flagValues } -func flagRegions(regions ...cdn.Region) func(flagValues map[string]string) { - return func(flagValues map[string]string) { - if len(regions) == 0 { - delete(flagValues, regionsFlag) - return - } - stringRegions := sdkUtils.EnumSliceToStringSlice(regions) - flagValues[regionsFlag] = strings.Join(stringRegions, ",") +func flagsHTTPBackend() func(m map[string]string) { + return func(m map[string]string) { + delete(m, flagBucket) + m[flagHTTP] = "true" + m[flagHTTPOriginURL] = "https://http-backend.example.com" } } -func flagOriginURL(originURL string) func(flagValues map[string]string) { - return func(flagValues map[string]string) { - if originURL == "" { - delete(flagValues, originURLFlag) - return - } - flagValues[originURLFlag] = originURL +func flagsBucketBackend() func(m map[string]string) { + return func(m map[string]string) { + delete(m, flagHTTP) + m[flagBucket] = "true" + m[flagBucketURL] = "https://bucket-backend.example.com" + m[flagBucketCredentialsAccessKeyID] = "access-key-id" + m[flagBucketRegion] = "eu" + } +} + +func flagsLoki() func(m map[string]string) { + return func(m map[string]string) { + m[flagLoki] = "true" + m[flagLokiPushURL] = "https://loki.example.com" + m[flagLokiUsername] = "loki-user" } } -func flagProjectID(id *string) func(flagValues map[string]string) { +func flagRegions(regions ...cdn.Region) func(flagValues map[string]string) { return func(flagValues map[string]string) { - if id == nil { - delete(flagValues, projectIdFlag) + if len(regions) == 0 { + delete(flagValues, flagRegion) return } - flagValues[projectIdFlag] = *id + stringRegions := sdkUtils.EnumSliceToStringSlice(regions) + flagValues[flagRegion] = strings.Join(stringRegions, ",") } } @@ -80,9 +85,9 @@ func fixtureModel(mods ...func(m *inputModel)) *inputModel { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - Regions: []cdn.Region{testRegions}, - OriginURL: testOriginURL, + Regions: []cdn.Region{testRegions}, } + modelHTTPBackend()(model) for _, mod := range mods { mod(model) } @@ -95,9 +100,43 @@ func modelRegions(regions ...cdn.Region) func(m *inputModel) { } } +func modelHTTPBackend() func(m *inputModel) { + return func(m *inputModel) { + m.Bucket = nil + m.HTTP = &httpInputModel{ + OriginURL: "https://http-backend.example.com", + } + } +} + +func modelBucketBackend() func(m *inputModel) { + return func(m *inputModel) { + m.HTTP = nil + m.Bucket = &bucketInputModel{ + URL: "https://bucket-backend.example.com", + AccessKeyID: "access-key-id", + Region: "eu", + } + } +} + +func modelLoki() func(m *inputModel) { + return func(m *inputModel) { + m.Loki = &lokiInputModel{ + PushURL: "https://loki.example.com", + Username: "loki-user", + } + } +} + func fixturePayload(mods ...func(p *cdn.CreateDistributionPayload)) cdn.CreateDistributionPayload { p := *cdn.NewCreateDistributionPayload( - testOriginURL, + cdn.CreateDistributionPayloadGetBackendArgType{ + HttpBackendCreate: &cdn.HttpBackendCreate{ + Type: utils.Ptr("http"), + OriginUrl: utils.Ptr("https://http-backend.example.com"), + }, + }, []cdn.Region{testRegions}, ) for _, mod := range mods { @@ -112,6 +151,34 @@ func payloadRegions(regions ...cdn.Region) func(p *cdn.CreateDistributionPayload } } +func payloadBucketBackend() func(p *cdn.CreateDistributionPayload) { + return func(p *cdn.CreateDistributionPayload) { + p.Backend = &cdn.CreateDistributionPayloadGetBackendArgType{ + BucketBackendCreate: &cdn.BucketBackendCreate{ + Type: utils.Ptr("bucket"), + BucketUrl: utils.Ptr("https://bucket-backend.example.com"), + Region: utils.Ptr("eu"), + Credentials: cdn.NewBucketCredentials( + "access-key-id", + "", + ), + }, + } + } +} + +func payloadLoki() func(p *cdn.CreateDistributionPayload) { + return func(p *cdn.CreateDistributionPayload) { + p.LogSink = &cdn.CreateDistributionPayloadGetLogSinkArgType{ + LokiLogSinkCreate: &cdn.LokiLogSinkCreate{ + Type: utils.Ptr("loki"), + PushUrl: utils.Ptr("https://loki.example.com"), + Credentials: cdn.NewLokiLogSinkCredentials("", "loki-user"), + }, + } + } +} + func fixtureRequest(mods ...func(p *cdn.CreateDistributionPayload)) cdn.ApiCreateDistributionRequest { req := testClient.CreateDistribution(testCtx, testProjectId) req = req.CreateDistributionPayload(fixturePayload(mods...)) @@ -134,28 +201,24 @@ func TestParseInput(t *testing.T) { }, { description: "project id missing", - flagValues: fixtureFlagValues(flagProjectID(nil)), - isValid: false, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, projectIdFlag) + }), + isValid: false, }, { description: "project id invalid 1", - flagValues: fixtureFlagValues(flagProjectID(utils.Ptr(""))), - isValid: false, + flagValues: fixtureFlagValues(func(m map[string]string) { + m[projectIdFlag] = "" + }), + isValid: false, }, { description: "project id invalid 2", - flagValues: fixtureFlagValues(flagProjectID(utils.Ptr("invalid-uuid"))), - isValid: false, - }, - { - description: "origin url missing", - flagValues: fixtureFlagValues(flagOriginURL("")), - isValid: false, - }, - { - description: "origin url invalid", - flagValues: fixtureFlagValues(flagOriginURL("://invalid-url")), - isValid: false, + flagValues: fixtureFlagValues(func(m map[string]string) { + m[projectIdFlag] = "invalid-uuid" + }), + isValid: false, }, { description: "regions missing", @@ -168,6 +231,175 @@ func TestParseInput(t *testing.T) { isValid: true, expected: fixtureModel(modelRegions(cdn.REGION_EU, cdn.REGION_AF)), }, + { + description: "bucket backend", + flagValues: fixtureFlagValues(flagsBucketBackend()), + isValid: true, + expected: fixtureModel(modelBucketBackend()), + }, + { + description: "bucket backend missing url", + flagValues: fixtureFlagValues( + flagsBucketBackend(), + func(m map[string]string) { + delete(m, flagBucketURL) + }, + ), + isValid: false, + }, + { + description: "bucket backend missing access key id", + flagValues: fixtureFlagValues( + flagsBucketBackend(), + func(m map[string]string) { + delete(m, flagBucketCredentialsAccessKeyID) + }, + ), + isValid: false, + }, + { + description: "bucket backend missing region", + flagValues: fixtureFlagValues( + flagsBucketBackend(), + func(m map[string]string) { + delete(m, flagBucketRegion) + }, + ), + isValid: false, + }, + { + description: "http backend missing url", + flagValues: fixtureFlagValues( + func(m map[string]string) { + delete(m, flagHTTPOriginURL) + }, + ), + isValid: false, + }, + { + description: "http backend with geofencing", + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagHTTPGeofencing] = "https://dach.example.com DE,AT,CH" + }, + ), + isValid: true, + expected: fixtureModel( + func(m *inputModel) { + m.HTTP.Geofencing = &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + } + }, + ), + }, + { + description: "http backend with origin request headers", + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagHTTPOriginRequestHeaders] = "X-Custom-Header:Value1,X-Another-Header:Value2" + }, + ), + isValid: true, + expected: fixtureModel( + func(m *inputModel) { + m.HTTP.OriginRequestHeaders = &map[string]string{ + "X-Custom-Header": "Value1", + "X-Another-Header": "Value2", + } + }, + ), + }, + { + description: "with blocked countries", + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagBlockedCountries] = "DE,AT" + }), + isValid: true, + expected: fixtureModel( + func(m *inputModel) { + m.BlockedCountries = []string{"DE", "AT"} + }, + ), + }, + { + description: "with blocked IPs", + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagBlockedIPs] = "127.0.0.1,10.0.0.8" + }), + isValid: true, + expected: fixtureModel( + func(m *inputModel) { + m.BlockedIPs = []string{"127.0.0.1", "10.0.0.8"} + }), + }, + { + description: "with default cache duration", + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagDefaultCacheDuration] = "PT1H30M" + }), + isValid: true, + expected: fixtureModel( + func(m *inputModel) { + m.DefaultCacheDuration = "PT1H30M" + }), + }, + { + description: "with optimizer", + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagOptimizer] = "true" + }), + isValid: true, + expected: fixtureModel( + func(m *inputModel) { + m.Optimizer = true + }), + }, + { + description: "with loki", + flagValues: fixtureFlagValues( + flagsLoki(), + ), + isValid: true, + expected: fixtureModel( + modelLoki(), + ), + }, + { + description: "loki with missing username", + flagValues: fixtureFlagValues( + flagsLoki(), + func(m map[string]string) { + delete(m, flagLokiUsername) + }, + ), + isValid: false, + }, + { + description: "loki with missing push url", + flagValues: fixtureFlagValues( + flagsLoki(), + func(m map[string]string) { + delete(m, flagLokiPushURL) + }, + ), + isValid: false, + }, + { + description: "with monthly limit bytes", + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagMonthlyLimitBytes] = "1073741824" // 1 GiB + }), + isValid: true, + expected: fixtureModel( + func(m *inputModel) { + m.MonthlyLimitBytes = ptr.To[int64](1073741824) + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { @@ -192,6 +424,66 @@ func TestBuildRequest(t *testing.T) { model: fixtureModel(modelRegions(cdn.REGION_AF, cdn.REGION_EU)), expected: fixtureRequest(payloadRegions(cdn.REGION_AF, cdn.REGION_EU)), }, + { + description: "bucket backend", + model: fixtureModel(modelBucketBackend()), + expected: fixtureRequest(payloadBucketBackend()), + }, + { + description: "http backend with geofencing and origin request headers", + model: fixtureModel( + func(m *inputModel) { + m.HTTP.Geofencing = &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + } + m.HTTP.OriginRequestHeaders = &map[string]string{ + "X-Custom-Header": "Value1", + "X-Another-Header": "Value2", + } + }, + ), + expected: fixtureRequest( + func(p *cdn.CreateDistributionPayload) { + p.Backend.HttpBackendCreate.Geofencing = &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + } + p.Backend.HttpBackendCreate.OriginRequestHeaders = &map[string]string{ + "X-Custom-Header": "Value1", + "X-Another-Header": "Value2", + } + }, + ), + }, + { + description: "with full options", + model: fixtureModel( + func(m *inputModel) { + m.MonthlyLimitBytes = ptr.To[int64](5368709120) // 5 GiB + m.Optimizer = true + m.BlockedCountries = []string{"DE", "AT"} + m.BlockedIPs = []string{"127.0.0.1"} + m.DefaultCacheDuration = "PT2H" + }, + ), + expected: fixtureRequest( + func(p *cdn.CreateDistributionPayload) { + p.MonthlyLimitBytes = utils.Ptr[int64](5368709120) + p.Optimizer = &cdn.CreateDistributionPayloadGetOptimizerArgType{ + Enabled: utils.Ptr(true), + } + p.BlockedCountries = &[]string{"DE", "AT"} + p.BlockedIps = &[]string{"127.0.0.1"} + p.DefaultCacheDuration = utils.Ptr("PT2H") + }, + ), + }, + { + description: "loki", + model: fixtureModel( + modelLoki(), + ), + expected: fixtureRequest(payloadLoki()), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { @@ -232,27 +524,6 @@ func TestOutputResult(t *testing.T) { }, expected: fmt.Sprintf("Created CDN distribution for %q. Id: dist-1234\n", testProjectId), }, - { - description: "json output", - outputFormat: "json", - response: &cdn.CreateDistributionResponse{ - Distribution: &cdn.Distribution{ - Id: ptr.To("dist-1234"), - }, - }, - expected: `{ - "distribution": { - "config": null, - "createdAt": null, - "domains": null, - "id": "dist-1234", - "projectId": null, - "status": null, - "updatedAt": null - } -} -`, - }, } p := print.NewPrinter() diff --git a/internal/cmd/beta/cdn/distribution/delete/delete.go b/internal/cmd/beta/cdn/distribution/delete/delete.go index b9c27174c..ddaf14843 100644 --- a/internal/cmd/beta/cdn/distribution/delete/delete.go +++ b/internal/cmd/beta/cdn/distribution/delete/delete.go @@ -1,14 +1,22 @@ package delete import ( + "context" + "fmt" + "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "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/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" ) -const distributionIDFlag = "distribution-id" +const argDistributionID = "DISTRIBUTION_ID" type inputModel struct { *globalflags.GlobalFlagModel @@ -20,23 +28,67 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Use: "delete", Short: "Delete a CDN distribution", Long: "Delete a CDN distribution by its ID.", - Args: args.NoArgs, + Args: args.SingleArg(argDistributionID, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Delete a CDN distribution with ID "xxx"`, - `$ stackit beta cdn distribution delete --distribution-id xxx`, + `$ stackit beta cdn distribution 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 + } + + // 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 + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete the CDN distribution %q for project %q?", model.DistributionID, 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 loadbalancer: %w", err) + } - // TODO + params.Printer.Outputf("CDN distribution %q deleted.", model.DistributionID) return nil }, } - configureFlags(cmd) return cmd } -func configureFlags(cmd *cobra.Command) { - //TODO +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + distributionID := inputArgs[0] + model := inputModel{ + GlobalFlagModel: globalFlags, + DistributionID: distributionID, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) cdn.ApiDeleteDistributionRequest { + return apiClient.DeleteDistribution(ctx, model.ProjectId, model.DistributionID) } diff --git a/internal/cmd/beta/cdn/distribution/delete/delete_test.go b/internal/cmd/beta/cdn/distribution/delete/delete_test.go new file mode 100644 index 000000000..03ec87f46 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/delete/delete_test.go @@ -0,0 +1,130 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testClient = &cdn.APIClient{} + testDistributionID = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argVales []string)) []string { + argVales := []string{ + testDistributionID, + } + for _, m := range mods { + m(argVales) + } + return argVales +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + } + 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, + }, + DistributionID: testDistributionID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *cdn.ApiDeleteDistributionRequest)) cdn.ApiDeleteDistributionRequest { + request := testClient.DeleteDistribution(testCtx, testProjectId, testDistributionID) + 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 values", + argsValues: []string{}, + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + }, + isValid: false, + }, + { + description: "no arg values", + argsValues: []string{}, + flagValues: fixtureFlagValues(), + 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 cdn.ApiDeleteDistributionRequest + }{ + { + 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), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/cdn/distribution/describe/describe.go b/internal/cmd/beta/cdn/distribution/describe/describe.go index aeedfe3ce..22b6443f5 100644 --- a/internal/cmd/beta/cdn/distribution/describe/describe.go +++ b/internal/cmd/beta/cdn/distribution/describe/describe.go @@ -1,18 +1,32 @@ package describe import ( + "context" + "fmt" + "strings" + "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" ) -const distributionIDFlag = "distribution-id" +const distributionIDArg = "DISTRIBUTION_ID_ARG" +const flagWithWaf = "with-waf" type inputModel struct { *globalflags.GlobalFlagModel DistributionID string + WithWAF bool } func NewCmd(params *params.CmdParams) *cobra.Command { @@ -20,16 +34,37 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Use: "describe", Short: "Describe a CDN distribution", Long: "Describe a CDN distribution by its ID.", - Args: args.NoArgs, + Args: args.SingleArg(distributionIDArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Get details of a CDN distribution with ID "xxx"`, - `$ stackit beta cdn distribution describe --distribution-id xxx`, + `$ stackit beta cdn distribution describe xxx`, + ), + examples.NewExample( + `Get details of a CDN, including WAF details, for ID "xxx"`, + `$ stackit beta cdn distribution describe xxx --with-waf`, ), ), RunE: func(cmd *cobra.Command, args []string) error { - // TODO - return nil + 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 distribution: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) }, } configureFlags(cmd) @@ -37,7 +72,143 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().String(distributionIDFlag, "", "The ID of the CDN distribution to describe") - err := cmd.MarkFlagRequired(distributionIDFlag) - cobra.CheckErr(err) + cmd.Flags().Bool(flagWithWaf, false, "Include WAF details in the distribution description") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + DistributionID: args[0], + WithWAF: flags.FlagToBoolValue(p, cmd, flagWithWaf), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) cdn.ApiGetDistributionRequest { + return apiClient.GetDistribution(ctx, model.ProjectId, model.DistributionID).WithWafStatus(model.WithWAF) +} + +func outputResult(p *print.Printer, outputFormat string, distribution *cdn.GetDistributionResponse) error { + if distribution == nil { + return fmt.Errorf("distribution response is empty") + } + return p.OutputResult(outputFormat, distribution, func() error { + d := distribution.Distribution + var content []tables.Table + + content = append(content, buildDistributionTable(d)) + + if d.Waf != nil { + content = append(content, buildWAFTable(d)) + } + + err := tables.DisplayTables(p, content) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} + +func buildDistributionTable(d *cdn.Distribution) tables.Table { + regions := strings.Join(sdkUtils.EnumSliceToStringSlice(*d.Config.Regions), ", ") + defaultCacheDuration := "" + if d.Config.DefaultCacheDuration != nil && d.Config.DefaultCacheDuration.IsSet() { + defaultCacheDuration = *d.Config.DefaultCacheDuration.Get() + } + logSinkPushUrl := "" + if d.Config.LogSink != nil && d.Config.LogSink.LokiLogSink != nil { + logSinkPushUrl = *d.Config.LogSink.LokiLogSink.PushUrl + } + monthlyLimitBytes := "" + if d.Config.MonthlyLimitBytes != nil { + monthlyLimitBytes = fmt.Sprintf("%d", *d.Config.MonthlyLimitBytes) + } + optimizerEnabled := "" + if d.Config.Optimizer != nil { + optimizerEnabled = fmt.Sprintf("%t", *d.Config.Optimizer.Enabled) + } + table := tables.NewTable() + table.SetTitle("Distribution") + table.AddRow("ID", utils.PtrString(d.Id)) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(d.Status)) + table.AddSeparator() + table.AddRow("REGIONS", regions) + table.AddSeparator() + table.AddRow("CREATED AT", utils.PtrString(d.CreatedAt)) + table.AddSeparator() + table.AddRow("UPDATED AT", utils.PtrString(d.UpdatedAt)) + table.AddSeparator() + table.AddRow("PROJECT ID", utils.PtrString(d.ProjectId)) + table.AddSeparator() + if d.Errors != nil && len(*d.Errors) > 0 { + var errorDescriptions []string + for _, err := range *d.Errors { + errorDescriptions = append(errorDescriptions, *err.En) + } + table.AddRow("ERRORS", strings.Join(errorDescriptions, "\n")) + table.AddSeparator() + } + if d.Config.Backend.BucketBackend != nil { + b := d.Config.Backend.BucketBackend + table.AddRow("BACKEND TYPE", "BUCKET") + table.AddSeparator() + table.AddRow("BUCKET URL", utils.PtrString(b.BucketUrl)) + table.AddSeparator() + table.AddRow("BUCKET REGION", utils.PtrString(b.Region)) + table.AddSeparator() + } else if d.Config.Backend.HttpBackend != nil { + h := d.Config.Backend.HttpBackend + var geofencing []string + for k, v := range *h.Geofencing { + geofencing = append(geofencing, fmt.Sprintf("%s: %s", k, strings.Join(v, ", "))) + } + table.AddRow("BACKEND TYPE", "HTTP") + table.AddSeparator() + table.AddRow("HTTP ORIGIN URL", utils.PtrString(h.OriginUrl)) + table.AddSeparator() + table.AddRow("HTTP ORIGIN REQUEST HEADERS", utils.JoinStringMap(*h.OriginRequestHeaders, ": ", ", ")) + table.AddSeparator() + table.AddRow("HTTP GEOFENCING PROPERTIES", strings.Join(geofencing, "\n")) + table.AddSeparator() + } + table.AddRow("BLOCKED COUNTRIES", strings.Join(*d.Config.BlockedCountries, ", ")) + table.AddSeparator() + table.AddRow("BLOCKED IPS", strings.Join(*d.Config.BlockedIps, ", ")) + table.AddSeparator() + table.AddRow("DEFAULT CACHE DURATION", defaultCacheDuration) + table.AddSeparator() + table.AddRow("LOG SINK PUSH URL", logSinkPushUrl) + table.AddSeparator() + table.AddRow("MONTHLY LIMIT (BYTES)", monthlyLimitBytes) + table.AddSeparator() + table.AddRow("OPTIMIZER ENABLED", optimizerEnabled) + table.AddSeparator() + // TODO config has yet another WAF block, left it out because the docs say to use the WAF block at the top level to determine enabled rules. There's also mode and type fields here, both left out. + return table +} + +func buildWAFTable(d *cdn.Distribution) tables.Table { + table := tables.NewTable() + table.SetTitle("WAF") + for _, disabled := range *d.Waf.DisabledRules { + table.AddRow("DISABLED RULE ID", utils.PtrString(disabled.Id)) + table.AddSeparator() + } + for _, enabled := range *d.Waf.EnabledRules { + table.AddRow("ENABLED RULE ID", utils.PtrString(enabled.Id)) + table.AddSeparator() + } + for _, logOnly := range *d.Waf.LogOnlyRules { + table.AddRow("LOG-ONLY RULE ID", utils.PtrString(logOnly.Id)) + table.AddSeparator() + } + return table } diff --git a/internal/cmd/beta/cdn/distribution/describe/describe_test.go b/internal/cmd/beta/cdn/distribution/describe/describe_test.go new file mode 100644 index 000000000..bcc67c13c --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/describe/describe_test.go @@ -0,0 +1,406 @@ +package describe + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectID = uuid.NewString() + testDistributionID = uuid.NewString() + testClient = &cdn.APIClient{} + testTime = time.Time{} +) + +func fixtureFlagValues(mods ...func(m map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectID, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(m *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectID, + Verbosity: globalflags.VerbosityDefault, + }, + DistributionID: testDistributionID, + WithWAF: false, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureResponse(mods ...func(r *cdn.GetDistributionResponse)) *cdn.GetDistributionResponse { + response := &cdn.GetDistributionResponse{ + Distribution: &cdn.Distribution{ + Config: &cdn.Config{ + Backend: &cdn.ConfigBackend{ + BucketBackend: &cdn.BucketBackend{ + BucketUrl: utils.Ptr("https://example.com"), + Region: utils.Ptr("eu"), + Type: utils.Ptr("bucket"), + }, + }, + BlockedCountries: utils.Ptr([]string{}), + BlockedIps: utils.Ptr([]string{}), + DefaultCacheDuration: nil, + LogSink: nil, + MonthlyLimitBytes: nil, + Optimizer: nil, + Regions: &[]cdn.Region{cdn.REGION_EU}, + Waf: nil, + }, + CreatedAt: utils.Ptr(testTime), + Domains: &[]cdn.Domain{}, + Errors: nil, + Id: utils.Ptr(testDistributionID), + ProjectId: utils.Ptr(testProjectID), + Status: utils.Ptr(cdn.DISTRIBUTIONSTATUS_ACTIVE), + UpdatedAt: utils.Ptr(testTime), + Waf: nil, + }, + } + for _, mod := range mods { + mod(response) + } + return response +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + args []string + flags map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + args: []string{testDistributionID}, + flags: fixtureFlagValues(), + isValid: true, + expected: fixtureInputModel(), + }, + { + description: "no args", + args: []string{}, + flags: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid distribution id", + args: []string{"invalid-uuid"}, + flags: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing project id", + args: []string{testDistributionID}, + flags: map[string]string{}, + isValid: false, + }, + { + description: "invalid project id", + args: []string{testDistributionID}, + flags: map[string]string{ + globalflags.ProjectIdFlag: "invalid-uuid", + }, + isValid: false, + }, + { + description: "with WAF", + args: []string{testDistributionID}, + flags: fixtureFlagValues(func(m map[string]string) { + m[flagWithWaf] = "true" + }), + isValid: true, + expected: fixtureInputModel(func(m *inputModel) { + m.WithWAF = true + }), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.args, tt.flags, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected cdn.ApiGetDistributionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expected: testClient.GetDistribution(testCtx, testProjectID, testDistributionID).WithWafStatus(false), + }, + { + description: "with WAF", + model: fixtureInputModel(func(m *inputModel) { + m.WithWAF = true + }), + expected: testClient.GetDistribution(testCtx, testProjectID, testDistributionID).WithWafStatus(true), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(got, tt.expected, + cmp.AllowUnexported(tt.expected), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + format string + distribution *cdn.GetDistributionResponse + wantErr bool + expected string + }{ + { + description: "empty", + format: "table", + wantErr: true, + }, + { + description: "no errors", + format: "table", + distribution: fixtureResponse(), + expected: fmt.Sprintf(` + Distribution  + ID │ %-37s +────────────────────────┼────────────────────────────────────── + STATUS │ ACTIVE +────────────────────────┼────────────────────────────────────── + REGIONS │ EU +────────────────────────┼────────────────────────────────────── + CREATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + UPDATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + PROJECT ID │ %-37s +────────────────────────┼────────────────────────────────────── + BACKEND TYPE │ BUCKET +────────────────────────┼────────────────────────────────────── + BUCKET URL │ https://example.com +────────────────────────┼────────────────────────────────────── + BUCKET REGION │ eu +────────────────────────┼────────────────────────────────────── + BLOCKED COUNTRIES │ +────────────────────────┼────────────────────────────────────── + BLOCKED IPS │ +────────────────────────┼────────────────────────────────────── + DEFAULT CACHE DURATION │ +────────────────────────┼────────────────────────────────────── + LOG SINK PUSH URL │ +────────────────────────┼────────────────────────────────────── + MONTHLY LIMIT (BYTES) │ +────────────────────────┼────────────────────────────────────── + OPTIMIZER ENABLED │ + +`, + testDistributionID, + testTime, + testTime, + testProjectID), + }, + { + description: "with errors", + format: "table", + distribution: fixtureResponse( + func(r *cdn.GetDistributionResponse) { + r.Distribution.Errors = &[]cdn.StatusError{ + { + En: utils.Ptr("First error message"), + }, + { + En: utils.Ptr("Second error message"), + }, + } + }, + ), + expected: fmt.Sprintf(` + Distribution  + ID │ %-37s +────────────────────────┼────────────────────────────────────── + STATUS │ ACTIVE +────────────────────────┼────────────────────────────────────── + REGIONS │ EU +────────────────────────┼────────────────────────────────────── + CREATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + UPDATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + PROJECT ID │ %-37s +────────────────────────┼────────────────────────────────────── + ERRORS │ First error message + │ Second error message +────────────────────────┼────────────────────────────────────── + BACKEND TYPE │ BUCKET +────────────────────────┼────────────────────────────────────── + BUCKET URL │ https://example.com +────────────────────────┼────────────────────────────────────── + BUCKET REGION │ eu +────────────────────────┼────────────────────────────────────── + BLOCKED COUNTRIES │ +────────────────────────┼────────────────────────────────────── + BLOCKED IPS │ +────────────────────────┼────────────────────────────────────── + DEFAULT CACHE DURATION │ +────────────────────────┼────────────────────────────────────── + LOG SINK PUSH URL │ +────────────────────────┼────────────────────────────────────── + MONTHLY LIMIT (BYTES) │ +────────────────────────┼────────────────────────────────────── + OPTIMIZER ENABLED │ + +`, testDistributionID, + testTime, + testTime, + testProjectID), + }, + { + description: "full", + format: "table", + distribution: fixtureResponse( + func(r *cdn.GetDistributionResponse) { + r.Distribution.Waf = &cdn.DistributionWaf{ + EnabledRules: &[]cdn.WafStatusRuleBlock{ + {utils.Ptr("rule-id-1")}, + {utils.Ptr("rule-id-2")}, + }, + DisabledRules: &[]cdn.WafStatusRuleBlock{ + {utils.Ptr("rule-id-3")}, + {utils.Ptr("rule-id-4")}, + }, + LogOnlyRules: &[]cdn.WafStatusRuleBlock{ + {utils.Ptr("rule-id-5")}, + {utils.Ptr("rule-id-6")}, + }, + } + r.Distribution.Config.Backend = &cdn.ConfigBackend{ + HttpBackend: &cdn.HttpBackend{ + OriginUrl: utils.Ptr("https://origin.example.com"), + OriginRequestHeaders: &map[string]string{ + "X-Custom-Header": "CustomValue", + }, + Geofencing: &map[string][]string{ + "origin1.example.com": {"US", "CA"}, + "origin2.example.com": {"FR", "DE"}, + }, + }, + } + r.Distribution.Config.BlockedCountries = &[]string{"US", "CN"} + r.Distribution.Config.BlockedIps = &[]string{"127.0.0.1"} + r.Distribution.Config.DefaultCacheDuration = cdn.NewNullableString(utils.Ptr("P1DT2H30M")) + r.Distribution.Config.LogSink = &cdn.ConfigLogSink{ + LokiLogSink: &cdn.LokiLogSink{ + PushUrl: utils.Ptr("https://logs.example.com"), + }, + } + r.Distribution.Config.MonthlyLimitBytes = utils.Ptr(int64(104857600)) + r.Distribution.Config.Optimizer = &cdn.Optimizer{ + Enabled: utils.Ptr(true), + } + }), + expected: fmt.Sprintf(` + Distribution  + ID │ %-37s +─────────────────────────────┼────────────────────────────────────── + STATUS │ ACTIVE +─────────────────────────────┼────────────────────────────────────── + REGIONS │ EU +─────────────────────────────┼────────────────────────────────────── + CREATED AT │ %-37s +─────────────────────────────┼────────────────────────────────────── + UPDATED AT │ %-37s +─────────────────────────────┼────────────────────────────────────── + PROJECT ID │ %-37s +─────────────────────────────┼────────────────────────────────────── + BACKEND TYPE │ HTTP +─────────────────────────────┼────────────────────────────────────── + HTTP ORIGIN URL │ https://origin.example.com +─────────────────────────────┼────────────────────────────────────── + HTTP ORIGIN REQUEST HEADERS │ X-Custom-Header: CustomValue +─────────────────────────────┼────────────────────────────────────── + HTTP GEOFENCING PROPERTIES │ origin1.example.com: US, CA + │ origin2.example.com: FR, DE +─────────────────────────────┼────────────────────────────────────── + BLOCKED COUNTRIES │ US, CN +─────────────────────────────┼────────────────────────────────────── + BLOCKED IPS │ 127.0.0.1 +─────────────────────────────┼────────────────────────────────────── + DEFAULT CACHE DURATION │ P1DT2H30M +─────────────────────────────┼────────────────────────────────────── + LOG SINK PUSH URL │ https://logs.example.com +─────────────────────────────┼────────────────────────────────────── + MONTHLY LIMIT (BYTES) │ 104857600 +─────────────────────────────┼────────────────────────────────────── + OPTIMIZER ENABLED │ true + + + WAF  + DISABLED RULE ID │ rule-id-3 +──────────────────┼─────────── + DISABLED RULE ID │ rule-id-4 +──────────────────┼─────────── + ENABLED RULE ID │ rule-id-1 +──────────────────┼─────────── + ENABLED RULE ID │ rule-id-2 +──────────────────┼─────────── + LOG-ONLY RULE ID │ rule-id-5 +──────────────────┼─────────── + LOG-ONLY RULE ID │ rule-id-6 + +`, testDistributionID, testTime, testTime, testProjectID), + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var buf bytes.Buffer + p.Cmd.SetOut(&buf) + if err := outputResult(p, tt.format, tt.distribution); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(buf.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/cdn/distribution/distribution.go b/internal/cmd/beta/cdn/distribution/distribution.go index 12c4ffedd..5812684c9 100644 --- a/internal/cmd/beta/cdn/distribution/distribution.go +++ b/internal/cmd/beta/cdn/distribution/distribution.go @@ -2,11 +2,11 @@ package distribution import ( "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/delete" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/describe" - "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/update" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/update" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) diff --git a/internal/cmd/beta/cdn/distribution/update/update.go b/internal/cmd/beta/cdn/distribution/update/update.go index df2eaa254..f7da2f5b0 100644 --- a/internal/cmd/beta/cdn/distribution/update/update.go +++ b/internal/cmd/beta/cdn/distribution/update/update.go @@ -1,40 +1,134 @@ package update import ( + "context" + "fmt" + "strings" + "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "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/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/cdn" ) const ( - distributionIDFlag = "distribution-id" - regionsFlag = "regions" + argDistributionID = "DISTRIBUTION_ID" + flagRegions = "regions" + flagHTTP = "http" + flagHTTPOriginURL = "http-origin-url" + flagHTTPGeofencing = "http-geofencing" + flagHTTPOriginRequestHeaders = "http-origin-request-headers" + flagBucket = "bucket" + flagBucketURL = "bucket-url" + flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" + flagBucketRegion = "bucket-region" + flagBlockedCountries = "blocked-countries" + flagBlockedIPs = "blocked-ips" + flagDefaultCacheDuration = "default-cache-duration" + flagLoki = "loki" + flagLokiUsername = "loki-username" + flagLokiPushURL = "loki-push-url" + flagMonthlyLimitBytes = "monthly-limit-bytes" + flagOptimizer = "optimizer" ) +type bucketInputModel struct { + URL string + AccessKeyID string + Password string + Region string +} + +type httpInputModel struct { + Geofencing *map[string][]string + OriginRequestHeaders *map[string]string + OriginURL string +} + +type lokiInputModel struct { + Password string + Username string + PushURL string +} + type inputModel struct { *globalflags.GlobalFlagModel - DistributionID string - Regions []cdn.Region + DistributionID string + Regions []cdn.Region + Bucket *bucketInputModel + HTTP *httpInputModel + BlockedCountries []string + BlockedIPs []string + DefaultCacheDuration string + MonthlyLimitBytes *int64 + Loki *lokiInputModel + Optimizer *bool } func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: "update", - Short: "Update a CDN distribution", - Long: "Update a CDN distribution by its ID, allowing replacement of its regions.", - Args: args.NoArgs, + Use: "update", + Short: "Update a CDN distribution", + Long: "Update a CDN distribution by its ID, allowing replacement of its regions.", + Args: args.SingleArg(argDistributionID, utils.ValidateUUID), Example: examples.Build( - examples.NewExample( - `Update a CDN distribution with ID "xxx" to be cached in reions "EU" and "AF"`, - `$ stackit beta cdn distribution update --distribution-id xxx --regions EU,AF`, - ), + // TODO ), RunE: func(cmd *cobra.Command, args []string) error { - // TODO - return nil + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + if model.Bucket != nil { + pw, err := params.Printer.PromptForPassword("enter your secret access key for the object storage bucket: ") + if err != nil { + return fmt.Errorf("reading secret access key: %w", err) + } + model.Bucket.Password = pw + } + if model.Loki != nil { + pw, err := params.Printer.PromptForPassword("enter your password for the loki log sink: ") + if err != nil { + return fmt.Errorf("reading loki password: %w", err) + } + model.Loki.Password = pw + } + + 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 + } + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update a CDN distribution for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update CDN distribution: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) }, } configureFlags(cmd) @@ -42,5 +136,234 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - //TODO + cmd.Flags().Var(flags.EnumSliceFlag(false, []string{}, sdkUtils.EnumSliceToStringSlice(cdn.AllowedRegionEnumValues)...), flagRegions, fmt.Sprintf("Regions in which content should be cached, multiple of: %q", cdn.AllowedRegionEnumValues)) + cmd.Flags().Bool(flagHTTP, false, "Use HTTP backend") + cmd.Flags().String(flagHTTPOriginURL, "", "Origin URL for HTTP backend") + cmd.Flags().StringSlice(flagHTTPOriginRequestHeaders, []string{}, "Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers!") + cmd.Flags().StringArray(flagHTTPGeofencing, []string{}, "Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable.") + cmd.Flags().Bool(flagBucket, false, "Use Object Storage backend") + cmd.Flags().String(flagBucketURL, "", "Bucket URL for Object Storage backend") + cmd.Flags().String(flagBucketCredentialsAccessKeyID, "", "Access Key ID for Object Storage backend") + cmd.Flags().String(flagBucketRegion, "", "Region for Object Storage backend") + cmd.Flags().StringSlice(flagBlockedCountries, []string{}, "Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR')") + cmd.Flags().StringSlice(flagBlockedIPs, []string{}, "Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')") + cmd.Flags().String(flagDefaultCacheDuration, "", "ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)") + cmd.Flags().Bool(flagLoki, false, "Enable Loki log sink for the CDN distribution") + cmd.Flags().String(flagLokiUsername, "", "Username for log sink") + cmd.Flags().String(flagLokiPushURL, "", "Push URL for log sink") + cmd.Flags().Int64(flagMonthlyLimitBytes, 0, "Monthly limit in bytes for the CDN distribution") + cmd.Flags().Bool(flagOptimizer, false, "Enable optimizer for the CDN distribution (paid feature).") + cmd.MarkFlagsMutuallyExclusive(flagHTTP, flagBucket) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + distributionID := args[0] + + regionStrings := flags.FlagToStringSliceValue(p, cmd, flagRegions) + regions := make([]cdn.Region, 0, len(regionStrings)) + for _, regionStr := range regionStrings { + regions = append(regions, cdn.Region(regionStr)) + } + + var http *httpInputModel + if flags.FlagToBoolValue(p, cmd, flagHTTP) { + originURL := flags.FlagToStringValue(p, cmd, flagHTTPOriginURL) + + var geofencing *map[string][]string + geofencingInput := flags.FlagToStringArrayValue(p, cmd, flagHTTPGeofencing) + if geofencingInput != nil { + geofencing = parseGeofencing(p, geofencingInput) + } + + var originRequestHeaders *map[string]string + originRequestHeadersInput := flags.FlagToStringSliceValue(p, cmd, flagHTTPOriginRequestHeaders) + if originRequestHeadersInput != nil { + originRequestHeaders = parseOriginRequestHeaders(p, originRequestHeadersInput) + } + + http = &httpInputModel{ + OriginURL: originURL, + Geofencing: geofencing, + OriginRequestHeaders: originRequestHeaders, + } + } + + var bucket *bucketInputModel + if flags.FlagToBoolValue(p, cmd, flagBucket) { + bucketURL := flags.FlagToStringValue(p, cmd, flagBucketURL) + accessKeyID := flags.FlagToStringValue(p, cmd, flagBucketCredentialsAccessKeyID) + region := flags.FlagToStringValue(p, cmd, flagBucketRegion) + + bucket = &bucketInputModel{ + URL: bucketURL, + AccessKeyID: accessKeyID, + Password: "", + Region: region, + } + } + + blockedCountries := flags.FlagToStringSliceValue(p, cmd, flagBlockedCountries) + blockedIPs := flags.FlagToStringSliceValue(p, cmd, flagBlockedIPs) + cacheDuration := flags.FlagToStringValue(p, cmd, flagDefaultCacheDuration) + monthlyLimit := flags.FlagToInt64Pointer(p, cmd, flagMonthlyLimitBytes) + + var loki *lokiInputModel + if flags.FlagToBoolValue(p, cmd, flagLoki) { + loki = &lokiInputModel{ + Username: flags.FlagToStringValue(p, cmd, flagLokiUsername), + PushURL: flags.FlagToStringValue(p, cmd, flagLokiPushURL), + Password: "", + } + } + + var optimizer *bool + if cmd.Flags().Changed(flagOptimizer) { + o := flags.FlagToBoolValue(p, cmd, flagOptimizer) + optimizer = &o + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DistributionID: distributionID, + Regions: regions, + HTTP: http, + Bucket: bucket, + BlockedCountries: blockedCountries, + BlockedIPs: blockedIPs, + DefaultCacheDuration: cacheDuration, + MonthlyLimitBytes: monthlyLimit, + Loki: loki, + Optimizer: optimizer, + } + + p.DebugInputModel(model) + return &model, nil +} + +// TODO both parseGeofencing and parseOriginRequestHeaders copied from create.go, move to another package and make public? +func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]string { + geofencing := make(map[string][]string) + for _, in := range geofencingInput { + firstSpace := strings.IndexRune(in, ' ') + if firstSpace == -1 { + p.Debug(print.ErrorLevel, "invalid geofencing entry (no space found): %q", in) + continue + } + urlPart := in[:firstSpace] + countriesPart := in[firstSpace+1:] + geofencing[urlPart] = nil + countries := strings.Split(countriesPart, ",") + for _, country := range countries { + country = strings.TrimSpace(country) + geofencing[urlPart] = append(geofencing[urlPart], country) + } + } + return &geofencing +} + +func parseOriginRequestHeaders(p *print.Printer, originRequestHeadersInput []string) *map[string]string { + originRequestHeaders := make(map[string]string) + for _, in := range originRequestHeadersInput { + parts := strings.Split(in, ":") + if len(parts) != 2 { + p.Debug(print.ErrorLevel, "invalid origin request header entry (no colon found): %q", in) + continue + } + originRequestHeaders[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + return &originRequestHeaders +} + +func buildRequest(ctx context.Context, apiClient *cdn.APIClient, model *inputModel) cdn.ApiPatchDistributionRequest { + req := apiClient.PatchDistribution(ctx, model.ProjectId, model.DistributionID) + payload := cdn.NewPatchDistributionPayload() + cfg := &cdn.ConfigPatch{} + payload.Config = cfg + if len(model.Regions) > 0 { + cfg.Regions = &model.Regions + } + if model.Bucket != nil { + bucket := &cdn.BucketBackendPatch{ + Type: utils.Ptr("bucket"), + } + cfg.Backend = &cdn.ConfigPatchBackend{ + BucketBackendPatch: bucket, + } + if model.Bucket.URL != "" { + bucket.BucketUrl = utils.Ptr(model.Bucket.URL) + } + if model.Bucket.AccessKeyID != "" { + bucket.Credentials = cdn.NewBucketCredentials( + model.Bucket.AccessKeyID, + model.Bucket.Password, + ) + } + if model.Bucket.Region != "" { + bucket.Region = utils.Ptr(model.Bucket.Region) + } + } else if model.HTTP != nil { + http := &cdn.HttpBackendPatch{ + Type: utils.Ptr("http"), + } + cfg.Backend = &cdn.ConfigPatchBackend{ + HttpBackendPatch: http, + } + if model.HTTP.OriginRequestHeaders != nil { + http.OriginRequestHeaders = model.HTTP.OriginRequestHeaders + } + if model.HTTP.Geofencing != nil { + http.Geofencing = model.HTTP.Geofencing + } + if model.HTTP.OriginURL != "" { + http.OriginUrl = utils.Ptr(model.HTTP.OriginURL) + } + } + if len(model.BlockedCountries) > 0 { + cfg.BlockedCountries = &model.BlockedCountries + } + if len(model.BlockedIPs) > 0 { + cfg.BlockedIps = &model.BlockedIPs + } + if model.DefaultCacheDuration != "" { + cfg.DefaultCacheDuration = cdn.NewNullableString(&model.DefaultCacheDuration) + } + if model.MonthlyLimitBytes != nil && *model.MonthlyLimitBytes > 0 { + cfg.MonthlyLimitBytes = model.MonthlyLimitBytes + } + if model.Loki != nil { + loki := &cdn.LokiLogSinkPatch{} + cfg.LogSink = cdn.NewNullableConfigPatchLogSink(&cdn.ConfigPatchLogSink{ + LokiLogSinkPatch: loki, + }) + if model.Loki.PushURL != "" { + loki.PushUrl = utils.Ptr(model.Loki.PushURL) + } + if model.Loki.Username != "" { + loki.Credentials = cdn.NewLokiLogSinkCredentials( + model.Loki.Password, + model.Loki.Username, + ) + } + } + if model.Optimizer != nil { + cfg.Optimizer = &cdn.OptimizerPatch{ + Enabled: model.Optimizer, + } + } + req = req.PatchDistributionPayload(*payload) + return req +} + +func outputResult(p *print.Printer, outputFormat string, projectLabel string, resp *cdn.PatchDistributionResponse) error { + if resp == nil { + return fmt.Errorf("update distribution response is empty") + } + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Updated CDN distribution for %q. ID: %s\n", projectLabel, utils.PtrString(resp.Distribution.Id)) + return nil + }) } diff --git a/internal/cmd/beta/cdn/distribution/update/update_test.go b/internal/cmd/beta/cdn/distribution/update/update_test.go new file mode 100644 index 000000000..5ffea44e8 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/update/update_test.go @@ -0,0 +1,366 @@ +package update + +import ( + "bytes" + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/cdn" + "k8s.io/utils/ptr" +) + +const testCacheDuration = "P1DT12H" + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &cdn.APIClient{} +var testProjectId = uuid.NewString() +var testDistributionID = uuid.NewString() + +const testMonthlyLimitBytes int64 = 1048576 + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + } + 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, + }, + DistributionID: testDistributionID, + Regions: []cdn.Region{}, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(p *cdn.PatchDistributionPayload)) cdn.ApiPatchDistributionRequest { + req := testClient.PatchDistribution(testCtx, testProjectId, testDistributionID) + if p := fixturePayload(mods...); p != nil { + req = req.PatchDistributionPayload(*fixturePayload(mods...)) + } + return req +} + +func fixturePayload(mods ...func(p *cdn.PatchDistributionPayload)) *cdn.PatchDistributionPayload { + p := cdn.NewPatchDistributionPayload() + p.Config = &cdn.ConfigPatch{} + for _, m := range mods { + m(p) + } + return p +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues(), + isValid: true, + expected: fixtureInputModel(), + }, + { + description: "distribution id missing", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid distribution id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid distribution id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "both backends", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagHTTP] = "true" + m[flagBucket] = "true" + }, + ), + isValid: false, + }, + { + description: "max config without backend", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagRegions] = "EU,US" + m[flagBlockedCountries] = "DE,AT,CH" + m[flagBlockedIPs] = "127.0.0.1,10.0.0.8" + m[flagDefaultCacheDuration] = "P1DT12H" + m[flagLoki] = "true" + m[flagLokiUsername] = "loki-user" + m[flagLokiPushURL] = "https://loki.example.com" + m[flagMonthlyLimitBytes] = fmt.Sprintf("%d", testMonthlyLimitBytes) + m[flagOptimizer] = "true" + }, + ), + isValid: true, + expected: fixtureInputModel( + func(m *inputModel) { + m.Regions = []cdn.Region{cdn.REGION_EU, cdn.REGION_US} + m.BlockedCountries = []string{"DE", "AT", "CH"} + m.BlockedIPs = []string{"127.0.0.1", "10.0.0.8"} + m.DefaultCacheDuration = "P1DT12H" + m.Loki = &lokiInputModel{ + Username: "loki-user", + PushURL: "https://loki.example.com", + } + m.MonthlyLimitBytes = utils.Ptr(testMonthlyLimitBytes) + m.Optimizer = utils.Ptr(true) + }, + ), + }, + { + description: "max config http backend", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagHTTP] = "true" + m[flagHTTPOriginURL] = "https://origin.example.com" + m[flagHTTPOriginRequestHeaders] = "X-Example-Header: example-value, X-Another-Header: another-value" + m[flagHTTPGeofencing] = "https://dach.example.com DE,AT,CH" + }, + ), + isValid: true, + expected: fixtureInputModel( + func(m *inputModel) { + m.HTTP = &httpInputModel{ + OriginURL: "https://origin.example.com", + OriginRequestHeaders: &map[string]string{ + "X-Example-Header": "example-value", + "X-Another-Header": "another-value", + }, + Geofencing: &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + }, + } + }, + ), + }, + { + description: "max config bucket backend", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(m map[string]string) { + m[flagBucket] = "true" + m[flagBucketURL] = "https://bucket.example.com" + m[flagBucketRegion] = "EU" + m[flagBucketCredentialsAccessKeyID] = "access-key-id" + }, + ), + isValid: true, + expected: fixtureInputModel( + func(m *inputModel) { + m.Bucket = &bucketInputModel{ + URL: "https://bucket.example.com", + Region: "EU", + AccessKeyID: "access-key-id", + } + }, + ), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected cdn.ApiPatchDistributionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expected: fixtureRequest(), + }, + { + description: "max without backend", + model: fixtureInputModel( + func(m *inputModel) { + m.Regions = []cdn.Region{cdn.REGION_EU, cdn.REGION_US} + m.BlockedCountries = []string{"DE", "AT", "CH"} + m.BlockedIPs = []string{"127.0.0.1", "10.0.0.8"} + m.DefaultCacheDuration = testCacheDuration + m.MonthlyLimitBytes = utils.Ptr(testMonthlyLimitBytes) + m.Loki = &lokiInputModel{ + Password: "loki-pass", + Username: "loki-user", + PushURL: "https://loki.example.com", + } + m.Optimizer = utils.Ptr(true) + }, + ), + expected: fixtureRequest( + func(p *cdn.PatchDistributionPayload) { + p.Config.Regions = &[]cdn.Region{cdn.REGION_EU, cdn.REGION_US} + p.Config.BlockedCountries = &[]string{"DE", "AT", "CH"} + p.Config.BlockedIps = &[]string{"127.0.0.1", "10.0.0.8"} + p.Config.DefaultCacheDuration = cdn.NewNullableString(utils.Ptr(testCacheDuration)) + p.Config.MonthlyLimitBytes = utils.Ptr(testMonthlyLimitBytes) + p.Config.LogSink = cdn.NewNullableConfigPatchLogSink(&cdn.ConfigPatchLogSink{ + LokiLogSinkPatch: &cdn.LokiLogSinkPatch{ + Credentials: cdn.NewLokiLogSinkCredentials("loki-pass", "loki-user"), + PushUrl: utils.Ptr("https://loki.example.com"), + }, + }) + p.Config.Optimizer = &cdn.OptimizerPatch{ + Enabled: utils.Ptr(true), + } + }, + ), + }, + { + description: "max http backend", + model: fixtureInputModel( + func(m *inputModel) { + m.HTTP = &httpInputModel{ + Geofencing: &map[string][]string{"https://dach.example.com": {"DE", "AT", "CH"}}, + OriginRequestHeaders: &map[string]string{"X-Example-Header": "example-value", "X-Another-Header": "another-value"}, + OriginURL: "https://http-backend.example.com", + } + }), + expected: fixtureRequest( + func(p *cdn.PatchDistributionPayload) { + p.Config.Backend = &cdn.ConfigPatchBackend{ + HttpBackendPatch: &cdn.HttpBackendPatch{ + Geofencing: &map[string][]string{"https://dach.example.com": {"DE", "AT", "CH"}}, + OriginRequestHeaders: &map[string]string{ + "X-Example-Header": "example-value", + "X-Another-Header": "another-value", + }, + OriginUrl: utils.Ptr("https://http-backend.example.com"), + Type: utils.Ptr("http"), + }, + } + }), + }, + { + description: "max bucket backend", + model: fixtureInputModel( + func(m *inputModel) { + m.Bucket = &bucketInputModel{ + URL: "https://bucket.example.com", + AccessKeyID: "bucket-access-key-id", + Password: "bucket-pass", + Region: "EU", + } + }), + expected: fixtureRequest( + func(p *cdn.PatchDistributionPayload) { + p.Config.Backend = &cdn.ConfigPatchBackend{ + BucketBackendPatch: &cdn.BucketBackendPatch{ + BucketUrl: utils.Ptr("https://bucket.example.com"), + Credentials: cdn.NewBucketCredentials("bucket-access-key-id", "bucket-pass"), + Region: utils.Ptr("EU"), + Type: utils.Ptr("bucket"), + }, + } + }), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.model) + + diff := cmp.Diff(request, tt.expected, + cmp.AllowUnexported(tt.expected, cdn.NullableString{}, cdn.NullableConfigPatchLogSink{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFormat string + response *cdn.PatchDistributionResponse + expected string + wantErr bool + }{ + { + description: "nil response", + outputFormat: "table", + response: nil, + wantErr: true, + }, + { + description: "table output", + outputFormat: "table", + response: &cdn.PatchDistributionResponse{ + Distribution: &cdn.Distribution{ + Id: ptr.To("dist-1234"), + }, + }, + expected: fmt.Sprintf("Updated CDN distribution for %q. ID: dist-1234\n", testProjectId), + }, + } + + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + buffer := &bytes.Buffer{} + p.Cmd.SetOut(buffer) + if err := outputResult(p, tt.outputFormat, testProjectId, tt.response); (err != nil) != tt.wantErr { + t.Fatalf("outputResult: %v", err) + } + if buffer.String() != tt.expected { + t.Errorf("want:\n%s\ngot:\n%s", tt.expected, buffer.String()) + } + }) + } + +} diff --git a/internal/pkg/flags/flag_to_value.go b/internal/pkg/flags/flag_to_value.go index 6385ba65a..f08904982 100644 --- a/internal/pkg/flags/flag_to_value.go +++ b/internal/pkg/flags/flag_to_value.go @@ -47,6 +47,20 @@ func FlagToStringSliceValue(p *print.Printer, cmd *cobra.Command, flag string) [ return nil } +// Returns the flag's value as a []string. +// Returns nil if flag is not set, if its value can not be converted to []string, or if the flag does not exist. +func FlagToStringArrayValue(p *print.Printer, cmd *cobra.Command, flag string) []string { + value, err := cmd.Flags().GetStringArray(flag) + if err != nil { + p.Debug(print.ErrorLevel, "convert flag to string array value: %v", err) + return nil + } + if !cmd.Flag(flag).Changed { + return nil + } + return value +} + // Returns a pointer to the flag's value. // Returns nil if the flag is not set, if its value can not be converted to map[string]string, or if the flag does not exist. func FlagToStringToStringPointer(p *print.Printer, cmd *cobra.Command, flag string) *map[string]string { //nolint:gocritic //convenient for setting the SDK payload @@ -75,6 +89,20 @@ func FlagToInt64Pointer(p *print.Printer, cmd *cobra.Command, flag string) *int6 return nil } +// Returns a pointer to the flag's value. +// Returns nil if the flag is not set, if its value can not be converted to int64, or if the flag does not exist. +func FlagToInt32Pointer(p *print.Printer, cmd *cobra.Command, flag string) *int32 { + value, err := cmd.Flags().GetInt32(flag) + if err != nil { + p.Debug(print.ErrorLevel, "convert flag to Int pointer: %v", err) + return nil + } + if cmd.Flag(flag).Changed { + return &value + } + return nil +} + // Returns a pointer to the flag's value. // Returns nil if the flag is not set, if its value can not be converted to string, or if the flag does not exist. func FlagToStringPointer(p *print.Printer, cmd *cobra.Command, flag string) *string { diff --git a/internal/pkg/utils/strings.go b/internal/pkg/utils/strings.go index 401287fa1..ef881bffd 100644 --- a/internal/pkg/utils/strings.go +++ b/internal/pkg/utils/strings.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "strings" "unicode/utf8" ) @@ -26,6 +27,18 @@ func JoinStringKeysPtr(m map[string]any, sep string) string { return JoinStringKeys(m, sep) } +// JoinStringMap concatenates the key-value pairs of a string map, key and value separated by kvSep, key value pairs separated by sep. +func JoinStringMap(m map[string]string, kvSep, sep string) string { + if m == nil { + return "" + } + parts := make([]string, 0, len(m)) + for k, v := range m { + parts = append(parts, fmt.Sprintf("%s%s%s", k, kvSep, v)) + } + return strings.Join(parts, sep) +} + // JoinStringPtr concatenates the strings of a string slice pointer, each separatore by the // [sep] string. func JoinStringPtr(vals *[]string, sep string) string { diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index 862b92c8f..b37443e2b 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -14,6 +14,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/config" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "golang.org/x/exp/constraints" ) // Ptr Returns the pointer to any type T @@ -259,3 +260,10 @@ func GetSliceFromPointer[T any](s *[]T) []T { } return *s } + +func Min[T constraints.Ordered](a, b T) T { + if a < b { + return a + } + return b +} From 568659cfdde963b346df7bc3cb90fe413dc3f3e7 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Mon, 1 Dec 2025 10:35:28 +0100 Subject: [PATCH 14/49] squash --- internal/pkg/testutils/testutils.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/pkg/testutils/testutils.go b/internal/pkg/testutils/testutils.go index 8f970fd0a..18730ef63 100644 --- a/internal/pkg/testutils/testutils.go +++ b/internal/pkg/testutils/testutils.go @@ -12,12 +12,14 @@ import ( // TestParseInput centralizes the logic to test a combination of inputs (arguments, flags) for a cobra command func TestParseInput[T any](t *testing.T, cmdFactory func(*params.CmdParams) *cobra.Command, parseInputFunc func(*print.Printer, *cobra.Command, []string) (T, error), expectedModel T, argValues []string, flagValues map[string]string, isValid bool) { + t.Helper() TestParseInputWithAdditionalFlags(t, cmdFactory, parseInputFunc, expectedModel, argValues, flagValues, map[string][]string{}, isValid) } // TestParseInputWithAdditionalFlags centralizes the logic to test a combination of inputs (arguments, flags) for a cobra command. // It allows to pass multiple instances of a single flag to the cobra command using the `additionalFlagValues` parameter. func TestParseInputWithAdditionalFlags[T any](t *testing.T, cmdFactory func(*params.CmdParams) *cobra.Command, parseInputFunc func(*print.Printer, *cobra.Command, []string) (T, error), expectedModel T, argValues []string, flagValues map[string]string, additionalFlagValues map[string][]string, isValid bool) { + t.Helper() p := print.NewPrinter() cmd := cmdFactory(¶ms.CmdParams{Printer: p}) err := globalflags.Configure(cmd.Flags()) @@ -49,6 +51,21 @@ func TestParseInputWithAdditionalFlags[T any](t *testing.T, cmdFactory func(*par } } + if cmd.PreRun != nil { + // can be used for dynamic flag configuration + cmd.PreRun(cmd, argValues) + } + + if cmd.PreRunE != nil { + err := cmd.PreRunE(cmd, argValues) + if err != nil { + if !isValid { + return + } + t.Fatalf("error in PreRunE: %v", err) + } + } + err = cmd.ValidateArgs(argValues) if err != nil { if !isValid { From a1838461dddd571405039dc9f2a5dfbb29b3d769 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Mon, 1 Dec 2025 12:54:37 +0100 Subject: [PATCH 15/49] squash --- .../beta/cdn/distribution/create/create.go | 22 ++++++++++++++----- .../beta/cdn/distribution/update/update.go | 13 ++++++----- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/create/create.go b/internal/cmd/beta/cdn/distribution/create/create.go index 32cf99b10..35c40ae45 100644 --- a/internal/cmd/beta/cdn/distribution/create/create.go +++ b/internal/cmd/beta/cdn/distribution/create/create.go @@ -25,7 +25,7 @@ const ( flagHTTP = "http" flagHTTPOriginURL = "http-origin-url" flagHTTPGeofencing = "http-geofencing" - flagHTTPOriginRequestHeaders = "http-origin-request-header" + flagHTTPOriginRequestHeaders = "http-origin-request-headers" flagBucket = "bucket" flagBucketURL = "bucket-url" flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" @@ -74,12 +74,22 @@ type inputModel struct { func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: "create", - Short: "Create a CDN distribution", - Long: "Create a CDN distribution for a given originUrl in multiple regions.", - Args: args.NoArgs, + Use: "create", + Short: "Create a CDN distribution", + Long: "Create a CDN distribution for a given originUrl in multiple regions.", + Args: args.NoArgs, Example: examples.Build( - //TODO + examples.NewExample( + `Create a CDN distribution with an HTTP backend`, + `$ stackit beta cdn create --http --http-origin-url https://example.com \ +--regions AF,EU`, + ), + examples.NewExample( + `Create a CDN distribution with an Object Storage backend`, + `$ stackit beta cdn create --bucket --bucket-url https://bucket.example.com \ +--bucket-credentials-access-key-id yyyy --bucket-region EU \ +--regions AF,EU`, + ), ), PreRun: func(cmd *cobra.Command, args []string) { // either flagHTTP or flagBucket must be set, depending on which we mark other flags as required diff --git a/internal/cmd/beta/cdn/distribution/update/update.go b/internal/cmd/beta/cdn/distribution/update/update.go index f7da2f5b0..cee590048 100644 --- a/internal/cmd/beta/cdn/distribution/update/update.go +++ b/internal/cmd/beta/cdn/distribution/update/update.go @@ -76,12 +76,15 @@ type inputModel struct { func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: "update", - Short: "Update a CDN distribution", - Long: "Update a CDN distribution by its ID, allowing replacement of its regions.", - Args: args.SingleArg(argDistributionID, utils.ValidateUUID), + Use: "update", + Short: "Update a CDN distribution", + Long: "Update a CDN distribution by its ID, allowing replacement of its regions.", + Args: args.SingleArg(argDistributionID, utils.ValidateUUID), Example: examples.Build( - // TODO + examples.NewExample( + `update a CDN distribution with ID "123e4567-e89b-12d3-a456-426614174000" to not use optimizer`, + `$ stackit beta cdn update 123e4567-e89b-12d3-a456-426614174000 --optimizer=false`, + ), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() From 4f4e0b12a98db8784028028d0bcdbf857db1adc0 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Mon, 1 Dec 2025 12:59:30 +0100 Subject: [PATCH 16/49] fix(cdn) regenerate docs --- docs/stackit_beta_cdn_distribution.md | 3 + docs/stackit_beta_cdn_distribution_create.md | 31 ++++++++-- docs/stackit_beta_cdn_distribution_delete.md | 40 +++++++++++++ .../stackit_beta_cdn_distribution_describe.md | 44 ++++++++++++++ docs/stackit_beta_cdn_distribution_list.md | 1 + docs/stackit_beta_cdn_distribution_update.md | 57 +++++++++++++++++++ 6 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 docs/stackit_beta_cdn_distribution_delete.md create mode 100644 docs/stackit_beta_cdn_distribution_describe.md create mode 100644 docs/stackit_beta_cdn_distribution_update.md diff --git a/docs/stackit_beta_cdn_distribution.md b/docs/stackit_beta_cdn_distribution.md index 9365bcddd..c9c26a931 100644 --- a/docs/stackit_beta_cdn_distribution.md +++ b/docs/stackit_beta_cdn_distribution.md @@ -31,5 +31,8 @@ stackit beta cdn distribution [flags] * [stackit beta cdn](./stackit_beta_cdn.md) - Manage CDN resources * [stackit beta cdn distribution create](./stackit_beta_cdn_distribution_create.md) - Create a CDN distribution +* [stackit beta cdn distribution delete](./stackit_beta_cdn_distribution_delete.md) - Delete a CDN distribution +* [stackit beta cdn distribution describe](./stackit_beta_cdn_distribution_describe.md) - Describe a CDN distribution * [stackit beta cdn distribution list](./stackit_beta_cdn_distribution_list.md) - List CDN distributions +* [stackit beta cdn distribution update](./stackit_beta_cdn_distribution_update.md) - Update a CDN distribution diff --git a/docs/stackit_beta_cdn_distribution_create.md b/docs/stackit_beta_cdn_distribution_create.md index 6c185bc3d..6b04842ee 100644 --- a/docs/stackit_beta_cdn_distribution_create.md +++ b/docs/stackit_beta_cdn_distribution_create.md @@ -13,16 +13,37 @@ stackit beta cdn distribution create [flags] ### Examples ``` - Create a distribution for regions EU and AF - $ stackit beta cdn distribution create --regions=EU,AF --origin-url=https://example.com + Create a CDN distribution with an HTTP backend + $ stackit beta cdn create --http --http-origin-url https://example.com \ +--regions AF,EU + + Create a CDN distribution with an Object Storage backend + $ stackit beta cdn create --bucket --bucket-url https://bucket.example.com \ +--bucket-credentials-access-key-id yyyy --bucket-region EU \ +--regions AF,EU ``` ### Options ``` - -h, --help Help for "stackit beta cdn distribution create" - --origin-url https The origin of the content that should be made available through the CDN. Note that the path and query parameters are ignored. Ports are allowed. If no protocol is provided, https is assumed. So `www.example.com:1234/somePath?q=123` is normalized to `https://www.example.com:1234` - --regions strings Regions in which content should be cached, multiple of: ["EU" "US" "AF" "SA" "ASIA"] (default []) + --blocked-countries strings Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR') + --blocked-ips strings Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1') + --bucket Use Object Storage backend + --bucket-credentials-access-key-id string Access Key ID for Object Storage backend + --bucket-region string Region for Object Storage backend + --bucket-url string Bucket URL for Object Storage backend + --default-cache-duration string ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes) + -h, --help Help for "stackit beta cdn distribution create" + --http Use HTTP backend + --http-geofencing stringArray Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable. + --http-origin-request-headers strings Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers! + --http-origin-url string Origin URL for HTTP backend + --loki Enable Loki log sink for the CDN distribution + --loki-push-url string Push URL for log sink + --loki-username string Username for log sink + --monthly-limit-bytes int Monthly limit in bytes for the CDN distribution + --optimizer Enable optimizer for the CDN distribution (paid feature). + --regions strings Regions in which content should be cached, multiple of: ["EU" "US" "AF" "SA" "ASIA"] (default []) ``` ### Options inherited from parent commands diff --git a/docs/stackit_beta_cdn_distribution_delete.md b/docs/stackit_beta_cdn_distribution_delete.md new file mode 100644 index 000000000..7313b5a39 --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_delete.md @@ -0,0 +1,40 @@ +## stackit beta cdn distribution delete + +Delete a CDN distribution + +### Synopsis + +Delete a CDN distribution by its ID. + +``` +stackit beta cdn distribution delete [flags] +``` + +### Examples + +``` + Delete a CDN distribution with ID "xxx" + $ stackit beta cdn distribution delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution 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 cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution_describe.md b/docs/stackit_beta_cdn_distribution_describe.md new file mode 100644 index 000000000..1e8f68a7e --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_describe.md @@ -0,0 +1,44 @@ +## stackit beta cdn distribution describe + +Describe a CDN distribution + +### Synopsis + +Describe a CDN distribution by its ID. + +``` +stackit beta cdn distribution describe [flags] +``` + +### Examples + +``` + Get details of a CDN distribution with ID "xxx" + $ stackit beta cdn distribution describe xxx + + Get details of a CDN, including WAF details, for ID "xxx" + $ stackit beta cdn distribution describe xxx --with-waf +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution describe" + --with-waf Include WAF details in the distribution description +``` + +### 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 cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution_list.md b/docs/stackit_beta_cdn_distribution_list.md index 38873ae02..4fc5d2750 100644 --- a/docs/stackit_beta_cdn_distribution_list.md +++ b/docs/stackit_beta_cdn_distribution_list.md @@ -23,6 +23,7 @@ stackit beta cdn distribution list [flags] ### Options ``` + -- int Limit the output to the first n elements -h, --help Help for "stackit beta cdn distribution list" --sort-by string Sort entries by a specific field, one of ["id" "createdAt" "updatedAt" "originUrl" "status" "originUrlRelated"] (default "createdAt") ``` diff --git a/docs/stackit_beta_cdn_distribution_update.md b/docs/stackit_beta_cdn_distribution_update.md new file mode 100644 index 000000000..f8f26dec9 --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_update.md @@ -0,0 +1,57 @@ +## stackit beta cdn distribution update + +Update a CDN distribution + +### Synopsis + +Update a CDN distribution by its ID, allowing replacement of its regions. + +``` +stackit beta cdn distribution update [flags] +``` + +### Examples + +``` + update a CDN distribution with ID "123e4567-e89b-12d3-a456-426614174000" to not use optimizer + $ stackit beta cdn update 123e4567-e89b-12d3-a456-426614174000 --optimizer=false +``` + +### Options + +``` + --blocked-countries strings Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR') + --blocked-ips strings Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1') + --bucket Use Object Storage backend + --bucket-credentials-access-key-id string Access Key ID for Object Storage backend + --bucket-region string Region for Object Storage backend + --bucket-url string Bucket URL for Object Storage backend + --default-cache-duration string ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes) + -h, --help Help for "stackit beta cdn distribution update" + --http Use HTTP backend + --http-geofencing stringArray Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable. + --http-origin-request-headers strings Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers! + --http-origin-url string Origin URL for HTTP backend + --loki Enable Loki log sink for the CDN distribution + --loki-push-url string Push URL for log sink + --loki-username string Username for log sink + --monthly-limit-bytes int Monthly limit in bytes for the CDN distribution + --optimizer Enable optimizer for the CDN distribution (paid feature). + --regions strings Regions in which content should be cached, multiple of: ["EU" "US" "AF" "SA" "ASIA"] (default []) +``` + +### 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 cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + From dbfea3de96d1974b487c436d8fc1080d8288af03 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Mon, 1 Dec 2025 14:12:56 +0100 Subject: [PATCH 17/49] fix(cdn) linting issues --- .../beta/cdn/distribution/create/create.go | 10 ++--- .../distribution/describe/describe_test.go | 12 +++--- .../beta/cdn/distribution/update/update.go | 8 ++-- .../cdn/distribution/update/update_test.go | 1 - internal/cmd/beta/cdn/domain/create/create.go | 37 ------------------- internal/cmd/beta/cdn/domain/domain.go | 23 ------------ 6 files changed, 15 insertions(+), 76 deletions(-) delete mode 100644 internal/cmd/beta/cdn/domain/create/create.go delete mode 100644 internal/cmd/beta/cdn/domain/domain.go diff --git a/internal/cmd/beta/cdn/distribution/create/create.go b/internal/cmd/beta/cdn/distribution/create/create.go index 35c40ae45..4f83829d3 100644 --- a/internal/cmd/beta/cdn/distribution/create/create.go +++ b/internal/cmd/beta/cdn/distribution/create/create.go @@ -28,7 +28,7 @@ const ( flagHTTPOriginRequestHeaders = "http-origin-request-headers" flagBucket = "bucket" flagBucketURL = "bucket-url" - flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" + flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" //nolint:gosec // linter false positive flagBucketRegion = "bucket-region" flagBlockedCountries = "blocked-countries" flagBlockedIPs = "blocked-ips" @@ -91,7 +91,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { --regions AF,EU`, ), ), - PreRun: func(cmd *cobra.Command, args []string) { + PreRun: func(cmd *cobra.Command, _ []string) { // either flagHTTP or flagBucket must be set, depending on which we mark other flags as required if flags.FlagToBoolValue(params.Printer, cmd, flagHTTP) { err := cmd.MarkFlagRequired(flagHTTPOriginURL) @@ -265,7 +265,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } -func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]string { +func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]string { //nolint:gocritic // ptrToRefParam is nice here because of awkward SDK API geofencing := make(map[string][]string) for _, in := range geofencingInput { firstSpace := strings.IndexRune(in, ' ') @@ -285,7 +285,7 @@ func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]s return &geofencing } -func parseOriginRequestHeaders(p *print.Printer, originRequestHeadersInput []string) *map[string]string { +func parseOriginRequestHeaders(p *print.Printer, originRequestHeadersInput []string) *map[string]string { //nolint:gocritic // ptrToRefParam is nice here because of awkward SDK API originRequestHeaders := make(map[string]string) for _, in := range originRequestHeadersInput { parts := strings.Split(in, ":") @@ -358,7 +358,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClie return req.CreateDistributionPayload(*payload) } -func outputResult(p *print.Printer, outputFormat string, projectLabel string, resp *cdn.CreateDistributionResponse) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *cdn.CreateDistributionResponse) error { if resp == nil { return fmt.Errorf("create distribution response is nil") } diff --git a/internal/cmd/beta/cdn/distribution/describe/describe_test.go b/internal/cmd/beta/cdn/distribution/describe/describe_test.go index bcc67c13c..176cff523 100644 --- a/internal/cmd/beta/cdn/distribution/describe/describe_test.go +++ b/internal/cmd/beta/cdn/distribution/describe/describe_test.go @@ -299,16 +299,16 @@ func TestOutputResult(t *testing.T) { func(r *cdn.GetDistributionResponse) { r.Distribution.Waf = &cdn.DistributionWaf{ EnabledRules: &[]cdn.WafStatusRuleBlock{ - {utils.Ptr("rule-id-1")}, - {utils.Ptr("rule-id-2")}, + {Id: utils.Ptr("rule-id-1")}, + {Id: utils.Ptr("rule-id-2")}, }, DisabledRules: &[]cdn.WafStatusRuleBlock{ - {utils.Ptr("rule-id-3")}, - {utils.Ptr("rule-id-4")}, + {Id: utils.Ptr("rule-id-3")}, + {Id: utils.Ptr("rule-id-4")}, }, LogOnlyRules: &[]cdn.WafStatusRuleBlock{ - {utils.Ptr("rule-id-5")}, - {utils.Ptr("rule-id-6")}, + {Id: utils.Ptr("rule-id-5")}, + {Id: utils.Ptr("rule-id-6")}, }, } r.Distribution.Config.Backend = &cdn.ConfigBackend{ diff --git a/internal/cmd/beta/cdn/distribution/update/update.go b/internal/cmd/beta/cdn/distribution/update/update.go index cee590048..c7c65ff3c 100644 --- a/internal/cmd/beta/cdn/distribution/update/update.go +++ b/internal/cmd/beta/cdn/distribution/update/update.go @@ -29,7 +29,7 @@ const ( flagHTTPOriginRequestHeaders = "http-origin-request-headers" flagBucket = "bucket" flagBucketURL = "bucket-url" - flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" + flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" //nolint:gosec // linter false positive flagBucketRegion = "bucket-region" flagBlockedCountries = "blocked-countries" flagBlockedIPs = "blocked-ips" @@ -248,7 +248,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputMode } // TODO both parseGeofencing and parseOriginRequestHeaders copied from create.go, move to another package and make public? -func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]string { +func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]string { //nolint:gocritic // convenient for setting the SDK payload geofencing := make(map[string][]string) for _, in := range geofencingInput { firstSpace := strings.IndexRune(in, ' ') @@ -268,7 +268,7 @@ func parseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]s return &geofencing } -func parseOriginRequestHeaders(p *print.Printer, originRequestHeadersInput []string) *map[string]string { +func parseOriginRequestHeaders(p *print.Printer, originRequestHeadersInput []string) *map[string]string { //nolint:gocritic // convenient for setting the SDK payload originRequestHeaders := make(map[string]string) for _, in := range originRequestHeadersInput { parts := strings.Split(in, ":") @@ -361,7 +361,7 @@ func buildRequest(ctx context.Context, apiClient *cdn.APIClient, model *inputMod return req } -func outputResult(p *print.Printer, outputFormat string, projectLabel string, resp *cdn.PatchDistributionResponse) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *cdn.PatchDistributionResponse) error { if resp == nil { return fmt.Errorf("update distribution response is empty") } diff --git a/internal/cmd/beta/cdn/distribution/update/update_test.go b/internal/cmd/beta/cdn/distribution/update/update_test.go index 5ffea44e8..c815cc284 100644 --- a/internal/cmd/beta/cdn/distribution/update/update_test.go +++ b/internal/cmd/beta/cdn/distribution/update/update_test.go @@ -362,5 +362,4 @@ func TestOutputResult(t *testing.T) { } }) } - } diff --git a/internal/cmd/beta/cdn/domain/create/create.go b/internal/cmd/beta/cdn/domain/create/create.go deleted file mode 100644 index ca0335673..000000000 --- a/internal/cmd/beta/cdn/domain/create/create.go +++ /dev/null @@ -1,37 +0,0 @@ -package create - -import ( - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/examples" -) - -const ( - flagDistributionID = "distribution-id" - flagName = "name" -) - -func NewCmd(params *params.CmdParams) *cobra.Command { - cmd := &cobra.Command{ - Use: "create", - Short: "Create a CDN domain", - Long: "Create a new CDN domain associated with a CDN distribution.", - Args: cobra.NoArgs, - Example: examples.Build( - examples.NewExample( - `Create a CDN domain named "example.com" for distribution with ID "xxx"`, - `$ stackit beta cdn domain create --name example.com --distribution-id xxx`, - ), - ), - RunE: func(cmd *cobra.Command, args []string) error { - // TODO - return nil - }, - } - configureFlags(cmd) - return cmd -} - -func configureFlags(cmd *cobra.Command) { - // TODO -} diff --git a/internal/cmd/beta/cdn/domain/domain.go b/internal/cmd/beta/cdn/domain/domain.go deleted file mode 100644 index 5e1ae37ff..000000000 --- a/internal/cmd/beta/cdn/domain/domain.go +++ /dev/null @@ -1,23 +0,0 @@ -package domain - -import ( - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" -) - -func NewCommand(params *params.CmdParams) *cobra.Command { - cmd := &cobra.Command{ - Use: "domain", - Short: "Manage CDN domains", - Long: "Manage the lifecycle of CDN domains.", - Args: cobra.NoArgs, - Run: utils.CmdHelp, - } - addSubcommands(cmd, params) - return cmd -} - -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { - -} From 9d512ea019a7cafaa23b8a760904c6912ed008a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:52:08 +0100 Subject: [PATCH 18/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/secretsmanager (#1112) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d01af7966..a18134f88 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0 github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 - github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1 + github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2 github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 diff --git a/go.sum b/go.sum index 9b1277165..a5647892c 100644 --- a/go.sum +++ b/go.sum @@ -603,8 +603,8 @@ github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0 h1:en8Io github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0/go.mod h1:0wVdzrB4PhwhTE6drOVA6rETTmJ+k3eHa0neuxpDM/g= github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 h1:CPIuqhQw+oPt08I2WLsxJDoVDsPMW2VkvKW7/SlUv10= github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1/go.mod h1:tip0Ob6x06luy3CmRrmnCMtU5cha95fQLdvZlno3J4w= -github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1 h1:WKFzlHllql3JsVcAq+Y1m5pSMkvwp1qH3Vf2N7i8CPg= -github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1/go.mod h1:WGMFtGugBmUxI+nibI7eUZIQk4AGlDvwqX+m17W1y5w= +github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 h1:z7ZJtp742W6AgleV2eEXrJFZ7ai9rXu9V1Lkmir0drI= +github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2/go.mod h1:xm0ARtIbfzmqw8e8qThtrYdHHEkpuYvKt13SZGBoWSE= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2 h1:tfKC4Z6Uah9AQZrtCn/ytqOgc//ChQRfJ6ozxovgads= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2/go.mod h1:wV7/BUV3BCLq5+E1bHXrKKt/eOPVdWgLArWLAq7rZ/U= github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 h1:hcHX2n5pUsOcv2PPPbSJph1fQ/I6P7g7781T1f1ycEI= From cd065fda8a06ad920f567a22241ac38283984d61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:01:25 +0000 Subject: [PATCH 19/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/serverbackup (#1111) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a18134f88..5465e9eaf 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0 github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 - github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2 + github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 diff --git a/go.sum b/go.sum index a5647892c..ff8b45b8e 100644 --- a/go.sum +++ b/go.sum @@ -605,8 +605,8 @@ github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 h1:CPIuqhQw+oP github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1/go.mod h1:tip0Ob6x06luy3CmRrmnCMtU5cha95fQLdvZlno3J4w= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 h1:z7ZJtp742W6AgleV2eEXrJFZ7ai9rXu9V1Lkmir0drI= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2/go.mod h1:xm0ARtIbfzmqw8e8qThtrYdHHEkpuYvKt13SZGBoWSE= -github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2 h1:tfKC4Z6Uah9AQZrtCn/ytqOgc//ChQRfJ6ozxovgads= -github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2/go.mod h1:wV7/BUV3BCLq5+E1bHXrKKt/eOPVdWgLArWLAq7rZ/U= +github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 h1:LONKouoyrgDfu4Tu152YFjd5cw3SoG8P7PHdd8tsBOU= +github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3/go.mod h1:mCiDOqMHQKM9cFs5/GULaYz6Ni0QA2TTMQZJcj4GFUE= github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 h1:hcHX2n5pUsOcv2PPPbSJph1fQ/I6P7g7781T1f1ycEI= github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1/go.mod h1:jZwTg3wU4/UxgNJ7TKlFZ3dTIlnfvppnW8kJTc4UXy8= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 h1:crKlHl7QVF97A8CTBK3PBtVpO9c/7qwFvRJi5UN/F7Y= From c5d46b4b5c4cda87410c43dc4e3813b0c13dc0be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:19:02 +0100 Subject: [PATCH 20/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/ske (#1110) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5465e9eaf..9e2a8ad5a 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 - github.com/stackitcloud/stackit-sdk-go/services/ske v1.4.0 + github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 github.com/zalando/go-keyring v0.2.6 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 diff --git a/go.sum b/go.sum index ff8b45b8e..c4d8061b4 100644 --- a/go.sum +++ b/go.sum @@ -613,8 +613,8 @@ github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 h1:crKlHl github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1/go.mod h1:QCrAW/Rmf+styT25ke8cUV6hDHpdKNmAY14kkJ3+Fd8= github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 h1:s2iag/Gc4tuQH7x5I0n4mQWVhpfl/cj+SVNAFAB5ck0= github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2/go.mod h1:DFEamKVoOjm/rjMwzfZK0Zg/hwsSkXOibdA4HcC6swk= -github.com/stackitcloud/stackit-sdk-go/services/ske v1.4.0 h1:V6RFvybDeJvvmT3g7/BZodF0gozz3TEpahbpiTftbeY= -github.com/stackitcloud/stackit-sdk-go/services/ske v1.4.0/go.mod h1:xRBgpJ8P5Nf1T5tD0tGAeNg1FNQzx5VF7qqOXt2Fp3s= +github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 h1:bQk5qKid5Kv3fZ2miWlS5Dvo+cW90hbePaxOyWF67EE= +github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0/go.mod h1:/Ujlw+qo6RgKm69dD8y6MgmJFcUmrHjuJPO6VFoQX9U= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 h1:aW8ehdoNRaCEs3xDr+YnGb6pru8zZTB8f7kl5lozlJE= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2/go.mod h1:Jsry+gfhuXv2P0ldfa48BaL605NhDjdQMgaoV8czlbo= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= From 50ebc7c72b80917e37cd2cbb34e25ea78748537d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:27:53 +0000 Subject: [PATCH 21/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/serverupdate (#1109) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9e2a8ad5a..b1cd891a7 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 - github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 + github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 diff --git a/go.sum b/go.sum index c4d8061b4..0293a814e 100644 --- a/go.sum +++ b/go.sum @@ -607,8 +607,8 @@ github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 h1:z7ZJtp github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2/go.mod h1:xm0ARtIbfzmqw8e8qThtrYdHHEkpuYvKt13SZGBoWSE= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 h1:LONKouoyrgDfu4Tu152YFjd5cw3SoG8P7PHdd8tsBOU= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3/go.mod h1:mCiDOqMHQKM9cFs5/GULaYz6Ni0QA2TTMQZJcj4GFUE= -github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 h1:hcHX2n5pUsOcv2PPPbSJph1fQ/I6P7g7781T1f1ycEI= -github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1/go.mod h1:jZwTg3wU4/UxgNJ7TKlFZ3dTIlnfvppnW8kJTc4UXy8= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2 h1:ubaIeflBZ70evMhU5Xl4NzzDUTk0Z309jnv18OikXgs= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2/go.mod h1:NH6dyKSkJ0WezDgIpXr5PkhSGUXJvwEFcsQmeGScDs8= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 h1:crKlHl7QVF97A8CTBK3PBtVpO9c/7qwFvRJi5UN/F7Y= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1/go.mod h1:QCrAW/Rmf+styT25ke8cUV6hDHpdKNmAY14kkJ3+Fd8= github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 h1:s2iag/Gc4tuQH7x5I0n4mQWVhpfl/cj+SVNAFAB5ck0= From 13cb16c8c1516f192fdf80b8e8569d28651d52a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:39:36 +0000 Subject: [PATCH 22/49] chore(deps): bump renovatebot/github-action from 44.0.3 to 44.0.4 (#1113) --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 8c230c100..8551dd43e 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -13,7 +13,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 - name: Self-hosted Renovate - uses: renovatebot/github-action@v44.0.3 + uses: renovatebot/github-action@v44.0.4 with: configurationFile: .github/renovate.json token: ${{ secrets.RENOVATE_TOKEN }} From b950c0620d3840d43f4bf4a9b1544b596cddbad8 Mon Sep 17 00:00:00 2001 From: cgoetz-inovex Date: Tue, 25 Nov 2025 14:52:38 +0100 Subject: [PATCH 23/49] feat(kms) describe key, keyring, wrappingkey (#1107) * feat(kms) describe key, keyring, wrappingkey * fix(kms): do not reuse buffer during output tests * fix(kms) use constant Time for TestOutputResult for constant table widths --- docs/stackit_beta_kms_key.md | 1 + docs/stackit_beta_kms_key_describe.md | 41 ++++ docs/stackit_beta_kms_keyring.md | 1 + docs/stackit_beta_kms_keyring_describe.md | 40 ++++ docs/stackit_beta_kms_wrapping-key.md | 1 + .../stackit_beta_kms_wrapping-key_describe.md | 41 ++++ .../cmd/beta/kms/key/describe/describe.go | 131 +++++++++++ .../beta/kms/key/describe/describe_test.go | 222 ++++++++++++++++++ internal/cmd/beta/kms/key/key.go | 2 + .../cmd/beta/kms/keyring/describe/describe.go | 106 +++++++++ .../kms/keyring/describe/describe_test.go | 183 +++++++++++++++ internal/cmd/beta/kms/keyring/keyring.go | 2 + .../beta/kms/wrappingkey/describe/describe.go | 131 +++++++++++ .../kms/wrappingkey/describe/describe_test.go | 215 +++++++++++++++++ .../cmd/beta/kms/wrappingkey/wrappingkey.go | 2 + 15 files changed, 1119 insertions(+) create mode 100644 docs/stackit_beta_kms_key_describe.md create mode 100644 docs/stackit_beta_kms_keyring_describe.md create mode 100644 docs/stackit_beta_kms_wrapping-key_describe.md create mode 100644 internal/cmd/beta/kms/key/describe/describe.go create mode 100644 internal/cmd/beta/kms/key/describe/describe_test.go create mode 100644 internal/cmd/beta/kms/keyring/describe/describe.go create mode 100644 internal/cmd/beta/kms/keyring/describe/describe_test.go create mode 100644 internal/cmd/beta/kms/wrappingkey/describe/describe.go create mode 100644 internal/cmd/beta/kms/wrappingkey/describe/describe_test.go diff --git a/docs/stackit_beta_kms_key.md b/docs/stackit_beta_kms_key.md index 631808f53..a22f3d97b 100644 --- a/docs/stackit_beta_kms_key.md +++ b/docs/stackit_beta_kms_key.md @@ -32,6 +32,7 @@ stackit beta kms key [flags] * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS * [stackit beta kms key create](./stackit_beta_kms_key_create.md) - Creates a KMS key * [stackit beta kms key delete](./stackit_beta_kms_key_delete.md) - Deletes a KMS key +* [stackit beta kms key describe](./stackit_beta_kms_key_describe.md) - Describe a KMS key * [stackit beta kms key import](./stackit_beta_kms_key_import.md) - Import a KMS key * [stackit beta kms key list](./stackit_beta_kms_key_list.md) - List all KMS keys * [stackit beta kms key restore](./stackit_beta_kms_key_restore.md) - Restore a key diff --git a/docs/stackit_beta_kms_key_describe.md b/docs/stackit_beta_kms_key_describe.md new file mode 100644 index 000000000..05e876491 --- /dev/null +++ b/docs/stackit_beta_kms_key_describe.md @@ -0,0 +1,41 @@ +## stackit beta kms key describe + +Describe a KMS key + +### Synopsis + +Describe a KMS key + +``` +stackit beta kms key describe KEY_ID [flags] +``` + +### Examples + +``` + Describe a KMS key with ID xxx of keyring yyy + $ stackit beta kms key describe xxx --keyring-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta kms key describe" + --keyring-id string Key Ring ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --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 kms key](./stackit_beta_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_beta_kms_keyring.md b/docs/stackit_beta_kms_keyring.md index 6e65f3a47..2d87f99d3 100644 --- a/docs/stackit_beta_kms_keyring.md +++ b/docs/stackit_beta_kms_keyring.md @@ -32,5 +32,6 @@ stackit beta kms keyring [flags] * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS * [stackit beta kms keyring create](./stackit_beta_kms_keyring_create.md) - Creates a KMS key ring * [stackit beta kms keyring delete](./stackit_beta_kms_keyring_delete.md) - Deletes a KMS key ring +* [stackit beta kms keyring describe](./stackit_beta_kms_keyring_describe.md) - Describe a KMS key ring * [stackit beta kms keyring list](./stackit_beta_kms_keyring_list.md) - Lists all KMS key rings diff --git a/docs/stackit_beta_kms_keyring_describe.md b/docs/stackit_beta_kms_keyring_describe.md new file mode 100644 index 000000000..9b1381dc0 --- /dev/null +++ b/docs/stackit_beta_kms_keyring_describe.md @@ -0,0 +1,40 @@ +## stackit beta kms keyring describe + +Describe a KMS key ring + +### Synopsis + +Describe a KMS key ring + +``` +stackit beta kms keyring describe KEYRING_ID [flags] +``` + +### Examples + +``` + Describe a KMS key ring with ID xxx + $ stackit beta kms keyring describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta kms keyring 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 kms keyring](./stackit_beta_kms_keyring.md) - Manage KMS key rings + diff --git a/docs/stackit_beta_kms_wrapping-key.md b/docs/stackit_beta_kms_wrapping-key.md index c10cb4946..2cef6b863 100644 --- a/docs/stackit_beta_kms_wrapping-key.md +++ b/docs/stackit_beta_kms_wrapping-key.md @@ -32,5 +32,6 @@ stackit beta kms wrapping-key [flags] * [stackit beta kms](./stackit_beta_kms.md) - Provides functionality for KMS * [stackit beta kms wrapping-key create](./stackit_beta_kms_wrapping-key_create.md) - Creates a KMS wrapping key * [stackit beta kms wrapping-key delete](./stackit_beta_kms_wrapping-key_delete.md) - Deletes a KMS wrapping key +* [stackit beta kms wrapping-key describe](./stackit_beta_kms_wrapping-key_describe.md) - Describe a KMS wrapping key * [stackit beta kms wrapping-key list](./stackit_beta_kms_wrapping-key_list.md) - Lists all KMS wrapping keys diff --git a/docs/stackit_beta_kms_wrapping-key_describe.md b/docs/stackit_beta_kms_wrapping-key_describe.md new file mode 100644 index 000000000..6e82cd595 --- /dev/null +++ b/docs/stackit_beta_kms_wrapping-key_describe.md @@ -0,0 +1,41 @@ +## stackit beta kms wrapping-key describe + +Describe a KMS wrapping key + +### Synopsis + +Describe a KMS wrapping key + +``` +stackit beta kms wrapping-key describe WRAPPING_KEY_ID [flags] +``` + +### Examples + +``` + Describe a KMS wrapping key with ID xxx of keyring yyy + $ stackit beta kms wrappingkey describe xxx --keyring-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta kms wrapping-key describe" + --keyring-id string Key Ring ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --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 kms wrapping-key](./stackit_beta_kms_wrapping-key.md) - Manage KMS wrapping keys + diff --git a/internal/cmd/beta/kms/key/describe/describe.go b/internal/cmd/beta/kms/key/describe/describe.go new file mode 100644 index 000000000..113cf96de --- /dev/null +++ b/internal/cmd/beta/kms/key/describe/describe.go @@ -0,0 +1,131 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +const ( + argKeyID = "KEY_ID" + flagKeyRingID = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyID string + KeyRingID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argKeyID), + Short: "Describe a KMS key", + Long: "Describe a KMS key", + Args: args.SingleArg(argKeyID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS key with ID xxx of keyring yyy`, + `$ stackit beta kms key describe xxx --keyring-id yyy`, + ), + ), + 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("get key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID") + err := flags.MarkFlagsRequired(cmd, flagKeyRingID) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + KeyID: args[0], + KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRequest { + return apiClient.GetKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.KeyID) +} + +func outputResult(p *print.Printer, outputFormat string, key *kms.Key) error { + if key == nil { + return fmt.Errorf("key response is empty") + } + return p.OutputResult(outputFormat, key, func() error { + table := tables.NewTable() + table.AddRow("ID", utils.PtrString(key.Id)) + table.AddSeparator() + table.AddRow("DISPLAY NAME", utils.PtrString(key.DisplayName)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.PtrString(key.CreatedAt)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(key.State)) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(key.Description)) + table.AddSeparator() + table.AddRow("ACCESS SCOPE", utils.PtrString(key.AccessScope)) + table.AddSeparator() + table.AddRow("ALGORITHM", utils.PtrString(key.Algorithm)) + table.AddSeparator() + table.AddRow("DELETION DATE", utils.PtrString(key.DeletionDate)) + table.AddSeparator() + table.AddRow("IMPORT ONLY", utils.PtrString(key.ImportOnly)) + table.AddSeparator() + table.AddRow("KEYRING ID", utils.PtrString(key.KeyRingId)) + table.AddSeparator() + table.AddRow("PROTECTION", utils.PtrString(key.Protection)) + table.AddSeparator() + table.AddRow("PURPOSE", utils.PtrString(key.Purpose)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/kms/key/describe/describe_test.go b/internal/cmd/beta/kms/key/describe/describe_test.go new file mode 100644 index 000000000..6abb30a09 --- /dev/null +++ b/internal/cmd/beta/kms/key/describe/describe_test.go @@ -0,0 +1,222 @@ +package describe + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testKeyID = uuid.NewString() +var testTime = time.Time{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + flagKeyRingID: testKeyRingID, + } + 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, + }, + KeyID: testKeyID, + KeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testKeyID}, + 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: []string{testKeyID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing key ring id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, flagKeyRingID) }), + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { + m[flagKeyRingID] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { m[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) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.GetKey(testCtx, testProjectId, testRegion, testKeyRingID, testKeyID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.Key + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.Key{ + AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), + Algorithm: utils.Ptr(kms.ALGORITHM_AES_256_GCM), + CreatedAt: utils.Ptr(testTime), + DeletionDate: nil, + Description: utils.Ptr("very secure and secret key"), + DisplayName: utils.Ptr("Test Key"), + Id: utils.Ptr(testKeyID), + ImportOnly: utils.Ptr(true), + KeyRingId: utils.Ptr(testKeyRingID), + Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), + Purpose: utils.Ptr(kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT), + State: utils.Ptr(kms.KEYSTATE_ACTIVE), + }, + expected: fmt.Sprintf(` + ID │ %-37s +───────────────┼────────────────────────────────────── + DISPLAY NAME │ Test Key +───────────────┼────────────────────────────────────── + CREATED AT │ %-37s +───────────────┼────────────────────────────────────── + STATE │ active +───────────────┼────────────────────────────────────── + DESCRIPTION │ very secure and secret key +───────────────┼────────────────────────────────────── + ACCESS SCOPE │ PUBLIC +───────────────┼────────────────────────────────────── + ALGORITHM │ aes_256_gcm +───────────────┼────────────────────────────────────── + DELETION DATE │ +───────────────┼────────────────────────────────────── + IMPORT ONLY │ true +───────────────┼────────────────────────────────────── + KEYRING ID │ %-37s +───────────────┼────────────────────────────────────── + PROTECTION │ software +───────────────┼────────────────────────────────────── + PURPOSE │ symmetric_encrypt_decrypt + +`, + testKeyID, + testTime, + testKeyRingID, + ), + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var buf bytes.Buffer + p.Cmd.SetOut(&buf) + if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(buf.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/kms/key/key.go b/internal/cmd/beta/kms/key/key.go index 4b2f7d8fa..b6ff239bc 100644 --- a/internal/cmd/beta/kms/key/key.go +++ b/internal/cmd/beta/kms/key/key.go @@ -3,6 +3,7 @@ package key import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/importKey" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/list" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/key/restore" @@ -33,4 +34,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(restore.NewCmd(params)) cmd.AddCommand(rotate.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) } diff --git a/internal/cmd/beta/kms/keyring/describe/describe.go b/internal/cmd/beta/kms/keyring/describe/describe.go new file mode 100644 index 000000000..f9dc11d0a --- /dev/null +++ b/internal/cmd/beta/kms/keyring/describe/describe.go @@ -0,0 +1,106 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +const ( + argKeyRingID = "KEYRING_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argKeyRingID), + Short: "Describe a KMS key ring", + Long: "Describe a KMS key ring", + Args: args.SingleArg(argKeyRingID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS key ring with ID xxx`, + `$ stackit beta kms keyring describe 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 + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get key ring: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + KeyRingID: args[0], + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRingRequest { + return apiClient.GetKeyRing(ctx, model.ProjectId, model.Region, model.KeyRingID) +} + +func outputResult(p *print.Printer, outputFormat string, keyRing *kms.KeyRing) error { + if keyRing == nil { + return fmt.Errorf("key ring response is empty") + } + return p.OutputResult(outputFormat, keyRing, func() error { + table := tables.NewTable() + table.AddRow("ID", utils.PtrString(keyRing.Id)) + table.AddSeparator() + table.AddRow("DISPLAY NAME", utils.PtrString(keyRing.DisplayName)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.PtrString(keyRing.CreatedAt)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(keyRing.State)) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(keyRing.Description)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/kms/keyring/describe/describe_test.go b/internal/cmd/beta/kms/keyring/describe/describe_test.go new file mode 100644 index 000000000..bdc3caa88 --- /dev/null +++ b/internal/cmd/beta/kms/keyring/describe/describe_test.go @@ -0,0 +1,183 @@ +package describe + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testTime = time.Time{} + +const testRegion = "eu01" + +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, + }, + KeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testKeyRingID}, + 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: []string{testKeyRingID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{"!invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testKeyRingID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testKeyRingID}, + flagValues: fixtureFlagValues(func(m map[string]string) { m[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) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.GetKeyRing(testCtx, testProjectId, testRegion, testKeyRingID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.KeyRing + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.KeyRing{ + Id: utils.Ptr(testKeyRingID), + DisplayName: utils.Ptr("Test Key Ring"), + CreatedAt: utils.Ptr(testTime), + Description: utils.Ptr("This is a test key ring."), + State: utils.Ptr(kms.KEYRINGSTATE_ACTIVE), + }, + expected: fmt.Sprintf(` + ID │ %-37s +──────────────┼────────────────────────────────────── + DISPLAY NAME │ Test Key Ring +──────────────┼────────────────────────────────────── + CREATED AT │ %-37s +──────────────┼────────────────────────────────────── + STATE │ active +──────────────┼────────────────────────────────────── + DESCRIPTION │ This is a test key ring. + +`, + testKeyRingID, + testTime, + ), + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var buf bytes.Buffer + p.Cmd.SetOut(&buf) + if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(buf.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/kms/keyring/keyring.go b/internal/cmd/beta/kms/keyring/keyring.go index 7a42ce131..f948fae41 100644 --- a/internal/cmd/beta/kms/keyring/keyring.go +++ b/internal/cmd/beta/kms/keyring/keyring.go @@ -3,6 +3,7 @@ package keyring import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/keyring/list" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -27,4 +28,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) } diff --git a/internal/cmd/beta/kms/wrappingkey/describe/describe.go b/internal/cmd/beta/kms/wrappingkey/describe/describe.go new file mode 100644 index 000000000..2c25a288e --- /dev/null +++ b/internal/cmd/beta/kms/wrappingkey/describe/describe.go @@ -0,0 +1,131 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +const ( + argWrappingKeyID = "WRAPPING_KEY_ID" + flagKeyRingID = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + WrappingKeyID string + KeyRingID string +} + +func NewCmd(params *params.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argWrappingKeyID), + Short: "Describe a KMS wrapping key", + Long: "Describe a KMS wrapping key", + Args: args.SingleArg(argWrappingKeyID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS wrapping key with ID xxx of keyring yyy`, + `$ stackit beta kms wrappingkey describe xxx --keyring-id yyy`, + ), + ), + 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("get wrapping key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID") + err := flags.MarkFlagsRequired(cmd, flagKeyRingID) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, args []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + WrappingKeyID: args[0], + KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetWrappingKeyRequest { + return apiClient.GetWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.WrappingKeyID) +} + +func outputResult(p *print.Printer, outputFormat string, wrappingKey *kms.WrappingKey) error { + if wrappingKey == nil { + return fmt.Errorf("wrapping key response is empty") + } + return p.OutputResult(outputFormat, wrappingKey, func() error { + table := tables.NewTable() + table.AddRow("ID", utils.PtrString(wrappingKey.Id)) + table.AddSeparator() + table.AddRow("DISPLAY NAME", utils.PtrString(wrappingKey.DisplayName)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.PtrString(wrappingKey.CreatedAt)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(wrappingKey.State)) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(wrappingKey.Description)) + table.AddSeparator() + table.AddRow("ACCESS SCOPE", utils.PtrString(wrappingKey.AccessScope)) + table.AddSeparator() + table.AddRow("ALGORITHM", utils.PtrString(wrappingKey.Algorithm)) + table.AddSeparator() + table.AddRow("EXPIRES AT", utils.PtrString(wrappingKey.ExpiresAt)) + table.AddSeparator() + table.AddRow("KEYRING ID", utils.PtrString(wrappingKey.KeyRingId)) + table.AddSeparator() + table.AddRow("PROTECTION", utils.PtrString(wrappingKey.Protection)) + table.AddSeparator() + table.AddRow("PUBLIC KEY", utils.PtrString(wrappingKey.PublicKey)) + table.AddSeparator() + table.AddRow("PURPOSE", utils.PtrString(wrappingKey.Purpose)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/kms/wrappingkey/describe/describe_test.go b/internal/cmd/beta/kms/wrappingkey/describe/describe_test.go new file mode 100644 index 000000000..9589b33fb --- /dev/null +++ b/internal/cmd/beta/kms/wrappingkey/describe/describe_test.go @@ -0,0 +1,215 @@ +package describe + +import ( + "bytes" + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "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/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/kms" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testWrappingKeyID = uuid.NewString() +var testTime = time.Time{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + flagKeyRingID: testKeyRingID, + } + 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, + }, + KeyRingID: testKeyRingID, + WrappingKeyID: testWrappingKeyID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testWrappingKeyID}, + 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: []string{testWrappingKeyID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[flagKeyRingID] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testWrappingKeyID}, + 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) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.GetWrappingKey(testCtx, testProjectId, testRegion, testKeyRingID, testWrappingKeyID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.WrappingKey + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.WrappingKey{ + Id: utils.Ptr(testWrappingKeyID), + DisplayName: utils.Ptr("Test Key Ring"), + CreatedAt: utils.Ptr(testTime), + Description: utils.Ptr("This is a test key ring."), + State: utils.Ptr(kms.WRAPPINGKEYSTATE_ACTIVE), + AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), + Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), + ExpiresAt: utils.Ptr(testTime), + KeyRingId: utils.Ptr(testKeyRingID), + Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), + PublicKey: utils.Ptr("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ...\n-----END PUBLIC KEY-----"), + Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), + }, + expected: fmt.Sprintf(` + ID │ %-46s +──────────────┼─────────────────────────────────────────────── + DISPLAY NAME │ Test Key Ring +──────────────┼─────────────────────────────────────────────── + CREATED AT │ %-46s +──────────────┼─────────────────────────────────────────────── + STATE │ active +──────────────┼─────────────────────────────────────────────── + DESCRIPTION │ This is a test key ring. +──────────────┼─────────────────────────────────────────────── + ACCESS SCOPE │ PUBLIC +──────────────┼─────────────────────────────────────────────── + ALGORITHM │ rsa_2048_oaep_sha256 +──────────────┼─────────────────────────────────────────────── + EXPIRES AT │ %-46s +──────────────┼─────────────────────────────────────────────── + KEYRING ID │ %-46s +──────────────┼─────────────────────────────────────────────── + PROTECTION │ software +──────────────┼─────────────────────────────────────────────── + PUBLIC KEY │ -----BEGIN PUBLIC KEY----- + │ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ... + │ -----END PUBLIC KEY----- +──────────────┼─────────────────────────────────────────────── + PURPOSE │ wrap_asymmetric_key + +`, + testWrappingKeyID, + testTime, + testTime, + testKeyRingID), + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + var buf bytes.Buffer + p.Cmd.SetOut(&buf) + if err := outputResult(p, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(buf.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/kms/wrappingkey/wrappingkey.go b/internal/cmd/beta/kms/wrappingkey/wrappingkey.go index 00184a521..168808e37 100644 --- a/internal/cmd/beta/kms/wrappingkey/wrappingkey.go +++ b/internal/cmd/beta/kms/wrappingkey/wrappingkey.go @@ -3,6 +3,7 @@ package wrappingkey import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/create" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/kms/wrappingkey/list" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -27,4 +28,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) } From 0b693ecb13f6406baf60a6a7ca79c4205ad591cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:21:24 +0000 Subject: [PATCH 24/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/rabbitmq (#1118) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b1cd891a7..0dfda1607 100644 --- a/go.mod +++ b/go.mod @@ -251,7 +251,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1 github.com/stackitcloud/stackit-sdk-go/services/observability v0.15.1 - github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1 + github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2 github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 0293a814e..0f794ed62 100644 --- a/go.sum +++ b/go.sum @@ -595,8 +595,8 @@ github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.2 h1:J9WP0lBoqm github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.2/go.mod h1:QenOJF1LD39d/arGFGZFCzHoQuwF6VuWCvS8CbdoMBw= github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 h1:K8vXele3U6b5urcSIpq21EkVblWfPDY3eMPSuQ48TkI= github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1/go.mod h1:hyhw+I19NtjKmRLcUkY4boaTxnYSPFGbpn4RxvGqH2s= -github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1 h1:ALrDCBih8Fu8e6530KdOjuH0iMxOLntO381BbKFlTFY= -github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1/go.mod h1:+qGWSehoV0Js3FalgvT/bOgPj+UqW4I7lP5s8uAxP+o= +github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2 h1:Ww0baLTiZha4H1thfEEsDq+O0Ce0hNhdbkJ5eDdGEoE= +github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2/go.mod h1:lPz9iQ3kLvpzPR7jt6P1VJyjSumo2+D1i3RkjFGpVTI= github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 h1:8uPt82Ez34OYMOijjEYxB1zUW6kiybkt6veQKl0AL68= github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1/go.mod h1:1Y2GEICmZDt+kr8aGnBx/sjYVAIYHmtfC8xYi9oxNEE= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0 h1:en8IomJeGqZZgGOyLH53PNDCv2F/x4zURz4mGSlhTKs= From d29f75aafc9d69667003936b959e62bc176b7843 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:31:04 +0000 Subject: [PATCH 25/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/alb (#1117) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0dfda1607..d5fdd45eb 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stackitcloud/stackit-sdk-go/core v0.20.0 - github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1 + github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 diff --git a/go.sum b/go.sum index 0f794ed62..11b53ba9d 100644 --- a/go.sum +++ b/go.sum @@ -563,8 +563,8 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stackitcloud/stackit-sdk-go/core v0.20.0 h1:4rrUk6uT1g4nOn5/g1uXukP07Tux/o5xbMz/f/qE1rY= github.com/stackitcloud/stackit-sdk-go/core v0.20.0/go.mod h1:fqto7M82ynGhEnpZU6VkQKYWYoFG5goC076JWXTUPRQ= -github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1 h1:DaJkEN/6l+AJEQ3Dr+3IdKM4jywDQsTvuYHmRvFj3ho= -github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.1/go.mod h1:SzA+UsSNv4D9IvNT7hwYPewgAvUgj5WXIU2tZ0XaMBI= +github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2 h1:x7ndqw6yaOw+TmThNeAkI+eN9vK5hWgjIJlFZrYPREo= +github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2/go.mod h1:wbPNu6e5r/5xhzznCKbC7fEJahrAOb89gmaIm+0w2/s= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 h1:CiOlfCsCDwHP0kas7qyhfp5XtL2kVmn9e4wjtc3LO10= From e95f9876ba63574d96ff12237404e344b71aecb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:59:26 +0100 Subject: [PATCH 26/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/mongodbflex (#1116) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d5fdd45eb..51225d626 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0 github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0 - github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2 + github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.2 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0 diff --git a/go.sum b/go.sum index 11b53ba9d..3619a3d63 100644 --- a/go.sum +++ b/go.sum @@ -585,8 +585,8 @@ github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2 h1:g3xzRqwul8W638g github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2/go.mod h1:OlGmMlXKp33ZYpUm9TqaLYf8SdzhDW5uBKcbgq1zXOk= github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 h1:Db/ebOL2vbpIeh5XB2Ews2B9Lj5DJlMWIEJh60FfZ4Y= github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1/go.mod h1:8jdN4v2euK3f9gfdzbRi8e4nBJ8g/Q5YF9aPB4M4fCQ= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2 h1:BQ+qAkVS/aGHepE/+gVsvSg1sRkPOyIUI/jkCyUOrWg= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2/go.mod h1:oc8Mpwl7O6EZwG0YxfhOzNCJwNQBWK5rFh764OtxoMY= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3 h1:tGa+NcjNKTWvChN+0OMdLomb9Jod4MmY6YAiPTJMgfo= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3/go.mod h1:ciuOzwN5GcqplRy95fXRaS44dFmhfNxvmzTl/ALwV/k= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1 h1:I9B/zUU7R74xuH/ztcPrDIuMp2KV3QQMjeE7lFudboM= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1/go.mod h1:h4aX5tyTQoO6KLrugkvfkqgKTjIzh7e4q9N92kT5OBs= github.com/stackitcloud/stackit-sdk-go/services/observability v0.15.1 h1:zk+47GhutK2ajO4Yiek0laGm2PdXvY8BvFZc8yHFnSE= From be30de57378c77cf9ed79132995cc6c47f4608e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 09:07:54 +0000 Subject: [PATCH 27/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/serviceaccount (#1115) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 51225d626..c9fcf933a 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2 - github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 + github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2 github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 diff --git a/go.sum b/go.sum index 3619a3d63..1d3c78e5b 100644 --- a/go.sum +++ b/go.sum @@ -609,8 +609,8 @@ github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 h1:LONKouoyr github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3/go.mod h1:mCiDOqMHQKM9cFs5/GULaYz6Ni0QA2TTMQZJcj4GFUE= github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2 h1:ubaIeflBZ70evMhU5Xl4NzzDUTk0Z309jnv18OikXgs= github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2/go.mod h1:NH6dyKSkJ0WezDgIpXr5PkhSGUXJvwEFcsQmeGScDs8= -github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1 h1:crKlHl7QVF97A8CTBK3PBtVpO9c/7qwFvRJi5UN/F7Y= -github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.1/go.mod h1:QCrAW/Rmf+styT25ke8cUV6hDHpdKNmAY14kkJ3+Fd8= +github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2 h1:+S5yPftGLH99ByzDCwzdI927bvKOKMQxMkd/tuPeQTE= +github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2/go.mod h1:gaHXopzXPDP1AmquUVhMmz9opAr2QYVBL0XbBdPtB7s= github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 h1:s2iag/Gc4tuQH7x5I0n4mQWVhpfl/cj+SVNAFAB5ck0= github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2/go.mod h1:DFEamKVoOjm/rjMwzfZK0Zg/hwsSkXOibdA4HcC6swk= github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 h1:bQk5qKid5Kv3fZ2miWlS5Dvo+cW90hbePaxOyWF67EE= From f6a0b1a42594383bc9d1eb50ad790484153a3132 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:08:40 +0100 Subject: [PATCH 28/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/serviceenablement (#1123) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c9fcf933a..9762060dc 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2 - github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 + github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.3 github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 github.com/zalando/go-keyring v0.2.6 diff --git a/go.sum b/go.sum index 1d3c78e5b..c266f8d58 100644 --- a/go.sum +++ b/go.sum @@ -611,8 +611,8 @@ github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2 h1:ubaIeflBZ github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2/go.mod h1:NH6dyKSkJ0WezDgIpXr5PkhSGUXJvwEFcsQmeGScDs8= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2 h1:+S5yPftGLH99ByzDCwzdI927bvKOKMQxMkd/tuPeQTE= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2/go.mod h1:gaHXopzXPDP1AmquUVhMmz9opAr2QYVBL0XbBdPtB7s= -github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 h1:s2iag/Gc4tuQH7x5I0n4mQWVhpfl/cj+SVNAFAB5ck0= -github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2/go.mod h1:DFEamKVoOjm/rjMwzfZK0Zg/hwsSkXOibdA4HcC6swk= +github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.3 h1:zcfL+rpQZWXZazL8w8DqXYxGbIOInaUc155BWTshNRA= +github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.3/go.mod h1:icu5WtsZ8c57/pUrXeFLmZu29Qhwr/rsjTkVRWJYTqY= github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 h1:bQk5qKid5Kv3fZ2miWlS5Dvo+cW90hbePaxOyWF67EE= github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0/go.mod h1:/Ujlw+qo6RgKm69dD8y6MgmJFcUmrHjuJPO6VFoQX9U= github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 h1:aW8ehdoNRaCEs3xDr+YnGb6pru8zZTB8f7kl5lozlJE= From 8faa48d7bc2801ed738e486960db9e693463ad85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:56:42 +0100 Subject: [PATCH 29/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/dns (#1122) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9762060dc..b098be738 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 - github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 + github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2 github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0 github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0 diff --git a/go.sum b/go.sum index c266f8d58..9075be44f 100644 --- a/go.sum +++ b/go.sum @@ -569,8 +569,8 @@ github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 h1:CiOlfCsCDwHP0kas7qyhfp5XtL2kVmn9e4wjtc3LO10= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1/go.mod h1:PyZ6g9JsGZZyeISAF+5E7L1lAlMnmbl2YbPj5Teu8to= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 h1:CnhAMLql0MNmAeq4roQKN8OpSKX4FSgTU6Eu6detB4I= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1/go.mod h1:7Bx85knfNSBxulPdJUFuBePXNee3cO+sOTYnUG6M+iQ= +github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2 h1:x1i5rqhEVuUPq5M0eb68ZD2KL1C8OFD8RG2sWMQGL6o= +github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2/go.mod h1:nOdpHeRWeiPlioOGovHzLpojlilbxAxoXsAy+TiOpw4= github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 h1:RgWfaWDY8ZGZp5gEBe/A1r7s5NCRuLiYuHhscH6Ej9U= github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1/go.mod h1:HgScss2f+/k/wDxcsM1K8SzLgDso/EZyhoJC2eZ+tFA= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0 h1:bkvq3Y4OcGyCR5N69tyY7ZTcrVN+htGTa2ZBJL5BQTQ= From 8cda53baf13d640073f94f5c6967c649bedf5f37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 09:05:38 +0000 Subject: [PATCH 30/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/iaas (#1121) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index b098be738..9e6790227 100644 --- a/go.mod +++ b/go.mod @@ -21,12 +21,12 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2 github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 - github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0 + github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.2 github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.2 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 - github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0 + github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1 github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 diff --git a/go.sum b/go.sum index 9075be44f..2b8643324 100644 --- a/go.sum +++ b/go.sum @@ -573,8 +573,8 @@ github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2 h1:x1i5rqhEVuUPq5M0e github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2/go.mod h1:nOdpHeRWeiPlioOGovHzLpojlilbxAxoXsAy+TiOpw4= github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 h1:RgWfaWDY8ZGZp5gEBe/A1r7s5NCRuLiYuHhscH6Ej9U= github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1/go.mod h1:HgScss2f+/k/wDxcsM1K8SzLgDso/EZyhoJC2eZ+tFA= -github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0 h1:bkvq3Y4OcGyCR5N69tyY7ZTcrVN+htGTa2ZBJL5BQTQ= -github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.0/go.mod h1:tg1rHvgFRG9gNCYnTepZFaSCBahhLLjkIb3nRbtyjDQ= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.2 h1:afGHMCqBM/E/FPUvbfSTFb9ddI+eDm2a7DpWPCkxMzs= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.2/go.mod h1:/DlO7+cOqyYKROIxkBYIUdMoEfFevkVXhsShglxyUOQ= github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0 h1:OdY3eXn98hWIhZBH8qQTFsYP6cag8B29UdwTjMwAjfw= github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0/go.mod h1:mf+DQwwTVfc8MD0vwTNPzin2unKAIyQRYywv3wUyH38= github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1 h1:F/2qLBATi0nDjKR8EGbsmSX9CLFp3nBcWV8JAeTz4p8= @@ -599,8 +599,8 @@ github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2 h1:Ww0baLTiZha4 github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2/go.mod h1:lPz9iQ3kLvpzPR7jt6P1VJyjSumo2+D1i3RkjFGpVTI= github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 h1:8uPt82Ez34OYMOijjEYxB1zUW6kiybkt6veQKl0AL68= github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1/go.mod h1:1Y2GEICmZDt+kr8aGnBx/sjYVAIYHmtfC8xYi9oxNEE= -github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0 h1:en8IomJeGqZZgGOyLH53PNDCv2F/x4zURz4mGSlhTKs= -github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.0/go.mod h1:0wVdzrB4PhwhTE6drOVA6rETTmJ+k3eHa0neuxpDM/g= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1 h1:KDa5sy6NSzMOXaf4a9skxOm8oUoleI45fLbD3ww7qsc= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1/go.mod h1:+k3iHkWpehO+FLC5WsW7eGhYdNjDklYqRcpIxQBLbZg= github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 h1:CPIuqhQw+oPt08I2WLsxJDoVDsPMW2VkvKW7/SlUv10= github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1/go.mod h1:tip0Ob6x06luy3CmRrmnCMtU5cha95fQLdvZlno3J4w= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 h1:z7ZJtp742W6AgleV2eEXrJFZ7ai9rXu9V1Lkmir0drI= From 4ae1761891f01c1becb90b31611dbba4c1ff7aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20H=C3=B6nle?= Date: Thu, 27 Nov 2025 11:11:46 +0100 Subject: [PATCH 31/49] fix(alb): print valid JSON/YAML output for list cmds (#1045) relates to STACKITCLI-273 / #893 --- internal/cmd/beta/alb/list/list.go | 25 +++++++++---------- internal/cmd/beta/alb/list/list_test.go | 20 +++++++++------ .../observability-credentials/list/list.go | 18 ++++++------- internal/cmd/beta/alb/plans/plans.go | 18 ++++++------- internal/cmd/beta/alb/plans/plans_test.go | 6 +++-- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/internal/cmd/beta/alb/list/list.go b/internal/cmd/beta/alb/list/list.go index 2c4d8924d..292e2e3ae 100644 --- a/internal/cmd/beta/alb/list/list.go +++ b/internal/cmd/beta/alb/list/list.go @@ -25,8 +25,7 @@ type inputModel struct { } const ( - labelSelectorFlag = "label-selector" - limitFlag = "limit" + limitFlag = "limit" ) func NewCmd(params *params.CmdParams) *cobra.Command { @@ -73,19 +72,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list load balancerse: %w", err) } + items := response.GetLoadBalancers() - if items := response.LoadBalancers; items == nil || len(*items) == 0 { - params.Printer.Info("No load balancers found for project %q", projectLabel) - } else { - if model.Limit != nil && len(*items) > int(*model.Limit) { - *items = (*items)[:*model.Limit] - } - if err := outputResult(params.Printer, model.OutputFormat, *items); err != nil { - return fmt.Errorf("output loadbalancers: %w", err) - } + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] } - return nil + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } @@ -125,8 +119,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClie return request } -func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalancer) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []alb.LoadBalancer) error { return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No load balancers found for project %q", projectLabel) + return nil + } + table := tables.NewTable() table.SetHeader("NAME", "EXTERNAL ADDRESS", "REGION", "STATUS", "VERSION", "ERRORS") for i := range items { diff --git a/internal/cmd/beta/alb/list/list_test.go b/internal/cmd/beta/alb/list/list_test.go index c623ea0b8..5ffc642e2 100644 --- a/internal/cmd/beta/alb/list/list_test.go +++ b/internal/cmd/beta/alb/list/list_test.go @@ -5,6 +5,8 @@ import ( "strconv" "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -19,11 +21,14 @@ import ( type testCtxKey struct{} var ( - testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &alb.APIClient{} - testProjectId = uuid.NewString() - testRegion = "eu01" - testLimit int64 = 10 + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &alb.APIClient{} + testProjectId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testLimit int64 = 10 ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -41,7 +46,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, - Limit: &testLimit, + Limit: utils.Ptr(testLimit), } for _, mod := range mods { mod(model) @@ -136,6 +141,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []alb.LoadBalancer } tests := []struct { @@ -164,7 +170,7 @@ func Test_outputResult(t *testing.T) { p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/observability-credentials/list/list.go b/internal/cmd/beta/alb/observability-credentials/list/list.go index 961455b53..51347cae2 100644 --- a/internal/cmd/beta/alb/observability-credentials/list/list.go +++ b/internal/cmd/beta/alb/observability-credentials/list/list.go @@ -68,13 +68,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list credentials: %w", err) } + items := resp.GetCredentials() - if resp.Credentials == nil || len(*resp.Credentials) == 0 { - params.Printer.Info("No credentials found\n") - return nil - } - - items := *resp.Credentials + // Truncate output if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } @@ -116,12 +112,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClie } func outputResult(p *print.Printer, outputFormat string, items []alb.CredentialsResponse) error { - if items == nil { - p.Outputln("no credentials found") - return nil - } - return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No credentials found\n") + return nil + } + table := tables.NewTable() table.SetHeader("CREDENTIAL REF", "DISPLAYNAME", "USERNAME", "REGION") diff --git a/internal/cmd/beta/alb/plans/plans.go b/internal/cmd/beta/alb/plans/plans.go index 6bb5e01fc..0724860a1 100644 --- a/internal/cmd/beta/alb/plans/plans.go +++ b/internal/cmd/beta/alb/plans/plans.go @@ -62,16 +62,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list plans: %w", err) } + items := response.GetValidPlans() - if items := response.ValidPlans; items == nil || len(*items) == 0 { - params.Printer.Info("No plans found for project %q", projectLabel) - } else { - if err := outputResult(params.Printer, model.OutputFormat, *items); err != nil { - return fmt.Errorf("output plans: %w", err) - } - } - - return nil + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } @@ -98,8 +91,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClie return request } -func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []alb.PlanDetails) error { return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No plans found for project %q", projectLabel) + return nil + } + table := tables.NewTable() table.SetHeader("PLAN ID", "NAME", "FLAVOR", "MAX CONNS", "DESCRIPTION") for _, item := range items { diff --git a/internal/cmd/beta/alb/plans/plans_test.go b/internal/cmd/beta/alb/plans/plans_test.go index 1a31d711b..ebb7ed8f0 100644 --- a/internal/cmd/beta/alb/plans/plans_test.go +++ b/internal/cmd/beta/alb/plans/plans_test.go @@ -21,9 +21,10 @@ var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") testClient = &alb.APIClient{} testProjectId = uuid.NewString() - testRegion = "eu01" ) +const testRegion = "eu01" + func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, @@ -132,6 +133,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []alb.PlanDetails } tests := []struct { @@ -160,7 +162,7 @@ func Test_outputResult(t *testing.T) { p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 0edc3c7416f09e909e01769a4d02810fd9dadf7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:14:55 +0100 Subject: [PATCH 32/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/mariadb (#1129) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9e6790227..b5372bfb3 100644 --- a/go.mod +++ b/go.mod @@ -248,7 +248,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2 - github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 + github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.2 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1 github.com/stackitcloud/stackit-sdk-go/services/observability v0.15.1 github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2 diff --git a/go.sum b/go.sum index 2b8643324..850f5bf8f 100644 --- a/go.sum +++ b/go.sum @@ -583,8 +583,8 @@ github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 h1:q33ZaCBVE github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0/go.mod h1:20QOZ3rBC9wTGgzXzLz9M6YheX0VaxWE0/JI+s8On7k= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2 h1:g3xzRqwul8W638gOKTZRAnnQuMhYqaliuz/A8BcfjhU= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2/go.mod h1:OlGmMlXKp33ZYpUm9TqaLYf8SdzhDW5uBKcbgq1zXOk= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 h1:Db/ebOL2vbpIeh5XB2Ews2B9Lj5DJlMWIEJh60FfZ4Y= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1/go.mod h1:8jdN4v2euK3f9gfdzbRi8e4nBJ8g/Q5YF9aPB4M4fCQ= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.2 h1:SfRbw3DxvDnZF2q6D9xfSy8EKHyrG5TgLMP0qRW8r9o= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.2/go.mod h1:VmXwRQHZsGUjGWdLf8d2WhKNyuPi5+JgCAF/meOp4DE= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3 h1:tGa+NcjNKTWvChN+0OMdLomb9Jod4MmY6YAiPTJMgfo= github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3/go.mod h1:ciuOzwN5GcqplRy95fXRaS44dFmhfNxvmzTl/ALwV/k= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1 h1:I9B/zUU7R74xuH/ztcPrDIuMp2KV3QQMjeE7lFudboM= From f0a12ee27d1cd963e44e5accc414e43665f04bc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:24:31 +0000 Subject: [PATCH 33/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/redis (#1128) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b5372bfb3..f96f4ebe1 100644 --- a/go.mod +++ b/go.mod @@ -252,7 +252,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1 github.com/stackitcloud/stackit-sdk-go/services/observability v0.15.1 github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2 - github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 + github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.2 github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sys v0.38.0 // indirect diff --git a/go.sum b/go.sum index 850f5bf8f..a358c00d2 100644 --- a/go.sum +++ b/go.sum @@ -597,8 +597,8 @@ github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 h1:K8vXele3U github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1/go.mod h1:hyhw+I19NtjKmRLcUkY4boaTxnYSPFGbpn4RxvGqH2s= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2 h1:Ww0baLTiZha4H1thfEEsDq+O0Ce0hNhdbkJ5eDdGEoE= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.2/go.mod h1:lPz9iQ3kLvpzPR7jt6P1VJyjSumo2+D1i3RkjFGpVTI= -github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 h1:8uPt82Ez34OYMOijjEYxB1zUW6kiybkt6veQKl0AL68= -github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1/go.mod h1:1Y2GEICmZDt+kr8aGnBx/sjYVAIYHmtfC8xYi9oxNEE= +github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.2 h1:VWx+u5b9r+HEm2rCtGlS7OFKl6Fnqe6s2xyCBA3IbM8= +github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.2/go.mod h1:fg1pAqju7q5A696aiok2L4SHZIjZCCiBCpsm7FrQZMA= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1 h1:KDa5sy6NSzMOXaf4a9skxOm8oUoleI45fLbD3ww7qsc= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1/go.mod h1:+k3iHkWpehO+FLC5WsW7eGhYdNjDklYqRcpIxQBLbZg= github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 h1:CPIuqhQw+oPt08I2WLsxJDoVDsPMW2VkvKW7/SlUv10= From 1580200ab22136cbbed2415ec499e887bfbecc8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:34:22 +0000 Subject: [PATCH 34/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/runcommand (#1127) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f96f4ebe1..58b93c279 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.2 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1 - github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 + github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.2 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.2 diff --git a/go.sum b/go.sum index a358c00d2..cf705a2c6 100644 --- a/go.sum +++ b/go.sum @@ -601,8 +601,8 @@ github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.2 h1:VWx+u5b9r+HEm2r github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.2/go.mod h1:fg1pAqju7q5A696aiok2L4SHZIjZCCiBCpsm7FrQZMA= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1 h1:KDa5sy6NSzMOXaf4a9skxOm8oUoleI45fLbD3ww7qsc= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.18.1/go.mod h1:+k3iHkWpehO+FLC5WsW7eGhYdNjDklYqRcpIxQBLbZg= -github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 h1:CPIuqhQw+oPt08I2WLsxJDoVDsPMW2VkvKW7/SlUv10= -github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1/go.mod h1:tip0Ob6x06luy3CmRrmnCMtU5cha95fQLdvZlno3J4w= +github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.2 h1:Jts49V7XvLLr7MwI9o5Mo0NW3iUVVt8DtqNqRDt6ags= +github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.2/go.mod h1:hV1rmsyb3jlL5kWO+flhtXTbm5EM9vU+BJsV4U/YEUU= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2 h1:z7ZJtp742W6AgleV2eEXrJFZ7ai9rXu9V1Lkmir0drI= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.2/go.mod h1:xm0ARtIbfzmqw8e8qThtrYdHHEkpuYvKt13SZGBoWSE= github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.3 h1:LONKouoyrgDfu4Tu152YFjd5cw3SoG8P7PHdd8tsBOU= From 3cc971e75c96bdad00d25db4c2b4b722eca8b365 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:44:40 +0100 Subject: [PATCH 35/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/authorization (#1120) --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 58b93c279..0b697f874 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/stackitcloud/stackit-sdk-go/core v0.20.0 github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2 - github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 + github.com/stackitcloud/stackit-sdk-go/services/authorization v0.10.0 github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2 github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 From 784d53a9057ac5870a8ee2e2ad1e648238e6a5a1 Mon Sep 17 00:00:00 2001 From: Marcel Jacek <72880145+marceljk@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:57:57 +0100 Subject: [PATCH 36/49] chore: increase linter timeout (#1131) --- golang-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/golang-ci.yaml b/golang-ci.yaml index 3487f7456..611e2e172 100644 --- a/golang-ci.yaml +++ b/golang-ci.yaml @@ -7,7 +7,7 @@ run: concurrency: 4 # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 5m + timeout: 10m linters-settings: goimports: # put imports beginning with prefix after 3rd-party packages; From 97cde200583a3943670384c369ed7bc4762d9875 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:29:23 +0100 Subject: [PATCH 37/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/intake (#1132) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b697f874..5c348fcf9 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2 github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.2 - github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0 + github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.0 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.3 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.2 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 diff --git a/go.sum b/go.sum index cf705a2c6..e56007599 100644 --- a/go.sum +++ b/go.sum @@ -575,8 +575,8 @@ github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1 h1:RgWfaWDY8ZGZp5gEBe github.com/stackitcloud/stackit-sdk-go/services/git v0.9.1/go.mod h1:HgScss2f+/k/wDxcsM1K8SzLgDso/EZyhoJC2eZ+tFA= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.2 h1:afGHMCqBM/E/FPUvbfSTFb9ddI+eDm2a7DpWPCkxMzs= github.com/stackitcloud/stackit-sdk-go/services/iaas v1.2.2/go.mod h1:/DlO7+cOqyYKROIxkBYIUdMoEfFevkVXhsShglxyUOQ= -github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0 h1:OdY3eXn98hWIhZBH8qQTFsYP6cag8B29UdwTjMwAjfw= -github.com/stackitcloud/stackit-sdk-go/services/intake v0.3.0/go.mod h1:mf+DQwwTVfc8MD0vwTNPzin2unKAIyQRYywv3wUyH38= +github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.0 h1:KwjR5L+IoUbRYS8k3dyqHgtBUuq8cqRPrUrzzZSSnRI= +github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.0/go.mod h1:Nea8wkoPGvcjKCsjfbAB3pE3kA7oZLi+Zk9hUtunjRI= github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1 h1:F/2qLBATi0nDjKR8EGbsmSX9CLFp3nBcWV8JAeTz4p8= github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1/go.mod h1:Wh1NKX5ZI0FuIdYavOYu0Cjh0yA3S9rlm4j5g0vuPVI= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 h1:q33ZaCBVEBUsnMDxYyuJKtJvGcE5nKgvuPed3s8zXNI= From 9ccaaf8c8a72cff1bb24e0a5d4c7d5d715788b34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:40:03 +0000 Subject: [PATCH 38/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/loadbalancer (#1133) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5c348fcf9..e67b7e2ad 100644 --- a/go.mod +++ b/go.mod @@ -246,7 +246,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1 - github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 + github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.1 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2 github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.2 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.4.1 diff --git a/go.sum b/go.sum index e56007599..fd70aa217 100644 --- a/go.sum +++ b/go.sum @@ -579,8 +579,8 @@ github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.0 h1:KwjR5L+IoUbRYS8 github.com/stackitcloud/stackit-sdk-go/services/intake v0.4.0/go.mod h1:Nea8wkoPGvcjKCsjfbAB3pE3kA7oZLi+Zk9hUtunjRI= github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1 h1:F/2qLBATi0nDjKR8EGbsmSX9CLFp3nBcWV8JAeTz4p8= github.com/stackitcloud/stackit-sdk-go/services/kms v1.1.1/go.mod h1:Wh1NKX5ZI0FuIdYavOYu0Cjh0yA3S9rlm4j5g0vuPVI= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0 h1:q33ZaCBVEBUsnMDxYyuJKtJvGcE5nKgvuPed3s8zXNI= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.0/go.mod h1:20QOZ3rBC9wTGgzXzLz9M6YheX0VaxWE0/JI+s8On7k= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.1 h1:BHNjq4+OsmVrGu1KBOv0dh/++nwysyINtAUTxNFz2Uo= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.6.1/go.mod h1:sTV6ylmBoMOrOxUED8Ebts4a1PaJSPLtmNh5m+s5fus= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2 h1:g3xzRqwul8W638gOKTZRAnnQuMhYqaliuz/A8BcfjhU= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.2/go.mod h1:OlGmMlXKp33ZYpUm9TqaLYf8SdzhDW5uBKcbgq1zXOk= github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.2 h1:SfRbw3DxvDnZF2q6D9xfSy8EKHyrG5TgLMP0qRW8r9o= From e93e3473d5a6409da931db5dd541984e07df5b99 Mon Sep 17 00:00:00 2001 From: Marcel Jacek <72880145+marceljk@users.noreply.github.com> Date: Fri, 28 Nov 2025 17:19:33 +0100 Subject: [PATCH 39/49] fix: log browser url to stderr instead of stdout (#1136) fixes #1125 --- internal/pkg/auth/user_login.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/pkg/auth/user_login.go b/internal/pkg/auth/user_login.go index 2ec2040dd..c01dbccba 100644 --- a/internal/pkg/auth/user_login.go +++ b/internal/pkg/auth/user_login.go @@ -245,8 +245,8 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error { } // Print the link - p.Outputln("Your browser has been opened to visit:\n") - p.Outputf("%s\n\n", authorizationURL) + p.Info("Your browser has been opened to visit:\n\n") + p.Info("%s\n\n", authorizationURL) // Start the blocking web server loop // It will exit when the handlers get fired and call server.Close() From 589ce8f15d8bb39809da2d7f0809340a5484e4dd Mon Sep 17 00:00:00 2001 From: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:44:33 +0100 Subject: [PATCH 40/49] fix(deps): update module github.com/goccy/go-yaml to v1.19.0 (#1137) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e67b7e2ad..289553d21 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.0 require ( github.com/fatih/color v1.18.0 - github.com/goccy/go-yaml v1.18.0 + github.com/goccy/go-yaml v1.19.0 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index fd70aa217..8c587409d 100644 --- a/go.sum +++ b/go.sum @@ -213,8 +213,8 @@ github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUW github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= +github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= From 0908904f54688649cfa08120fa0d86f975b0cf8c Mon Sep 17 00:00:00 2001 From: Piet van der Meulen Date: Mon, 1 Dec 2025 16:17:18 +0100 Subject: [PATCH 41/49] feat(mongodbflex): add readAnyDatabase and stackitAdmin roles for users (#1049) Co-authored-by: Ruben Hoenle --- docs/stackit_mongodbflex_user_create.md | 2 +- docs/stackit_mongodbflex_user_update.md | 2 +- internal/cmd/mongodbflex/user/create/create.go | 4 ++-- internal/cmd/mongodbflex/user/update/update.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/stackit_mongodbflex_user_create.md b/docs/stackit_mongodbflex_user_create.md index 99075fee8..e8d4cdace 100644 --- a/docs/stackit_mongodbflex_user_create.md +++ b/docs/stackit_mongodbflex_user_create.md @@ -29,7 +29,7 @@ stackit mongodbflex user create [flags] --database string The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it -h, --help Help for "stackit mongodbflex user create" --instance-id string ID of the instance - --role strings Roles of the user, possible values are ["read" "readWrite" "readWriteAnyDatabase"] (default [read]) + --role strings Roles of the user, possible values are ["read" "readWrite" "readAnyDatabase" "readWriteAnyDatabase" "stackitAdmin"]. The "readAnyDatabase", "readWriteAnyDatabase" and "stackitAdmin" roles will always be created in the admin database. (default [read]) --username string Username of the user. If not specified, a random username will be assigned ``` diff --git a/docs/stackit_mongodbflex_user_update.md b/docs/stackit_mongodbflex_user_update.md index 31a674972..02e0d42af 100644 --- a/docs/stackit_mongodbflex_user_update.md +++ b/docs/stackit_mongodbflex_user_update.md @@ -23,7 +23,7 @@ stackit mongodbflex user update USER_ID [flags] --database string The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it -h, --help Help for "stackit mongodbflex user update" --instance-id string ID of the instance - --role strings Roles of the user, possible values are ["read" "readWrite" "readWriteAnyDatabase"] (default []) + --role strings Roles of the user, possible values are ["read" "readWrite" "readAnyDatabase" "readWriteAnyDatabase" "stackitAdmin"]. The "readAnyDatabase", "readWriteAnyDatabase" and "stackitAdmin" roles will always be created in the admin database. (default []) ``` ### Options inherited from parent commands diff --git a/internal/cmd/mongodbflex/user/create/create.go b/internal/cmd/mongodbflex/user/create/create.go index f589c3996..cfeb7b6ba 100644 --- a/internal/cmd/mongodbflex/user/create/create.go +++ b/internal/cmd/mongodbflex/user/create/create.go @@ -101,12 +101,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - roleOptions := []string{"read", "readWrite", "readWriteAnyDatabase"} + roleOptions := []string{"read", "readWrite", "readAnyDatabase", "readWriteAnyDatabase", "stackitAdmin"} cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance") cmd.Flags().String(usernameFlag, "", "Username of the user. If not specified, a random username will be assigned") cmd.Flags().String(databaseFlag, "", "The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it") - cmd.Flags().Var(flags.EnumSliceFlag(false, rolesDefault, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q", roleOptions)) + cmd.Flags().Var(flags.EnumSliceFlag(false, rolesDefault, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q. The \"readAnyDatabase\", \"readWriteAnyDatabase\" and \"stackitAdmin\" roles will always be created in the admin database.", roleOptions)) err := flags.MarkFlagsRequired(cmd, instanceIdFlag, databaseFlag) cobra.CheckErr(err) diff --git a/internal/cmd/mongodbflex/user/update/update.go b/internal/cmd/mongodbflex/user/update/update.go index df408025e..a8fcfee3e 100644 --- a/internal/cmd/mongodbflex/user/update/update.go +++ b/internal/cmd/mongodbflex/user/update/update.go @@ -97,11 +97,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - roleOptions := []string{"read", "readWrite", "readWriteAnyDatabase"} + roleOptions := []string{"read", "readWrite", "readAnyDatabase", "readWriteAnyDatabase", "stackitAdmin"} cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance") cmd.Flags().String(databaseFlag, "", "The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it") - cmd.Flags().Var(flags.EnumSliceFlag(false, nil, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q", roleOptions)) + cmd.Flags().Var(flags.EnumSliceFlag(false, nil, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q. The \"readAnyDatabase\", \"readWriteAnyDatabase\" and \"stackitAdmin\" roles will always be created in the admin database.", roleOptions)) err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) From 2b014bc1f93588776136127888660f80deda5d10 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Ferrero Date: Mon, 1 Dec 2025 16:52:31 +0100 Subject: [PATCH 42/49] fix: refresh token flow uses x-www-form encoding (#1135) --- internal/pkg/auth/user_token_flow.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/pkg/auth/user_token_flow.go b/internal/pkg/auth/user_token_flow.go index 215db2fa3..cdb852f77 100644 --- a/internal/pkg/auth/user_token_flow.go +++ b/internal/pkg/auth/user_token_flow.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/url" + "strings" "time" "github.com/golang-jwt/jwt/v5" @@ -166,21 +167,21 @@ func buildRequestToRefreshTokens(utf *userTokenFlow) (*http.Request, error) { return nil, err } + form := url.Values{} + form.Set("grant_type", "refresh_token") + form.Set("client_id", idpClientID) + form.Set("refresh_token", utf.refreshToken) + req, err := http.NewRequest( http.MethodPost, utf.tokenEndpoint, - http.NoBody, + strings.NewReader(form.Encode()), ) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + if err != nil { return nil, err } - reqQuery := url.Values{} - reqQuery.Set("grant_type", "refresh_token") - reqQuery.Set("client_id", idpClientID) - reqQuery.Set("refresh_token", utf.refreshToken) - reqQuery.Set("token_format", "jwt") - req.URL.RawQuery = reqQuery.Encode() - return req, nil } From fb6d721ca98de1ae7774f447e42bf531152a1bd0 Mon Sep 17 00:00:00 2001 From: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:59:29 +0100 Subject: [PATCH 43/49] chore(deps): update renovatebot/github-action action to v44.0.5 (#1141) --- .github/workflows/renovate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml index 8551dd43e..d9eddd18a 100644 --- a/.github/workflows/renovate.yaml +++ b/.github/workflows/renovate.yaml @@ -13,7 +13,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 - name: Self-hosted Renovate - uses: renovatebot/github-action@v44.0.4 + uses: renovatebot/github-action@v44.0.5 with: configurationFile: .github/renovate.json token: ${{ secrets.RENOVATE_TOKEN }} From a228bda051ed6741c39099a79e27fcb340f9fa31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 08:08:35 +0000 Subject: [PATCH 44/49] chore(deps): bump github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex (#1139) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 289553d21..993ea2d61 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.2 github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.3 github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 - github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 + github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.3 github.com/zalando/go-keyring v0.2.6 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/mod v0.30.0 diff --git a/go.sum b/go.sum index 8c587409d..c0fdfa73d 100644 --- a/go.sum +++ b/go.sum @@ -615,8 +615,8 @@ github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.3 h1:zcfL github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.3/go.mod h1:icu5WtsZ8c57/pUrXeFLmZu29Qhwr/rsjTkVRWJYTqY= github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0 h1:bQk5qKid5Kv3fZ2miWlS5Dvo+cW90hbePaxOyWF67EE= github.com/stackitcloud/stackit-sdk-go/services/ske v1.5.0/go.mod h1:/Ujlw+qo6RgKm69dD8y6MgmJFcUmrHjuJPO6VFoQX9U= -github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2 h1:aW8ehdoNRaCEs3xDr+YnGb6pru8zZTB8f7kl5lozlJE= -github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.2/go.mod h1:Jsry+gfhuXv2P0ldfa48BaL605NhDjdQMgaoV8czlbo= +github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.3 h1:TFefEGGxvcI7euqyosbLS/zSEOy+3JMGOirW3vNj/84= +github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.3/go.mod h1:Jsry+gfhuXv2P0ldfa48BaL605NhDjdQMgaoV8czlbo= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 883acc36a0c43b0c567704628ff7730e1b94cf75 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 19 Nov 2025 17:38:12 +0100 Subject: [PATCH 45/49] feat(cdn): add cdn client, config, list command --- go.sum | 1 + internal/pkg/config/config.go | 1 + 2 files changed, 2 insertions(+) diff --git a/go.sum b/go.sum index c0fdfa73d..e7837e71f 100644 --- a/go.sum +++ b/go.sum @@ -567,6 +567,7 @@ github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2 h1:x7ndqw6yaOw+TmThNe github.com/stackitcloud/stackit-sdk-go/services/alb v0.7.2/go.mod h1:wbPNu6e5r/5xhzznCKbC7fEJahrAOb89gmaIm+0w2/s= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0 h1:7ZKd3b+E/R4TEVShLTXxx5FrsuDuJBOyuVOuKTMa4mo= github.com/stackitcloud/stackit-sdk-go/services/authorization v0.9.0/go.mod h1:/FoXa6hF77Gv8brrvLBCKa5ie1Xy9xn39yfHwaln9Tw= +github.com/stackitcloud/stackit-sdk-go/services/authorization v0.10.0/go.mod h1:40XVgsSOcVCjoIAsbSycDh8Ikp2y88AdAeqwqIIHvZE= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1 h1:CiOlfCsCDwHP0kas7qyhfp5XtL2kVmn9e4wjtc3LO10= github.com/stackitcloud/stackit-sdk-go/services/cdn v1.8.1/go.mod h1:PyZ6g9JsGZZyeISAF+5E7L1lAlMnmbl2YbPj5Teu8to= github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.2 h1:x1i5rqhEVuUPq5M0eb68ZD2KL1C8OFD8RG2sWMQGL6o= diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 26ea98c95..7951672d0 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -48,6 +48,7 @@ const ( IaaSCustomEndpointKey = "iaas_custom_endpoint" TokenCustomEndpointKey = "token_custom_endpoint" GitCustomEndpointKey = "git_custom_endpoint" + CDNCustomEndpointKey = "cdn_custom_endpoint" IntakeCustomEndpointKey = "intake_custom_endpoint" CDNCustomEndpointKey = "cdn_custom_endpoint" From 5241898ce3c7e314d94c99ef1509604dee2c3d26 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Mon, 1 Dec 2025 10:35:11 +0100 Subject: [PATCH 46/49] feat(cdn) implement cdn distribution create/delete/describe/update --- internal/cmd/beta/cdn/distribution/create/create.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/create/create.go b/internal/cmd/beta/cdn/distribution/create/create.go index 4f83829d3..79e7d4545 100644 --- a/internal/cmd/beta/cdn/distribution/create/create.go +++ b/internal/cmd/beta/cdn/distribution/create/create.go @@ -74,10 +74,10 @@ type inputModel struct { func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: "create", - Short: "Create a CDN distribution", - Long: "Create a CDN distribution for a given originUrl in multiple regions.", - Args: args.NoArgs, + Use: "create", + Short: "Create a CDN distribution", + Long: "Create a CDN distribution for a given originUrl in multiple regions.", + Args: args.NoArgs, Example: examples.Build( examples.NewExample( `Create a CDN distribution with an HTTP backend`, From 90f05e988b6962c347f7643e45a0785705d0cca2 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Tue, 2 Dec 2025 10:50:34 +0100 Subject: [PATCH 47/49] fix(cdn) manual merge fixes --- internal/cmd/beta/cdn/distribution/create/create.go | 8 ++++---- internal/pkg/config/config.go | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/create/create.go b/internal/cmd/beta/cdn/distribution/create/create.go index 79e7d4545..4f83829d3 100644 --- a/internal/cmd/beta/cdn/distribution/create/create.go +++ b/internal/cmd/beta/cdn/distribution/create/create.go @@ -74,10 +74,10 @@ type inputModel struct { func NewCmd(params *params.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: "create", - Short: "Create a CDN distribution", - Long: "Create a CDN distribution for a given originUrl in multiple regions.", - Args: args.NoArgs, + Use: "create", + Short: "Create a CDN distribution", + Long: "Create a CDN distribution for a given originUrl in multiple regions.", + Args: args.NoArgs, Example: examples.Build( examples.NewExample( `Create a CDN distribution with an HTTP backend`, diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 7951672d0..098e289e1 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -50,7 +50,6 @@ const ( GitCustomEndpointKey = "git_custom_endpoint" CDNCustomEndpointKey = "cdn_custom_endpoint" IntakeCustomEndpointKey = "intake_custom_endpoint" - CDNCustomEndpointKey = "cdn_custom_endpoint" ProjectNameKey = "project_name" DefaultProfileName = "default" From 76d91fb05abe68cdf815ad77752e1db1ff78c543 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Tue, 2 Dec 2025 11:24:44 +0100 Subject: [PATCH 48/49] fix(cdn) review fixes - test Min - test JoinStringMap - rm superfluous var for constant - rm file committed by accident - add nil checks when dereferencing pointers --- .../cdn/distribution/create/create_test.go | 12 +++--- .../cdn/distribution/describe/describe.go | 12 ++++-- .../cmd/beta/cdn/domain/describe/describe.go | 1 - internal/pkg/utils/strings_test.go | 36 ++++++++++++++++ internal/pkg/utils/utils_test.go | 41 +++++++++++++++++++ 5 files changed, 90 insertions(+), 12 deletions(-) delete mode 100644 internal/cmd/beta/cdn/domain/describe/describe.go diff --git a/internal/cmd/beta/cdn/distribution/create/create_test.go b/internal/cmd/beta/cdn/distribution/create/create_test.go index 8f78edd49..c350d713d 100644 --- a/internal/cmd/beta/cdn/distribution/create/create_test.go +++ b/internal/cmd/beta/cdn/distribution/create/create_test.go @@ -20,8 +20,6 @@ import ( "k8s.io/utils/ptr" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") @@ -32,8 +30,8 @@ const testRegions = cdn.REGION_EU func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - flagRegion: string(testRegions), + globalflags.ProjectIdFlag: testProjectId, + flagRegion: string(testRegions), } flagsHTTPBackend()(flagValues) for _, mod := range mods { @@ -202,21 +200,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(m map[string]string) { - delete(m, projectIdFlag) + delete(m, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(m map[string]string) { - m[projectIdFlag] = "" + m[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(m map[string]string) { - m[projectIdFlag] = "invalid-uuid" + m[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, diff --git a/internal/cmd/beta/cdn/distribution/describe/describe.go b/internal/cmd/beta/cdn/distribution/describe/describe.go index 22b6443f5..3efaa3231 100644 --- a/internal/cmd/beta/cdn/distribution/describe/describe.go +++ b/internal/cmd/beta/cdn/distribution/describe/describe.go @@ -167,15 +167,19 @@ func buildDistributionTable(d *cdn.Distribution) tables.Table { } else if d.Config.Backend.HttpBackend != nil { h := d.Config.Backend.HttpBackend var geofencing []string - for k, v := range *h.Geofencing { - geofencing = append(geofencing, fmt.Sprintf("%s: %s", k, strings.Join(v, ", "))) + if h.Geofencing != nil { + for k, v := range *h.Geofencing { + geofencing = append(geofencing, fmt.Sprintf("%s: %s", k, strings.Join(v, ", "))) + } } table.AddRow("BACKEND TYPE", "HTTP") table.AddSeparator() table.AddRow("HTTP ORIGIN URL", utils.PtrString(h.OriginUrl)) table.AddSeparator() - table.AddRow("HTTP ORIGIN REQUEST HEADERS", utils.JoinStringMap(*h.OriginRequestHeaders, ": ", ", ")) - table.AddSeparator() + if h.OriginRequestHeaders != nil { + table.AddRow("HTTP ORIGIN REQUEST HEADERS", utils.JoinStringMap(*h.OriginRequestHeaders, ": ", ", ")) + table.AddSeparator() + } table.AddRow("HTTP GEOFENCING PROPERTIES", strings.Join(geofencing, "\n")) table.AddSeparator() } diff --git a/internal/cmd/beta/cdn/domain/describe/describe.go b/internal/cmd/beta/cdn/domain/describe/describe.go deleted file mode 100644 index cce268ec4..000000000 --- a/internal/cmd/beta/cdn/domain/describe/describe.go +++ /dev/null @@ -1 +0,0 @@ -package describe diff --git a/internal/pkg/utils/strings_test.go b/internal/pkg/utils/strings_test.go index a7fb023bc..44dadd5b6 100644 --- a/internal/pkg/utils/strings_test.go +++ b/internal/pkg/utils/strings_test.go @@ -30,3 +30,39 @@ func TestTruncate(t *testing.T) { }) } } + +func TestJoinStringMap(t *testing.T) { + tests := []struct { + name string + m map[string]string + want string + }{ + { + name: "nil map", + m: nil, + want: "", + }, + { + name: "empty map", + m: map[string]string{}, + want: "", + }, + { + name: "single element", + m: map[string]string{"key1": "value1"}, + want: "key1=value1", + }, + { + name: "multiple elements", + m: map[string]string{"key1": "value1", "key2": "value2"}, + want: "key1=value1, key2=value2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := JoinStringMap(tt.m, "=", ", "); got != tt.want { + t.Errorf("JoinStringMap() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index 4591c84c9..8ec07bc2c 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -590,3 +590,44 @@ func TestGetSliceFromPointer(t *testing.T) { }) } } + +func TestMin(t *testing.T) { + tests := []struct { + name string + a, b int + want int + }{ + { + name: "min(0, 0) = 0", + a: 0, + b: 0, + want: 0, + }, + { + name: "min(1, 2) = 1", + a: 1, + b: 2, + want: 1, + }, + { + name: "min(2, 1) = 1", + a: 2, + b: 1, + want: 1, + }, + { + name: "min(-1, 1) = -1", + a: -1, + b: 1, + want: -1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Min(tt.a, tt.b); got != tt.want { + t.Errorf("Min() = %v, want %v", got, tt.want) + } + }) + } + +} From 3d0f111262a28b43ab392f4e1a538285b645cf84 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Tue, 2 Dec 2025 11:33:33 +0100 Subject: [PATCH 49/49] fix(cdn) replace utils.Min usage with builtin min --- .../cmd/beta/cdn/distribution/list/list.go | 4 +- internal/pkg/utils/utils.go | 8 ---- internal/pkg/utils/utils_test.go | 41 ------------------- 3 files changed, 2 insertions(+), 51 deletions(-) diff --git a/internal/cmd/beta/cdn/distribution/list/list.go b/internal/cmd/beta/cdn/distribution/list/list.go index 414a1d11f..9632cca80 100644 --- a/internal/cmd/beta/cdn/distribution/list/list.go +++ b/internal/cmd/beta/cdn/distribution/list/list.go @@ -151,10 +151,10 @@ func fetchDistributions(ctx context.Context, model *inputModel, apiClient *cdn.A received := int32(0) limit := int32(math.MaxInt32) if model.Limit != nil { - limit = utils.Min(limit, *model.Limit) + limit = min(limit, *model.Limit) } for { - want := utils.Min(maxPageSize, limit-received) + want := min(maxPageSize, limit-received) request := buildRequest(ctx, model, apiClient, nextPageID, want) response, err := request.Execute() if err != nil { diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index b37443e2b..862b92c8f 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -14,7 +14,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/config" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "golang.org/x/exp/constraints" ) // Ptr Returns the pointer to any type T @@ -260,10 +259,3 @@ func GetSliceFromPointer[T any](s *[]T) []T { } return *s } - -func Min[T constraints.Ordered](a, b T) T { - if a < b { - return a - } - return b -} diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index 8ec07bc2c..4591c84c9 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -590,44 +590,3 @@ func TestGetSliceFromPointer(t *testing.T) { }) } } - -func TestMin(t *testing.T) { - tests := []struct { - name string - a, b int - want int - }{ - { - name: "min(0, 0) = 0", - a: 0, - b: 0, - want: 0, - }, - { - name: "min(1, 2) = 1", - a: 1, - b: 2, - want: 1, - }, - { - name: "min(2, 1) = 1", - a: 2, - b: 1, - want: 1, - }, - { - name: "min(-1, 1) = -1", - a: -1, - b: 1, - want: -1, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Min(tt.a, tt.b); got != tt.want { - t.Errorf("Min() = %v, want %v", got, tt.want) - } - }) - } - -}