From f5bde021245391bc928a5075e7322f2206918635 Mon Sep 17 00:00:00 2001 From: dhernando Date: Thu, 16 Jul 2026 16:14:16 +0200 Subject: [PATCH 1/2] chore: change golangci linting so that common std funcs don't need error checking, setup whitespace around if conditions and add modernize to catch llm outdated code --- .golangci.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 9c319e0..fd324b4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,6 +9,7 @@ linters: - goconst - godot - importas + - modernize - nilerr - promlinter - revive @@ -16,16 +17,19 @@ linters: - testifylint - unparam - usestdlibvars + - wsl_v5 settings: - errcheck: - exclude-functions: - - fmt.Fprint - - fmt.Fprintf - - fmt.Fprintln - - (*os.File).Write revive: rules: - name: exported + wsl_v5: + default: none + enable: + - after-block + - err + exclusions: + presets: + - std-error-handling formatters: enable: - gci From 89a8381bd3a5a4e321be4128178e62ee3086c323 Mon Sep 17 00:00:00 2001 From: dhernando Date: Thu, 16 Jul 2026 16:15:08 +0200 Subject: [PATCH 2/2] fix: fixup all the new golangci lint flagged issues --- cmd/qcloud/main.go | 1 + internal/cli/root.go | 2 + internal/cmd/account/describe.go | 5 +++ internal/cmd/account/list.go | 1 + internal/cmd/account/member_describe.go | 1 + internal/cmd/account/member_list.go | 1 + internal/cmd/account/update.go | 3 ++ internal/cmd/backup/create.go | 1 + internal/cmd/backup/create_test.go | 1 + internal/cmd/backup/describe.go | 5 +++ internal/cmd/backup/list.go | 2 + internal/cmd/backup/restore_list.go | 2 + internal/cmd/backup/schedule_completion.go | 1 + internal/cmd/backup/schedule_create.go | 1 + internal/cmd/backup/schedule_create_test.go | 1 + internal/cmd/backup/schedule_describe.go | 5 +++ internal/cmd/backup/schedule_list.go | 3 ++ internal/cmd/backup/schedule_update.go | 3 ++ internal/cmd/backup/schedule_update_test.go | 1 + internal/cmd/base/cmd.go | 2 + internal/cmd/base/create.go | 5 +++ internal/cmd/base/describe.go | 3 ++ internal/cmd/base/list.go | 5 +++ internal/cmd/base/update.go | 6 +++ internal/cmd/cluster/completion_test.go | 6 +++ internal/cmd/cluster/create.go | 7 ++++ internal/cmd/cluster/create_test.go | 5 +++ internal/cmd/cluster/describe.go | 32 ++++++++++++++++ internal/cmd/cluster/helpers.go | 4 ++ internal/cmd/cluster/hybrid_helpers.go | 9 +++++ internal/cmd/cluster/key_create.go | 5 +++ internal/cmd/cluster/key_create_test.go | 2 + internal/cmd/cluster/key_list.go | 2 + internal/cmd/cluster/list.go | 15 ++++++++ internal/cmd/cluster/logs.go | 5 +++ internal/cmd/cluster/restart.go | 2 + internal/cmd/cluster/scale.go | 5 +++ internal/cmd/cluster/shared_flags.go | 41 +++++++++++++++++++++ internal/cmd/cluster/update.go | 33 +++++++++++++++++ internal/cmd/cluster/update_test.go | 4 ++ internal/cmd/cluster/version.go | 3 ++ internal/cmd/cluster/wait.go | 1 + internal/cmd/cluster/wait_helpers.go | 1 + internal/cmd/clusterutil/resolve.go | 15 ++++++++ internal/cmd/clusterutil/wait.go | 3 ++ internal/cmd/completion/completion.go | 15 ++++++++ internal/cmd/completion/iam.go | 3 ++ internal/cmd/completion/package.go | 10 +++++ internal/cmd/context/completion.go | 1 + internal/cmd/context/list.go | 1 + internal/cmd/context/set.go | 5 +++ internal/cmd/hybrid/completion.go | 1 + internal/cmd/hybrid/create.go | 5 +++ internal/cmd/hybrid/describe.go | 8 ++++ internal/cmd/hybrid/list.go | 4 ++ internal/cmd/hybrid/update.go | 5 +++ internal/cmd/iam/completion.go | 3 ++ internal/cmd/iam/key_list.go | 1 + internal/cmd/iam/resolve.go | 5 +++ internal/cmd/iam/role_describe.go | 3 ++ internal/cmd/iam/role_list.go | 1 + internal/cmd/iam/role_update.go | 2 + internal/cmd/iam/user_assign_role.go | 2 + internal/cmd/iam/user_describe.go | 10 +++++ internal/cmd/iam/user_list.go | 4 ++ internal/cmd/iam/user_remove_role.go | 2 + internal/cmd/output/booking.go | 1 + internal/cmd/output/format.go | 5 +++ internal/cmd/output/output.go | 5 +++ internal/cmd/package/package.go | 5 +++ internal/cmd/selfupgrade/selfupgrade.go | 3 ++ internal/cmd/util/ips.go | 1 + internal/cmd/util/ips_test.go | 2 + internal/cmd/util/labels.go | 2 + internal/cmd/util/labels_test.go | 5 +++ internal/cmd/util/util.go | 2 + internal/qcloudapi/client.go | 2 + internal/qcloudapi/client_test.go | 2 + internal/qcloudapi/cluster.go | 9 +++++ internal/resource/bytes.go | 5 +++ internal/resource/bytes_test.go | 2 + internal/resource/millicores.go | 4 ++ internal/resource/millicores_test.go | 2 + internal/selfupgrade/github_updater.go | 4 ++ internal/selfupgrade/homebrew.go | 3 ++ internal/state/config/config.go | 16 ++++++++ internal/state/config/config_test.go | 1 + internal/state/state.go | 1 + internal/testutil/config.go | 2 + internal/testutil/method_spy.go | 5 +++ internal/testutil/server.go | 1 + test/e2e/cluster_helpers_test.go | 8 ++++ test/e2e/framework/binary.go | 20 ++++++++++ test/e2e/framework/env.go | 9 +++++ test/e2e/main_test.go | 1 + 95 files changed, 480 insertions(+) diff --git a/cmd/qcloud/main.go b/cmd/qcloud/main.go index 3a64d38..0417657 100644 --- a/cmd/qcloud/main.go +++ b/cmd/qcloud/main.go @@ -18,6 +18,7 @@ func versionString() string { if versionPrerelease != "" { return version + "-" + versionPrerelease } + return version } diff --git a/internal/cli/root.go b/internal/cli/root.go index 55a0f40..2848f1a 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -39,11 +39,13 @@ Documentation: https://github.com/qdrant/qcloud-cli`, if err := s.Config.Load(configPath); err != nil { return err } + if debug { s.Logger = slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{ Level: slog.LevelDebug, })) } + return nil }, SilenceUsage: true, diff --git a/internal/cmd/account/describe.go b/internal/cmd/account/describe.go index b2c26ad..17f6420 100644 --- a/internal/cmd/account/describe.go +++ b/internal/cmd/account/describe.go @@ -63,17 +63,21 @@ qcloud account describe --json`, fmt.Fprintf(w, "Domain: %s\n", company.GetDomain()) } } + if privs := acct.GetPrivileges(); len(privs) > 0 { fmt.Fprintf(w, "Privileges: %s\n", strings.Join(privs, ", ")) } + if acct.GetCreatedAt() != nil { t := acct.GetCreatedAt().AsTime() fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + if acct.GetLastModifiedAt() != nil { t := acct.GetLastModifiedAt().AsTime() fmt.Fprintf(w, "Modified: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + return nil }, ValidArgsFunction: completion.AccountIDCompletion(s), @@ -85,5 +89,6 @@ func resolveAccountID(s *state.State, args []string) (string, error) { if len(args) > 0 { return args[0], nil } + return s.AccountID() } diff --git a/internal/cmd/account/list.go b/internal/cmd/account/list.go index 0e6f329..b8b4b42 100644 --- a/internal/cmd/account/list.go +++ b/internal/cmd/account/list.go @@ -43,6 +43,7 @@ qcloud account list --json`, if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/account/member_describe.go b/internal/cmd/account/member_describe.go index 3ce0f6e..1308fce 100644 --- a/internal/cmd/account/member_describe.go +++ b/internal/cmd/account/member_describe.go @@ -59,6 +59,7 @@ qcloud account member describe a1b2c3d4-e5f6-7890-abcd-ef1234567890 --json`, t := user.GetCreatedAt().AsTime() fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + return nil }, ValidArgsFunction: completion.AccountMemberIDCompletion(s), diff --git a/internal/cmd/account/member_list.go b/internal/cmd/account/member_list.go index bc25a8d..3f714f2 100644 --- a/internal/cmd/account/member_list.go +++ b/internal/cmd/account/member_list.go @@ -63,6 +63,7 @@ qcloud account member list --json`, if v.GetAccountMember().GetCreatedAt() != nil { return output.HumanTime(v.GetAccountMember().GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/account/update.go b/internal/cmd/account/update.go index 5772923..f97fde2 100644 --- a/internal/cmd/account/update.go +++ b/internal/cmd/account/update.go @@ -80,10 +80,12 @@ qcloud account update --name "New Name" --json`, if updated.Company == nil { updated.Company = &accountv1.Company{} } + if cmd.Flags().Changed("company-name") { name, _ := cmd.Flags().GetString("company-name") updated.Company.Name = name } + if cmd.Flags().Changed("company-domain") { domain, _ := cmd.Flags().GetString("company-domain") updated.Company.Domain = &domain @@ -103,6 +105,7 @@ qcloud account update --name "New Name" --json`, if acct == nil { return } + fmt.Fprintf(out, "Account %s (%s) updated successfully.\n", acct.GetId(), acct.GetName()) }, }.CobraCommand(s) diff --git a/internal/cmd/backup/create.go b/internal/cmd/backup/create.go index e2831fa..a0dbb0c 100644 --- a/internal/cmd/backup/create.go +++ b/internal/cmd/backup/create.go @@ -61,6 +61,7 @@ func newCreateCommand(s *state.State) *cobra.Command { if err != nil { return nil, fmt.Errorf("failed to create backup: %w", err) } + return resp.GetBackup(), nil }, PrintResource: func(_ *cobra.Command, out io.Writer, b *backupv1.Backup) { diff --git a/internal/cmd/backup/create_test.go b/internal/cmd/backup/create_test.go index 8424673..74cb0b9 100644 --- a/internal/cmd/backup/create_test.go +++ b/internal/cmd/backup/create_test.go @@ -46,6 +46,7 @@ func TestBackupCreate_WithRetention(t *testing.T) { if req.GetBackup().GetRetentionPeriod() != nil { capturedRetention = int64(req.GetBackup().GetRetentionPeriod().AsDuration().Hours()) / 24 } + assert.Equal(t, int64(7), capturedRetention) } diff --git a/internal/cmd/backup/describe.go b/internal/cmd/backup/describe.go index 7e5b05e..7be645c 100644 --- a/internal/cmd/backup/describe.go +++ b/internal/cmd/backup/describe.go @@ -40,6 +40,7 @@ func newDescribeCommand(s *state.State) *cobra.Command { if err != nil { return nil, fmt.Errorf("failed to get backup: %w", err) } + return resp.GetBackup(), nil }, PrintText: func(_ *cobra.Command, w io.Writer, b *backupv1.Backup) error { @@ -51,18 +52,22 @@ func newDescribeCommand(s *state.State) *cobra.Command { t := b.GetCreatedAt().AsTime() fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + if b.GetBackupScheduleId() != "" { fmt.Fprintf(w, "Schedule: %s\n", b.GetBackupScheduleId()) } + if b.GetRetentionPeriod() != nil { days := int64(b.GetRetentionPeriod().AsDuration().Hours()) / 24 fmt.Fprintf(w, "Retention: %d days\n", days) } + if b.GetClusterInfo() != nil { ci := b.GetClusterInfo() fmt.Fprintf(w, "Cloud: %s\n", ci.GetCloudProviderId()) fmt.Fprintf(w, "Region: %s\n", ci.GetCloudProviderRegionId()) } + return nil }, }.CobraCommand(s) diff --git a/internal/cmd/backup/list.go b/internal/cmd/backup/list.go index de1e9b9..5987f9f 100644 --- a/internal/cmd/backup/list.go +++ b/internal/cmd/backup/list.go @@ -40,6 +40,7 @@ func newListCommand(s *state.State) *cobra.Command { if err != nil { return nil, fmt.Errorf("failed to list backups: %w", err) } + return resp, nil }, OutputTable: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupsResponse) (output.TableRenderer, error) { @@ -60,6 +61,7 @@ func newListCommand(s *state.State) *cobra.Command { if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/backup/restore_list.go b/internal/cmd/backup/restore_list.go index 8c4550c..42ee7c8 100644 --- a/internal/cmd/backup/restore_list.go +++ b/internal/cmd/backup/restore_list.go @@ -40,6 +40,7 @@ func newRestoreListCommand(s *state.State) *cobra.Command { if err != nil { return nil, fmt.Errorf("failed to list backup restores: %w", err) } + return resp, nil }, OutputTable: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupRestoresResponse) (output.TableRenderer, error) { @@ -60,6 +61,7 @@ func newRestoreListCommand(s *state.State) *cobra.Command { if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/backup/schedule_completion.go b/internal/cmd/backup/schedule_completion.go index 2f4502f..84194e2 100644 --- a/internal/cmd/backup/schedule_completion.go +++ b/internal/cmd/backup/schedule_completion.go @@ -37,6 +37,7 @@ func scheduleIDCompletion(s *state.State) func(*cobra.Command, []string, string) for _, sched := range resp.GetItems() { completions = append(completions, sched.GetId()+"\tcluster:"+sched.GetClusterId()+" | "+sched.GetSchedule()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } diff --git a/internal/cmd/backup/schedule_create.go b/internal/cmd/backup/schedule_create.go index d9c0ada..77a11c6 100644 --- a/internal/cmd/backup/schedule_create.go +++ b/internal/cmd/backup/schedule_create.go @@ -65,6 +65,7 @@ func newScheduleCreateCommand(s *state.State) *cobra.Command { if err != nil { return nil, fmt.Errorf("failed to create backup schedule: %w", err) } + return resp.GetBackupSchedule(), nil }, PrintResource: func(_ *cobra.Command, out io.Writer, sched *backupv1.BackupSchedule) { diff --git a/internal/cmd/backup/schedule_create_test.go b/internal/cmd/backup/schedule_create_test.go index 29845be..fa63fa9 100644 --- a/internal/cmd/backup/schedule_create_test.go +++ b/internal/cmd/backup/schedule_create_test.go @@ -51,6 +51,7 @@ func TestScheduleCreate_WithRetention(t *testing.T) { if req.GetBackupSchedule().GetRetentionPeriod() != nil { retentionDays = int64(req.GetBackupSchedule().GetRetentionPeriod().AsDuration().Hours()) / 24 } + assert.Equal(t, int64(30), retentionDays) } diff --git a/internal/cmd/backup/schedule_describe.go b/internal/cmd/backup/schedule_describe.go index 9f078ca..5768d0f 100644 --- a/internal/cmd/backup/schedule_describe.go +++ b/internal/cmd/backup/schedule_describe.go @@ -22,6 +22,7 @@ func nextScheduleRun(cronExpr string) (time.Time, bool) { if err != nil { return time.Time{}, false } + return s.Next(time.Now().UTC()), true } @@ -56,6 +57,7 @@ The --cluster-id flag is required because the API requires the cluster ID to loo if err != nil { return nil, fmt.Errorf("failed to get backup schedule: %w", err) } + return resp.GetBackupSchedule(), nil }, PrintText: func(_ *cobra.Command, w io.Writer, sched *backupv1.BackupSchedule) error { @@ -65,15 +67,18 @@ The --cluster-id flag is required because the API requires the cluster ID to loo if next, ok := nextScheduleRun(sched.GetSchedule()); ok { fmt.Fprintf(w, "Next Run: %s (%s)\n", output.HumanTime(next), output.FullDateTime(next)) } + fmt.Fprintf(w, "Status: %s\n", output.BackupScheduleStatus(sched.GetStatus())) if sched.GetCreatedAt() != nil { t := sched.GetCreatedAt().AsTime() fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + if sched.GetRetentionPeriod() != nil { days := int64(sched.GetRetentionPeriod().AsDuration().Hours()) / 24 fmt.Fprintf(w, "Retention: %d days\n", days) } + return nil }, }.CobraCommand(s) diff --git a/internal/cmd/backup/schedule_list.go b/internal/cmd/backup/schedule_list.go index b89ab21..3f6b86d 100644 --- a/internal/cmd/backup/schedule_list.go +++ b/internal/cmd/backup/schedule_list.go @@ -40,6 +40,7 @@ func newScheduleListCommand(s *state.State) *cobra.Command { if err != nil { return nil, fmt.Errorf("failed to list backup schedules: %w", err) } + return resp, nil }, OutputTable: func(_ *cobra.Command, w io.Writer, resp *backupv1.ListBackupSchedulesResponse) (output.TableRenderer, error) { @@ -60,12 +61,14 @@ func newScheduleListCommand(s *state.State) *cobra.Command { if next, ok := nextScheduleRun(v.GetSchedule()); ok { return output.HumanTime(next) } + return "" }) t.AddField("CREATED", func(v *backupv1.BackupSchedule) string { if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/backup/schedule_update.go b/internal/cmd/backup/schedule_update.go index 161a497..d73fb16 100644 --- a/internal/cmd/backup/schedule_update.go +++ b/internal/cmd/backup/schedule_update.go @@ -56,6 +56,7 @@ The --cluster-id flag is required because the API requires the cluster ID to loo if err != nil { return nil, fmt.Errorf("failed to get backup schedule: %w", err) } + return resp.GetBackupSchedule(), nil }, Update: func(s *state.State, cmd *cobra.Command, sched *backupv1.BackupSchedule) (*backupv1.BackupSchedule, error) { @@ -75,6 +76,7 @@ The --cluster-id flag is required because the API requires the cluster ID to loo if retentionDays < 1 { return nil, fmt.Errorf("--retention-days must be at least 1") } + d := time.Duration(retentionDays) * 24 * time.Hour sched.RetentionPeriod = durationpb.New(d) } @@ -85,6 +87,7 @@ The --cluster-id flag is required because the API requires the cluster ID to loo if err != nil { return nil, fmt.Errorf("failed to update backup schedule: %w", err) } + return resp.GetBackupSchedule(), nil }, PrintResource: func(_ *cobra.Command, out io.Writer, sched *backupv1.BackupSchedule) { diff --git a/internal/cmd/backup/schedule_update_test.go b/internal/cmd/backup/schedule_update_test.go index 42c9f31..c8caf8f 100644 --- a/internal/cmd/backup/schedule_update_test.go +++ b/internal/cmd/backup/schedule_update_test.go @@ -112,6 +112,7 @@ func TestScheduleUpdate_WithRetention(t *testing.T) { if req.GetBackupSchedule().GetRetentionPeriod() != nil { retentionDays = int64(req.GetBackupSchedule().GetRetentionPeriod().AsDuration().Hours()) / 24 } + assert.Equal(t, int64(14), retentionDays) } diff --git a/internal/cmd/base/cmd.go b/internal/cmd/base/cmd.go index 5bd512d..60b129d 100644 --- a/internal/cmd/base/cmd.go +++ b/internal/cmd/base/cmd.go @@ -22,6 +22,7 @@ func (gc Cmd) CobraCommand(s *state.State) *cobra.Command { if gc.Long != "" { cmd.Long = gc.Long } + cmd.Example = gc.Example cmd.RunE = func(cmd *cobra.Command, args []string) error { return gc.Run(s, cmd, args) @@ -29,5 +30,6 @@ func (gc Cmd) CobraCommand(s *state.State) *cobra.Command { if gc.ValidArgsFunction != nil { cmd.ValidArgsFunction = gc.ValidArgsFunction } + return cmd } diff --git a/internal/cmd/base/create.go b/internal/cmd/base/create.go index e515555..4b366f0 100644 --- a/internal/cmd/base/create.go +++ b/internal/cmd/base/create.go @@ -27,22 +27,27 @@ func (cc CreateCmd[T]) CobraCommand(s *state.State) *cobra.Command { if cc.Long != "" { cmd.Long = cc.Long } + cmd.Example = cc.Example cmd.RunE = func(cmd *cobra.Command, args []string) error { resource, err := cc.Run(s, cmd, args) if err != nil { return err } + if s.Config.JSONOutput() { return output.PrintJSON(cmd.OutOrStdout(), resource) } + if cc.PrintResource != nil { cc.PrintResource(cmd, cmd.OutOrStdout(), resource) } + return nil } if cc.ValidArgsFunction != nil { cmd.ValidArgsFunction = cc.ValidArgsFunction } + return cmd } diff --git a/internal/cmd/base/describe.go b/internal/cmd/base/describe.go index 35b3c2f..50fde82 100644 --- a/internal/cmd/base/describe.go +++ b/internal/cmd/base/describe.go @@ -34,14 +34,17 @@ func (dc DescribeCmd[T]) CobraCommand(s *state.State) *cobra.Command { if err != nil { return err } + if s.Config.JSONOutput() { return output.PrintJSON(cmd.OutOrStdout(), resource) } + return dc.PrintText(cmd, cmd.OutOrStdout(), resource) }, } if dc.ValidArgsFunction != nil { cmd.ValidArgsFunction = dc.ValidArgsFunction } + return cmd } diff --git a/internal/cmd/base/list.go b/internal/cmd/base/list.go index 31f8364..80243ae 100644 --- a/internal/cmd/base/list.go +++ b/internal/cmd/base/list.go @@ -31,6 +31,7 @@ func (lc ListCmd[T]) CobraCommand(s *state.State) *cobra.Command { if posArgs == nil { posArgs = cobra.NoArgs } + cmd := &cobra.Command{ Use: lc.Use, Short: lc.Short, @@ -42,13 +43,16 @@ func (lc ListCmd[T]) CobraCommand(s *state.State) *cobra.Command { if err != nil { return err } + if s.Config.JSONOutput() { return output.PrintJSON(cmd.OutOrStdout(), resp) } + r, err := lc.OutputTable(cmd, cmd.OutOrStdout(), resp) if err != nil { return err } + noHeaders, _ := cmd.Flags().GetBool("no-headers") r.SetNoHeaders(noHeaders) r.Render() @@ -59,5 +63,6 @@ func (lc ListCmd[T]) CobraCommand(s *state.State) *cobra.Command { if lc.ValidArgsFunction != nil { cmd.ValidArgsFunction = lc.ValidArgsFunction } + return cmd } diff --git a/internal/cmd/base/update.go b/internal/cmd/base/update.go index b0fa450..5c52d47 100644 --- a/internal/cmd/base/update.go +++ b/internal/cmd/base/update.go @@ -29,26 +29,32 @@ func (uc UpdateCmd[T]) CobraCommand(s *state.State) *cobra.Command { if uc.Long != "" { cmd.Long = uc.Long } + cmd.Example = uc.Example cmd.RunE = func(cmd *cobra.Command, args []string) error { resource, err := uc.Fetch(s, cmd, args) if err != nil { return err } + updated, err := uc.Update(s, cmd, resource) if err != nil { return err } + if s.Config.JSONOutput() { return output.PrintJSON(cmd.OutOrStdout(), updated) } + if uc.PrintResource != nil { uc.PrintResource(cmd, cmd.OutOrStdout(), updated) } + return nil } if uc.ValidArgsFunction != nil { cmd.ValidArgsFunction = uc.ValidArgsFunction } + return cmd } diff --git a/internal/cmd/cluster/completion_test.go b/internal/cmd/cluster/completion_test.go index ccc6a2c..8c246c2 100644 --- a/internal/cmd/cluster/completion_test.go +++ b/internal/cmd/cluster/completion_test.go @@ -154,6 +154,7 @@ func TestCPUCompletion(t *testing.T) { count++ } } + assert.Equal(t, 1, count, "1 core should appear only once") } @@ -177,6 +178,7 @@ func TestCPUCompletion_FilteredByRAM(t *testing.T) { found = true } } + assert.True(t, found, "expected '1' core in completions") assert.NotContains(t, stdout, "500m") } @@ -243,6 +245,7 @@ func TestGPUCompletion(t *testing.T) { count++ } } + assert.Equal(t, 1, count, "GPU count 1 should appear only once") } @@ -275,10 +278,12 @@ func TestRAMCompletion_APIFormatMismatch(t *testing.T) { if l == "1" { found1 = true } + if l == "2" { found2 = true } } + assert.True(t, found1, "expected '1' core in completions") assert.False(t, found2, "expected '2' cores not in completions") } @@ -302,6 +307,7 @@ func TestCPUCompletion_NormalizedOutput(t *testing.T) { found = true } } + assert.True(t, found, "expected '1' core in completions") } diff --git a/internal/cmd/cluster/create.go b/internal/cmd/cluster/create.go index f05cbc5..961c35d 100644 --- a/internal/cmd/cluster/create.go +++ b/internal/cmd/cluster/create.go @@ -89,8 +89,10 @@ qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 4 --ra if err != nil { return nil, fmt.Errorf("failed to suggest cluster name: %w", err) } + name = suggested.GetName() } + cloudProvider, _ := cmd.Flags().GetString("cloud-provider") cloudRegion, _ := cmd.Flags().GetString("cloud-region") nodes, _ := cmd.Flags().GetUint32("nodes") @@ -106,9 +108,11 @@ qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 4 --ra if cpuChanged { cpu = *cmd.Flags().Lookup("cpu").Value.(*resource.Millicores) } + if ramChanged { ram = *cmd.Flags().Lookup("ram").Value.(*resource.ByteQuantity) } + if cmd.Flags().Changed("gpu") { gpu = *cmd.Flags().Lookup("gpu").Value.(*resource.Millicores) } @@ -149,6 +153,7 @@ qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 4 --ra if err != nil { return nil, err } + packageID = pkg.GetId() } } else { @@ -164,6 +169,7 @@ qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 4 --ra if err != nil { return nil, err } + packageID = pkg.GetId() } @@ -188,6 +194,7 @@ qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 4 --ra if err != nil { return nil, err } + if additionalDisk > 0 { cluster.Configuration.AdditionalResources = &clusterv1.AdditionalResources{ Disk: additionalDisk, diff --git a/internal/cmd/cluster/create_test.go b/internal/cmd/cluster/create_test.go index db2c03a..3846577 100644 --- a/internal/cmd/cluster/create_test.go +++ b/internal/cmd/cluster/create_test.go @@ -38,6 +38,7 @@ func TestCreateCluster_WithLabels(t *testing.T) { for _, kv := range req.GetCluster().GetLabels() { capturedLabels[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"env": "prod", "team": "platform"}, capturedLabels) } @@ -965,6 +966,7 @@ func TestCreateCluster_HybridKubernetesPlacement(t *testing.T) { for _, kv := range cfg.GetNodeSelector() { nodeSelectors[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"disktype": "ssd", "zone": "us-east"}, nodeSelectors) require.Len(t, cfg.GetTolerations(), 2) @@ -983,18 +985,21 @@ func TestCreateCluster_HybridKubernetesPlacement(t *testing.T) { for _, kv := range cfg.GetAnnotations() { annotations[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"prometheus.io/scrape": "true"}, annotations) podLabels := make(map[string]string) for _, kv := range cfg.GetPodLabels() { podLabels[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"app": "qdrant"}, podLabels) svcAnnotations := make(map[string]string) for _, kv := range cfg.GetServiceAnnotations() { svcAnnotations[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"service.beta.kubernetes.io/aws-load-balancer-type": "nlb"}, svcAnnotations) } diff --git a/internal/cmd/cluster/describe.go b/internal/cmd/cluster/describe.go index 20278c5..486957f 100644 --- a/internal/cmd/cluster/describe.go +++ b/internal/cmd/cluster/describe.go @@ -57,28 +57,33 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if cluster.GetState() != nil { fmt.Fprintf(w, "Status: %s\n", output.ClusterPhase(cluster.GetState().GetPhase())) } + if cluster.GetConfiguration() != nil { cfg := cluster.GetConfiguration() fmt.Fprintf(w, "Version: %s\n", cfg.GetVersion()) fmt.Fprintf(w, "Nodes: %d\n", cfg.GetNumberOfNodes()) fmt.Fprintf(w, "Package: %s\n", cfg.GetPackageId()) } + fmt.Fprintf(w, "Cloud: %s\n", cluster.GetCloudProviderId()) if isHybrid { fmt.Fprintf(w, "Environment: %s\n", cluster.GetCloudProviderRegionId()) } else { fmt.Fprintf(w, "Region: %s\n", cluster.GetCloudProviderRegionId()) } + if cluster.GetCreatedAt() != nil { t := cluster.GetCreatedAt().AsTime() fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + if labels := cluster.GetLabels(); len(labels) > 0 { fmt.Fprintf(w, "Labels: ") for i, kv := range labels { if i > 0 { fmt.Fprintf(w, " ") } + fmt.Fprintf(w, "%s=%s\n", kv.GetKey(), kv.GetValue()) } } @@ -101,18 +106,22 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if cfg.ServiceType != nil { fmt.Fprintf(w, " Service Type: %s\n", serviceTypeString(cfg.GetServiceType())) } + if cfg.ReservedCpuPercentage != nil { fmt.Fprintf(w, " Reserved CPU %%: %d\n", cfg.GetReservedCpuPercentage()) } + if cfg.ReservedMemoryPercentage != nil { fmt.Fprintf(w, " Reserved Memory %%: %d\n", cfg.GetReservedMemoryPercentage()) } + if ns := cfg.GetNodeSelector(); len(ns) > 0 { fmt.Fprintf(w, " Node Selectors:\n") for _, kv := range ns { fmt.Fprintf(w, " %s=%s\n", kv.GetKey(), kv.GetValue()) } } + if tols := cfg.GetTolerations(); len(tols) > 0 { fmt.Fprintf(w, " Tolerations:\n") for _, t := range tols { @@ -125,6 +134,7 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, } } } + if tscs := cfg.GetTopologySpreadConstraints(); len(tscs) > 0 { fmt.Fprintf(w, " Topology Spread Constraints:\n") for _, tsc := range tscs { @@ -132,24 +142,29 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if tsc.MaxSkew != nil { fmt.Fprintf(w, " maxSkew=%d", tsc.GetMaxSkew()) } + if tsc.WhenUnsatisfiable != nil { fmt.Fprintf(w, " whenUnsatisfiable=%s", strings.TrimPrefix(tsc.GetWhenUnsatisfiable().String(), "TOPOLOGY_SPREAD_CONSTRAINT_WHEN_UNSATISFIABLE_")) } + fmt.Fprintln(w) } } + if anns := cfg.GetAnnotations(); len(anns) > 0 { fmt.Fprintf(w, " Annotations:\n") for _, kv := range anns { fmt.Fprintf(w, " %s=%s\n", kv.GetKey(), kv.GetValue()) } } + if pl := cfg.GetPodLabels(); len(pl) > 0 { fmt.Fprintf(w, " Pod Labels:\n") for _, kv := range pl { fmt.Fprintf(w, " %s=%s\n", kv.GetKey(), kv.GetValue()) } } + if sa := cfg.GetServiceAnnotations(); len(sa) > 0 { fmt.Fprintf(w, " Service Annotations:\n") for _, kv := range sa { @@ -167,12 +182,15 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if sc.DatabaseStorageClass != nil { fmt.Fprintf(w, " Database Storage Class: %s\n", sc.GetDatabaseStorageClass()) } + if sc.SnapshotStorageClass != nil { fmt.Fprintf(w, " Snapshot Storage Class: %s\n", sc.GetSnapshotStorageClass()) } + if sc.VolumeSnapshotClass != nil { fmt.Fprintf(w, " Volume Snapshot Class: %s\n", sc.GetVolumeSnapshotClass()) } + if sc.VolumeAttributesClass != nil { fmt.Fprintf(w, " Volume Attributes Class: %s\n", sc.GetVolumeAttributesClass()) } @@ -228,31 +246,38 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if svc.EnableTls != nil { fmt.Fprintf(w, " Enable TLS: %s\n", output.BoolYesNo(svc.GetEnableTls())) } + if svc.ApiKey != nil { fmt.Fprintf(w, " API Key Secret: %s\n", secretKeyRefString(svc.GetApiKey())) } + if svc.ReadOnlyApiKey != nil { fmt.Fprintf(w, " Read-Only API Key Secret: %s\n", secretKeyRefString(svc.GetReadOnlyApiKey())) } } + if tls := dbCfg.GetTls(); tls != nil && (tls.Cert != nil || tls.Key != nil) { fmt.Fprintln(w, " TLS:") if tls.Cert != nil { fmt.Fprintf(w, " Cert Secret: %s\n", secretKeyRefString(tls.GetCert())) } + if tls.Key != nil { fmt.Fprintf(w, " Key Secret: %s\n", secretKeyRefString(tls.GetKey())) } } + if al := dbCfg.GetAuditLogging(); al != nil { fmt.Fprintln(w, " Audit Logging:") fmt.Fprintf(w, " Enabled: %s\n", output.BoolYesNo(al.GetEnabled())) if al.Rotation != nil { fmt.Fprintf(w, " Rotation: %s\n", auditLogRotationString(al.GetRotation())) } + if al.MaxLogFiles != nil { fmt.Fprintf(w, " Max Log Files: %d\n", al.GetMaxLogFiles()) } + if al.TrustForwardedHeaders != nil { fmt.Fprintf(w, " Trust Forwarded Headers: %s\n", output.BoolYesNo(al.GetTrustForwardedHeaders())) } @@ -271,9 +296,11 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if len(ips) > 0 { fmt.Fprintf(w, " Allowed IPs: %s\n", strings.Join(ips, ", ")) } + if restartMode != "" { fmt.Fprintf(w, " Restart Mode: %s\n", restartMode) } + if rebalance != "" { fmt.Fprintf(w, " Rebalance Strategy: %s\n", rebalance) } @@ -303,17 +330,21 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if tier != "" { tierSuffix = fmt.Sprintf(" (tier: %s)\n", tier) } + fmt.Fprintf(w, " Disk: %s base, %s available"+tierSuffix, formatGiB(disk.GetBase()), formatGiB(disk.GetAvailable())) } + if ram := res.GetRam(); ram != nil { fmt.Fprintf(w, " RAM: %s base, %s reserved, %s available\n", formatGiB(ram.GetBase()), formatGiB(ram.GetReserved()), formatGiB(ram.GetAvailable())) } + if cpu := res.GetCpu(); cpu != nil { fmt.Fprintf(w, " CPU: %s base, %s reserved, %s available\n", formatMillicores(cpu.GetBase()), formatMillicores(cpu.GetReserved()), formatMillicores(cpu.GetAvailable())) } + if gpu := res.GetGpu(); gpu != nil { fmt.Fprintf(w, " GPU: %s base, %s reserved, %s available\n", formatMillicores(gpu.GetBase()), formatMillicores(gpu.GetReserved()), formatMillicores(gpu.GetAvailable())) @@ -328,6 +359,7 @@ qcloud cluster describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if n.GetStartedAt() != nil { started = "started " + output.HumanTime(n.GetStartedAt().AsTime()) } + fmt.Fprintf(w, " %-40s %-12s %-10s %s\n", n.GetName(), output.ClusterNodeState(n.GetState()), n.GetVersion(), started) } diff --git a/internal/cmd/cluster/helpers.go b/internal/cmd/cluster/helpers.go index 80c1f25..f454b78 100644 --- a/internal/cmd/cluster/helpers.go +++ b/internal/cmd/cluster/helpers.go @@ -208,6 +208,7 @@ func applyTolerations(existing []*clusterv1.Toleration, raw []string) ([]*cluste if err != nil { return nil, err } + adds = append(adds, tol) } } @@ -218,6 +219,7 @@ func applyTolerations(existing []*clusterv1.Toleration, raw []string) ([]*cluste result = append(result, t) } } + return append(result, adds...), nil } @@ -237,6 +239,7 @@ func applyTopologySpreadConstraints(existing []*commonv1.TopologySpreadConstrain if err != nil { return nil, err } + adds = append(adds, tsc) } } @@ -253,5 +256,6 @@ func applyTopologySpreadConstraints(existing []*commonv1.TopologySpreadConstrain result = append(result, t) } } + return append(result, adds...), nil } diff --git a/internal/cmd/cluster/hybrid_helpers.go b/internal/cmd/cluster/hybrid_helpers.go index 2827b32..a837561 100644 --- a/internal/cmd/cluster/hybrid_helpers.go +++ b/internal/cmd/cluster/hybrid_helpers.go @@ -62,6 +62,7 @@ func parseToleration(s string) (*clusterv1.Toleration, error) { if key == "" { return nil, fmt.Errorf("invalid toleration %q: key must not be empty", s) } + tol.Key = &key tol.Value = &value op := clusterv1.TolerationOperator_TOLERATION_OPERATOR_EQUAL @@ -71,6 +72,7 @@ func parseToleration(s string) (*clusterv1.Toleration, error) { if err != nil { return nil, fmt.Errorf("invalid toleration %q: %w", s, err) } + tol.Effect = &effect } } else { @@ -80,6 +82,7 @@ func parseToleration(s string) (*clusterv1.Toleration, error) { if key == "" { return nil, fmt.Errorf("invalid toleration %q: key must not be empty", s) } + tol.Key = &key if len(parts) >= 2 { @@ -92,6 +95,7 @@ func parseToleration(s string) (*clusterv1.Toleration, error) { if err != nil { return nil, fmt.Errorf("invalid toleration %q: %w", s, err) } + tol.Effect = &effect } default: @@ -100,6 +104,7 @@ func parseToleration(s string) (*clusterv1.Toleration, error) { if err != nil { return nil, fmt.Errorf("invalid toleration %q: %w", s, err) } + tol.Effect = &effect } } @@ -220,6 +225,7 @@ func parseTopologySpreadConstraint(s string) (*commonv1.TopologySpreadConstraint if err != nil { return nil, fmt.Errorf("invalid topology spread constraint %q: maxSkew must be an integer", s) } + v := int32(skew) tsc.MaxSkew = &v } @@ -229,6 +235,7 @@ func parseTopologySpreadConstraint(s string) (*commonv1.TopologySpreadConstraint if err != nil { return nil, fmt.Errorf("invalid topology spread constraint %q: %w", s, err) } + tsc.WhenUnsatisfiable = wu.Enum() } @@ -253,6 +260,7 @@ func parseSecretKeyRef(s string) (*commonv1.SecretKeyRef, error) { if !ok || name == "" || key == "" { return nil, fmt.Errorf("invalid secret key ref %q: must be in format 'secretName:key'", s) } + return &commonv1.SecretKeyRef{Name: name, Key: key}, nil } @@ -260,5 +268,6 @@ func secretKeyRefString(ref *commonv1.SecretKeyRef) string { if ref == nil { return "" } + return ref.GetName() + ":" + ref.GetKey() } diff --git a/internal/cmd/cluster/key_create.go b/internal/cmd/cluster/key_create.go index c043d52..e4c01f6 100644 --- a/internal/cmd/cluster/key_create.go +++ b/internal/cmd/cluster/key_create.go @@ -81,6 +81,7 @@ qcloud cluster key create 7b2ea926-724b-4de2-b73a-8675c42a6ebe \ default: return nil, fmt.Errorf("invalid --access-type %q: must be manage or read-only", accessType) } + key.AccessRules = []*clusterauthv2.AccessRule{ { Scope: &clusterauthv2.AccessRule_GlobalAccess{ @@ -97,6 +98,7 @@ qcloud cluster key create 7b2ea926-724b-4de2-b73a-8675c42a6ebe \ if err != nil { return nil, fmt.Errorf("invalid --expires %q: must be in YYYY-MM-DD format", expiresStr) } + key.ExpiresAt = timestamppb.New(t) } @@ -166,12 +168,14 @@ func newKeyProbe( if port == 0 { port = defaultQdrantRESTPort } + endpointURL := fmt.Sprintf("%s:%d", ep.GetUrl(), port) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointURL, nil) if err != nil { return err } + req.Header.Set("api-key", apiKey) resp, err := httpClient.Do(req) if err != nil { @@ -182,6 +186,7 @@ func newKeyProbe( if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil } + return fmt.Errorf("cluster endpoint responded with HTTP %d at %s", resp.StatusCode, endpointURL) } } diff --git a/internal/cmd/cluster/key_create_test.go b/internal/cmd/cluster/key_create_test.go index 037a6ed..c61f219 100644 --- a/internal/cmd/cluster/key_create_test.go +++ b/internal/cmd/cluster/key_create_test.go @@ -160,11 +160,13 @@ func TestKeyCreate_WaitSuccess(t *testing.T) { w.WriteHeader(http.StatusUnauthorized) return } + n := calls.Add(1) if n <= 2 { w.WriteHeader(http.StatusForbidden) return } + w.WriteHeader(http.StatusOK) })) diff --git a/internal/cmd/cluster/key_list.go b/internal/cmd/cluster/key_list.go index b0d60c7..c3d3736 100644 --- a/internal/cmd/cluster/key_list.go +++ b/internal/cmd/cluster/key_list.go @@ -61,12 +61,14 @@ qcloud cluster key list 7b2ea926-724b-4de2-b73a-8675c42a6ebe`, if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.AddField("EXPIRES", func(v *clusterauthv2.DatabaseApiKey) string { if v.GetExpiresAt() != nil { return output.FullDateTime(v.GetExpiresAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/cluster/list.go b/internal/cmd/cluster/list.go index 1112862..d567591 100644 --- a/internal/cmd/cluster/list.go +++ b/internal/cmd/cluster/list.go @@ -50,6 +50,7 @@ qcloud cluster list --page-size 10`, if cloudProviderChanged { cloudProvider, _ = cmd.Flags().GetString("cloud-provider") } + if cloudRegionChanged { cloudRegion, _ = cmd.Flags().GetString("cloud-region") } @@ -63,22 +64,28 @@ qcloud cluster list --page-size 10`, if nextToken != nil { req.PageToken = nextToken } + if cloudProviderChanged { req.CloudProviderId = &cloudProvider } + if cloudRegionChanged { req.CloudProviderRegionId = &cloudRegion } + resp, err := client.Cluster().ListClusters(ctx, req) if err != nil { return nil, fmt.Errorf("failed to list clusters: %w", err) } + allItems = append(allItems, resp.Items...) if resp.NextPageToken == nil || *resp.NextPageToken == "" { break } + nextToken = resp.NextPageToken } + return &clusterv1.ListClustersResponse{Items: allItems}, nil } @@ -88,20 +95,25 @@ qcloud cluster list --page-size 10`, ps, _ := cmd.Flags().GetInt32("page-size") req.PageSize = &ps } + if pageTokenChanged { pt, _ := cmd.Flags().GetString("page-token") req.PageToken = &pt } + if cloudProviderChanged { req.CloudProviderId = &cloudProvider } + if cloudRegionChanged { req.CloudProviderRegionId = &cloudRegion } + resp, err := client.Cluster().ListClusters(ctx, req) if err != nil { return nil, fmt.Errorf("failed to list clusters: %w", err) } + return resp, nil }, OutputTable: func(_ *cobra.Command, w io.Writer, resp *clusterv1.ListClustersResponse) (output.TableRenderer, error) { @@ -116,12 +128,14 @@ qcloud cluster list --page-size 10`, if v.GetState() != nil { return output.ClusterPhase(v.GetState().GetPhase()) } + return "" }) t.AddField("VERSION", func(v *clusterv1.Cluster) string { if v.GetConfiguration() != nil { return v.GetConfiguration().GetVersion() } + return "" }) t.AddField("CLOUD", func(v *clusterv1.Cluster) string { @@ -134,6 +148,7 @@ qcloud cluster list --page-size 10`, if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.Items) diff --git a/internal/cmd/cluster/logs.go b/internal/cmd/cluster/logs.go index 450c222..5346d95 100644 --- a/internal/cmd/cluster/logs.go +++ b/internal/cmd/cluster/logs.go @@ -56,6 +56,7 @@ qcloud cluster logs abc-123 --json`, if err != nil { return nil, fmt.Errorf("invalid --since %q: must be RFC3339 or YYYY-MM-DD", sinceStr) } + req.Since = timestamppb.New(t) } @@ -65,6 +66,7 @@ qcloud cluster logs abc-123 --json`, if err != nil { return nil, fmt.Errorf("invalid --until %q: must be RFC3339 or YYYY-MM-DD", untilStr) } + req.Until = timestamppb.New(t) } @@ -72,6 +74,7 @@ qcloud cluster logs abc-123 --json`, if err != nil { return nil, fmt.Errorf("failed to get cluster logs: %w", err) } + return resp, nil }, PrintText: func(cmd *cobra.Command, w io.Writer, resp *monitoringv1.GetClusterLogsResponse) error { @@ -83,6 +86,7 @@ qcloud cluster logs abc-123 --json`, fmt.Fprintln(w, entry.GetMessage()) } } + return nil }, ValidArgsFunction: completion.ClusterIDCompletion(s), @@ -100,5 +104,6 @@ func parseLogTime(s string) (time.Time, error) { if err == nil { return t, nil } + return time.Parse("2006-01-02", s) } diff --git a/internal/cmd/cluster/restart.go b/internal/cmd/cluster/restart.go index 45efd14..de855e5 100644 --- a/internal/cmd/cluster/restart.go +++ b/internal/cmd/cluster/restart.go @@ -76,11 +76,13 @@ qcloud cluster restart 7b2ea926-724b-4de2-b73a-8675c42a6ebe --force --wait`, if err != nil { return err } + if ep := cluster.GetState().GetEndpoint(); ep != nil && ep.GetUrl() != "" { fmt.Fprintf(cmd.OutOrStdout(), "Cluster %s (%s) is ready. Endpoint: %s\n", cluster.GetId(), cluster.GetName(), ep.GetUrl()) } else { fmt.Fprintf(cmd.OutOrStdout(), "Cluster %s (%s) is ready.\n", cluster.GetId(), cluster.GetName()) } + return nil }, ValidArgsFunction: completion.ClusterIDCompletion(s), diff --git a/internal/cmd/cluster/scale.go b/internal/cmd/cluster/scale.go index 899cddb..ac10da8 100644 --- a/internal/cmd/cluster/scale.go +++ b/internal/cmd/cluster/scale.go @@ -245,6 +245,7 @@ match.`, } } } + newStorageTier := storageTierString(cluster.Configuration.GetClusterStorageConfiguration().GetStorageTierType()) force, _ := cmd.Flags().GetBool("force") @@ -294,10 +295,12 @@ match.`, if updated == nil { return } + if updated.GetState().GetPhase() != clusterv1.ClusterPhase_CLUSTER_PHASE_HEALTHY { fmt.Fprintf(out, "Cluster %s (%s) is scaling, it will take some time to take effect. Use 'cluster wait %s' to wait for it to become healthy\n", updated.GetId(), updated.GetName(), updated.GetId()) return } + fmt.Fprintf(out, "Cluster %s (%s) scaled successfully.\n", updated.GetId(), updated.GetName()) }, ValidArgsFunction: completion.ClusterIDCompletion(s), @@ -340,9 +343,11 @@ func scaleConfirmPrompt( if oldRC.GetGpu() != "" || newRC.GetGpu() != "" { prompt += fmt.Sprintf("\n GPU: %s", output.DiffValue(oldRC.GetGpu(), newRC.GetGpu())) } + if oldStorageTier != "" || newStorageTier != "" { prompt += fmt.Sprintf("\n Storage tier: %s", output.DiffValue(oldStorageTier, newStorageTier)) } + prompt += "\nProceed?" return prompt } diff --git a/internal/cmd/cluster/shared_flags.go b/internal/cmd/cluster/shared_flags.go index d43f745..d47a2bf 100644 --- a/internal/cmd/cluster/shared_flags.go +++ b/internal/cmd/cluster/shared_flags.go @@ -71,6 +71,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + cluster.Labels = util.ApplyLabels(cluster.Labels, changes) } @@ -78,6 +79,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if cluster.Configuration == nil { cluster.Configuration = &clusterv1.ClusterConfiguration{} } + cfg := cluster.Configuration // Version @@ -93,9 +95,11 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + if cfg.ClusterStorageConfiguration == nil { cfg.ClusterStorageConfiguration = &clusterv1.ClusterStorageConfiguration{} } + cfg.ClusterStorageConfiguration.StorageTierType = tierType } @@ -110,19 +114,23 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if cfg.ClusterStorageConfiguration == nil { cfg.ClusterStorageConfiguration = &clusterv1.ClusterStorageConfiguration{} } + sc := cfg.ClusterStorageConfiguration if cmd.Flags().Changed("database-storage-class") { v, _ := cmd.Flags().GetString("database-storage-class") sc.DatabaseStorageClass = &v } + if cmd.Flags().Changed("snapshot-storage-class") { v, _ := cmd.Flags().GetString("snapshot-storage-class") sc.SnapshotStorageClass = &v } + if cmd.Flags().Changed("volume-snapshot-class") { v, _ := cmd.Flags().GetString("volume-snapshot-class") sc.VolumeSnapshotClass = &v } + if cmd.Flags().Changed("volume-attributes-class") { v, _ := cmd.Flags().GetString("volume-attributes-class") sc.VolumeAttributesClass = &v @@ -134,19 +142,23 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if cfg.DatabaseConfiguration == nil { cfg.DatabaseConfiguration = &clusterv1.DatabaseConfiguration{} } + dbCfg := cfg.DatabaseConfiguration if util.AnyFlagChanged(cmd, performanceFlags) { if dbCfg.Storage == nil { dbCfg.Storage = &clusterv1.DatabaseConfigurationStorage{} } + if dbCfg.Storage.Performance == nil { dbCfg.Storage.Performance = &clusterv1.DatabaseConfigurationStoragePerformance{} } + if cmd.Flags().Changed("async-scorer") { v, _ := cmd.Flags().GetBool("async-scorer") dbCfg.Storage.Performance.AsyncScorer = &v } + if cmd.Flags().Changed("optimizer-cpu-budget") { v, _ := cmd.Flags().GetInt32("optimizer-cpu-budget") dbCfg.Storage.Performance.OptimizerCpuBudget = &v @@ -157,19 +169,23 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if dbCfg.Collection == nil { dbCfg.Collection = &clusterv1.DatabaseConfigurationCollection{} } + if cmd.Flags().Changed("replication-factor") { v, _ := cmd.Flags().GetUint32("replication-factor") dbCfg.Collection.ReplicationFactor = &v } + if cmd.Flags().Changed("write-consistency-factor") { v, _ := cmd.Flags().GetInt32("write-consistency-factor") dbCfg.Collection.WriteConsistencyFactor = &v } + if cmd.Flags().Changed("vectors-on-disk") { v, _ := cmd.Flags().GetBool("vectors-on-disk") if dbCfg.Collection.Vectors == nil { dbCfg.Collection.Vectors = &clusterv1.DatabaseConfigurationCollectionVectors{} } + dbCfg.Collection.Vectors.OnDisk = &v } } @@ -180,6 +196,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + dbCfg.LogLevel = ll.Enum() } @@ -187,24 +204,29 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if dbCfg.Service == nil { dbCfg.Service = &clusterv1.DatabaseConfigurationService{} } + if cmd.Flags().Changed("enable-tls") { v, _ := cmd.Flags().GetBool("enable-tls") dbCfg.Service.EnableTls = &v } + if cmd.Flags().Changed("api-key-secret") { v, _ := cmd.Flags().GetString("api-key-secret") ref, err := parseSecretKeyRef(v) if err != nil { return fmt.Errorf("--api-key-secret: %w", err) } + dbCfg.Service.ApiKey = ref } + if cmd.Flags().Changed("read-only-api-key-secret") { v, _ := cmd.Flags().GetString("read-only-api-key-secret") ref, err := parseSecretKeyRef(v) if err != nil { return fmt.Errorf("--read-only-api-key-secret: %w", err) } + dbCfg.Service.ReadOnlyApiKey = ref } } @@ -213,20 +235,24 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if dbCfg.Tls == nil { dbCfg.Tls = &clusterv1.DatabaseConfigurationTls{} } + if cmd.Flags().Changed("tls-cert-secret") { v, _ := cmd.Flags().GetString("tls-cert-secret") ref, err := parseSecretKeyRef(v) if err != nil { return fmt.Errorf("--tls-cert-secret: %w", err) } + dbCfg.Tls.Cert = ref } + if cmd.Flags().Changed("tls-key-secret") { v, _ := cmd.Flags().GetString("tls-key-secret") ref, err := parseSecretKeyRef(v) if err != nil { return fmt.Errorf("--tls-key-secret: %w", err) } + dbCfg.Tls.Key = ref } } @@ -235,22 +261,27 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if dbCfg.AuditLogging == nil { dbCfg.AuditLogging = &clusterv1.DatabaseConfigurationAuditLogging{} } + if cmd.Flags().Changed("audit-logging") { v, _ := cmd.Flags().GetBool("audit-logging") dbCfg.AuditLogging.Enabled = v } + if cmd.Flags().Changed("audit-log-rotation") { v, _ := cmd.Flags().GetString("audit-log-rotation") r, err := parseAuditLogRotation(v) if err != nil { return err } + dbCfg.AuditLogging.Rotation = r.Enum() } + if cmd.Flags().Changed("audit-log-max-files") { v, _ := cmd.Flags().GetUint32("audit-log-max-files") dbCfg.AuditLogging.MaxLogFiles = &v } + if cmd.Flags().Changed("audit-log-trust-forwarded-headers") { v, _ := cmd.Flags().GetBool("audit-log-trust-forwarded-headers") dbCfg.AuditLogging.TrustForwardedHeaders = &v @@ -265,6 +296,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + cfg.AllowedIpSourceRanges = util.ApplyIPs(cfg.AllowedIpSourceRanges, changes) } @@ -275,6 +307,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + cfg.RestartPolicy = mode.Enum() } @@ -285,6 +318,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + cfg.RebalanceStrategy = strat.Enum() } @@ -295,6 +329,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return fmt.Errorf("--node-selector: %w", err) } + cfg.NodeSelector = util.ApplyLabels(cfg.NodeSelector, changes) } @@ -304,6 +339,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return fmt.Errorf("--annotation: %w", err) } + cfg.Annotations = util.ApplyLabels(cfg.Annotations, changes) } @@ -313,6 +349,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return fmt.Errorf("--pod-label: %w", err) } + cfg.PodLabels = util.ApplyLabels(cfg.PodLabels, changes) } @@ -322,6 +359,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return fmt.Errorf("--service-annotation: %w", err) } + cfg.ServiceAnnotations = util.ApplyLabels(cfg.ServiceAnnotations, changes) } @@ -341,6 +379,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + cfg.Tolerations = tols } @@ -350,6 +389,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + cfg.TopologySpreadConstraints = tscs } @@ -359,6 +399,7 @@ func applySharedClusterFlags(cmd *cobra.Command, cluster *clusterv1.Cluster) err if err != nil { return err } + cfg.ServiceType = st.Enum() } diff --git a/internal/cmd/cluster/update.go b/internal/cmd/cluster/update.go index 49c3142..857f64a 100644 --- a/internal/cmd/cluster/update.go +++ b/internal/cmd/cluster/update.go @@ -156,6 +156,7 @@ remove a constraint.`, if updated == nil { return } + fmt.Fprintf(out, "Cluster %s (%s) updated successfully.\n", updated.GetId(), updated.GetName()) }, ValidArgsFunction: completion.ClusterIDCompletion(s), @@ -185,6 +186,7 @@ func updateRestartPrompt(old, updated *clusterv1.Cluster, cmd *cobra.Command, ve if oldVersion == "" { oldVersion = old.GetConfiguration().GetVersion() } + lines = append(lines, fmt.Sprintf(" Version: %s", output.DiffValue(oldVersion, updated.GetConfiguration().GetVersion()))) } @@ -203,45 +205,57 @@ func updateRestartPrompt(old, updated *clusterv1.Cluster, cmd *cobra.Command, ve if oldCol != nil { oldRF = oldCol.ReplicationFactor } + lines = append(lines, fmt.Sprintf(" Replication factor: %s", output.DiffValue(output.OptionalValue(oldRF, notSet), fmt.Sprintf("%d", newCol.GetReplicationFactor())))) } + if cmd.Flags().Changed("write-consistency-factor") { var oldWCF *int32 if oldCol != nil { oldWCF = oldCol.WriteConsistencyFactor } + lines = append(lines, fmt.Sprintf(" Write consistency factor: %s", output.DiffValue(output.OptionalValue(oldWCF, notSet), fmt.Sprintf("%d", newCol.GetWriteConsistencyFactor())))) } + if cmd.Flags().Changed("vectors-on-disk") { var oldVOD *bool if oldCol != nil && oldCol.Vectors != nil { oldVOD = oldCol.Vectors.OnDisk } + var newVOD *bool if newCol != nil && newCol.Vectors != nil { newVOD = newCol.Vectors.OnDisk } + lines = append(lines, fmt.Sprintf(" Vectors on disk: %s", output.DiffValue(output.OptionalValue(oldVOD, notSet), output.OptionalValue(newVOD, notSet)))) } + if cmd.Flags().Changed("async-scorer") { var oldAS *bool if oldPerf != nil { oldAS = oldPerf.AsyncScorer } + lines = append(lines, fmt.Sprintf(" Async scorer: %s", output.DiffValue(output.OptionalValue(oldAS, notSet), output.BoolYesNo(newPerf.GetAsyncScorer())))) } + if cmd.Flags().Changed("optimizer-cpu-budget") { var oldBudget *int32 if oldPerf != nil { oldBudget = oldPerf.OptimizerCpuBudget } + lines = append(lines, fmt.Sprintf(" Optimizer CPU budget: %s", output.DiffValue(output.OptionalValue(oldBudget, notSet), fmt.Sprintf("%d", newPerf.GetOptimizerCpuBudget())))) } + if cmd.Flags().Changed("db-log-level") { oldLL := old.GetConfiguration().GetDatabaseConfiguration().GetLogLevel() newLL := updated.GetConfiguration().GetDatabaseConfiguration().GetLogLevel() lines = append(lines, fmt.Sprintf(" DB log level: %s", output.DiffValue(dbLogLevelString(oldLL), dbLogLevelString(newLL)))) } + if cmd.Flags().Changed("audit-logging") { var oldEnabled string if oldAudit != nil { @@ -249,48 +263,62 @@ func updateRestartPrompt(old, updated *clusterv1.Cluster, cmd *cobra.Command, ve } else { oldEnabled = notSet } + lines = append(lines, fmt.Sprintf(" Audit logging: %s", output.DiffValue(oldEnabled, output.BoolYesNo(newAudit.GetEnabled())))) } + if cmd.Flags().Changed("audit-log-rotation") { var oldRot string if oldAudit != nil { oldRot = auditLogRotationString(oldAudit.GetRotation()) } + if oldRot == "" { oldRot = notSet } + lines = append(lines, fmt.Sprintf(" Audit log rotation: %s", output.DiffValue(oldRot, auditLogRotationString(newAudit.GetRotation())))) } + if cmd.Flags().Changed("audit-log-max-files") { var oldMax *uint32 if oldAudit != nil { oldMax = oldAudit.MaxLogFiles } + lines = append(lines, fmt.Sprintf(" Audit log max files: %s", output.DiffValue(output.OptionalValue(oldMax, notSet), fmt.Sprintf("%d", newAudit.GetMaxLogFiles())))) } + if cmd.Flags().Changed("audit-log-trust-forwarded-headers") { var oldTFH *bool if oldAudit != nil { oldTFH = oldAudit.TrustForwardedHeaders } + lines = append(lines, fmt.Sprintf(" Audit log trust fwd headers: %s", output.DiffValue(output.OptionalValue(oldTFH, notSet), output.BoolYesNo(newAudit.GetTrustForwardedHeaders())))) } + if cmd.Flags().Changed("enable-tls") { var oldTLS *bool if oldSvc != nil { oldTLS = oldSvc.EnableTls } + lines = append(lines, fmt.Sprintf(" Enable TLS: %s", output.DiffValue(output.OptionalValue(oldTLS, notSet), output.BoolYesNo(newSvc.GetEnableTls())))) } + if cmd.Flags().Changed("api-key-secret") { lines = append(lines, " API key secret: (changed)") } + if cmd.Flags().Changed("read-only-api-key-secret") { lines = append(lines, " Read-only API key secret: (changed)") } + if cmd.Flags().Changed("tls-cert-secret") { lines = append(lines, " TLS cert secret: (changed)") } + if cmd.Flags().Changed("tls-key-secret") { lines = append(lines, " TLS key secret: (changed)") } @@ -302,20 +330,25 @@ func updateRestartPrompt(old, updated *clusterv1.Cluster, cmd *cobra.Command, ve newST := updated.GetConfiguration().GetServiceType() lines = append(lines, fmt.Sprintf(" Service type: %s", output.DiffValue(serviceTypeString(oldST), serviceTypeString(newST)))) } + if cmd.Flags().Changed("reserved-cpu-percentage") { var oldPct *uint32 if cfg := old.GetConfiguration(); cfg != nil { oldPct = cfg.ReservedCpuPercentage } + lines = append(lines, fmt.Sprintf(" Reserved CPU %%: %s", output.DiffValue(output.OptionalValue(oldPct, notSet), fmt.Sprintf("%d", updated.GetConfiguration().GetReservedCpuPercentage())))) } + if cmd.Flags().Changed("reserved-memory-percentage") { var oldPct *uint32 if cfg := old.GetConfiguration(); cfg != nil { oldPct = cfg.ReservedMemoryPercentage } + lines = append(lines, fmt.Sprintf(" Reserved memory %%: %s", output.DiffValue(output.OptionalValue(oldPct, notSet), fmt.Sprintf("%d", updated.GetConfiguration().GetReservedMemoryPercentage())))) } + for _, flag := range undiffableFlags { if cmd.Flags().Changed(flag) { lines = append(lines, fmt.Sprintf(" %-32s (changed)", flag+":")) diff --git a/internal/cmd/cluster/update_test.go b/internal/cmd/cluster/update_test.go index 9ddded0..e5015d2 100644 --- a/internal/cmd/cluster/update_test.go +++ b/internal/cmd/cluster/update_test.go @@ -43,6 +43,7 @@ func TestUpdateCluster_SetLabels(t *testing.T) { for _, kv := range req.GetCluster().GetLabels() { capturedLabels[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"env": "prod", "team": "platform"}, capturedLabels) } @@ -110,6 +111,7 @@ func TestUpdateCluster_ApplyLabels(t *testing.T) { for _, kv := range req.GetCluster().GetLabels() { capturedLabels[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"env": "prod", "team": "infra"}, capturedLabels) } @@ -144,6 +146,7 @@ func TestUpdateCluster_RemoveLabel(t *testing.T) { for _, kv := range req.GetCluster().GetLabels() { capturedLabels[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, map[string]string{"env": "staging"}, capturedLabels) } @@ -813,6 +816,7 @@ func TestUpdateCluster_HybridKeyValueFlags(t *testing.T) { for _, kv := range kvs { m[kv.GetKey()] = kv.GetValue() } + return m } diff --git a/internal/cmd/cluster/version.go b/internal/cmd/cluster/version.go index 1b713b7..51a2a80 100644 --- a/internal/cmd/cluster/version.go +++ b/internal/cmd/cluster/version.go @@ -35,16 +35,19 @@ qcloud cluster version list`, if err != nil { return nil, err } + accountID, err := s.AccountID() if err != nil { return nil, err } + resp, err := client.Cluster().ListQdrantReleases(ctx, &clusterv1.ListQdrantReleasesRequest{ AccountId: accountID, }) if err != nil { return nil, fmt.Errorf("failed to list versions: %w", err) } + return resp, nil }, OutputTable: func(_ *cobra.Command, w io.Writer, resp *clusterv1.ListQdrantReleasesResponse) (output.TableRenderer, error) { diff --git a/internal/cmd/cluster/wait.go b/internal/cmd/cluster/wait.go index c4fbf52..f0310fc 100644 --- a/internal/cmd/cluster/wait.go +++ b/internal/cmd/cluster/wait.go @@ -60,6 +60,7 @@ qcloud cluster wait 7b2ea926-724b-4de2-b73a-8675c42a6ebe --timeout 30m`, fmt.Fprintf(cmd.OutOrStdout(), "Cluster %s (%s) is ready.\n", cluster.GetId(), cluster.GetName()) } + return nil }, ValidArgsFunction: completion.ClusterIDCompletion(s), diff --git a/internal/cmd/cluster/wait_helpers.go b/internal/cmd/cluster/wait_helpers.go index ed20ff7..58478ce 100644 --- a/internal/cmd/cluster/wait_helpers.go +++ b/internal/cmd/cluster/wait_helpers.go @@ -39,6 +39,7 @@ func waitForKeyReady( fmt.Fprintf(out, "waiting for API key... %v (%s)\n", err, elapsed) continue } + fmt.Fprintf(out, "API key is active (%s)\n", elapsed) return nil } diff --git a/internal/cmd/clusterutil/resolve.go b/internal/cmd/clusterutil/resolve.go index a54f81d..072f03c 100644 --- a/internal/cmd/clusterutil/resolve.go +++ b/internal/cmd/clusterutil/resolve.go @@ -67,20 +67,24 @@ func FilteredPackages( continue } } + if f.RAM != 0 { pkgRAM, _ := resource.ParseByteQuantity(rc.GetRam()) if pkgRAM != f.RAM { continue } } + if f.GPU != 0 { pkgGPU, _ := resource.ParseMillicores(rc.GetGpu()) if pkgGPU != f.GPU { continue } } + result = append(result, p) } + return result, nil } @@ -134,6 +138,7 @@ func ResolvePackageByName( return p, nil } } + return nil, fmt.Errorf("package %q not found for provider=%s", name, cloudProvider) } @@ -186,18 +191,21 @@ func ResolvePackageByResources( continue } } + if q.RAM != 0 { pkgRAM, _ := resource.ParseByteQuantity(rc.GetRam()) if pkgRAM != q.RAM { continue } } + if q.GPU != 0 { pkgGPU, _ := resource.ParseMillicores(rc.GetGpu()) if pkgGPU != q.GPU { continue } } + matches = append(matches, p) } @@ -205,12 +213,15 @@ func ResolvePackageByResources( if q.CPU != 0 { filterDesc = append(filterDesc, fmt.Sprintf("cpu=%q", q.CPU.String())) } + if q.RAM != 0 { filterDesc = append(filterDesc, fmt.Sprintf("ram=%q", q.RAM.String())) } + if q.GPU != 0 { filterDesc = append(filterDesc, fmt.Sprintf("gpu=%q", q.GPU.String())) } + desc := strings.Join(filterDesc, " ") switch len(matches) { @@ -223,6 +234,7 @@ func ResolvePackageByResources( for i, p := range matches { names[i] = p.GetName() } + return nil, fmt.Errorf("multiple packages match %s: %s; use 'package list' to see available packages", desc, strings.Join(names, ", ")) } } @@ -235,12 +247,15 @@ func CalculateAdditionalDisk(requestedDisk resource.ByteQuantity, pkg *bookingv1 if pkgDiskStr == "" { return 0, nil } + pkgDisk, err := resource.ParseByteQuantity(pkgDiskStr) if err != nil { return 0, err } + if requestedDisk > pkgDisk { return uint32(requestedDisk.GiB() - pkgDisk.GiB()), nil } + return 0, nil } diff --git a/internal/cmd/clusterutil/wait.go b/internal/cmd/clusterutil/wait.go index 02ab221..b139723 100644 --- a/internal/cmd/clusterutil/wait.go +++ b/internal/cmd/clusterutil/wait.go @@ -46,6 +46,7 @@ func WaitForClusterHealthy( if s, ok := status.FromError(err); ok && s.Code() == codes.DeadlineExceeded { return nil, fmt.Errorf("timed out waiting for cluster to become healthy: %w", err) } + return nil, fmt.Errorf("failed to get cluster status: %w", err) } @@ -58,10 +59,12 @@ func WaitForClusterHealthy( if phase == clusterv1.ClusterPhase_CLUSTER_PHASE_HEALTHY { return cluster, nil } + if clusterFailurePhases[phase] { reason := cluster.GetState().GetReason() return nil, fmt.Errorf("failed waiting for cluster to become healthy: phase=%s, reason=%s", phaseStr, reason) } + return nil, nil //nolint:nilnil // nil cluster means keep polling } diff --git a/internal/cmd/completion/completion.go b/internal/cmd/completion/completion.go index cce1567..ed19a36 100644 --- a/internal/cmd/completion/completion.go +++ b/internal/cmd/completion/completion.go @@ -38,6 +38,7 @@ func CloudProviderCompletion(s *state.State) func(*cobra.Command, []string, stri for _, p := range resp.GetItems() { completions = append(completions, p.GetId()+"\t"+p.GetName()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -73,6 +74,7 @@ func CloudRegionCompletion(s *state.State) func(*cobra.Command, []string, string for _, r := range resp.GetItems() { completions = append(completions, r.GetId()+"\t"+r.GetName()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -106,6 +108,7 @@ func ManagementKeyIDCompletion(s *state.State) func(*cobra.Command, []string, st for _, k := range resp.GetItems() { completions = append(completions, k.GetId()+"\t"+k.GetPrefix()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -143,6 +146,7 @@ func BackupIDCompletion(s *state.State) func(*cobra.Command, []string, string) ( for _, b := range resp.GetItems() { completions = append(completions, b.GetId()+"\t"+b.GetName()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -174,6 +178,7 @@ func ClusterIDCompletion(s *state.State) func(*cobra.Command, []string, string) for _, c := range clusters { completions = append(completions, c.GetId()+"\t"+c.GetName()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -204,28 +209,36 @@ func VersionCompletion(s *state.State) func(*cobra.Command, []string, string) ([ if r.GetUnavailable() { continue } + desc := "" if r.GetDefault() { desc += "(default)" } + if r.GetEndOfLife() { if desc != "" { desc += " " } + desc += "(end of life)" } + if remarks := r.GetRemarks(); remarks != "" { if desc != "" { desc += " " } + desc += remarks } + entry := r.GetVersion() if desc != "" { entry += "\t" + desc } + completions = append(completions, entry) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -252,6 +265,7 @@ func AccountIDCompletion(s *state.State) func(*cobra.Command, []string, string) for _, a := range resp.GetItems() { completions = append(completions, a.GetId()+"\t"+a.GetName()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -285,6 +299,7 @@ func AccountMemberIDCompletion(s *state.State) func(*cobra.Command, []string, st for _, m := range resp.GetItems() { completions = append(completions, m.GetAccountMember().GetId()+"\t"+m.GetAccountMember().GetEmail()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } diff --git a/internal/cmd/completion/iam.go b/internal/cmd/completion/iam.go index 54a42ff..313d4ba 100644 --- a/internal/cmd/completion/iam.go +++ b/internal/cmd/completion/iam.go @@ -32,6 +32,7 @@ func RoleCompletion(s *state.State) func(*cobra.Command, []string, string) ([]st for _, r := range resp.GetItems() { completions = append(completions, r.GetName()+"\t"+r.GetId()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -65,6 +66,7 @@ func RoleIDCompletion(s *state.State) func(*cobra.Command, []string, string) ([] for _, r := range resp.GetItems() { completions = append(completions, r.GetId()+"\t"+r.GetName()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -94,6 +96,7 @@ func PermissionCompletion(s *state.State) func(*cobra.Command, []string, string) for _, p := range resp.GetPermissions() { completions = append(completions, p.GetValue()+"\t"+p.GetCategory()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } diff --git a/internal/cmd/completion/package.go b/internal/cmd/completion/package.go index d5e9a76..9bacbb3 100644 --- a/internal/cmd/completion/package.go +++ b/internal/cmd/completion/package.go @@ -42,11 +42,13 @@ func CPUCompletion(s *state.State) func(*cobra.Command, []string, string) ([]str cobra.CompErrorln(fmt.Sprintf("package %s: %v", p.GetName(), err)) continue } + if _, ok := seen[v]; !ok { seen[v] = struct{}{} completions = append(completions, v.String()) } } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -80,11 +82,13 @@ func RAMCompletion(s *state.State) func(*cobra.Command, []string, string) ([]str cobra.CompErrorln(fmt.Sprintf("package %s: %v", p.GetName(), err)) continue } + if _, ok := seen[v]; !ok { seen[v] = struct{}{} completions = append(completions, resource.FormatByteQuantity(v, resource.UnitGiB)) } } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -120,11 +124,13 @@ func DiskCompletion(s *state.State) func(*cobra.Command, []string, string) ([]st cobra.CompErrorln(fmt.Sprintf("package %s: %v", p.GetName(), err)) continue } + if _, ok := seen[v]; !ok { seen[v] = struct{}{} completions = append(completions, v.String()) } } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -157,11 +163,13 @@ func GPUCompletion(s *state.State) func(*cobra.Command, []string, string) ([]str if err != nil || m <= 0 || int64(m)%1000 != 0 { continue } + if _, ok := seen[m]; !ok { seen[m] = struct{}{} completions = append(completions, m.String()) } } + return completions, cobra.ShellCompDirectiveNoFileComp } } @@ -202,9 +210,11 @@ func PackageNameCompletion(s *state.State) func(*cobra.Command, []string, string if rc := p.GetResourceConfiguration(); rc != nil { desc += fmt.Sprintf(" | %s RAM / %s CPU / %s disk", rc.GetRam(), rc.GetCpu(), rc.GetDisk()) } + desc += " | " + output.FormatMillicents(p.GetUnitIntPricePerHour(), p.GetCurrency()) + "/hr" completions = append(completions, p.GetName()+"\t"+desc) } + return completions, cobra.ShellCompDirectiveNoFileComp } } diff --git a/internal/cmd/context/completion.go b/internal/cmd/context/completion.go index b0919a3..823d067 100644 --- a/internal/cmd/context/completion.go +++ b/internal/cmd/context/completion.go @@ -12,6 +12,7 @@ func contextNameCompletion(s *state.State) func(*cobra.Command, []string, string if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } + return s.Config.ContextNames(), cobra.ShellCompDirectiveNoFileComp } } diff --git a/internal/cmd/context/list.go b/internal/cmd/context/list.go index 3ffb904..5839dbc 100644 --- a/internal/cmd/context/list.go +++ b/internal/cmd/context/list.go @@ -32,6 +32,7 @@ qcloud context list`, if name == current { return "*" } + return "" }) t.AddField("NAME", func(name string) string { diff --git a/internal/cmd/context/set.go b/internal/cmd/context/set.go index 1e19a47..5578506 100644 --- a/internal/cmd/context/set.go +++ b/internal/cmd/context/set.go @@ -77,6 +77,7 @@ still take precedence over the command at runtime.`, for k := range apiKeyHelpers { names = append(names, k) } + sort.Strings(names) return names, cobra.ShellCompDirectiveNoFileComp }) @@ -139,6 +140,7 @@ still take precedence over the command at runtime.`, s.Config.SetCurrentContext(name) activated = true } + if err := s.Config.WriteToFile(); err != nil { return err } @@ -147,6 +149,7 @@ still take precedence over the command at runtime.`, if activated { fmt.Fprintf(cmd.OutOrStdout(), "Switched to context %q.\n", name) } + return nil }, }.CobraCommand(s) @@ -173,6 +176,7 @@ func resolveAPIKeyFlags(cmd *cobra.Command) (string, error) { for k := range apiKeyHelpers { names = append(names, k) } + sort.Strings(names) return "", fmt.Errorf("unknown api-key-helper %q (supported: %s)", helper, strings.Join(names, ", ")) } @@ -184,6 +188,7 @@ func flagChangedWithValue(cmd *cobra.Command, name string) (string, bool) { if !cmd.Flags().Changed(name) { return "", false } + v, _ := cmd.Flags().GetString(name) return v, true } diff --git a/internal/cmd/hybrid/completion.go b/internal/cmd/hybrid/completion.go index db39372..bf6f11a 100644 --- a/internal/cmd/hybrid/completion.go +++ b/internal/cmd/hybrid/completion.go @@ -37,6 +37,7 @@ func envIDCompletion(s *state.State) func(*cobra.Command, []string, string) ([]s for _, env := range resp.GetItems() { completions = append(completions, env.GetId()+"\t"+env.GetName()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } diff --git a/internal/cmd/hybrid/create.go b/internal/cmd/hybrid/create.go index b93484b..e88d5ad 100644 --- a/internal/cmd/hybrid/create.go +++ b/internal/cmd/hybrid/create.go @@ -72,20 +72,24 @@ If your account does not have access, you will be prompted to contact us.`, ns, _ := cmd.Flags().GetString("namespace") env.Configuration.Namespace = ns } + if cmd.Flags().Changed("database-storage-class") { v, _ := cmd.Flags().GetString("database-storage-class") env.Configuration.DatabaseStorageClass = &v } + if cmd.Flags().Changed("snapshot-storage-class") { v, _ := cmd.Flags().GetString("snapshot-storage-class") env.Configuration.SnapshotStorageClass = &v } + if cmd.Flags().Changed("log-level") { lvlStr, _ := cmd.Flags().GetString("log-level") lvl, err := parseLogLevel(lvlStr) if err != nil { return nil, err } + env.Configuration.LogLevel = &lvl } } @@ -97,6 +101,7 @@ If your account does not have access, you will be prompted to contact us.`, if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied { return nil, fmt.Errorf("your account does not have access to Hybrid Cloud\n\nTo get started, contact us at: https://qdrant.tech/contact-us/") } + return nil, fmt.Errorf("failed to create hybrid cloud environment: %w", err) } diff --git a/internal/cmd/hybrid/describe.go b/internal/cmd/hybrid/describe.go index 8178695..bd23ebe 100644 --- a/internal/cmd/hybrid/describe.go +++ b/internal/cmd/hybrid/describe.go @@ -48,10 +48,12 @@ func newDescribeCommand(s *state.State) *cobra.Command { if env.GetStatus() != nil { fmt.Fprintf(w, "Status: %s\n", output.HybridEnvironmentPhase(env.GetStatus().GetPhase())) } + if env.GetCreatedAt() != nil { t := env.GetCreatedAt().AsTime() fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + fmt.Fprintf(w, "Bootstrap Generated: %s\n", output.BoolYesNo(env.GetBootstrapCommandsGenerated())) if cfg := env.GetConfiguration(); cfg != nil { @@ -60,15 +62,19 @@ func newDescribeCommand(s *state.State) *cobra.Command { if cfg.DatabaseStorageClass != nil { fmt.Fprintf(w, " Database Storage Class: %s\n", cfg.GetDatabaseStorageClass()) } + if cfg.SnapshotStorageClass != nil { fmt.Fprintf(w, " Snapshot Storage Class: %s\n", cfg.GetSnapshotStorageClass()) } + if cfg.LogLevel != nil { fmt.Fprintf(w, " Log Level: %s\n", logLevelString(cfg.GetLogLevel())) } + if cfg.HttpProxyUrl != nil { fmt.Fprintf(w, " HTTP Proxy: %s\n", cfg.GetHttpProxyUrl()) } + if cfg.HttpsProxyUrl != nil { fmt.Fprintf(w, " HTTPS Proxy: %s\n", cfg.GetHttpsProxyUrl()) } @@ -79,10 +85,12 @@ func newDescribeCommand(s *state.State) *cobra.Command { if st.KubernetesVersion != "" { fmt.Fprintf(w, " Kubernetes Version: %s\n", st.GetKubernetesVersion()) } + if st.KubernetesDistribution != nil { dist := st.GetKubernetesDistribution().String() fmt.Fprintf(w, " Distribution: %s\n", dist) } + fmt.Fprintf(w, " Node Count: %d\n", st.GetNumberOfNodes()) fmt.Fprintf(w, " Cluster Creation: %s\n", output.ClusterCreationStatus(st.GetClusterCreationReadiness())) if len(st.GetComponentStatuses()) > 0 { diff --git a/internal/cmd/hybrid/list.go b/internal/cmd/hybrid/list.go index b55c7de..4fbf765 100644 --- a/internal/cmd/hybrid/list.go +++ b/internal/cmd/hybrid/list.go @@ -35,6 +35,7 @@ func newListCommand(s *state.State) *cobra.Command { if err != nil { return nil, fmt.Errorf("failed to list hybrid cloud environments: %w", err) } + return resp, nil }, OutputTable: func(_ *cobra.Command, w io.Writer, resp *hybridv1.ListHybridCloudEnvironmentsResponse) (output.TableRenderer, error) { @@ -49,18 +50,21 @@ func newListCommand(s *state.State) *cobra.Command { if v.GetStatus() != nil { return output.HybridEnvironmentPhase(v.GetStatus().GetPhase()) } + return "" }) t.AddField("NODES", func(v *hybridv1.HybridCloudEnvironment) string { if v.GetStatus() != nil { return fmt.Sprintf("%d", v.GetStatus().GetNumberOfNodes()) } + return "" }) t.AddField("CREATED", func(v *hybridv1.HybridCloudEnvironment) string { if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.Items) diff --git a/internal/cmd/hybrid/update.go b/internal/cmd/hybrid/update.go index 363c132..881f647 100644 --- a/internal/cmd/hybrid/update.go +++ b/internal/cmd/hybrid/update.go @@ -81,26 +81,31 @@ qcloud hybrid update 7b2ea926-724b-4de2-b73a-8675c42a6ebe --log-level debug`, if updated.Configuration == nil { updated.Configuration = &hybridv1.HybridCloudEnvironmentConfiguration{} } + cfg := updated.Configuration if cmd.Flags().Changed("namespace") { ns, _ := cmd.Flags().GetString("namespace") cfg.Namespace = ns } + if cmd.Flags().Changed("database-storage-class") { v, _ := cmd.Flags().GetString("database-storage-class") cfg.DatabaseStorageClass = &v } + if cmd.Flags().Changed("snapshot-storage-class") { v, _ := cmd.Flags().GetString("snapshot-storage-class") cfg.SnapshotStorageClass = &v } + if cmd.Flags().Changed("log-level") { lvlStr, _ := cmd.Flags().GetString("log-level") lvl, err := parseLogLevel(lvlStr) if err != nil { return nil, err } + cfg.LogLevel = &lvl } } diff --git a/internal/cmd/iam/completion.go b/internal/cmd/iam/completion.go index 7b28594..a68bff4 100644 --- a/internal/cmd/iam/completion.go +++ b/internal/cmd/iam/completion.go @@ -15,11 +15,13 @@ func userCompletion(s *state.State) func(*cobra.Command, []string, string) ([]st if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } + ctx := cmd.Context() client, err := s.Client(ctx) if err != nil { return nil, cobra.ShellCompDirectiveError } + accountID, err := s.AccountID() if err != nil { return nil, cobra.ShellCompDirectiveError @@ -34,6 +36,7 @@ func userCompletion(s *state.State) func(*cobra.Command, []string, string) ([]st for _, u := range resp.GetItems() { completions = append(completions, u.GetId()+"\t"+u.GetEmail()) } + return completions, cobra.ShellCompDirectiveNoFileComp } } diff --git a/internal/cmd/iam/key_list.go b/internal/cmd/iam/key_list.go index 2ef051f..021e9db 100644 --- a/internal/cmd/iam/key_list.go +++ b/internal/cmd/iam/key_list.go @@ -54,6 +54,7 @@ qcloud iam key list --json`, if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/iam/resolve.go b/internal/cmd/iam/resolve.go index 0c07837..c0efc9c 100644 --- a/internal/cmd/iam/resolve.go +++ b/internal/cmd/iam/resolve.go @@ -21,6 +21,7 @@ func resolveUser(cmd *cobra.Command, client *qcloudapi.Client, accountID, idOrEm if err != nil { return nil, fmt.Errorf("failed to list users: %w", err) } + for _, u := range resp.GetItems() { if util.IsUUID(idOrEmail) { if u.GetId() == idOrEmail { @@ -32,6 +33,7 @@ func resolveUser(cmd *cobra.Command, client *qcloudapi.Client, accountID, idOrEm } } } + return nil, fmt.Errorf("user %s not found", idOrEmail) } @@ -58,6 +60,7 @@ func resolveRoleIDs(ctx context.Context, client *qcloudapi.Client, accountID str if err != nil { return nil, fmt.Errorf("failed to list roles: %w", err) } + rolesByName = make(map[string]string, len(resp.GetItems())) for _, r := range resp.GetItems() { rolesByName[r.GetName()] = r.GetId() @@ -73,9 +76,11 @@ func resolveRoleIDs(ctx context.Context, client *qcloudapi.Client, accountID str if !ok { return nil, fmt.Errorf("role %q not found", v) } + ids = append(ids, id) } } + return ids, nil } diff --git a/internal/cmd/iam/role_describe.go b/internal/cmd/iam/role_describe.go index b816138..1016192 100644 --- a/internal/cmd/iam/role_describe.go +++ b/internal/cmd/iam/role_describe.go @@ -58,9 +58,11 @@ qcloud iam role describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, if role.GetCreatedAt() != nil { fmt.Fprintf(w, "Created: %s\n", output.FullDateTime(role.GetCreatedAt().AsTime())) } + if role.GetLastModifiedAt() != nil { fmt.Fprintf(w, "Last Modified: %s\n", output.FullDateTime(role.GetLastModifiedAt().AsTime())) } + fmt.Fprintf(w, "\nPermissions:\n") for _, p := range role.GetPermissions() { if cat := p.GetCategory(); cat != "" { @@ -69,6 +71,7 @@ qcloud iam role describe 7b2ea926-724b-4de2-b73a-8675c42a6ebe --json`, fmt.Fprintf(w, " %s\n", p.GetValue()) } } + return nil }, }.CobraCommand(s) diff --git a/internal/cmd/iam/role_list.go b/internal/cmd/iam/role_list.go index ac0cd4a..2ad6b61 100644 --- a/internal/cmd/iam/role_list.go +++ b/internal/cmd/iam/role_list.go @@ -60,6 +60,7 @@ qcloud iam role list --json`, if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/iam/role_update.go b/internal/cmd/iam/role_update.go index 65c45f4..6bffd30 100644 --- a/internal/cmd/iam/role_update.go +++ b/internal/cmd/iam/role_update.go @@ -70,6 +70,7 @@ qcloud iam role update 7b2ea926-724b-4de2-b73a-8675c42a6ebe --description "Updat name, _ := cmd.Flags().GetString("name") role.Name = name } + if cmd.Flags().Changed("description") { description, _ := cmd.Flags().GetString("description") role.Description = description @@ -88,6 +89,7 @@ qcloud iam role update 7b2ea926-724b-4de2-b73a-8675c42a6ebe --description "Updat if role == nil { return } + fmt.Fprintf(out, "Role %s (%s) updated.\n", role.GetId(), role.GetName()) }, }.CobraCommand(s) diff --git a/internal/cmd/iam/user_assign_role.go b/internal/cmd/iam/user_assign_role.go index 35b51df..d8ec24b 100644 --- a/internal/cmd/iam/user_assign_role.go +++ b/internal/cmd/iam/user_assign_role.go @@ -46,10 +46,12 @@ qcloud iam user assign-role user@example.com --role admin,viewer`, if err != nil { return err } + accountID, err := s.AccountID() if err != nil { return err } + user, err := resolveUser(cmd, client, accountID, args[0]) if err != nil { return err diff --git a/internal/cmd/iam/user_describe.go b/internal/cmd/iam/user_describe.go index 3b0d9f2..c01f92a 100644 --- a/internal/cmd/iam/user_describe.go +++ b/internal/cmd/iam/user_describe.go @@ -39,10 +39,12 @@ qcloud iam user describe user@example.com --json`, if err != nil { return nil, err } + accountID, err := s.AccountID() if err != nil { return nil, err } + return resolveUser(cmd, client, accountID, args[0]) }, PrintText: func(cmd *cobra.Command, w io.Writer, user *iamv1.User) error { @@ -51,10 +53,12 @@ qcloud iam user describe user@example.com --json`, if err != nil { return err } + accountID, err := s.AccountID() if err != nil { return err } + rolesResp, err := client.IAM().ListUserRoles(ctx, &iamv1.ListUserRolesRequest{ AccountId: accountID, UserId: user.GetId(), @@ -62,6 +66,7 @@ qcloud iam user describe user@example.com --json`, if err != nil { return fmt.Errorf("failed to list user roles: %w", err) } + roles := rolesResp.GetRoles() return printUserWithRoles(w, user, roles, effectivePermissions(roles)) }, @@ -76,16 +81,19 @@ func printUserWithRoles(w io.Writer, user *iamv1.User, roles []*iamv1.Role, perm t := user.GetCreatedAt().AsTime() fmt.Fprintf(w, "Created: %s (%s)\n", output.HumanTime(t), output.FullDateTime(t)) } + if len(roles) > 0 { fmt.Fprintln(w) fmt.Fprintln(w, "Roles:") printRoles(w, roles) } + if len(permissions) > 0 { fmt.Fprintln(w) fmt.Fprintln(w, "Effective Permissions:") printPermissions(w, permissions) } + return nil } @@ -130,6 +138,7 @@ func effectivePermissions(roles []*iamv1.Role) []rolePermission { } } } + sort.Strings(order) out := make([]rolePermission, 0, len(order)) for _, v := range order { @@ -137,5 +146,6 @@ func effectivePermissions(roles []*iamv1.Role) []rolePermission { sort.Strings(e.roleNames) out = append(out, rolePermission{permission: e.permission, roleNames: e.roleNames}) } + return out } diff --git a/internal/cmd/iam/user_list.go b/internal/cmd/iam/user_list.go index e17d04c..76105a3 100644 --- a/internal/cmd/iam/user_list.go +++ b/internal/cmd/iam/user_list.go @@ -32,16 +32,19 @@ qcloud iam user list --json`, if err != nil { return nil, err } + accountID, err := s.AccountID() if err != nil { return nil, err } + resp, err := client.IAM().ListUsers(ctx, &iamv1.ListUsersRequest{ AccountId: accountID, }) if err != nil { return nil, fmt.Errorf("failed to list users: %w", err) } + return resp, nil }, OutputTable: func(_ *cobra.Command, w io.Writer, resp *iamv1.ListUsersResponse) (output.TableRenderer, error) { @@ -53,6 +56,7 @@ qcloud iam user list --json`, if v.GetCreatedAt() != nil { return output.HumanTime(v.GetCreatedAt().AsTime()) } + return "" }) t.SetItems(resp.GetItems()) diff --git a/internal/cmd/iam/user_remove_role.go b/internal/cmd/iam/user_remove_role.go index 484c715..c7a81ef 100644 --- a/internal/cmd/iam/user_remove_role.go +++ b/internal/cmd/iam/user_remove_role.go @@ -43,10 +43,12 @@ qcloud iam user remove-role user@example.com --role admin --role viewer`, if err != nil { return err } + accountID, err := s.AccountID() if err != nil { return err } + user, err := resolveUser(cmd, client, accountID, args[0]) if err != nil { return err diff --git a/internal/cmd/output/booking.go b/internal/cmd/output/booking.go index 38215d0..d44ecee 100644 --- a/internal/cmd/output/booking.go +++ b/internal/cmd/output/booking.go @@ -19,5 +19,6 @@ func FormatMillicents(mc int32, currency string) string { if mc == 0 { return "free" } + return fmt.Sprintf("%.4f %s", float64(mc)/100_000.0, currency) } diff --git a/internal/cmd/output/format.go b/internal/cmd/output/format.go index 41960be..ee42c26 100644 --- a/internal/cmd/output/format.go +++ b/internal/cmd/output/format.go @@ -14,6 +14,7 @@ func HumanTime(t time.Time) string { if t.IsZero() { return "" } + return humanize.Time(t) } @@ -23,6 +24,7 @@ func FullDateTime(t time.Time) string { if t.IsZero() { return "" } + return t.UTC().Format("2006-01-02 15:04:05 UTC") } @@ -42,6 +44,7 @@ func BoolYesNo(v bool) string { if v { return boolYes } + return "no" } @@ -50,6 +53,7 @@ func BoolMark(v bool) string { if v { return boolYes } + return "" } @@ -67,6 +71,7 @@ func OptionalValue(v any, fallback string) string { if b { return boolYes } + return "no" } diff --git a/internal/cmd/output/output.go b/internal/cmd/output/output.go index 9f07bb3..09d2d93 100644 --- a/internal/cmd/output/output.go +++ b/internal/cmd/output/output.go @@ -83,6 +83,7 @@ func (t *Table[T]) render(items []T) { for i, h := range t.headers { header[i] = h } + tw.AppendHeader(header) } @@ -91,8 +92,10 @@ func (t *Table[T]) render(items []T) { for i, fn := range t.fields { row[i] = fn(item) } + tw.AppendRow(row) } + tw.Render() } @@ -105,10 +108,12 @@ func PrintJSON(w io.Writer, v any) error { if err != nil { return err } + _, _ = w.Write(b) fmt.Fprintln(w) return nil } + // Fallback: standard JSON. enc := json.NewEncoder(w) enc.SetIndent("", " ") diff --git a/internal/cmd/package/package.go b/internal/cmd/package/package.go index 2dae9eb..4e0606c 100644 --- a/internal/cmd/package/package.go +++ b/internal/cmd/package/package.go @@ -57,6 +57,7 @@ qcloud package list --cloud-provider hybrid`, if cloudRegion == "" { return nil, fmt.Errorf("--cloud-region is required when --cloud-provider is not %q", qcloudapi.HybridCloudProviderID) } + cloudRegionPtr = &cloudRegion } @@ -87,18 +88,21 @@ qcloud package list --cloud-provider hybrid`, if rc := p.GetResourceConfiguration(); rc != nil { return rc.GetRam() } + return "" }) t.AddField("CPU", func(p *bookingv1.Package) string { if rc := p.GetResourceConfiguration(); rc != nil { return rc.GetCpu() } + return "" }) t.AddField("DISK", func(p *bookingv1.Package) string { if rc := p.GetResourceConfiguration(); rc != nil { return rc.GetDisk() } + return "" }) t.AddField("GPU", func(p *bookingv1.Package) string { @@ -107,6 +111,7 @@ qcloud package list --cloud-provider hybrid`, return v } } + return "n/a" }) t.AddField("MULTI-AZ", func(p *bookingv1.Package) string { diff --git a/internal/cmd/selfupgrade/selfupgrade.go b/internal/cmd/selfupgrade/selfupgrade.go index 6785e71..997f23d 100644 --- a/internal/cmd/selfupgrade/selfupgrade.go +++ b/internal/cmd/selfupgrade/selfupgrade.go @@ -72,6 +72,7 @@ func NewCommand(s *state.State) *cobra.Command { if isHomebrew { fmt.Fprintln(out, "This installation is managed by Homebrew; use \"brew upgrade qcloud\" instead.") } + return nil } @@ -95,8 +96,10 @@ func NewCommand(s *state.State) *cobra.Command { if runtime.GOOS == "windows" { hint = "running as Administrator" } + return fmt.Errorf("permission denied: try %s", hint) } + return fmt.Errorf("failed to update: %w", err) } diff --git a/internal/cmd/util/ips.go b/internal/cmd/util/ips.go index e8b93c1..bbcc770 100644 --- a/internal/cmd/util/ips.go +++ b/internal/cmd/util/ips.go @@ -64,6 +64,7 @@ func ApplyIPs(existing []string, changes *IPChanges) []string { if changes.Remove[ip] { continue } + if !seen[ip] { seen[ip] = true result = append(result, ip) diff --git a/internal/cmd/util/ips_test.go b/internal/cmd/util/ips_test.go index 91ca1d2..23187aa 100644 --- a/internal/cmd/util/ips_test.go +++ b/internal/cmd/util/ips_test.go @@ -73,6 +73,7 @@ func TestParseIPs(t *testing.T) { assert.Contains(t, err.Error(), tt.wantError) return } + require.NoError(t, err) if tt.wantAdd != nil { @@ -85,6 +86,7 @@ func TestParseIPs(t *testing.T) { for k := range changes.Remove { gotRm = append(gotRm, k) } + if tt.wantRm != nil { assert.ElementsMatch(t, tt.wantRm, gotRm) } else { diff --git a/internal/cmd/util/labels.go b/internal/cmd/util/labels.go index 7bf3b24..ac4a4dc 100644 --- a/internal/cmd/util/labels.go +++ b/internal/cmd/util/labels.go @@ -77,12 +77,14 @@ func ApplyLabels(existing []*commonv1.KeyValue, changes *LabelChanges) []*common for key := range changes.Remove { delete(merged, key) } + maps.Copy(merged, changes.Set) result := make([]*commonv1.KeyValue, 0, len(merged)) for k, v := range merged { result = append(result, &commonv1.KeyValue{Key: k, Value: v}) } + sort.Slice(result, func(i, j int) bool { return result[i].GetKey() < result[j].GetKey() }) diff --git a/internal/cmd/util/labels_test.go b/internal/cmd/util/labels_test.go index ef4a7e8..d343c80 100644 --- a/internal/cmd/util/labels_test.go +++ b/internal/cmd/util/labels_test.go @@ -95,6 +95,7 @@ func TestParseLabels(t *testing.T) { assert.Contains(t, err.Error(), tt.wantError) return } + require.NoError(t, err) if tt.wantSet != nil { @@ -107,6 +108,7 @@ func TestParseLabels(t *testing.T) { for k := range changes.Remove { gotRm = append(gotRm, k) } + if tt.wantRm != nil { assert.ElementsMatch(t, tt.wantRm, gotRm) } else { @@ -176,6 +178,7 @@ func TestApplyLabels(t *testing.T) { if changes.Set == nil { changes.Set = make(map[string]string) } + for _, k := range tt.remove { changes.Remove[k] = true } @@ -186,6 +189,7 @@ func TestApplyLabels(t *testing.T) { for _, kv := range result { got[kv.GetKey()] = kv.GetValue() } + assert.Equal(t, tt.want, got) // Verify sorted by key @@ -202,5 +206,6 @@ func kvs(pairs ...string) []*commonv1.KeyValue { for i := 0; i < len(pairs); i += 2 { result = append(result, &commonv1.KeyValue{Key: pairs[i], Value: pairs[i+1]}) } + return result } diff --git a/internal/cmd/util/util.go b/internal/cmd/util/util.go index f96c6ba..da9dbd6 100644 --- a/internal/cmd/util/util.go +++ b/internal/cmd/util/util.go @@ -25,6 +25,7 @@ func ConfirmAction(force bool, w io.Writer, prompt string) bool { if force { return true } + fmt.Fprintf(w, "%s [y/N]: ", prompt) reader := bufio.NewReader(os.Stdin) answer, _ := reader.ReadString('\n') @@ -45,6 +46,7 @@ func ExactArgs(n int, usage string) cobra.PositionalArgs { if len(args) != n { return fmt.Errorf("requires %s\n\nUsage: %s", usage, cmd.UseLine()) } + return nil } } diff --git a/internal/qcloudapi/client.go b/internal/qcloudapi/client.go index 6216ff4..8c813fc 100644 --- a/internal/qcloudapi/client.go +++ b/internal/qcloudapi/client.go @@ -52,6 +52,7 @@ func NewWithDialOptions(endpoint, apiKey string, opts ...grpc.DialOption) (*Clie if err != nil { return nil, err } + return newFromConn(conn), nil } @@ -146,6 +147,7 @@ func authInterceptor(apiKey string) grpc.UnaryClientInterceptor { return fmt.Errorf("%w (trace ID: %s)", err, strings.Join(ids, "|")) } } + return err } } diff --git a/internal/qcloudapi/client_test.go b/internal/qcloudapi/client_test.go index 34b94d9..3952f23 100644 --- a/internal/qcloudapi/client_test.go +++ b/internal/qcloudapi/client_test.go @@ -40,6 +40,7 @@ func TestAuthInterceptor_TraceID(t *testing.T) { *t.TrailerAddr = metadata.Pairs(traceIDTrailer, "abc-123") } } + return orig } err := interceptor(context.Background(), "/test", nil, nil, nil, invoker) @@ -55,6 +56,7 @@ func TestAuthInterceptor_TraceID(t *testing.T) { *t.TrailerAddr = metadata.Pairs(traceIDTrailer, "id-1", traceIDTrailer, "id-2") } } + return orig } err := interceptor(context.Background(), "/test", nil, nil, nil, invoker) diff --git a/internal/qcloudapi/cluster.go b/internal/qcloudapi/cluster.go index afea544..e4d13a4 100644 --- a/internal/qcloudapi/cluster.go +++ b/internal/qcloudapi/cluster.go @@ -34,16 +34,20 @@ func (c *ClusterClient) ListAllClusters(ctx context.Context, accountID string) ( if nextToken != nil { req.PageToken = nextToken } + resp, err := c.ListClusters(ctx, req) if err != nil { return nil, fmt.Errorf("failed to list clusters: %w", err) } + all = append(all, resp.Items...) if resp.NextPageToken == nil || *resp.NextPageToken == "" { break } + nextToken = resp.NextPageToken } + return all, nil } @@ -55,20 +59,25 @@ func (c *ClusterClient) listFiltered(ctx context.Context, accountID string, hybr if nextToken != nil { req.PageToken = nextToken } + resp, err := c.ListClusters(ctx, req) if err != nil { return nil, fmt.Errorf("failed to list clusters: %w", err) } + for _, cluster := range resp.Items { isHybrid := cluster.GetCloudProviderId() == HybridCloudProviderID if isHybrid == hybrid { all = append(all, cluster) } } + if resp.NextPageToken == nil || *resp.NextPageToken == "" { break } + nextToken = resp.NextPageToken } + return all, nil } diff --git a/internal/resource/bytes.go b/internal/resource/bytes.go index 4be4bf6..03b1dc3 100644 --- a/internal/resource/bytes.go +++ b/internal/resource/bytes.go @@ -73,6 +73,7 @@ func ParseByteQuantity(s string) (ByteQuantity, error) { if err != nil { return 0, fmt.Errorf("cannot parse %q as a byte quantity", s) } + return ByteQuantity(n) * GiB, nil } @@ -82,6 +83,7 @@ func (b *ByteQuantity) Set(s string) error { if err != nil { return err } + *b = v return nil } @@ -92,11 +94,13 @@ func (b ByteQuantity) String() string { if b == 0 { return "" } + for _, u := range unitTable { if b%u.mult == 0 { return fmt.Sprintf("%d%s", int64(b/u.mult), u.suffix) } } + return fmt.Sprintf("%d", int64(b)) } @@ -119,6 +123,7 @@ func FormatByteQuantity(b ByteQuantity, unit string) string { return fmt.Sprintf("%d%s", int64(b/u.mult), u.suffix) } } + return fmt.Sprintf("%d", int64(b)) } diff --git a/internal/resource/bytes_test.go b/internal/resource/bytes_test.go index 7b6ea77..93d36c8 100644 --- a/internal/resource/bytes_test.go +++ b/internal/resource/bytes_test.go @@ -51,6 +51,7 @@ func TestParseByteQuantity(t *testing.T) { require.Error(t, err) return } + require.NoError(t, err) assert.Equal(t, tt.want, got) }) @@ -76,6 +77,7 @@ func TestByteQuantity_Set(t *testing.T) { require.Error(t, err) return } + require.NoError(t, err) assert.Equal(t, tt.want, b) }) diff --git a/internal/resource/millicores.go b/internal/resource/millicores.go index 8932298..0d17543 100644 --- a/internal/resource/millicores.go +++ b/internal/resource/millicores.go @@ -22,6 +22,7 @@ func ParseMillicores(s string) (Millicores, error) { return Millicores(v), nil } + v, err := strconv.ParseFloat(s, 64) if err != nil { return 0, fmt.Errorf("cannot parse %q as a millicore value", s) @@ -36,6 +37,7 @@ func (m *Millicores) Set(s string) error { if err != nil { return err } + *m = v return nil } @@ -46,9 +48,11 @@ func (m Millicores) String() string { if m == 0 { return "" } + if int64(m)%1000 == 0 { return strconv.FormatInt(int64(m)/1000, 10) } + return fmt.Sprintf("%dm", int64(m)) } diff --git a/internal/resource/millicores_test.go b/internal/resource/millicores_test.go index 8f37bd9..ae1e75d 100644 --- a/internal/resource/millicores_test.go +++ b/internal/resource/millicores_test.go @@ -38,6 +38,7 @@ func TestParseMillicores(t *testing.T) { require.Error(t, err) return } + require.NoError(t, err) assert.Equal(t, tt.want, got) }) @@ -63,6 +64,7 @@ func TestMillicores_Set(t *testing.T) { require.Error(t, err) return } + require.NoError(t, err) assert.Equal(t, tt.want, m) }) diff --git a/internal/selfupgrade/github_updater.go b/internal/selfupgrade/github_updater.go index 775c30a..dc72721 100644 --- a/internal/selfupgrade/github_updater.go +++ b/internal/selfupgrade/github_updater.go @@ -29,6 +29,7 @@ func (r *ReleaseInfo) Version() string { if r.Release != nil { return r.Release.Version() } + return r.version } @@ -37,6 +38,7 @@ func (r *ReleaseInfo) Equal(version string) bool { if r.Release != nil { return r.Release.Equal(version) } + return r.version == version } @@ -68,6 +70,7 @@ func (g *GithubUpdater) DetectLatest(ctx context.Context) (*ReleaseInfo, bool, e if err != nil || !found { return nil, found, err } + return NewReleaseInfoFromSelfUpdate(rel), true, nil } @@ -77,5 +80,6 @@ func (g *GithubUpdater) UpdateSelf(ctx context.Context, currentVersion string) ( if err != nil { return nil, err } + return NewReleaseInfoFromSelfUpdate(rel), nil } diff --git a/internal/selfupgrade/homebrew.go b/internal/selfupgrade/homebrew.go index 7f0408e..326d34e 100644 --- a/internal/selfupgrade/homebrew.go +++ b/internal/selfupgrade/homebrew.go @@ -29,6 +29,7 @@ func IsHomebrewInstall(exePath string) bool { return true } } + return false } @@ -39,9 +40,11 @@ func ResolveExecutablePath() string { if err != nil { return "" } + resolved, err := filepath.EvalSymlinks(exe) if err != nil { return "" } + return resolved } diff --git a/internal/state/config/config.go b/internal/state/config/config.go index a966b6d..546a01c 100644 --- a/internal/state/config/config.go +++ b/internal/state/config/config.go @@ -57,6 +57,7 @@ func DefaultConfigPath() string { if err != nil { return "" } + return filepath.Join(home, ".config", "qcloud", "config.yaml") } @@ -78,6 +79,7 @@ func (c *Config) Load(configPath string) error { if configPath == "" { configPath = c.v.GetString("config") } + if configPath != "" { c.v.SetConfigFile(configPath) } else { @@ -85,6 +87,7 @@ func (c *Config) Load(configPath string) error { if err != nil { return fmt.Errorf("resolving home directory: %w", err) } + c.v.SetConfigName("config") c.v.AddConfigPath(filepath.Join(home, ".config", "qcloud")) } @@ -114,6 +117,7 @@ func (c *Config) Load(configPath string) error { if ctx.Endpoint != "" { flat[KeyEndpoint] = ctx.Endpoint } + if ctx.APIKey != "" { flat[KeyAPIKey] = ctx.APIKey } else if ctx.APIKeyCommand != "" { @@ -121,11 +125,14 @@ func (c *Config) Load(configPath string) error { if err != nil { return err } + flat[KeyAPIKey] = key } + if ctx.AccountID != "" { flat[KeyAccountID] = ctx.AccountID } + _ = c.v.MergeConfigMap(flat) break } @@ -183,6 +190,7 @@ func (c *Config) ActiveContext() string { if ctx := c.v.GetString("context"); ctx != "" { return ctx } + return c.file.CurrentContext } @@ -192,6 +200,7 @@ func (c *Config) ContextNames() []string { for _, ctx := range c.file.Contexts { names = append(names, ctx.Name) } + sort.Strings(names) return names } @@ -203,6 +212,7 @@ func (c *Config) GetContext(name string) (ContextEntry, bool) { return ctx, true } } + return ContextEntry{}, false } @@ -224,6 +234,7 @@ func (c *Config) UpsertContext(ctx ContextEntry) { return } } + c.file.Contexts = append(c.file.Contexts, ctx) } @@ -236,6 +247,7 @@ func (c *Config) DeleteContext(name string) { filtered = append(filtered, ctx) } } + c.file.Contexts = filtered if c.file.CurrentContext == name { c.file.CurrentContext = "" @@ -261,6 +273,7 @@ func (c *Config) WriteToFile() error { if err != nil { return fmt.Errorf("marshaling config: %w", err) } + data = append(data, '\n') } else { data, err = yaml.Marshal(c.file) @@ -292,11 +305,14 @@ func resolveAPIKeyCommand(command string) (string, error) { if msg != "" { return "", fmt.Errorf("api_key_command failed: %w: %s", err, msg) } + return "", fmt.Errorf("api_key_command failed: %w", err) } + key := strings.TrimSpace(stdout.String()) if key == "" { return "", fmt.Errorf("api_key_command returned empty output") } + return key, nil } diff --git a/internal/state/config/config_test.go b/internal/state/config/config_test.go index 4447e4b..b03037c 100644 --- a/internal/state/config/config_test.go +++ b/internal/state/config/config_test.go @@ -401,6 +401,7 @@ func TestConfigFileTagsAreSnakeCase(t *testing.T) { if val == "" || val == "-" { continue } + name, _, _ := strings.Cut(val, ",") assert.Truef(t, snakeCase.MatchString(name), "%s.%s: tag %q value %q is not snake_case", typ.Name(), field.Name, tagName, name) diff --git a/internal/state/state.go b/internal/state/state.go index 307c888..a7ade53 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -91,5 +91,6 @@ func (s *State) AccountID() (string, error) { if id == "" { return "", errNoAccountID } + return id, nil } diff --git a/internal/testutil/config.go b/internal/testutil/config.go index a0f18fe..f1c7d41 100644 --- a/internal/testutil/config.go +++ b/internal/testutil/config.go @@ -47,6 +47,7 @@ func WriteContextConfigFile(t *testing.T, dir, currentContext string, contexts m for name := range contexts { names = append(names, name) } + sort.Strings(names) for _, name := range names { @@ -82,5 +83,6 @@ func FindContextEntry(t *testing.T, path, name string) *config.ContextEntry { return &fd.Contexts[i] } } + return nil } diff --git a/internal/testutil/method_spy.go b/internal/testutil/method_spy.go index f32b10d..3478fc3 100644 --- a/internal/testutil/method_spy.go +++ b/internal/testutil/method_spy.go @@ -24,6 +24,7 @@ func (m *MethodSpy[Req, Resp]) OnCall(i int, fn func(context.Context, Req) (Resp for len(m.handlers) <= i { m.handlers = append(m.handlers, nil) } + m.handlers[i] = fn return m } @@ -68,6 +69,7 @@ func (m *MethodSpy[Req, Resp]) Last() (Req, bool) { var zero Req return zero, false } + return m.requests[len(m.requests)-1], true } @@ -96,13 +98,16 @@ func (m *MethodSpy[Req, Resp]) dispatch(ctx context.Context, req Req, unimpl fun if idx >= 0 && idx < len(m.handlers) { handler = m.handlers[idx] } + if handler == nil { handler = m.fallback } + m.mu.Unlock() if handler != nil { return handler(ctx, req) } + return unimpl(ctx, req) } diff --git a/internal/testutil/server.go b/internal/testutil/server.go index 1824dc1..4328a6b 100644 --- a/internal/testutil/server.go +++ b/internal/testutil/server.go @@ -159,6 +159,7 @@ func newBaseTestEnv(t *testing.T, cfg *envConfig) *TestEnv { if cfg.version != "" { dialOpts = append(dialOpts, grpc.WithUserAgent("qcloud-cli/"+cfg.version)) } + client, err := qcloudapi.NewWithDialOptions("passthrough:///bufnet", cfg.apiKey, dialOpts...) if err != nil { t.Fatalf("failed to create test client: %v", err) diff --git a/test/e2e/cluster_helpers_test.go b/test/e2e/cluster_helpers_test.go index b1ee90f..e679703 100644 --- a/test/e2e/cluster_helpers_test.go +++ b/test/e2e/cluster_helpers_test.go @@ -37,12 +37,15 @@ func createCluster(t *testing.T, env *framework.Env, opts createClusterOpts) str if opts.Name == "" { opts.Name = e2eClusterPrefix + randomSuffix(t) } + if opts.CloudProvider == "" { opts.CloudProvider = "aws" } + if opts.CloudRegion == "" { opts.CloudRegion = "eu-central-1" } + if opts.Package == "" { opts.Package = "free2" } @@ -81,6 +84,7 @@ func deleteCluster(t *testing.T, env *framework.Env, id string) { if id == "" { return } + res := env.RunAllowFail(t, "cluster", "delete", id, "--force") if res.Err != nil { t.Logf("cleanup: cluster delete %s failed: %v", id, res.Err) @@ -106,6 +110,7 @@ func sweepLeakedClusters(t *testing.T, env *framework.Env, maxAge time.Duration) t.Logf("leak sweep: cluster list failed: %v", res.Err) return } + if err := json.Unmarshal([]byte(res.Stdout), &resp); err != nil { t.Logf("leak sweep: decoding cluster list: %v", err) return @@ -116,9 +121,11 @@ func sweepLeakedClusters(t *testing.T, env *framework.Env, maxAge time.Duration) if !strings.HasPrefix(c.Name, e2eClusterPrefix) { continue } + if !c.CreatedAt.IsZero() && c.CreatedAt.After(cutoff) { continue } + t.Logf("leak sweep: deleting stale cluster %s (%s, created %s)", c.ID, c.Name, c.CreatedAt.Format(time.RFC3339)) deleteCluster(t, env, c.ID) @@ -132,5 +139,6 @@ func randomSuffix(t *testing.T) string { if _, err := rand.Read(b[:]); err != nil { t.Fatalf("rand: %v", err) } + return hex.EncodeToString(b[:]) } diff --git a/test/e2e/framework/binary.go b/test/e2e/framework/binary.go index 5cc7bd4..1fea251 100644 --- a/test/e2e/framework/binary.go +++ b/test/e2e/framework/binary.go @@ -56,6 +56,7 @@ func resolveBinary() (string, error) { if _, err := os.Stat(p); err != nil { return "", fmt.Errorf("%s=%s: %w", envBinary, p, err) } + return p, nil } @@ -99,14 +100,17 @@ func resolveBinary() (string, error) { if err != nil { return "", err } + defer func() { _ = os.Remove(tmp) }() if err := extractQcloud(tmp, binary); err != nil { return "", err } + if err := os.Chmod(binary, 0o755); err != nil { return "", fmt.Errorf("chmod binary: %w", err) } + return binary, nil } @@ -115,6 +119,7 @@ func binaryCacheDir(sha string) (string, error) { if err != nil { return "", fmt.Errorf("resolving user cache dir: %w", err) } + return filepath.Join(root, "qcloud-e2e", sha[:16]), nil } @@ -129,6 +134,7 @@ func fetchExpectedSHA(url, archive string) (string, error) { if err != nil { return "", err } + defer func() { _ = body.Close() }() scan := bufio.NewScanner(body) @@ -137,13 +143,16 @@ func fetchExpectedSHA(url, archive string) (string, error) { if len(fields) != 2 { continue } + if fields[1] == archive { return fields[0], nil } } + if err := scan.Err(); err != nil { return "", fmt.Errorf("reading checksums: %w", err) } + return "", fmt.Errorf("%s not listed in %s", archive, url) } @@ -154,12 +163,14 @@ func downloadVerified(url, expected string) (string, error) { if err != nil { return "", err } + defer func() { _ = body.Close() }() tmp, err := os.CreateTemp("", "qcloud-e2e-*.tar.gz") if err != nil { return "", fmt.Errorf("creating temp archive: %w", err) } + defer func() { _ = tmp.Close() }() h := sha256.New() @@ -173,6 +184,7 @@ func downloadVerified(url, expected string) (string, error) { _ = os.Remove(tmp.Name()) return "", fmt.Errorf("checksum mismatch for %s: got %s, want %s", url, got, expected) } + return tmp.Name(), nil } @@ -182,10 +194,12 @@ func httpGetBody(url string) (io.ReadCloser, error) { if err != nil { return nil, fmt.Errorf("GET %s: %w", url, err) } + if resp.StatusCode != http.StatusOK { _ = resp.Body.Close() return nil, fmt.Errorf("GET %s: %s", url, resp.Status) } + return resp.Body, nil } @@ -196,12 +210,14 @@ func extractQcloud(src, dst string) error { if err != nil { return err } + defer func() { _ = f.Close() }() gr, err := gzip.NewReader(f) if err != nil { return fmt.Errorf("gzip reader: %w", err) } + defer func() { _ = gr.Close() }() tr := tar.NewReader(gr) @@ -210,9 +226,11 @@ func extractQcloud(src, dst string) error { if errors.Is(err, io.EOF) { return fmt.Errorf("qcloud binary not found in %s", src) } + if err != nil { return fmt.Errorf("tar reader: %w", err) } + if filepath.Base(hdr.Name) != "qcloud" { continue } @@ -221,10 +239,12 @@ func extractQcloud(src, dst string) error { if err != nil { return err } + if _, err := io.Copy(out, tr); err != nil { _ = out.Close() return err } + return out.Close() } } diff --git a/test/e2e/framework/env.go b/test/e2e/framework/env.go index aa58052..b173ea9 100644 --- a/test/e2e/framework/env.go +++ b/test/e2e/framework/env.go @@ -72,6 +72,7 @@ func mustEnv(t *testing.T, name string) string { if v == "" { t.Fatalf("%s must be set", name) } + return v } @@ -83,6 +84,7 @@ func writeEmptyConfig(t *testing.T) string { if err := os.WriteFile(path, []byte{}, 0o600); err != nil { t.Fatalf("writing empty config: %v", err) } + return path } @@ -109,6 +111,7 @@ func (e *Env) RunWithTimeout(t *testing.T, timeout time.Duration, args ...string if res.Err != nil { t.Fatalf("qcloud %s failed: %v", strings.Join(args, " "), res.Err) } + return res } @@ -149,6 +152,7 @@ func (e *Env) runRaw(t *testing.T, timeout time.Duration, args []string) RunResu if cmd.ProcessState != nil { exitCode = cmd.ProcessState.ExitCode() } + return RunResult{ Stdout: stdout.String(), Stderr: stderr.String(), @@ -168,6 +172,7 @@ func (e *Env) commandEnv() []string { if e.Endpoint != "" { env = append(env, fmt.Sprintf("%s=%s", envEndpoint, e.Endpoint)) } + return env } @@ -177,6 +182,7 @@ func (e *Env) homeDir() string { if dir := filepath.Dir(e.configPath); dir != "." { return dir } + return os.TempDir() } @@ -186,6 +192,7 @@ func (e *Env) Redact(s string) string { if e.APIKey != "" { s = strings.ReplaceAll(s, e.APIKey, "[REDACTED]") } + return jwtLikeRe.ReplaceAllString(s, "[REDACTED]") } @@ -209,9 +216,11 @@ func (w testLogWriter) Write(p []byte) (int, error) { if w.redact != "" { s = strings.ReplaceAll(s, w.redact, "[REDACTED]") } + s = jwtLikeRe.ReplaceAllString(s, "[REDACTED]") for line := range strings.SplitSeq(strings.TrimRight(s, "\n"), "\n") { w.t.Log(w.prefix + line) } + return len(p), nil } diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 28a502d..f25ba8e 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -20,5 +20,6 @@ func TestMain(m *testing.M) { log.Fatalf("e2e: resolving qcloud binary: %v", err) } } + os.Exit(m.Run()) }