From c22bedc0c8006911b2724c0d884208ff8a09ec1f Mon Sep 17 00:00:00 2001 From: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:58:46 +0200 Subject: [PATCH 01/13] fix(deps): update module github.com/stackitcloud/stackit-sdk-go/services/ske to v0.11.0 (#180) Co-authored-by: Renovate Bot --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0733646ca..9f8c6b4fe 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.7.7 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.6.0 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.6 - github.com/stackitcloud/stackit-sdk-go/services/ske v0.10.1 + github.com/stackitcloud/stackit-sdk-go/services/ske v0.11.0 github.com/zalando/go-keyring v0.2.4 golang.org/x/mod v0.16.0 golang.org/x/oauth2 v0.18.0 diff --git a/go.sum b/go.sum index 2720421f0..8f4def413 100644 --- a/go.sum +++ b/go.sum @@ -103,8 +103,8 @@ github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.6.0 h1:VC7VWad github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.6.0/go.mod h1:KRoLXZdH8yuO6FBu2Grl5VGqW9arH03qYAC0P6H8h9o= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.6 h1:3kkNh2kHi55w9dgh0MC1Zbn8fDpYxcXl3tvYjH8t9xo= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.6/go.mod h1:OOciROyQxPOYLo8OM/DE5ESH11+DvAyRt6wg7R+HVkg= -github.com/stackitcloud/stackit-sdk-go/services/ske v0.10.1 h1:MZABtJ8HFOKG3KCCv5duibxBSAU1zTFAO0V9bso3N9M= -github.com/stackitcloud/stackit-sdk-go/services/ske v0.10.1/go.mod h1:7M7bsVHN0REuwoZRYz5nK2yBwsMJcHTsVFHlG83QP2A= +github.com/stackitcloud/stackit-sdk-go/services/ske v0.11.0 h1:BJ1Op7f3KJPNROkEXzqAREl55JCqyIAyQJ+Gfu4LYCM= +github.com/stackitcloud/stackit-sdk-go/services/ske v0.11.0/go.mod h1:yFLjTx58pjHCp0KZTaqHlW9Qk60CY5HpnBWR/zztv8Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= From 4144f6e4873e9e1fcdf3c2444275b0b1bc7ee210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 12:44:56 +0100 Subject: [PATCH 02/13] Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#183) Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9f8c6b4fe..c769ba14a 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ require ( golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/sys v0.16.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 8f4def413..24e7a24c4 100644 --- a/go.sum +++ b/go.sum @@ -158,8 +158,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 81b75942e3c381edccd93277db3144ab6910eb4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Tue, 2 Apr 2024 11:04:09 +0100 Subject: [PATCH 03/13] Only write to config file when needed (#185) --- internal/pkg/config/config.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index b94b82127..d5fb213b9 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -87,11 +87,6 @@ func InitConfig() { cobra.CheckErr(err) setConfigDefaults() - err = viper.WriteConfigAs(configFilePath) - cobra.CheckErr(err) - - // Needs to be done after WriteConfigAs, otherwise it would write - // the environment variables to the config file viper.AutomaticEnv() viper.SetEnvPrefix("stackit") } From 3e43e1f51750bedb58ce1ac46c29992c6f76e09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 2 Apr 2024 15:29:41 +0100 Subject: [PATCH 04/13] UX improvements (#187) * add default to project list * project list: improve implementation, add testing * Fix examples in project list * Add version column to plans in DSAs * Add dns record data to list and describe * minor improvements to code * fix linting * add docs * change keyring testing * address comments in PR --- docs/stackit_postgresflex_instance_clone.md | 2 +- docs/stackit_project_list.md | 3 ++ docs/stackit_project_member_list.md | 6 ++-- .../cmd/dns/record-set/describe/describe.go | 7 ++-- internal/cmd/dns/record-set/list/list.go | 9 +++-- internal/cmd/logme/plans/plans.go | 6 ++-- internal/cmd/mariadb/plans/plans.go | 6 ++-- internal/cmd/opensearch/plans/plans.go | 6 ++-- internal/cmd/project/list/list.go | 24 +++++++++---- internal/cmd/project/list/list_test.go | 35 +++++++++++++------ internal/cmd/project/member/list/list.go | 6 ++-- internal/cmd/rabbitmq/plans/plans.go | 6 ++-- internal/cmd/redis/plans/plans.go | 6 ++-- 13 files changed, 77 insertions(+), 45 deletions(-) diff --git a/docs/stackit_postgresflex_instance_clone.md b/docs/stackit_postgresflex_instance_clone.md index 64acbc209..cbc9f1e9b 100644 --- a/docs/stackit_postgresflex_instance_clone.md +++ b/docs/stackit_postgresflex_instance_clone.md @@ -27,7 +27,7 @@ stackit postgresflex instance clone INSTANCE_ID [flags] ``` -h, --help Help for "stackit postgresflex instance clone" - --recovery-timestamp string Recovery timestamp for the instance, specified in UTC time following the format, e.g. 2024-03-12T09:28:00+00:00 + --recovery-timestamp string Recovery timestamp for the instance, in a date-time with the RFC3339 layout format, e.g. 2024-01-01T00:00:00Z --storage-class string Storage class. If not specified, storage class from the existing instance will be used. --storage-size int Storage size (in GB). If not specified, storage size from the existing instance will be used. ``` diff --git a/docs/stackit_project_list.md b/docs/stackit_project_list.md index 388768532..f0fdf46af 100644 --- a/docs/stackit_project_list.md +++ b/docs/stackit_project_list.md @@ -13,6 +13,9 @@ stackit project list [flags] ### Examples ``` + List all STACKIT projects that authenticated user is a member of + $ stackit project list + List all STACKIT projects that are children of a specific parent $ stackit project list --parent-id xxx diff --git a/docs/stackit_project_member_list.md b/docs/stackit_project_member_list.md index 6761cb374..43f371879 100644 --- a/docs/stackit_project_member_list.md +++ b/docs/stackit_project_member_list.md @@ -14,13 +14,13 @@ stackit project member list [flags] ``` List all members of a project - $ stackit project role list --project-id xxx + $ stackit project member list --project-id xxx List all members of a project, sorted by role - $ stackit project role list --project-id xxx --sort-by role + $ stackit project member list --project-id xxx --sort-by role List up to 10 members of a project - $ stackit project role list --project-id xxx --limit 10 + $ stackit project member list --project-id xxx --limit 10 ``` ### Options diff --git a/internal/cmd/dns/record-set/describe/describe.go b/internal/cmd/dns/record-set/describe/describe.go index 46c8546bc..4d9349f02 100644 --- a/internal/cmd/dns/record-set/describe/describe.go +++ b/internal/cmd/dns/record-set/describe/describe.go @@ -103,10 +103,9 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClie func outputResult(cmd *cobra.Command, outputFormat string, recordSet *dns.RecordSet) error { switch outputFormat { case globalflags.PrettyOutputFormat: - records := *recordSet.Records - recordsData := []string{} - for i := range records { - recordsData = append(recordsData, *records[i].Content) + recordsData := make([]string, 0, len(*recordSet.Records)) + for _, r := range *recordSet.Records { + recordsData = append(recordsData, *r.Content) } recordsDataJoin := strings.Join(recordsData, ",") diff --git a/internal/cmd/dns/record-set/list/list.go b/internal/cmd/dns/record-set/list/list.go index 95a195e44..a551abdef 100644 --- a/internal/cmd/dns/record-set/list/list.go +++ b/internal/cmd/dns/record-set/list/list.go @@ -232,10 +232,15 @@ func outputResult(cmd *cobra.Command, outputFormat string, recordSets []dns.Reco return nil default: table := tables.NewTable() - table.SetHeader("ID", "NAME", "STATUS", "TTL", "TYPE") + table.SetHeader("ID", "NAME", "STATUS", "TTL", "TYPE", "RECORD DATA") for i := range recordSets { rs := recordSets[i] - table.AddRow(*rs.Id, *rs.Name, *rs.State, *rs.Ttl, *rs.Type) + recordData := make([]string, 0, len(*rs.Records)) + for _, r := range *rs.Records { + recordData = append(recordData, *r.Content) + } + recordDataJoin := strings.Join(recordData, ", ") + table.AddRow(*rs.Id, *rs.Name, *rs.State, *rs.Ttl, *rs.Type, recordDataJoin) } err := table.Display(cmd) if err != nil { diff --git a/internal/cmd/logme/plans/plans.go b/internal/cmd/logme/plans/plans.go index f7c46d3b9..8bf3fd69d 100644 --- a/internal/cmd/logme/plans/plans.go +++ b/internal/cmd/logme/plans/plans.go @@ -127,16 +127,16 @@ func outputResult(cmd *cobra.Command, outputFormat string, plans []logme.Offerin return nil default: table := tables.NewTable() - table.SetHeader("OFFERING NAME", "ID", "NAME", "DESCRIPTION") + table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] for j := range *o.Plans { p := (*o.Plans)[j] - table.AddRow(*o.Name, *p.Id, *p.Name, *p.Description) + table.AddRow(*o.Name, *o.Version, *p.Id, *p.Name, *p.Description) } table.AddSeparator() } - table.EnableAutoMergeOnColumns(1) + table.EnableAutoMergeOnColumns(1, 2) err := table.Display(cmd) if err != nil { return fmt.Errorf("render table: %w", err) diff --git a/internal/cmd/mariadb/plans/plans.go b/internal/cmd/mariadb/plans/plans.go index 7e400f7f8..f7135658e 100644 --- a/internal/cmd/mariadb/plans/plans.go +++ b/internal/cmd/mariadb/plans/plans.go @@ -127,16 +127,16 @@ func outputResult(cmd *cobra.Command, outputFormat string, plans []mariadb.Offer return nil default: table := tables.NewTable() - table.SetHeader("OFFERING NAME", "ID", "NAME", "DESCRIPTION") + table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] for j := range *o.Plans { p := (*o.Plans)[j] - table.AddRow(*o.Name, *p.Id, *p.Name, *p.Description) + table.AddRow(*o.Name, *o.Version, *p.Id, *p.Name, *p.Description) } table.AddSeparator() } - table.EnableAutoMergeOnColumns(1) + table.EnableAutoMergeOnColumns(1, 2) err := table.Display(cmd) if err != nil { return fmt.Errorf("render table: %w", err) diff --git a/internal/cmd/opensearch/plans/plans.go b/internal/cmd/opensearch/plans/plans.go index db7932d0b..e46bbb888 100644 --- a/internal/cmd/opensearch/plans/plans.go +++ b/internal/cmd/opensearch/plans/plans.go @@ -127,16 +127,16 @@ func outputResult(cmd *cobra.Command, outputFormat string, plans []opensearch.Of return nil default: table := tables.NewTable() - table.SetHeader("OFFERING NAME", "ID", "NAME", "DESCRIPTION") + table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] for j := range *o.Plans { p := (*o.Plans)[j] - table.AddRow(*o.Name, *p.Id, *p.Name, *p.Description) + table.AddRow(*o.Name, *o.Version, *p.Id, *p.Name, *p.Description) } table.AddSeparator() } - table.EnableAutoMergeOnColumns(1) + table.EnableAutoMergeOnColumns(1, 2) err := table.Display(cmd) if err != nil { return fmt.Errorf("render table: %w", err) diff --git a/internal/cmd/project/list/list.go b/internal/cmd/project/list/list.go index 6743cd337..20fd35013 100644 --- a/internal/cmd/project/list/list.go +++ b/internal/cmd/project/list/list.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -47,6 +48,9 @@ func NewCmd() *cobra.Command { Long: "Lists all STACKIT projects that match certain criteria.", Args: args.NoArgs, Example: examples.Build( + examples.NewExample( + `List all STACKIT projects that the authenticated user is a member of`, + "$ stackit project list"), examples.NewExample( `List all STACKIT projects that are children of a specific parent`, "$ stackit project list --parent-id xxx"), @@ -94,9 +98,6 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(creationTimeAfterFlag, "", "Filter by creation timestamp, in a date-time with the RFC3339 layout format, e.g. 2023-01-01T00:00:00Z. The list of projects that were created after the given timestamp will be shown") cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") cmd.Flags().Int64(pageSizeFlag, pageSizeDefault, "Number of items fetched in each API call. Does not affect the number of items in the command output") - - // At least one of parent-id, project-id-like or member flag must be provided - cmd.MarkFlagsOneRequired(parentIdFlag, projectIdLikeFlag, memberFlag) } func parseInput(cmd *cobra.Command) (*inputModel, error) { @@ -137,7 +138,7 @@ func parseInput(cmd *cobra.Command) (*inputModel, error) { }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient resourceManagerClient, offset int) resourcemanager.ApiListProjectsRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient resourceManagerClient, offset int) (resourcemanager.ApiListProjectsRequest, error) { req := apiClient.ListProjects(ctx) if model.ParentId != nil { req = req.ContainerParentId(*model.ParentId) @@ -151,9 +152,17 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient resourceMana if model.CreationTimeAfter != nil { req = req.CreationTimeStart(*model.CreationTimeAfter) } + + if model.ParentId == nil && model.ProjectIdLike == nil && model.Member == nil { + email, err := auth.GetAuthField(auth.USER_EMAIL) + if err != nil { + return req, fmt.Errorf("get email of authenticated user: %w", err) + } + req = req.Member(email) + } req = req.Limit(float32(model.PageSize)) req = req.Offset(float32(offset)) - return req + return req, nil } type resourceManagerClient interface { @@ -169,7 +178,10 @@ func fetchProjects(ctx context.Context, model *inputModel, apiClient resourceMan projects := []resourcemanager.ProjectResponse{} for { // Call API - req := buildRequest(ctx, model, apiClient, offset) + req, err := buildRequest(ctx, model, apiClient, offset) + if err != nil { + return nil, fmt.Errorf("build list projects request: %w", err) + } resp, err := req.Execute() if err != nil { return nil, fmt.Errorf("get projects: %w", err) diff --git a/internal/cmd/project/list/list_test.go b/internal/cmd/project/list/list_test.go index 8bbd3b70d..1a4ca5755 100644 --- a/internal/cmd/project/list/list_test.go +++ b/internal/cmd/project/list/list_test.go @@ -9,8 +9,10 @@ import ( "testing" "time" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/zalando/go-keyring" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -139,15 +141,12 @@ func TestParseInput(t *testing.T) { { description: "no values", flagValues: map[string]string{}, - isValid: false, - }, - { - description: "none of required fields provided", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, parentIdFlag) - delete(flagValues, memberFlag) + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ParentId = nil + model.Member = nil + model.CreationTimeAfter = nil }), - isValid: false, }, { description: "projectIdLike invalid", @@ -258,6 +257,17 @@ func TestParseInput(t *testing.T) { } func TestBuildRequest(t *testing.T) { + keyring.MockInit() + err := auth.SetAuthField(auth.USER_EMAIL, "test@test.com") + if err != nil { + t.Fatalf("Failed to set auth user email: %v", err) + } + + authUserEmail, err := auth.GetAuthField(auth.USER_EMAIL) + if err != nil { + t.Fatalf("Failed to get auth user email: %v", err) + } + tests := []struct { description string model *inputModel @@ -278,12 +288,12 @@ func TestBuildRequest(t *testing.T) { expectedRequest: fixtureRequest().Offset(10), }, { - description: "required fields only", + description: "fetch email from auth user", model: &inputModel{ PageSize: pageSizeDefault, }, offset: 1, - expectedRequest: testClient.ListProjects(testCtx).Offset(1).Limit(pageSizeDefault), + expectedRequest: testClient.ListProjects(testCtx).Offset(1).Limit(pageSizeDefault).Member(authUserEmail), }, { description: "projectIdLike set", @@ -299,7 +309,10 @@ func TestBuildRequest(t *testing.T) { if tt.projectIdLike != nil { tt.model.ProjectIdLike = tt.projectIdLike } - request := buildRequest(testCtx, tt.model, testClient, tt.offset) + request, err := buildRequest(testCtx, tt.model, testClient, tt.offset) + if err != nil { + t.Fatalf("Failed to build request: %v", err) + } diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), diff --git a/internal/cmd/project/member/list/list.go b/internal/cmd/project/member/list/list.go index 1979785f2..5337048e4 100644 --- a/internal/cmd/project/member/list/list.go +++ b/internal/cmd/project/member/list/list.go @@ -44,13 +44,13 @@ func NewCmd() *cobra.Command { Example: examples.Build( examples.NewExample( `List all members of a project`, - "$ stackit project role list --project-id xxx"), + "$ stackit project member list --project-id xxx"), examples.NewExample( `List all members of a project, sorted by role`, - "$ stackit project role list --project-id xxx --sort-by role"), + "$ stackit project member list --project-id xxx --sort-by role"), examples.NewExample( `List up to 10 members of a project`, - "$ stackit project role list --project-id xxx --limit 10"), + "$ stackit project member list --project-id xxx --limit 10"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() diff --git a/internal/cmd/rabbitmq/plans/plans.go b/internal/cmd/rabbitmq/plans/plans.go index 773e6679b..fc26d00f0 100644 --- a/internal/cmd/rabbitmq/plans/plans.go +++ b/internal/cmd/rabbitmq/plans/plans.go @@ -127,16 +127,16 @@ func outputResult(cmd *cobra.Command, outputFormat string, plans []rabbitmq.Offe return nil default: table := tables.NewTable() - table.SetHeader("OFFERING NAME", "ID", "NAME", "DESCRIPTION") + table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] for j := range *o.Plans { p := (*o.Plans)[j] - table.AddRow(*o.Name, *p.Id, *p.Name, *p.Description) + table.AddRow(*o.Name, *o.Version, *p.Id, *p.Name, *p.Description) } table.AddSeparator() } - table.EnableAutoMergeOnColumns(1) + table.EnableAutoMergeOnColumns(1, 2) err := table.Display(cmd) if err != nil { return fmt.Errorf("render table: %w", err) diff --git a/internal/cmd/redis/plans/plans.go b/internal/cmd/redis/plans/plans.go index 86232c1fb..4aadf5fdd 100644 --- a/internal/cmd/redis/plans/plans.go +++ b/internal/cmd/redis/plans/plans.go @@ -127,16 +127,16 @@ func outputResult(cmd *cobra.Command, outputFormat string, plans []redis.Offerin return nil default: table := tables.NewTable() - table.SetHeader("OFFERING NAME", "ID", "NAME", "DESCRIPTION") + table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] for j := range *o.Plans { p := (*o.Plans)[j] - table.AddRow(*o.Name, *p.Id, *p.Name, *p.Description) + table.AddRow(*o.Name, *o.Version, *p.Id, *p.Name, *p.Description) } table.AddSeparator() } - table.EnableAutoMergeOnColumns(1) + table.EnableAutoMergeOnColumns(1, 2) err := table.Display(cmd) if err != nil { return fmt.Errorf("render table: %w", err) From a0e351dcd424233e55145a4e802bc0d271dec690 Mon Sep 17 00:00:00 2001 From: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:44:54 +0200 Subject: [PATCH 05/13] fix(deps): update module github.com/stackitcloud/stackit-sdk-go/services/postgresflex to v0.11.0 (#189) Co-authored-by: Renovate Bot --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c769ba14a..0923fc9c2 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.11.1 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.10.1 - github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.10.0 + github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.11.0 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.7.7 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.6.0 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.6 diff --git a/go.sum b/go.sum index 24e7a24c4..83285a8f3 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.8.6 h1:+mcoBKs6 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.8.6/go.mod h1:W9BML8bqZb2dOZe1K+M+qBBs8/QNirr3jA0xxy9tNRY= github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.10.1 h1:LKic8dXtXKsRst2+wY9dNjjkMyJ05QIDpOJuRmVb410= github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.10.1/go.mod h1:g1o1bmqtTliy9UkFlRV/6bn6GQk+hkvnny3UjMI69S0= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.10.0 h1:+pGOyx15jkU6PcS6wqjBd7gxNIwB72vofzJ1rhGEt3M= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.10.0/go.mod h1:qzbM0fmR9YiUzu3S+XnbOvvjIGzIV+Dz59zEizg6xvw= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.11.0 h1:w3vUPJcPE81nItkkbPs1pxm+QF4c0YIbPyY0dd6qI2w= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.11.0/go.mod h1:P0YyvgwIsVKJijdWGVJVOp/ac7PVX99Oj+dr4v1zECc= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.10.0 h1:Fle394socpyf662g3jMrtZpZaWVgBMBIEFnh4fnGock= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.10.0/go.mod h1:JvqOSrTCiynS0x6Y9OsK54yvdB6AtIWLwXDEjoCkAIg= github.com/stackitcloud/stackit-sdk-go/services/redis v0.10.1 h1:/tRad17HUcGRm448l8XyX6uhnnHVfj3VdUQquIwNq2Q= From 386982b63191a1ef558e919daa5ed7741ed50f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Wed, 3 Apr 2024 11:41:59 +0100 Subject: [PATCH 06/13] UX Improvements: fixes and improvements (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add default to project list * project list: improve implementation, add testing * minor improvements to code * fix linting * change keyring testing * address comments in PR * address PR comments * generate docs * Update internal/pkg/args/args.go Co-authored-by: João Palet * fix linting --------- Co-authored-by: João Palet --- docs/stackit_project_list.md | 2 +- .../cmd/dns/record-set/describe/describe.go | 2 +- internal/cmd/project/describe/describe.go | 27 +++++-- .../cmd/project/describe/describe_test.go | 51 ++++++++++++- internal/cmd/project/list/list.go | 2 +- internal/pkg/args/args.go | 25 +++++++ internal/pkg/args/args_test.go | 71 +++++++++++++++++++ internal/pkg/errors/errors.go | 13 ++++ 8 files changed, 180 insertions(+), 13 deletions(-) diff --git a/docs/stackit_project_list.md b/docs/stackit_project_list.md index f0fdf46af..8643290ee 100644 --- a/docs/stackit_project_list.md +++ b/docs/stackit_project_list.md @@ -13,7 +13,7 @@ stackit project list [flags] ### Examples ``` - List all STACKIT projects that authenticated user is a member of + List all STACKIT projects that the authenticated user or service account is a member of $ stackit project list List all STACKIT projects that are children of a specific parent diff --git a/internal/cmd/dns/record-set/describe/describe.go b/internal/cmd/dns/record-set/describe/describe.go index 4d9349f02..4ac1b7989 100644 --- a/internal/cmd/dns/record-set/describe/describe.go +++ b/internal/cmd/dns/record-set/describe/describe.go @@ -107,7 +107,7 @@ func outputResult(cmd *cobra.Command, outputFormat string, recordSet *dns.Record for _, r := range *recordSet.Records { recordsData = append(recordsData, *r.Content) } - recordsDataJoin := strings.Join(recordsData, ",") + recordsDataJoin := strings.Join(recordsData, ", ") table := tables.NewTable() table.AddRow("ID", *recordSet.Id) diff --git a/internal/cmd/project/describe/describe.go b/internal/cmd/project/describe/describe.go index f6ca653ef..460204e3f 100644 --- a/internal/cmd/project/describe/describe.go +++ b/internal/cmd/project/describe/describe.go @@ -6,12 +6,12 @@ import ( "fmt" "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/services/resourcemanager/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" @@ -19,10 +19,13 @@ import ( const ( includeParentsFlag = "include-parents" + + projectIdArg = "PROJECT_ID" ) type inputModel struct { *globalflags.GlobalFlagModel + ArgProjectId string IncludeParents bool } @@ -31,7 +34,7 @@ func NewCmd() *cobra.Command { Use: "describe", Short: "Shows details of a STACKIT project", Long: "Shows details of a STACKIT project.", - Args: args.NoArgs, + Args: args.SingleOptionalArg(projectIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Get the details of the configured STACKIT project`, @@ -45,7 +48,7 @@ func NewCmd() *cobra.Command { ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(cmd) + model, err := parseInput(cmd, args) if err != nil { return err } @@ -74,20 +77,30 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(includeParentsFlag, false, "When true, the details of the parent resources will be included in the output") } -func parseInput(cmd *cobra.Command) (*inputModel, error) { +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + var projectId string + if len(inputArgs) > 0 { + projectId = inputArgs[0] + } + globalFlags := globalflags.Parse(cmd) - if globalFlags.ProjectId == "" { - return nil, &errors.ProjectIdError{} + if globalFlags.ProjectId == "" && projectId == "" { + return nil, fmt.Errorf("Project ID needs to be provided either as an argument or as a flag") + } + + if projectId == "" { + projectId = globalFlags.ProjectId } return &inputModel{ GlobalFlagModel: globalFlags, + ArgProjectId: projectId, IncludeParents: flags.FlagToBoolValue(cmd, includeParentsFlag), }, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiGetProjectRequest { - req := apiClient.GetProject(ctx, model.ProjectId) + req := apiClient.GetProject(ctx, model.ArgProjectId) req.IncludeParents(model.IncludeParents) return req } diff --git a/internal/cmd/project/describe/describe_test.go b/internal/cmd/project/describe/describe_test.go index 7584bf55d..c2d7537cd 100644 --- a/internal/cmd/project/describe/describe_test.go +++ b/internal/cmd/project/describe/describe_test.go @@ -19,10 +19,20 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testClient = &resourcemanager.APIClient{} var testProjectId = uuid.NewString() +var testProjectId2 = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testProjectId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, includeParentsFlag: "false", } for _, mod := range mods { @@ -34,9 +44,10 @@ 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, + ProjectId: "", }, IncludeParents: false, + ArgProjectId: testProjectId, } for _, mod := range mods { mod(model) @@ -55,6 +66,7 @@ func fixtureRequest(mods ...func(request *resourcemanager.ApiGetProjectRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string labelValues []string isValid bool @@ -62,17 +74,42 @@ func TestParseInput(t *testing.T) { }{ { description: "base", + argValues: fixtureArgValues(), flagValues: fixtureFlagValues(), isValid: true, expectedModel: fixtureInputModel(), }, { description: "no values", + argValues: []string{}, flagValues: map[string]string{}, isValid: false, }, + { + description: "project id arg takes precedence", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = testProjectId2 + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ProjectId = testProjectId2 + }), + }, + { + description: "project id arg missing", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = testProjectId + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ProjectId = testProjectId + }), + }, { description: "project id missing", + argValues: []string{}, flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, projectIdFlag) }), @@ -112,6 +149,14 @@ func TestParseInput(t *testing.T) { } } + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + err = cmd.ValidateRequiredFlags() if err != nil { if !tt.isValid { @@ -120,7 +165,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(cmd) + model, err := parseInput(cmd, tt.argValues) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/project/list/list.go b/internal/cmd/project/list/list.go index 20fd35013..0809e8928 100644 --- a/internal/cmd/project/list/list.go +++ b/internal/cmd/project/list/list.go @@ -49,7 +49,7 @@ func NewCmd() *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `List all STACKIT projects that the authenticated user is a member of`, + `List all STACKIT projects that the authenticated user or service account is a member of`, "$ stackit project list"), examples.NewExample( `List all STACKIT projects that are children of a specific parent`, diff --git a/internal/pkg/args/args.go b/internal/pkg/args/args.go index 2307ed342..961480ed1 100644 --- a/internal/pkg/args/args.go +++ b/internal/pkg/args/args.go @@ -42,3 +42,28 @@ func SingleArg(argName string, validate func(value string) error) cobra.Position return nil } } + +// SingleOptionalArg checks if one or no arguments were provided and validates it if provided +// using the validate function. It returns an error if more than one argument is provided, or if +// the argument is invalid. For no validation, you can pass a nil validate function +func SingleOptionalArg(argName string, validate func(value string) error) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + return &errors.SingleOptionalArgExpectedError{ + Cmd: cmd, + Expected: argName, + Count: len(args), + } + } + if len(args) == 1 && validate != nil { + err := validate(args[0]) + if err != nil { + return &errors.ArgValidationError{ + Arg: argName, + Details: err.Error(), + } + } + } + return nil + } +} diff --git a/internal/pkg/args/args_test.go b/internal/pkg/args/args_test.go index ad477e50e..2a1360b44 100644 --- a/internal/pkg/args/args_test.go +++ b/internal/pkg/args/args_test.go @@ -107,3 +107,74 @@ func TestSingleArg(t *testing.T) { }) } } + +func TestSingleOptionalArg(t *testing.T) { + tests := []struct { + description string + args []string + validateFunc func(value string) error + isValid bool + }{ + { + description: "valid", + args: []string{"arg"}, + validateFunc: func(value string) error { + return nil + }, + isValid: true, + }, + { + description: "no_arg", + args: []string{}, + isValid: true, + }, + { + description: "more_than_one_arg", + args: []string{"arg", "arg2"}, + isValid: false, + }, + { + description: "empty_arg", + args: []string{""}, + isValid: true, + }, + { + description: "invalid_arg", + args: []string{"arg"}, + validateFunc: func(value string) error { + return fmt.Errorf("error") + }, + isValid: false, + }, + { + description: "nil validation function", + args: []string{"arg"}, + validateFunc: nil, + isValid: true, + }, + { + description: "nil validation function, no args", + args: []string{}, + validateFunc: nil, + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Short: "Test command", + } + + argFunction := SingleOptionalArg("test", tt.validateFunc) + err := argFunction(cmd, tt.args) + + if tt.isValid && err != nil { + t.Fatalf("should not have failed: %v", err) + } + if !tt.isValid && err == nil { + t.Fatalf("should have failed") + } + }) + } +} diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 711526a68..d421aa1b7 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -107,6 +107,8 @@ For more details on the available storages for the configured flavor (%[3]s), ru SINGLE_ARG_EXPECTED = `expected 1 argument %q, %d were provided` + SINGLE_OPTIONAL_ARG_EXPECTED = `expected no more than 1 argument %q, %d were provided` + SUBCOMMAND_UNKNOWN = `unknown subcommand %q` SUBCOMMAND_MISSING = `missing subcommand` @@ -260,6 +262,17 @@ func (e *SingleArgExpectedError) Error() string { return AppendUsageTip(err, e.Cmd).Error() } +type SingleOptionalArgExpectedError struct { + Cmd *cobra.Command + Expected string + Count int +} + +func (e *SingleOptionalArgExpectedError) Error() string { + err := fmt.Errorf(SINGLE_OPTIONAL_ARG_EXPECTED, e.Expected, e.Count) + return AppendUsageTip(err, e.Cmd).Error() +} + // Used when an unexpected non-flag input (either arg or subcommand) is found type InputUnknownError struct { ProvidedInput string From 282383f83c6008fca0742d553422b2161d0c9e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Wed, 3 Apr 2024 12:54:50 +0100 Subject: [PATCH 07/13] Add support for SKE new credentials rotation flow (#186) * SKE Credentials Rotation: start-rotation command (#179) * initial start rotation command implementation * update go mod, rename files, generate docs * start-rotation update long description * Improve start-rotation documentation * SKE Credentials Rotation: complete-rotation command (#181) * initial complete rotation command * complete rotation command: improve documentation * improve documentation * SKE Credentials Rotation: kubeconfig create command (#184) * initial kubeconfig create command * finish create command implementation, add testing * fix linting, generate docs * address PR comments * extract funcs to utils, add testing * improve function documentation * fix linting * address PR comments, minor improvements * make utils testing work on all OSes * SKE Credentials Rotation: fixes and improvements (#188) * improve docs * Improve documentation, address acceptance comments * fix testing, finish renaming filepath flag * SKE Credentials Rotation: deprecate old commands (#191) * deprecate describe and rotate commands * improve deprecation messages, add docs * SKE Credentials Rotation: deprecate old commands improvements (#192) * remove example cluster name from deprecation message * address comments --- docs/stackit_ske.md | 1 + docs/stackit_ske_credentials.md | 4 +- ...ackit_ske_credentials_complete-rotation.md | 53 ++++ docs/stackit_ske_credentials_describe.md | 41 --- docs/stackit_ske_credentials_rotate.md | 38 --- .../stackit_ske_credentials_start-rotation.md | 57 +++++ docs/stackit_ske_kubeconfig.md | 32 +++ docs/stackit_ske_kubeconfig_create.md | 54 ++++ .../complete-rotation/complete_rotation.go | 125 ++++++++++ .../complete_rotation_test.go | 203 +++++++++++++++ internal/cmd/ske/credentials/credentials.go | 4 + .../cmd/ske/credentials/describe/describe.go | 6 + internal/cmd/ske/credentials/rotate/rotate.go | 7 + .../start-rotation/start_rotation.go | 128 ++++++++++ .../start-rotation/start_rotation_test.go | 203 +++++++++++++++ internal/cmd/ske/kubeconfig/create/create.go | 163 ++++++++++++ .../cmd/ske/kubeconfig/create/create_test.go | 236 ++++++++++++++++++ internal/cmd/ske/kubeconfig/kubeconfig.go | 25 ++ internal/cmd/ske/ske.go | 2 + internal/pkg/services/ske/utils/utils.go | 74 ++++++ internal/pkg/services/ske/utils/utils_test.go | 169 +++++++++++++ 21 files changed, 1544 insertions(+), 81 deletions(-) create mode 100644 docs/stackit_ske_credentials_complete-rotation.md delete mode 100644 docs/stackit_ske_credentials_describe.md delete mode 100644 docs/stackit_ske_credentials_rotate.md create mode 100644 docs/stackit_ske_credentials_start-rotation.md create mode 100644 docs/stackit_ske_kubeconfig.md create mode 100644 docs/stackit_ske_kubeconfig_create.md create mode 100644 internal/cmd/ske/credentials/complete-rotation/complete_rotation.go create mode 100644 internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go create mode 100644 internal/cmd/ske/credentials/start-rotation/start_rotation.go create mode 100644 internal/cmd/ske/credentials/start-rotation/start_rotation_test.go create mode 100644 internal/cmd/ske/kubeconfig/create/create.go create mode 100644 internal/cmd/ske/kubeconfig/create/create_test.go create mode 100644 internal/cmd/ske/kubeconfig/kubeconfig.go diff --git a/docs/stackit_ske.md b/docs/stackit_ske.md index 4cf76387c..0f3834afc 100644 --- a/docs/stackit_ske.md +++ b/docs/stackit_ske.md @@ -33,5 +33,6 @@ stackit ske [flags] * [stackit ske describe](./stackit_ske_describe.md) - Shows overall details regarding SKE * [stackit ske disable](./stackit_ske_disable.md) - Disables SKE for a project * [stackit ske enable](./stackit_ske_enable.md) - Enables SKE for a project +* [stackit ske kubeconfig](./stackit_ske_kubeconfig.md) - Provides functionality for SKE kubeconfig * [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options diff --git a/docs/stackit_ske_credentials.md b/docs/stackit_ske_credentials.md index 28e4beddb..8b7c6f4b9 100644 --- a/docs/stackit_ske_credentials.md +++ b/docs/stackit_ske_credentials.md @@ -28,6 +28,6 @@ stackit ske credentials [flags] ### SEE ALSO * [stackit ske](./stackit_ske.md) - Provides functionality for SKE -* [stackit ske credentials describe](./stackit_ske_credentials_describe.md) - Shows details of the credentials associated to a SKE cluster -* [stackit ske credentials rotate](./stackit_ske_credentials_rotate.md) - Rotates credentials associated to a SKE cluster +* [stackit ske credentials complete-rotation](./stackit_ske_credentials_complete-rotation.md) - Completes the rotation of the credentials associated to a SKE cluster +* [stackit ske credentials start-rotation](./stackit_ske_credentials_start-rotation.md) - Starts the rotation of the credentials associated to a SKE cluster diff --git a/docs/stackit_ske_credentials_complete-rotation.md b/docs/stackit_ske_credentials_complete-rotation.md new file mode 100644 index 000000000..404d7509f --- /dev/null +++ b/docs/stackit_ske_credentials_complete-rotation.md @@ -0,0 +1,53 @@ +## stackit ske credentials complete-rotation + +Completes the rotation of the credentials associated to a SKE cluster + +### Synopsis + +Completes the rotation of the credentials associated to a STACKIT Kubernetes Engine (SKE) cluster. + +This is step 2 of a 2-step process to rotate all SKE cluster credentials. Tasks accomplished in this phase include: + - The old certification authority will be dropped from the package. + - The old signing key for the service account will be dropped from the bundle. +To ensure continued access to the Kubernetes cluster, please update your kubeconfig with the new credentials: + $ stackit ske kubeconfig create my-cluster + +If you haven't, please start the process by running: + $ stackit ske credentials start-rotation my-cluster +For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html + +``` +stackit ske credentials complete-rotation CLUSTER_NAME [flags] +``` + +### Examples + +``` + Complete the rotation of the credentials associated to the SKE cluster with name "my-cluster" + $ stackit ske credentials complete-rotation my-cluster + + Flow of the 2-step process to rotate all SKE cluster credentials, including generating a new kubeconfig file + $ stackit ske credentials start-rotation my-cluster + $ stackit ske kubeconfig create my-cluster + $ stackit ske credentials complete-rotation my-cluster +``` + +### Options + +``` + -h, --help Help for "stackit ske credentials complete-rotation" +``` + +### 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"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit ske credentials](./stackit_ske_credentials.md) - Provides functionality for SKE credentials + diff --git a/docs/stackit_ske_credentials_describe.md b/docs/stackit_ske_credentials_describe.md deleted file mode 100644 index 712453cd4..000000000 --- a/docs/stackit_ske_credentials_describe.md +++ /dev/null @@ -1,41 +0,0 @@ -## stackit ske credentials describe - -Shows details of the credentials associated to a SKE cluster - -### Synopsis - -Shows details of the credentials associated to a STACKIT Kubernetes Engine (SKE) cluster - -``` -stackit ske credentials describe CLUSTER_NAME [flags] -``` - -### Examples - -``` - Get details of the credentials associated to the SKE cluster with name "my-cluster" - $ stackit ske credentials describe my-cluster - - Get details of the credentials associated to the SKE cluster with name "my-cluster" in a table format - $ stackit ske credentials describe my-cluster --output-format pretty -``` - -### Options - -``` - -h, --help Help for "stackit ske credentials 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"] - -p, --project-id string Project ID -``` - -### SEE ALSO - -* [stackit ske credentials](./stackit_ske_credentials.md) - Provides functionality for SKE credentials - diff --git a/docs/stackit_ske_credentials_rotate.md b/docs/stackit_ske_credentials_rotate.md deleted file mode 100644 index ff3acb9a1..000000000 --- a/docs/stackit_ske_credentials_rotate.md +++ /dev/null @@ -1,38 +0,0 @@ -## stackit ske credentials rotate - -Rotates credentials associated to a SKE cluster - -### Synopsis - -Rotates credentials associated to a STACKIT Kubernetes Engine (SKE) cluster. The old credentials will be invalid after the operation. - -``` -stackit ske credentials rotate CLUSTER_NAME [flags] -``` - -### Examples - -``` - Rotate credentials associated to the SKE cluster with name "my-cluster" - $ stackit ske credentials rotate my-cluster -``` - -### Options - -``` - -h, --help Help for "stackit ske credentials rotate" -``` - -### 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"] - -p, --project-id string Project ID -``` - -### SEE ALSO - -* [stackit ske credentials](./stackit_ske_credentials.md) - Provides functionality for SKE credentials - diff --git a/docs/stackit_ske_credentials_start-rotation.md b/docs/stackit_ske_credentials_start-rotation.md new file mode 100644 index 000000000..b1c2cac66 --- /dev/null +++ b/docs/stackit_ske_credentials_start-rotation.md @@ -0,0 +1,57 @@ +## stackit ske credentials start-rotation + +Starts the rotation of the credentials associated to a SKE cluster + +### Synopsis + +Starts the rotation of the credentials associated to a STACKIT Kubernetes Engine (SKE) cluster. + +This is step 1 of a 2-step process to rotate all SKE cluster credentials. Tasks accomplished in this phase include: + - Rolling recreation of all worker nodes + - A new Certificate Authority (CA) will be established and incorporated into the existing CA bundle. + - A new etcd encryption key is generated and added to the Certificate Authority (CA) bundle. + - A new signing key will be generated for the service account and added to the Certificate Authority (CA) bundle. + - The kube-apiserver will rewrite all secrets in the cluster, encrypting them with the new encryption key. +The old CA, encryption key and signing key will be retained until the rotation is completed. + +After completing the rotation of credentials, you can generate a new kubeconfig file by running: + $ stackit ske kubeconfig create my-cluster +Complete the rotation by running: + $ stackit ske credentials complete-rotation my-cluster +For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html + +``` +stackit ske credentials start-rotation CLUSTER_NAME [flags] +``` + +### Examples + +``` + Start the rotation of the credentials associated to the SKE cluster with name "my-cluster" + $ stackit ske credentials start-rotation my-cluster + + Flow of the 2-step process to rotate all SKE cluster credentials, including generating a new kubeconfig file + $ stackit ske credentials start-rotation my-cluster + $ stackit ske kubeconfig create my-cluster + $ stackit ske credentials complete-rotation my-cluster +``` + +### Options + +``` + -h, --help Help for "stackit ske credentials start-rotation" +``` + +### 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"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit ske credentials](./stackit_ske_credentials.md) - Provides functionality for SKE credentials + diff --git a/docs/stackit_ske_kubeconfig.md b/docs/stackit_ske_kubeconfig.md new file mode 100644 index 000000000..ec9e7011f --- /dev/null +++ b/docs/stackit_ske_kubeconfig.md @@ -0,0 +1,32 @@ +## stackit ske kubeconfig + +Provides functionality for SKE kubeconfig + +### Synopsis + +Provides functionality for STACKIT Kubernetes Engine (SKE) kubeconfig. + +``` +stackit ske kubeconfig [flags] +``` + +### Options + +``` + -h, --help Help for "stackit ske kubeconfig" +``` + +### 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"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit ske](./stackit_ske.md) - Provides functionality for SKE +* [stackit ske kubeconfig create](./stackit_ske_kubeconfig_create.md) - Creates a kubeconfig for an SKE cluster + diff --git a/docs/stackit_ske_kubeconfig_create.md b/docs/stackit_ske_kubeconfig_create.md new file mode 100644 index 000000000..39718071d --- /dev/null +++ b/docs/stackit_ske_kubeconfig_create.md @@ -0,0 +1,54 @@ +## stackit ske kubeconfig create + +Creates a kubeconfig for an SKE cluster + +### Synopsis + +Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster. + +By default the kubeconfig is created in the .kube folder, in the user's home directory. The kubeconfig file will be overwritten if it already exists. +You can override this behavior by specifying a custom filepath with the --filepath flag. +An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 1h. +Note that the format is , e.g. 30d for 30 days and you can't combine units. + +``` +stackit ske kubeconfig create CLUSTER_NAME [flags] +``` + +### Examples + +``` + Create a kubeconfig for the SKE cluster with name "my-cluster" + $ stackit ske kubeconfig create my-cluster + + Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days + $ stackit ske kubeconfig create my-cluster --expiration 30d + + Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months + $ stackit ske kubeconfig create my-cluster --expiration 2M + + Create a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath + $ stackit ske kubeconfig create my-cluster --filepath /path/to/config +``` + +### Options + +``` + -e, --expiration string Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h + --filepath string Path to create the kubeconfig file. By default, the kubeconfig is created as 'config' in the .kube folder, in the user's home directory. + -h, --help Help for "stackit ske kubeconfig create" +``` + +### 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"] + -p, --project-id string Project ID +``` + +### SEE ALSO + +* [stackit ske kubeconfig](./stackit_ske_kubeconfig.md) - Provides functionality for SKE kubeconfig + diff --git a/internal/cmd/ske/credentials/complete-rotation/complete_rotation.go b/internal/cmd/ske/credentials/complete-rotation/complete_rotation.go new file mode 100644 index 000000000..d7953efe2 --- /dev/null +++ b/internal/cmd/ske/credentials/complete-rotation/complete_rotation.go @@ -0,0 +1,125 @@ +package completerotation + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + "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/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" +) + +const ( + clusterNameArg = "CLUSTER_NAME" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ClusterName string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("complete-rotation %s", clusterNameArg), + Short: "Completes the rotation of the credentials associated to a SKE cluster", + Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s\n%s\n\n%s\n%s\n%s", + "Completes the rotation of the credentials associated to a STACKIT Kubernetes Engine (SKE) cluster.", + "This is step 2 of a 2-step process to rotate all SKE cluster credentials. Tasks accomplished in this phase include:", + " - The old certification authority will be dropped from the package.", + " - The old signing key for the service account will be dropped from the bundle.", + "To ensure continued access to the Kubernetes cluster, please update your kubeconfig with the new credentials:", + " $ stackit ske kubeconfig create my-cluster", + "If you haven't, please start the process by running:", + " $ stackit ske credentials start-rotation my-cluster", + "For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html", + ), + Args: args.SingleArg(clusterNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Complete the rotation of the credentials associated to the SKE cluster with name "my-cluster"`, + "$ stackit ske credentials complete-rotation my-cluster", + ), + examples.NewExample( + `Flow of the 2-step process to rotate all SKE cluster credentials, including generating a new kubeconfig file`, + "$ stackit ske credentials start-rotation my-cluster", + "$ stackit ske kubeconfig create my-cluster", + "$ stackit ske credentials complete-rotation my-cluster", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to complete the rotation of the credentials for SKE cluster %q?", model.ClusterName) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("complete rotation of SKE credentials: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(cmd) + s.Start("Completing credentials rotation") + _, err = wait.CompleteCredentialsRotationWaitHandler(ctx, apiClient, model.ProjectId, model.ClusterName).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for completing SKE credentials rotation %w", err) + } + s.Stop() + } + + operationState := "Rotation of credentials is completed" + if model.Async { + operationState = "Triggered completion of credentials rotation" + } + cmd.Printf("%s for cluster %q\n", operationState, model.ClusterName) + cmd.Printf("Consider updating your kubeconfig with the new credentials, create a new kubeconfig by running:\n $ stackit ske kubeconfig create %s\n", model.ClusterName) + return nil + }, + } + return cmd +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + clusterName := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + ClusterName: clusterName, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiCompleteCredentialsRotationRequest { + req := apiClient.CompleteCredentialsRotation(ctx, model.ProjectId, model.ClusterName) + return req +} diff --git a/internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go b/internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go new file mode 100644 index 000000000..42bc1e71e --- /dev/null +++ b/internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go @@ -0,0 +1,203 @@ +package completerotation + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} +var testProjectId = uuid.NewString() +var testClusterName = "cluster" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testClusterName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + ClusterName: testClusterName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ske.ApiCompleteCredentialsRotationRequest)) ske.ApiCompleteCredentialsRotationRequest { + request := testClient.CompleteCredentialsRotation(testCtx, testProjectId, testClusterName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ske.ApiCompleteCredentialsRotationRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/ske/credentials/credentials.go b/internal/cmd/ske/credentials/credentials.go index be20e1a72..96c2f0d49 100644 --- a/internal/cmd/ske/credentials/credentials.go +++ b/internal/cmd/ske/credentials/credentials.go @@ -1,8 +1,10 @@ package credentials import ( + completerotation "github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/complete-rotation" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/rotate" + startrotation "github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/start-rotation" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -24,4 +26,6 @@ func NewCmd() *cobra.Command { func addSubcommands(cmd *cobra.Command) { cmd.AddCommand(describe.NewCmd()) cmd.AddCommand(rotate.NewCmd()) + cmd.AddCommand(startrotation.NewCmd()) + cmd.AddCommand(completerotation.NewCmd()) } diff --git a/internal/cmd/ske/credentials/describe/describe.go b/internal/cmd/ske/credentials/describe/describe.go index 01917510c..84c683a11 100644 --- a/internal/cmd/ske/credentials/describe/describe.go +++ b/internal/cmd/ske/credentials/describe/describe.go @@ -32,6 +32,12 @@ func NewCmd() *cobra.Command { Short: "Shows details of the credentials associated to a SKE cluster", Long: "Shows details of the credentials associated to a STACKIT Kubernetes Engine (SKE) cluster", Args: args.SingleArg(clusterNameArg, nil), + Deprecated: fmt.Sprintf("%s\n%s\n%s\n%s\n", + "and will be removed in a future release.", + "Please use the following command to obtain a kubeconfig file instead:", + " $ stackit ske kubeconfig create CLUSTER_NAME", + "For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html", + ), Example: examples.Build( examples.NewExample( `Get details of the credentials associated to the SKE cluster with name "my-cluster"`, diff --git a/internal/cmd/ske/credentials/rotate/rotate.go b/internal/cmd/ske/credentials/rotate/rotate.go index cba8ea31e..57b3c8a36 100644 --- a/internal/cmd/ske/credentials/rotate/rotate.go +++ b/internal/cmd/ske/credentials/rotate/rotate.go @@ -32,6 +32,13 @@ func NewCmd() *cobra.Command { Short: "Rotates credentials associated to a SKE cluster", Long: "Rotates credentials associated to a STACKIT Kubernetes Engine (SKE) cluster. The old credentials will be invalid after the operation.", Args: args.SingleArg(clusterNameArg, nil), + Deprecated: fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n", + "and will be removed in a future release.", + "Please use the 2-step credential rotation flow instead, by running the commands:", + " $ stackit ske credentials start-rotation CLUSTER_NAME", + " $ stackit ske credentials complete-rotation CLUSTER_NAME", + "For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html", + ), Example: examples.Build( examples.NewExample( `Rotate credentials associated to the SKE cluster with name "my-cluster"`, diff --git a/internal/cmd/ske/credentials/start-rotation/start_rotation.go b/internal/cmd/ske/credentials/start-rotation/start_rotation.go new file mode 100644 index 000000000..da9b0cb91 --- /dev/null +++ b/internal/cmd/ske/credentials/start-rotation/start_rotation.go @@ -0,0 +1,128 @@ +package startrotation + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + "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/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" +) + +const ( + clusterNameArg = "CLUSTER_NAME" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ClusterName string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("start-rotation %s", clusterNameArg), + Short: "Starts the rotation of the credentials associated to a SKE cluster", + Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n%s\n%s\n%s\n%s\n%s", + "Starts the rotation of the credentials associated to a STACKIT Kubernetes Engine (SKE) cluster.", + "This is step 1 of a 2-step process to rotate all SKE cluster credentials. Tasks accomplished in this phase include:", + " - Rolling recreation of all worker nodes", + " - A new Certificate Authority (CA) will be established and incorporated into the existing CA bundle.", + " - A new etcd encryption key is generated and added to the Certificate Authority (CA) bundle.", + " - A new signing key will be generated for the service account and added to the Certificate Authority (CA) bundle.", + " - The kube-apiserver will rewrite all secrets in the cluster, encrypting them with the new encryption key.", + "The old CA, encryption key and signing key will be retained until the rotation is completed.", + "After completing the rotation of credentials, you can generate a new kubeconfig file by running:", + " $ stackit ske kubeconfig create my-cluster", + "Complete the rotation by running:", + " $ stackit ske credentials complete-rotation my-cluster", + "For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html", + ), + Args: args.SingleArg(clusterNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Start the rotation of the credentials associated to the SKE cluster with name "my-cluster"`, + "$ stackit ske credentials start-rotation my-cluster"), + examples.NewExample( + `Flow of the 2-step process to rotate all SKE cluster credentials, including generating a new kubeconfig file`, + "$ stackit ske credentials start-rotation my-cluster", + "$ stackit ske kubeconfig create my-cluster", + "$ stackit ske credentials complete-rotation my-cluster", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to start the rotation of the credentials for SKE cluster %q?", model.ClusterName) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("start rotation of SKE credentials: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(cmd) + s.Start("Starting credentials rotation") + _, err = wait.StartCredentialsRotationWaitHandler(ctx, apiClient, model.ProjectId, model.ClusterName).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for start SKE credentials rotation %w", err) + } + s.Stop() + } + + operationState := "Rotation of credentials is ready to be completed" + if model.Async { + operationState = "Triggered start of credentials rotation" + } + cmd.Printf("%s for cluster %q\n", operationState, model.ClusterName) + cmd.Printf("Complete the rotation by running:\n $ stackit ske credentials complete-rotation %s\n", model.ClusterName) + return nil + }, + } + return cmd +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + clusterName := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + ClusterName: clusterName, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiStartCredentialsRotationRequest { + req := apiClient.StartCredentialsRotation(ctx, model.ProjectId, model.ClusterName) + return req +} diff --git a/internal/cmd/ske/credentials/start-rotation/start_rotation_test.go b/internal/cmd/ske/credentials/start-rotation/start_rotation_test.go new file mode 100644 index 000000000..9960008d4 --- /dev/null +++ b/internal/cmd/ske/credentials/start-rotation/start_rotation_test.go @@ -0,0 +1,203 @@ +package startrotation + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} +var testProjectId = uuid.NewString() +var testClusterName = "cluster" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testClusterName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + ClusterName: testClusterName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ske.ApiStartCredentialsRotationRequest)) ske.ApiStartCredentialsRotationRequest { + request := testClient.StartCredentialsRotation(testCtx, testProjectId, testClusterName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ske.ApiStartCredentialsRotationRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/ske/kubeconfig/create/create.go b/internal/cmd/ske/kubeconfig/create/create.go new file mode 100644 index 000000000..6308a729a --- /dev/null +++ b/internal/cmd/ske/kubeconfig/create/create.go @@ -0,0 +1,163 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" + "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/services/ske/client" + skeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +const ( + clusterNameArg = "CLUSTER_NAME" + + expirationFlag = "expiration" + filepathFlag = "filepath" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ClusterName string + Filepath *string + ExpirationTime *string +} + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("create %s", clusterNameArg), + Short: "Creates a kubeconfig for an SKE cluster", + Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s", + "Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster.", + "By default the kubeconfig is created in the .kube folder, in the user's home directory. The kubeconfig file will be overwritten if it already exists.", + "You can override this behavior by specifying a custom filepath with the --filepath flag.", + "An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 1h.", + "Note that the format is , e.g. 30d for 30 days and you can't combine units."), + Args: args.SingleArg(clusterNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Create a kubeconfig for the SKE cluster with name "my-cluster"`, + "$ stackit ske kubeconfig create my-cluster"), + examples.NewExample( + `Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days`, + "$ stackit ske kubeconfig create my-cluster --expiration 30d"), + examples.NewExample( + `Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months`, + "$ stackit ske kubeconfig create my-cluster --expiration 2M"), + examples.NewExample( + `Create a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath`, + "$ stackit ske kubeconfig create my-cluster --filepath /path/to/config"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(cmd) + if err != nil { + return err + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a kubeconfig for SKE cluster %q? This will OVERWRITE your current kubeconfig file, if it exists.", model.ClusterName) + err = confirm.PromptForConfirmation(cmd, prompt) + if err != nil { + return err + } + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("build kubeconfig create request: %w", err) + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create kubeconfig for SKE cluster: %w", err) + } + + // Create the config file + if resp.Kubeconfig == nil { + return fmt.Errorf("no kubeconfig returned from the API") + } + + var kubeconfigPath string + if model.Filepath == nil { + kubeconfigPath, err = skeUtils.GetDefaultKubeconfigPath() + if err != nil { + return fmt.Errorf("get default kubeconfig path: %w", err) + } + } else { + kubeconfigPath = *model.Filepath + } + + err = skeUtils.WriteConfigFile(kubeconfigPath, *resp.Kubeconfig) + if err != nil { + return fmt.Errorf("write kubeconfig file: %w", err) + } + + fmt.Printf("Created kubeconfig file for cluster %s in %q, with expiration date %v (UTC)\n", model.ClusterName, kubeconfigPath, *resp.ExpirationTimestamp) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(expirationFlag, "e", "", "Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h") + cmd.Flags().String(filepathFlag, "", "Path to create the kubeconfig file. By default, the kubeconfig is created as 'config' in the .kube folder, in the user's home directory.") +} + +func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + clusterName := inputArgs[0] + + globalFlags := globalflags.Parse(cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + expTime := flags.FlagToStringPointer(cmd, expirationFlag) + + if expTime != nil { + var err error + expTime, err = skeUtils.ConvertToSeconds(*expTime) + if err != nil { + return nil, &errors.FlagValidationError{ + Flag: expirationFlag, + Details: err.Error(), + } + } + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + ClusterName: clusterName, + Filepath: flags.FlagToStringPointer(cmd, filepathFlag), + ExpirationTime: expTime, + }, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) (ske.ApiCreateKubeconfigRequest, error) { + req := apiClient.CreateKubeconfig(ctx, model.ProjectId, model.ClusterName) + + payload := ske.CreateKubeconfigPayload{} + + if model.ExpirationTime != nil { + payload.ExpirationSeconds = model.ExpirationTime + } + + return req.CreateKubeconfigPayload(payload), nil +} diff --git a/internal/cmd/ske/kubeconfig/create/create_test.go b/internal/cmd/ske/kubeconfig/create/create_test.go new file mode 100644 index 000000000..ec907dbd3 --- /dev/null +++ b/internal/cmd/ske/kubeconfig/create/create_test.go @@ -0,0 +1,236 @@ +package create + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} +var testProjectId = uuid.NewString() +var testClusterName = "cluster" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testClusterName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + ClusterName: testClusterName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.ApiCreateKubeconfigRequest { + request := testClient.CreateKubeconfig(testCtx, testProjectId, testClusterName) + request = request.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{}) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "30d expiration time", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues["expiration"] = "30d" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ExpirationTime = utils.Ptr("2592000") + }), + }, + + { + description: "custom filepath", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues["filepath"] = "/path/to/config" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Filepath = utils.Ptr("/path/to/config") + }), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd() + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest ske.ApiCreateKubeconfigRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "expiration time", + model: fixtureInputModel(func(model *inputModel) { + model.ExpirationTime = utils.Ptr("2592000") + }), + expectedRequest: fixtureRequest().CreateKubeconfigPayload(ske.CreateKubeconfigPayload{ + ExpirationSeconds: utils.Ptr("2592000")}), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, _ := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/ske/kubeconfig/kubeconfig.go b/internal/cmd/ske/kubeconfig/kubeconfig.go new file mode 100644 index 000000000..5411cd0a2 --- /dev/null +++ b/internal/cmd/ske/kubeconfig/kubeconfig.go @@ -0,0 +1,25 @@ +package kubeconfig + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/kubeconfig/create" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "kubeconfig", + Short: "Provides functionality for SKE kubeconfig", + Long: "Provides functionality for STACKIT Kubernetes Engine (SKE) kubeconfig.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd) + return cmd +} + +func addSubcommands(cmd *cobra.Command) { + cmd.AddCommand(create.NewCmd()) +} diff --git a/internal/cmd/ske/ske.go b/internal/cmd/ske/ske.go index ebfe89036..32a40662c 100644 --- a/internal/cmd/ske/ske.go +++ b/internal/cmd/ske/ske.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/ske/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/disable" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/enable" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/kubeconfig" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -28,6 +29,7 @@ func NewCmd() *cobra.Command { func addSubcommands(cmd *cobra.Command) { cmd.AddCommand(describe.NewCmd()) cmd.AddCommand(enable.NewCmd()) + cmd.AddCommand(kubeconfig.NewCmd()) cmd.AddCommand(disable.NewCmd()) cmd.AddCommand(cluster.NewCmd()) cmd.AddCommand(credentials.NewCmd()) diff --git a/internal/pkg/services/ske/utils/utils.go b/internal/pkg/services/ske/utils/utils.go index b055d12a9..901e961c3 100644 --- a/internal/pkg/services/ske/utils/utils.go +++ b/internal/pkg/services/ske/utils/utils.go @@ -3,6 +3,9 @@ package utils import ( "context" "fmt" + "os" + "path/filepath" + "strconv" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -191,3 +194,74 @@ func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) return output, nil } + +// ConvertToSeconds converts a time string to seconds. +// The time string must be in the format of , where unit is one of s, m, h, d, M. +func ConvertToSeconds(timeStr string) (*string, error) { + if len(timeStr) < 2 { + return nil, fmt.Errorf("invalid time: %s", timeStr) + } + + unit := timeStr[len(timeStr)-1:] + + valueStr := timeStr[:len(timeStr)-1] + value, err := strconv.ParseUint(valueStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid time value: %s", valueStr) + } + + var multiplier uint64 + switch unit { + // second + case "s": + multiplier = 1 + // minute + case "m": + multiplier = 60 + // hour + case "h": + multiplier = 60 * 60 + // day + case "d": + multiplier = 60 * 60 * 24 + // month, assume 30 days + case "M": + multiplier = 60 * 60 * 24 * 30 + default: + return nil, fmt.Errorf("invalid time unit: %s", unit) + } + + result := uint64(value) * multiplier + return utils.Ptr(strconv.FormatUint(result, 10)), nil +} + +// WriteConfigFile writes the given data to the given path. +// The directory is created if it does not exist. +func WriteConfigFile(configPath, data string) error { + if data == "" { + return fmt.Errorf("no data to write") + } + + dir := filepath.Dir(configPath) + + err := os.MkdirAll(dir, 0o700) + if err != nil { + return fmt.Errorf("create config directory: %w", err) + } + + err = os.WriteFile(configPath, []byte(data), 0o600) + if err != nil { + return fmt.Errorf("write file: %w", err) + } + return nil +} + +// GetDefaultKubeconfigPath returns the default location for the kubeconfig file. +func GetDefaultKubeconfigPath() (string, error) { + userHome, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("get user home directory: %w", err) + } + + return filepath.Join(userHome, ".kube", "config"), nil +} diff --git a/internal/pkg/services/ske/utils/utils_test.go b/internal/pkg/services/ske/utils/utils_test.go index 52e151891..fe977e1f2 100644 --- a/internal/pkg/services/ske/utils/utils_test.go +++ b/internal/pkg/services/ske/utils/utils_test.go @@ -3,6 +3,8 @@ package utils import ( "context" "fmt" + "os" + "path/filepath" "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -441,3 +443,170 @@ func TestGetDefaultPayload(t *testing.T) { }) } } + +func TestConvertToSeconds(t *testing.T) { + tests := []struct { + description string + expirationTime string + isValid bool + expectedOutput string + }{ + { + description: "seconds", + expirationTime: "30s", + isValid: true, + expectedOutput: "30", + }, + { + description: "minutes", + expirationTime: "30m", + isValid: true, + expectedOutput: "1800", + }, + { + description: "hours", + expirationTime: "30h", + isValid: true, + expectedOutput: "108000", + }, + { + description: "days", + expirationTime: "30d", + isValid: true, + expectedOutput: "2592000", + }, + { + description: "months", + expirationTime: "30M", + isValid: true, + expectedOutput: "77760000", + }, + { + description: "leading zero", + expirationTime: "0030M", + isValid: true, + expectedOutput: "77760000", + }, + { + description: "invalid unit", + expirationTime: "30x", + isValid: false, + }, + { + description: "invalid unit 2", + expirationTime: "3000abcdef", + isValid: false, + }, + { + description: "invalid unit 3", + expirationTime: "3000abcdef000", + isValid: false, + }, + { + description: "invalid time", + expirationTime: "x", + isValid: false, + }, + { + description: "empty", + expirationTime: "", + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := ConvertToSeconds(tt.expirationTime) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if *output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, *output) + } + }) + } +} + +func TestWriteConfigFile(t *testing.T) { + tests := []struct { + description string + location string + kubeconfig string + isValid bool + isLocationDir bool + isLocationEmpty bool + expectedErr string + }{ + { + description: "base", + location: filepath.Join("base", "config"), + kubeconfig: "kubeconfig", + isValid: true, + }, + { + description: "empty location", + location: "", + kubeconfig: "kubeconfig", + isValid: false, + isLocationEmpty: true, + }, + { + description: "path is only dir", + location: "only_dir", + kubeconfig: "kubeconfig", + isValid: false, + isLocationDir: true, + }, + { + description: "empty kubeconfig", + location: filepath.Join("empty", "config"), + kubeconfig: "", + isValid: false, + }, + } + + baseTestDir := "test_data/" + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testLocation := filepath.Join(baseTestDir, tt.location) + // make sure empty case still works + if tt.isLocationEmpty { + testLocation = "" + } + // filepath Join cleans trailing separators + if tt.isLocationDir { + testLocation += string(filepath.Separator) + } + err := WriteConfigFile(testLocation, tt.kubeconfig) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + + if tt.isValid { + data, err := os.ReadFile(testLocation) + if err != nil { + t.Errorf("could not read file: %s", tt.location) + } + if string(data) != tt.kubeconfig { + t.Errorf("expected file content to be %s, got %s", tt.kubeconfig, string(data)) + } + } + }) + } + // Cleanup + err := os.RemoveAll(baseTestDir) + if err != nil { + t.Errorf("failed cleaning test data") + } +} From 3cc4fd7c0e406d9ee8c4ee80daee3844819e905e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Thu, 4 Apr 2024 09:28:49 +0100 Subject: [PATCH 08/13] UX improvements: update project describe help (#193) * update project describe examples * generate docs --- docs/stackit_project_describe.md | 2 +- internal/cmd/project/describe/describe.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stackit_project_describe.md b/docs/stackit_project_describe.md index a0900fbb6..d2f9f7eb5 100644 --- a/docs/stackit_project_describe.md +++ b/docs/stackit_project_describe.md @@ -17,7 +17,7 @@ stackit project describe [flags] $ stackit project describe Get the details of a STACKIT project by explicitly providing the project ID - $ stackit project describe --project-id xxx + $ stackit project describe xxx Get the details of the configured STACKIT project, including details of the parent resources $ stackit project describe --include-parents diff --git a/internal/cmd/project/describe/describe.go b/internal/cmd/project/describe/describe.go index 460204e3f..4d692b8c0 100644 --- a/internal/cmd/project/describe/describe.go +++ b/internal/cmd/project/describe/describe.go @@ -41,7 +41,7 @@ func NewCmd() *cobra.Command { "$ stackit project describe"), examples.NewExample( `Get the details of a STACKIT project by explicitly providing the project ID`, - "$ stackit project describe --project-id xxx"), + "$ stackit project describe xxx"), examples.NewExample( `Get the details of the configured STACKIT project, including details of the parent resources`, "$ stackit project describe --include-parents"), From 09252ea130a764e18a85844df3b41a0845c06a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Thu, 4 Apr 2024 16:48:42 +0100 Subject: [PATCH 09/13] Onboard PostgreSQL Flex force delete (#194) * update create examples to existing flavor * add the --force flag to delete command * generate docs * add forcedeleteinstance waiter * extract deletion logic to function, test it. Address PR comments * fix linting, generate docs * rename util function * update sdk version --- docs/stackit_postgresflex_instance_create.md | 4 +- docs/stackit_postgresflex_instance_delete.md | 8 +- go.mod | 2 +- go.sum | 4 +- .../postgresflex/instance/create/create.go | 4 +- .../postgresflex/instance/delete/delete.go | 109 ++++++++++-- .../instance/delete/delete_test.go | 162 +++++++++++++++++- .../pkg/services/postgresflex/utils/utils.go | 8 + .../services/postgresflex/utils/utils_test.go | 51 ++++++ 9 files changed, 325 insertions(+), 27 deletions(-) diff --git a/docs/stackit_postgresflex_instance_create.md b/docs/stackit_postgresflex_instance_create.md index 0078f9e11..b081370e6 100644 --- a/docs/stackit_postgresflex_instance_create.md +++ b/docs/stackit_postgresflex_instance_create.md @@ -14,13 +14,13 @@ stackit postgresflex instance create [flags] ``` Create a PostgreSQL Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by CPU and RAM. Other parameters are set to default values - $ stackit postgresflex instance create --name my-instance --cpu 1 --ram 4 --acl 0.0.0.0/0 + $ stackit postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 0.0.0.0/0 Create a PostgreSQL Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by ID. Other parameters are set to default values $ stackit postgresflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0 Create a PostgreSQL Flex instance with name "my-instance", allow access to a specific range of IP addresses, specify flavor by CPU and RAM and set storage size to 20 GB. Other parameters are set to default values - $ stackit postgresflex instance create --name my-instance --cpu 1 --ram 4 --acl 1.2.3.0/24 --storage-size 20 + $ stackit postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 1.2.3.0/24 --storage-size 20 ``` ### Options diff --git a/docs/stackit_postgresflex_instance_delete.md b/docs/stackit_postgresflex_instance_delete.md index ad64987a5..21dbbe393 100644 --- a/docs/stackit_postgresflex_instance_delete.md +++ b/docs/stackit_postgresflex_instance_delete.md @@ -5,6 +5,8 @@ Deletes a PostgreSQL Flex instance ### Synopsis Deletes a PostgreSQL Flex instance. +By default, instances will be kept in a delayed deleted state for 7 days before being permanently deleted. +Use the --force flag to force the immediate deletion of a delayed deleted instance. ``` stackit postgresflex instance delete INSTANCE_ID [flags] @@ -15,12 +17,16 @@ stackit postgresflex instance delete INSTANCE_ID [flags] ``` Delete a PostgreSQL Flex instance with ID "xxx" $ stackit postgresflex instance delete xxx + + Force the deletion of a delayed deleted PostgreSQL Flex instance with ID "xxx" + $ stackit postgresflex instance delete xxx --force ``` ### Options ``` - -h, --help Help for "stackit postgresflex instance delete" + -f, --force Force deletion of a delayed deleted instance + -h, --help Help for "stackit postgresflex instance delete" ``` ### Options inherited from parent commands diff --git a/go.mod b/go.mod index 0923fc9c2..ed6dc1ea9 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4 github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.11.1 github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.10.1 - github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.11.0 + github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.12.0 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.7.7 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.6.0 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.6 diff --git a/go.sum b/go.sum index 83285a8f3..549215f25 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.8.6 h1:+mcoBKs6 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.8.6/go.mod h1:W9BML8bqZb2dOZe1K+M+qBBs8/QNirr3jA0xxy9tNRY= github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.10.1 h1:LKic8dXtXKsRst2+wY9dNjjkMyJ05QIDpOJuRmVb410= github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.10.1/go.mod h1:g1o1bmqtTliy9UkFlRV/6bn6GQk+hkvnny3UjMI69S0= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.11.0 h1:w3vUPJcPE81nItkkbPs1pxm+QF4c0YIbPyY0dd6qI2w= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.11.0/go.mod h1:P0YyvgwIsVKJijdWGVJVOp/ac7PVX99Oj+dr4v1zECc= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.12.0 h1:W2WSYUyhKaHQ+BZfmyRw9PKv5q7ihGRyNhNgIlyM+Y8= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.12.0/go.mod h1:P0YyvgwIsVKJijdWGVJVOp/ac7PVX99Oj+dr4v1zECc= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.10.0 h1:Fle394socpyf662g3jMrtZpZaWVgBMBIEFnh4fnGock= github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.10.0/go.mod h1:JvqOSrTCiynS0x6Y9OsK54yvdB6AtIWLwXDEjoCkAIg= github.com/stackitcloud/stackit-sdk-go/services/redis v0.10.1 h1:/tRad17HUcGRm448l8XyX6uhnnHVfj3VdUQquIwNq2Q= diff --git a/internal/cmd/postgresflex/instance/create/create.go b/internal/cmd/postgresflex/instance/create/create.go index a94a7513c..8e43ae3a8 100644 --- a/internal/cmd/postgresflex/instance/create/create.go +++ b/internal/cmd/postgresflex/instance/create/create.go @@ -64,13 +64,13 @@ func NewCmd() *cobra.Command { Example: examples.Build( examples.NewExample( `Create a PostgreSQL Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by CPU and RAM. Other parameters are set to default values`, - `$ stackit postgresflex instance create --name my-instance --cpu 1 --ram 4 --acl 0.0.0.0/0`), + `$ stackit postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 0.0.0.0/0`), examples.NewExample( `Create a PostgreSQL Flex instance with name "my-instance", ACL 0.0.0.0/0 (open access) and specify flavor by ID. Other parameters are set to default values`, `$ stackit postgresflex instance create --name my-instance --flavor-id xxx --acl 0.0.0.0/0`), examples.NewExample( `Create a PostgreSQL Flex instance with name "my-instance", allow access to a specific range of IP addresses, specify flavor by CPU and RAM and set storage size to 20 GB. Other parameters are set to default values`, - `$ stackit postgresflex instance create --name my-instance --cpu 1 --ram 4 --acl 1.2.3.0/24 --storage-size 20`), + `$ stackit postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 1.2.3.0/24 --storage-size 20`), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() diff --git a/internal/cmd/postgresflex/instance/delete/delete.go b/internal/cmd/postgresflex/instance/delete/delete.go index 2b5cae439..025d1bf0f 100644 --- a/internal/cmd/postgresflex/instance/delete/delete.go +++ b/internal/cmd/postgresflex/instance/delete/delete.go @@ -8,6 +8,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/confirm" "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/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" @@ -21,23 +22,33 @@ import ( const ( instanceIdArg = "INSTANCE_ID" + + forceDeleteFlag = "force" ) type inputModel struct { *globalflags.GlobalFlagModel - InstanceId string + InstanceId string + ForceDelete bool } func NewCmd() *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a PostgreSQL Flex instance", - Long: "Deletes a PostgreSQL Flex instance.", - Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), + Long: fmt.Sprintf("%s\n%s\n%s", + "Deletes a PostgreSQL Flex instance.", + "By default, instances will be kept in a delayed deleted state for 7 days before being permanently deleted.", + "Use the --force flag to force the immediate deletion of a delayed deleted instance.", + ), + Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Delete a PostgreSQL Flex instance with ID "xxx"`, "$ stackit postgresflex instance delete xxx"), + examples.NewExample( + `Force the deletion of a delayed deleted PostgreSQL Flex instance with ID "xxx"`, + "$ stackit postgresflex instance delete xxx --force"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -65,35 +76,74 @@ func NewCmd() *cobra.Command { } } - // Call API - req := buildRequest(ctx, model, apiClient) - err = req.Execute() + toDelete, toForceDelete, err := getNextOperations(ctx, model, apiClient) if err != nil { - return fmt.Errorf("delete PostgreSQL Flex instance: %w", err) + return err } - // Wait for async operation, if async mode not enabled - if !model.Async { - s := spinner.New(cmd) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + if toDelete { + // Call API + delReq := buildDeleteRequest(ctx, model, apiClient) + err = delReq.Execute() if err != nil { - return fmt.Errorf("wait for PostgreSQL Flex instance deletion: %w", err) + return fmt.Errorf("delete PostgreSQL Flex instance: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(cmd) + s.Start("Deleting instance") + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for PostgreSQL Flex instance deletion: %w", err) + } + s.Stop() + } + } + + if toForceDelete { + // Call API + forceDelReq := buildForceDeleteRequest(ctx, model, apiClient) + err = forceDelReq.Execute() + if err != nil { + return fmt.Errorf("force delete PostgreSQL Flex instance: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + s := spinner.New(cmd) + s.Start("Forcing deletion of instance") + _, err = wait.ForceDeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for PostgreSQL Flex instance force deletion: %w", err) + } + s.Stop() } - s.Stop() } operationState := "Deleted" + if toForceDelete { + operationState = "Forcefully deleted" + } if model.Async { operationState = "Triggered deletion of" + if toForceDelete { + operationState = "Triggered forced deletion of" + } } + cmd.Printf("%s instance %q\n", operationState, instanceLabel) return nil }, } + configureFlags(cmd) return cmd } +func configureFlags(cmd *cobra.Command) { + cmd.Flags().BoolP(forceDeleteFlag, "f", false, "Force deletion of a delayed deleted instance") +} + func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { instanceId := inputArgs[0] @@ -105,10 +155,39 @@ func parseInput(cmd *cobra.Command, inputArgs []string) (*inputModel, error) { return &inputModel{ GlobalFlagModel: globalFlags, InstanceId: instanceId, + ForceDelete: flags.FlagToBoolValue(cmd, forceDeleteFlag), }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiDeleteInstanceRequest { +func buildDeleteRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiDeleteInstanceRequest { req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) return req } + +func buildForceDeleteRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiForceDeleteInstanceRequest { + req := apiClient.ForceDeleteInstance(ctx, model.ProjectId, model.InstanceId) + return req +} + +type PostgreSQLFlexClient interface { + GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*postgresflex.InstanceResponse, error) + ListVersionsExecute(ctx context.Context, projectId string) (*postgresflex.ListVersionsResponse, error) + GetUserExecute(ctx context.Context, projectId, instanceId, userId string) (*postgresflex.GetUserResponse, error) +} + +func getNextOperations(ctx context.Context, model *inputModel, apiClient PostgreSQLFlexClient) (toDelete, toForceDelete bool, err error) { + instanceStatus, err := postgresflexUtils.GetInstanceStatus(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + return false, false, fmt.Errorf("get PostgreSQL Flex instance status: %w", err) + } + + if instanceStatus == wait.InstanceStateDeleted { + if !model.ForceDelete { + return false, false, fmt.Errorf("instance is already deleted, use --force to force the deletion of a delayed deleted instance") + } + + return false, model.ForceDelete, nil + } + + return true, model.ForceDelete, nil +} diff --git a/internal/cmd/postgresflex/instance/delete/delete_test.go b/internal/cmd/postgresflex/instance/delete/delete_test.go index a9b5f8735..26197f744 100644 --- a/internal/cmd/postgresflex/instance/delete/delete_test.go +++ b/internal/cmd/postgresflex/instance/delete/delete_test.go @@ -2,14 +2,17 @@ package delete import ( "context" + "fmt" "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,6 +24,27 @@ var testClient = &postgresflex.APIClient{} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +type postgresFlexClientMocked struct { + getInstanceFails bool + getInstanceResp *postgresflex.InstanceResponse +} + +func (c *postgresFlexClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*postgresflex.InstanceResponse, error) { + if c.getInstanceFails { + return nil, fmt.Errorf("get instance failed") + } + return c.getInstanceResp, nil +} + +func (c *postgresFlexClientMocked) ListVersionsExecute(_ context.Context, _ string) (*postgresflex.ListVersionsResponse, error) { + // Not used in testing + return nil, nil +} +func (c *postgresFlexClientMocked) GetUserExecute(_ context.Context, _, _, _ string) (*postgresflex.GetUserResponse, error) { + // Not used in testing + return nil, nil +} + func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testInstanceId, @@ -54,7 +78,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRequest)) postgresflex.ApiDeleteInstanceRequest { +func fixtureDeleteRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRequest)) postgresflex.ApiDeleteInstanceRequest { request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) @@ -62,6 +86,14 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRequest) return request } +func fixtureForceDeleteRequest(mods ...func(request *postgresflex.ApiForceDeleteInstanceRequest)) postgresflex.ApiForceDeleteInstanceRequest { + request := testClient.ForceDeleteInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + func TestParseInput(t *testing.T) { tests := []struct { description string @@ -186,7 +218,7 @@ func TestParseInput(t *testing.T) { } } -func TestBuildRequest(t *testing.T) { +func TestBuildDeleteRequest(t *testing.T) { tests := []struct { description string model *inputModel @@ -195,13 +227,41 @@ func TestBuildRequest(t *testing.T) { { description: "base", model: fixtureInputModel(), - expectedRequest: fixtureRequest(), + expectedRequest: fixtureDeleteRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildDeleteRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildForceDeleteRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest postgresflex.ApiForceDeleteInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureForceDeleteRequest(), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildForceDeleteRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), @@ -213,3 +273,97 @@ func TestBuildRequest(t *testing.T) { }) } } + +func TestCheckIfInstanceIsDeleted(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedToDelete bool + expectedToForceDelete bool + getInstanceResponse *postgresflex.InstanceResponse + getInstanceFails bool + isValid bool + }{ + { + description: "delete instance state Ready", + model: fixtureInputModel(), + expectedToDelete: true, + expectedToForceDelete: false, + getInstanceResponse: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateSuccess), + }, + }, + isValid: true, + }, + { + description: "force delete instance state Ready", + model: fixtureInputModel(func(model *inputModel) { + model.ForceDelete = true + }), + expectedToDelete: true, + expectedToForceDelete: true, + getInstanceResponse: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateSuccess), + }, + }, + isValid: true, + }, + { + description: "force delete instance state Deleted", + model: fixtureInputModel(func(model *inputModel) { + model.ForceDelete = true + }), + expectedToDelete: false, + expectedToForceDelete: true, + getInstanceResponse: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateDeleted), + }, + }, + isValid: true, + }, + { + description: "delete instance state Deleted", + model: fixtureInputModel(), + getInstanceResponse: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateDeleted), + }, + }, + isValid: false, + }, + { + description: "delete instance get instance fails", + model: fixtureInputModel(), + getInstanceFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &postgresFlexClientMocked{ + getInstanceResp: tt.getInstanceResponse, + getInstanceFails: tt.getInstanceFails, + } + + toDelete, toForceDelete, err := getNextOperations(testCtx, tt.model, client) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error checking if instance is deleted: %v", err) + } + + if toDelete != tt.expectedToDelete { + t.Fatalf("toDelete does not match: got %v, expected %v", toDelete, tt.expectedToDelete) + } + + if toForceDelete != tt.expectedToForceDelete { + t.Fatalf("toForceDelete does not match: got %v, expected %v", toForceDelete, tt.expectedToForceDelete) + } + }) + } +} diff --git a/internal/pkg/services/postgresflex/utils/utils.go b/internal/pkg/services/postgresflex/utils/utils.go index d5582b2f4..298df752e 100644 --- a/internal/pkg/services/postgresflex/utils/utils.go +++ b/internal/pkg/services/postgresflex/utils/utils.go @@ -149,6 +149,14 @@ func GetInstanceName(ctx context.Context, apiClient PostgresFlexClient, projectI return *resp.Item.Name, nil } +func GetInstanceStatus(ctx context.Context, apiClient PostgresFlexClient, projectId, instanceId string) (string, error) { + resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) + if err != nil { + return "", fmt.Errorf("get PostgreSQL Flex instance: %w", err) + } + return *resp.Item.Status, nil +} + func GetUserName(ctx context.Context, apiClient PostgresFlexClient, projectId, instanceId, userId string) (string, error) { resp, err := apiClient.GetUserExecute(ctx, projectId, instanceId, userId) if err != nil { diff --git a/internal/pkg/services/postgresflex/utils/utils_test.go b/internal/pkg/services/postgresflex/utils/utils_test.go index e5a2c6455..c25d4a1d3 100644 --- a/internal/pkg/services/postgresflex/utils/utils_test.go +++ b/internal/pkg/services/postgresflex/utils/utils_test.go @@ -21,6 +21,7 @@ var ( const ( testInstanceName = "instance" testUserName = "user" + testStatus = "running" ) type postgresFlexClientMocked struct { @@ -469,6 +470,56 @@ func TestGetInstanceName(t *testing.T) { } } +func TestGetInstanceStatus(t *testing.T) { + tests := []struct { + description string + getInstanceFails bool + getInstanceResp *postgresflex.InstanceResponse + isValid bool + expectedOutput string + }{ + { + description: "base", + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(testStatus), + }, + }, + isValid: true, + expectedOutput: testStatus, + }, + { + description: "get instance fails", + getInstanceFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &postgresFlexClientMocked{ + getInstanceFails: tt.getInstanceFails, + getInstanceResp: tt.getInstanceResp, + } + + output, err := GetInstanceStatus(context.Background(), client, testProjectId, testInstanceId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} + func TestGetUserName(t *testing.T) { tests := []struct { description string From ecee7b74de1d51719bee1c7ab2a802189fe5c1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Fri, 5 Apr 2024 09:41:03 +0100 Subject: [PATCH 10/13] Re-add APT installation instructions with new domain (#195) * Revert "Temporarily remove APT installation instructions from guide (#162)" This reverts commit 64faf3994fd528c817cdd2944dc7fa7fafb83e05. * Replace APT repo URL --- INSTALLATION.md | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 2fe283545..7d79bf20c 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -20,9 +20,45 @@ brew install stackit ### Linux -Currently, you can install via [Homebrew](https://brew.sh/) or refer to one of the installation methods below. +#### Debian/Ubuntu (`APT`) -> We are currently working on distributing the CLI on package managers for Linux. +The STACKIT CLI can be installed through the [`APT`](https://ubuntu.com/server/docs/package-management) package manager. + +##### Before you begin + +To install the STACKIT CLI package, you will need to have the `curl` and `gnupg` packages installed: + +```shell +sudo apt-get update +sudo apt-get install curl gnupg +``` + +##### Installing + +1. Import the STACKIT public key: + +```shell +curl https://packages.stackit.cloud/keys/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/stackit.gpg +``` + +2. Add the STACKIT CLI package repository as a package source: + +```shell +echo "deb [signed-by=/usr/share/keyrings/stackit.gpg] https://packages.stackit.cloud/apt/cli stackit main" | sudo tee -a /etc/apt/sources.list.d/stackit.list +``` + +3. Update repository information and install the `stackit` package: + +```shell +sudo apt-get update +sudo apt-get install stackit +``` + +#### Any distribution + +Alternatively, you can install via [Homebrew](https://brew.sh/) or refer to one of the installation methods below. + +> We are currently working on distributing the CLI on more package managers for Linux. ### Windows From 281a5a581e906771ecaad623da0e3076acfd4bc9 Mon Sep 17 00:00:00 2001 From: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:18:10 +0200 Subject: [PATCH 11/13] fix(deps): update module golang.org/x/oauth2 to v0.19.0 (#197) Co-authored-by: Renovate Bot --- go.mod | 5 +---- go.sum | 40 ++-------------------------------------- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index ed6dc1ea9..f21af02bb 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/ske v0.11.0 github.com/zalando/go-keyring v0.2.4 golang.org/x/mod v0.16.0 - golang.org/x/oauth2 v0.18.0 + golang.org/x/oauth2 v0.19.0 golang.org/x/text v0.14.0 ) @@ -32,7 +32,6 @@ require ( github.com/danieljoos/wincred v1.2.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -58,8 +57,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/sys v0.16.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 549215f25..40ae017bb 100644 --- a/go.sum +++ b/go.sum @@ -16,11 +16,6 @@ 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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -115,51 +110,20 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 803d8881364b3eff41752c272553608bb6e17829 Mon Sep 17 00:00:00 2001 From: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Date: Fri, 5 Apr 2024 11:25:00 +0200 Subject: [PATCH 12/13] fix(deps): update module golang.org/x/mod to v0.17.0 (#196) * Update module golang.org/x/mod to v0.16.0 * fix(deps): update module golang.org/x/mod to v0.17.0 --------- Co-authored-by: Renovate Bot Co-authored-by: vicentepinto98 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f21af02bb..e6dc74dd7 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.3.6 github.com/stackitcloud/stackit-sdk-go/services/ske v0.11.0 github.com/zalando/go-keyring v0.2.4 - golang.org/x/mod v0.16.0 + golang.org/x/mod v0.17.0 golang.org/x/oauth2 v0.19.0 golang.org/x/text v0.14.0 ) diff --git a/go.sum b/go.sum index 40ae017bb..465e8b7f4 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= From cb2d132e3fc915665f20710e4e1780d1e63e3846 Mon Sep 17 00:00:00 2001 From: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:31:40 +0200 Subject: [PATCH 13/13] fix(deps): update module github.com/jedib0t/go-pretty/v6 to v6.5.8 (#199) Co-authored-by: Renovate Bot --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e6dc74dd7..ec34d43df 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 - github.com/jedib0t/go-pretty/v6 v6.5.6 + github.com/jedib0t/go-pretty/v6 v6.5.8 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 @@ -56,7 +56,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/sys v0.17.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 465e8b7f4..960afea53 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.5.6 h1:nKXVLqPfAwY7sWcYXdNZZZ2fjqDpAtj9UeWupgfUxSg= -github.com/jedib0t/go-pretty/v6 v6.5.6/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg= +github.com/jedib0t/go-pretty/v6 v6.5.8 h1:8BCzJdSvUbaDuRba4YVh+SKMGcAAKdkcF3SVFbrHAtQ= +github.com/jedib0t/go-pretty/v6 v6.5.8/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -120,8 +120,8 @@ golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=