From 8f1637ef9263521911150904a823aa8198a4de82 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 10:12:36 +0100 Subject: [PATCH 01/14] Set up wait command as a copy of get --- cmd/infractl/cluster/wait/command.go | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 cmd/infractl/cluster/wait/command.go diff --git a/cmd/infractl/cluster/wait/command.go b/cmd/infractl/cluster/wait/command.go new file mode 100644 index 000000000..8456ff1bb --- /dev/null +++ b/cmd/infractl/cluster/wait/command.go @@ -0,0 +1,45 @@ +// Package get implements the infractl get command. +package get + +import ( + "context" + "errors" + + "github.com/spf13/cobra" + "github.com/stackrox/infra/cmd/infractl/cluster/utils" + "github.com/stackrox/infra/cmd/infractl/common" + v1 "github.com/stackrox/infra/generated/api/v1" + "google.golang.org/grpc" +) + +const examples = `Lookup info for the "example-s3maj" cluster. +$ infractl get example-s3maj` + +// Command defines the handler for infractl get. +func Command() *cobra.Command { + // $ infractl get + return &cobra.Command{ + Use: "get CLUSTER", + Short: "Get info for a specific cluster", + Long: "Displays info for a single cluster", + Example: examples, + Args: common.ArgsWithHelp(cobra.ExactArgs(1), args), + RunE: common.WithGRPCHandler(run), + } +} + +func args(_ *cobra.Command, args []string) error { + if args[0] == "" { + return errors.New("no cluster ID given") + } + return utils.ValidateClusterName(args[0]) +} + +func run(ctx context.Context, conn *grpc.ClientConn, _ *cobra.Command, args []string) (common.PrettyPrinter, error) { + resp, err := v1.NewClusterServiceClient(conn).Info(ctx, &v1.ResourceByID{Id: args[0]}) + if err != nil { + return nil, err + } + + return &prettyCluster{resp}, nil +} From d2f27027bf5161cffc91514a2abe2e27ef1dda33 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 10:21:42 +0100 Subject: [PATCH 02/14] Add fancy.go as a copy of create command's one --- cmd/infractl/cluster/wait/fancy.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 cmd/infractl/cluster/wait/fancy.go diff --git a/cmd/infractl/cluster/wait/fancy.go b/cmd/infractl/cluster/wait/fancy.go new file mode 100644 index 000000000..a49c8279d --- /dev/null +++ b/cmd/infractl/cluster/wait/fancy.go @@ -0,0 +1,27 @@ +package create + +import ( + "encoding/json" + + "github.com/spf13/cobra" + + v1 "github.com/stackrox/infra/generated/api/v1" +) + +type prettyResourceByID struct { + *v1.ResourceByID +} + +func (p prettyResourceByID) PrettyPrint(cmd *cobra.Command) { + cmd.Printf("ID: %s\n", p.Id) +} + +func (p prettyResourceByID) PrettyJSONPrint(cmd *cobra.Command) error { + data, err := json.MarshalIndent(p.ResourceByID, "", " ") + if err != nil { + return err + } + + cmd.Printf("%s\n", string(data)) + return nil +} From 0359e8ae463cb1db50d0e928ef490481861e47da Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 10:41:15 +0100 Subject: [PATCH 03/14] Set up shared `waitForCluster` as an exact copy of create's function --- cmd/infractl/common/wait.go | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 cmd/infractl/common/wait.go diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go new file mode 100644 index 000000000..34ffefda9 --- /dev/null +++ b/cmd/infractl/common/wait.go @@ -0,0 +1,41 @@ +package common + +import ( + "context" + "errors" + "fmt" + "os" + "time" + + v1 "github.com/stackrox/infra/generated/api/v1" +) + +func waitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) error { + const timeoutSleep = 30 * time.Second + const timeoutAPI = 15 * time.Second + + fmt.Fprintf(os.Stderr, "...creating %s\n", clusterID.Id) + for { + time.Sleep(timeoutSleep) + ctx, cancel := context.WithTimeout(context.Background(), timeoutAPI) + + cluster, err := client.Info(ctx, clusterID) + cancel() + if err != nil { + fmt.Fprintln(os.Stderr, "...error") + continue + } + + switch cluster.Status { + case v1.Status_CREATING: + fmt.Fprintln(os.Stderr, "...creating") + continue + case v1.Status_READY: + fmt.Fprintln(os.Stderr, "...ready") + return nil + default: + fmt.Fprintln(os.Stderr, "...failed") + return errors.New("failed to provision cluster") + } + } +} From cae097cee1d09d29db55c1742ca5fea4595833bb Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 10:43:12 +0100 Subject: [PATCH 04/14] Export and improve `WaitForCluster` a tiny bit --- cmd/infractl/common/wait.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go index 34ffefda9..9b5d67ba1 100644 --- a/cmd/infractl/common/wait.go +++ b/cmd/infractl/common/wait.go @@ -10,11 +10,11 @@ import ( v1 "github.com/stackrox/infra/generated/api/v1" ) -func waitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) error { +func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) error { const timeoutSleep = 30 * time.Second const timeoutAPI = 15 * time.Second - fmt.Fprintf(os.Stderr, "...creating %s\n", clusterID.Id) + fmt.Fprintf(os.Stderr, "...waiting for %s\n", clusterID.Id) for { time.Sleep(timeoutSleep) ctx, cancel := context.WithTimeout(context.Background(), timeoutAPI) @@ -22,7 +22,7 @@ func waitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) cluster, err := client.Info(ctx, clusterID) cancel() if err != nil { - fmt.Fprintln(os.Stderr, "...error") + fmt.Fprintf(os.Stderr, "...error %s\n", err) continue } @@ -35,7 +35,7 @@ func waitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) return nil default: fmt.Fprintln(os.Stderr, "...failed") - return errors.New("failed to provision cluster") + return errors.New("cluster failed provisioning") } } } From 6c26f5814405a9caca8a9b0fbe3dc4fe79bcfce1 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 10:46:14 +0100 Subject: [PATCH 05/14] Make `create` command use common `WaitForCluster` function --- cmd/infractl/cluster/create/command.go | 34 +------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/cmd/infractl/cluster/create/command.go b/cmd/infractl/cluster/create/command.go index c231cea2e..e1e0370f1 100644 --- a/cmd/infractl/cluster/create/command.go +++ b/cmd/infractl/cluster/create/command.go @@ -4,11 +4,9 @@ package create import ( "context" "fmt" - "os" "strings" "time" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/stackrox/infra/cmd/infractl/cluster/artifacts" "github.com/stackrox/infra/cmd/infractl/cluster/utils" @@ -129,7 +127,7 @@ func run(ctx context.Context, conn *grpc.ClientConn, cmd *cobra.Command, args [] } if wait { - if err := waitForCluster(client, clusterID); err != nil { + if err := common.WaitForCluster(client, clusterID); err != nil { return nil, err } if downloadDir != "" { @@ -161,36 +159,6 @@ func assignDefaults(cmd *cobra.Command, req *v1.CreateClusterRequest, cwe *curre req.Parameters["main-image"] = registry + "/main:" + getCleaned(cwe.tag) } -func waitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) error { - const timeoutSleep = 30 * time.Second - const timeoutAPI = 15 * time.Second - - fmt.Fprintf(os.Stderr, "...creating %s\n", clusterID.Id) - for { - time.Sleep(timeoutSleep) - ctx, cancel := context.WithTimeout(context.Background(), timeoutAPI) - - cluster, err := client.Info(ctx, clusterID) - cancel() - if err != nil { - fmt.Fprintln(os.Stderr, "...error") - continue - } - - switch cluster.Status { - case v1.Status_CREATING: - fmt.Fprintln(os.Stderr, "...creating") - continue - case v1.Status_READY: - fmt.Fprintln(os.Stderr, "...ready") - return nil - default: - fmt.Fprintln(os.Stderr, "...failed") - return errors.New("failed to provision cluster") - } - } -} - func displayUserNotes(cmd *cobra.Command, args []string, req *v1.CreateClusterRequest, cwe *currentWorkingEnvironment) { if wasNameProvided(args) { if isQaDemoFlavor(args[0]) && cwe.isInStackroxRepo() { From 85fb8774743d1014254054bcecc1be9a5be5b910 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 11:10:07 +0100 Subject: [PATCH 06/14] Teach `WaitForCluster` respect `--timeout` --- cmd/infractl/common/wait.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go index 9b5d67ba1..d36e971c5 100644 --- a/cmd/infractl/common/wait.go +++ b/cmd/infractl/common/wait.go @@ -1,7 +1,6 @@ package common import ( - "context" "errors" "fmt" "os" @@ -12,13 +11,11 @@ import ( func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) error { const timeoutSleep = 30 * time.Second - const timeoutAPI = 15 * time.Second fmt.Fprintf(os.Stderr, "...waiting for %s\n", clusterID.Id) for { time.Sleep(timeoutSleep) - ctx, cancel := context.WithTimeout(context.Background(), timeoutAPI) - + ctx, cancel := ContextWithTimeout() cluster, err := client.Info(ctx, clusterID) cancel() if err != nil { From 7f4042796f26fa1c56ef3603ba7acf76fbefb9cd Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 11:14:13 +0100 Subject: [PATCH 07/14] Make `WaitForCluster` not sleep before the first request --- cmd/infractl/common/wait.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go index d36e971c5..01079d9bd 100644 --- a/cmd/infractl/common/wait.go +++ b/cmd/infractl/common/wait.go @@ -14,7 +14,6 @@ func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) fmt.Fprintf(os.Stderr, "...waiting for %s\n", clusterID.Id) for { - time.Sleep(timeoutSleep) ctx, cancel := ContextWithTimeout() cluster, err := client.Info(ctx, clusterID) cancel() @@ -26,7 +25,6 @@ func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) switch cluster.Status { case v1.Status_CREATING: fmt.Fprintln(os.Stderr, "...creating") - continue case v1.Status_READY: fmt.Fprintln(os.Stderr, "...ready") return nil @@ -34,5 +32,7 @@ func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) fmt.Fprintln(os.Stderr, "...failed") return errors.New("cluster failed provisioning") } + + time.Sleep(timeoutSleep) } } From c1d4ca5704b7441de42dec40e783ffb011bb6d5e Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 13:44:12 +0100 Subject: [PATCH 08/14] Make the waiting function give up on too many errors E.g. when the connection got broken, the token is wrong or the cluster is gone. --- cmd/infractl/cluster/create/command.go | 4 ++- cmd/infractl/common/wait.go | 35 ++++++++++++++++---------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/cmd/infractl/cluster/create/command.go b/cmd/infractl/cluster/create/command.go index e1e0370f1..c386510f2 100644 --- a/cmd/infractl/cluster/create/command.go +++ b/cmd/infractl/cluster/create/command.go @@ -63,6 +63,7 @@ func Command() *cobra.Command { cmd.Flags().String("description", "", "description for this cluster") cmd.Flags().Duration("lifespan", 3*time.Hour, "initial lifespan of the cluster") cmd.Flags().Bool("wait", false, "wait for cluster to be ready") + cmd.Flags().Int("wait-max-errors", common.DefaultMaxConsecutiveWaitErrors, "maximum number of consecutive errors before giving up waiting") cmd.Flags().Bool("no-slack", false, "skip sending Slack messages for lifecycle events") cmd.Flags().Bool("slack-me", false, "send slack messages directly and not to the #infra_notifications channel") cmd.Flags().StringP("download-dir", "d", "", "wait for readiness and download artifacts to this dir") @@ -80,6 +81,7 @@ func run(ctx context.Context, conn *grpc.ClientConn, cmd *cobra.Command, args [] description, _ := cmd.Flags().GetString("description") lifespan, _ := cmd.Flags().GetDuration("lifespan") wait, _ := cmd.Flags().GetBool("wait") + maxWaitErrors, _ := cmd.Flags().GetInt("wait-max-errors") noSlack, _ := cmd.Flags().GetBool("no-slack") slackDM, _ := cmd.Flags().GetBool("slack-me") downloadDir, _ := cmd.Flags().GetString("download-dir") @@ -127,7 +129,7 @@ func run(ctx context.Context, conn *grpc.ClientConn, cmd *cobra.Command, args [] } if wait { - if err := common.WaitForCluster(client, clusterID); err != nil { + if err := common.WaitForCluster(client, clusterID, maxWaitErrors); err != nil { return nil, err } if downloadDir != "" { diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go index 01079d9bd..d565dece5 100644 --- a/cmd/infractl/common/wait.go +++ b/cmd/infractl/common/wait.go @@ -9,28 +9,37 @@ import ( v1 "github.com/stackrox/infra/generated/api/v1" ) -func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID) error { +const DefaultMaxConsecutiveWaitErrors = 10 + +func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID, maxConsecutiveErrors int) error { const timeoutSleep = 30 * time.Second + nErrors := 0 + fmt.Fprintf(os.Stderr, "...waiting for %s\n", clusterID.Id) for { ctx, cancel := ContextWithTimeout() cluster, err := client.Info(ctx, clusterID) cancel() + if err != nil { fmt.Fprintf(os.Stderr, "...error %s\n", err) - continue - } - - switch cluster.Status { - case v1.Status_CREATING: - fmt.Fprintln(os.Stderr, "...creating") - case v1.Status_READY: - fmt.Fprintln(os.Stderr, "...ready") - return nil - default: - fmt.Fprintln(os.Stderr, "...failed") - return errors.New("cluster failed provisioning") + nErrors += 1 + if nErrors >= maxConsecutiveErrors { + return errors.New("too many errors while waiting") + } + } else { + nErrors = 0 + switch cluster.Status { + case v1.Status_CREATING: + fmt.Fprintln(os.Stderr, "...creating") + case v1.Status_READY: + fmt.Fprintln(os.Stderr, "...ready") + return nil + default: + fmt.Fprintln(os.Stderr, "...failed") + return errors.New("cluster failed provisioning") + } } time.Sleep(timeoutSleep) From 67d3417c1c239fa123244c6a2907ce0f2b69f131 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 13:57:56 +0100 Subject: [PATCH 09/14] Introduce reusable functions for adding and reading a flag --- cmd/infractl/cluster/create/command.go | 4 ++-- cmd/infractl/common/wait.go | 22 +++++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/cmd/infractl/cluster/create/command.go b/cmd/infractl/cluster/create/command.go index c386510f2..28b9e255d 100644 --- a/cmd/infractl/cluster/create/command.go +++ b/cmd/infractl/cluster/create/command.go @@ -63,7 +63,7 @@ func Command() *cobra.Command { cmd.Flags().String("description", "", "description for this cluster") cmd.Flags().Duration("lifespan", 3*time.Hour, "initial lifespan of the cluster") cmd.Flags().Bool("wait", false, "wait for cluster to be ready") - cmd.Flags().Int("wait-max-errors", common.DefaultMaxConsecutiveWaitErrors, "maximum number of consecutive errors before giving up waiting") + common.AddMaxWaitErrorsFlag(cmd) cmd.Flags().Bool("no-slack", false, "skip sending Slack messages for lifecycle events") cmd.Flags().Bool("slack-me", false, "send slack messages directly and not to the #infra_notifications channel") cmd.Flags().StringP("download-dir", "d", "", "wait for readiness and download artifacts to this dir") @@ -81,7 +81,7 @@ func run(ctx context.Context, conn *grpc.ClientConn, cmd *cobra.Command, args [] description, _ := cmd.Flags().GetString("description") lifespan, _ := cmd.Flags().GetDuration("lifespan") wait, _ := cmd.Flags().GetBool("wait") - maxWaitErrors, _ := cmd.Flags().GetInt("wait-max-errors") + maxWaitErrors := common.GetMaxWaitErrorsFlagValue(cmd) noSlack, _ := cmd.Flags().GetBool("no-slack") slackDM, _ := cmd.Flags().GetBool("slack-me") downloadDir, _ := cmd.Flags().GetString("download-dir") diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go index d565dece5..6467a2a97 100644 --- a/cmd/infractl/common/wait.go +++ b/cmd/infractl/common/wait.go @@ -6,12 +6,28 @@ import ( "os" "time" + "github.com/spf13/cobra" v1 "github.com/stackrox/infra/generated/api/v1" ) -const DefaultMaxConsecutiveWaitErrors = 10 +const ( + flagName = "wait-max-errors" + defaultMaxConsecutiveWaitErrors = 10 +) + +func AddMaxWaitErrorsFlag(cmd *cobra.Command) { + cmd.Flags().Int(flagName, defaultMaxConsecutiveWaitErrors, "maximum number of consecutive errors before giving up waiting") +} + +func GetMaxWaitErrorsFlagValue(cmd *cobra.Command) int { + maxWaitErrors, err := cmd.Flags().GetInt(flagName) + if err != nil { + panic(err) + } + return maxWaitErrors +} -func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID, maxConsecutiveErrors int) error { +func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID, maxWaitErrors int) error { const timeoutSleep = 30 * time.Second nErrors := 0 @@ -25,7 +41,7 @@ func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID, if err != nil { fmt.Fprintf(os.Stderr, "...error %s\n", err) nErrors += 1 - if nErrors >= maxConsecutiveErrors { + if nErrors >= maxWaitErrors { return errors.New("too many errors while waiting") } } else { From 6364f4f60df88cd278699261f9ccb8c526e27f97 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 13:58:30 +0100 Subject: [PATCH 10/14] Implement and add wait command --- cmd/infractl/cluster/wait/command.go | 36 +++++++++++++++------------- cmd/infractl/cluster/wait/fancy.go | 22 +++++------------ cmd/infractl/main.go | 4 ++++ 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/cmd/infractl/cluster/wait/command.go b/cmd/infractl/cluster/wait/command.go index 8456ff1bb..871ce7c73 100644 --- a/cmd/infractl/cluster/wait/command.go +++ b/cmd/infractl/cluster/wait/command.go @@ -1,5 +1,5 @@ -// Package get implements the infractl get command. -package get +// Package wait implements the infractl wait command. +package wait import ( "context" @@ -12,20 +12,24 @@ import ( "google.golang.org/grpc" ) -const examples = `Lookup info for the "example-s3maj" cluster. -$ infractl get example-s3maj` +const examples = `Wait for the "example-s3maj" cluster to become ready. +$ infractl wait example-s3maj` -// Command defines the handler for infractl get. +// Command defines the handler for infractl wait. func Command() *cobra.Command { - // $ infractl get - return &cobra.Command{ - Use: "get CLUSTER", - Short: "Get info for a specific cluster", - Long: "Displays info for a single cluster", + // $ infractl wait + cmd := &cobra.Command{ + Use: "wait CLUSTER", + Short: "Wait for a specific cluster", + Long: "Wait for the the specific cluster to become ready.", Example: examples, Args: common.ArgsWithHelp(cobra.ExactArgs(1), args), RunE: common.WithGRPCHandler(run), } + + common.AddMaxWaitErrorsFlag(cmd) + + return cmd } func args(_ *cobra.Command, args []string) error { @@ -35,11 +39,11 @@ func args(_ *cobra.Command, args []string) error { return utils.ValidateClusterName(args[0]) } -func run(ctx context.Context, conn *grpc.ClientConn, _ *cobra.Command, args []string) (common.PrettyPrinter, error) { - resp, err := v1.NewClusterServiceClient(conn).Info(ctx, &v1.ResourceByID{Id: args[0]}) - if err != nil { - return nil, err - } +func run(_ context.Context, conn *grpc.ClientConn, cmd *cobra.Command, args []string) (common.PrettyPrinter, error) { + maxWaitErrors := common.GetMaxWaitErrorsFlagValue(cmd) + + client := v1.NewClusterServiceClient(conn) + err := common.WaitForCluster(client, &v1.ResourceByID{Id: args[0]}, maxWaitErrors) - return &prettyCluster{resp}, nil + return prettyNoop{}, err } diff --git a/cmd/infractl/cluster/wait/fancy.go b/cmd/infractl/cluster/wait/fancy.go index a49c8279d..fd6066327 100644 --- a/cmd/infractl/cluster/wait/fancy.go +++ b/cmd/infractl/cluster/wait/fancy.go @@ -1,27 +1,17 @@ -package create +package wait import ( - "encoding/json" - "github.com/spf13/cobra" - - v1 "github.com/stackrox/infra/generated/api/v1" ) -type prettyResourceByID struct { - *v1.ResourceByID +type prettyNoop struct { } -func (p prettyResourceByID) PrettyPrint(cmd *cobra.Command) { - cmd.Printf("ID: %s\n", p.Id) +func (p prettyNoop) PrettyPrint(cmd *cobra.Command) { + cmd.Printf("\n") } -func (p prettyResourceByID) PrettyJSONPrint(cmd *cobra.Command) error { - data, err := json.MarshalIndent(p.ResourceByID, "", " ") - if err != nil { - return err - } - - cmd.Printf("%s\n", string(data)) +func (p prettyNoop) PrettyJSONPrint(cmd *cobra.Command) error { + cmd.Printf("{}\n") return nil } diff --git a/cmd/infractl/main.go b/cmd/infractl/main.go index 82ab430bd..d56f908b9 100644 --- a/cmd/infractl/main.go +++ b/cmd/infractl/main.go @@ -12,6 +12,7 @@ import ( "github.com/stackrox/infra/cmd/infractl/cluster/lifespan" "github.com/stackrox/infra/cmd/infractl/cluster/list" "github.com/stackrox/infra/cmd/infractl/cluster/logs" + "github.com/stackrox/infra/cmd/infractl/cluster/wait" "github.com/stackrox/infra/cmd/infractl/common" "github.com/stackrox/infra/cmd/infractl/flavor" janitorFind "github.com/stackrox/infra/cmd/infractl/janitor/find" @@ -98,6 +99,9 @@ func main() { // $ infractl version version.Command(), + // $ infractl wait + wait.Command(), + // $ infractl whoami whoami.Command(), ) From 5caa302fc5940eb6b3d33d41336eee827267f5cc Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 15:10:39 +0100 Subject: [PATCH 11/14] Try satisfy go lint --- cmd/infractl/common/wait.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go index 6467a2a97..769c74337 100644 --- a/cmd/infractl/common/wait.go +++ b/cmd/infractl/common/wait.go @@ -15,10 +15,12 @@ const ( defaultMaxConsecutiveWaitErrors = 10 ) +// AddMaxWaitErrorsFlag adds a flag definition to cmd. func AddMaxWaitErrorsFlag(cmd *cobra.Command) { cmd.Flags().Int(flagName, defaultMaxConsecutiveWaitErrors, "maximum number of consecutive errors before giving up waiting") } +// GetMaxWaitErrorsFlagValue gets effective value of the flag after arguments are parsed. func GetMaxWaitErrorsFlagValue(cmd *cobra.Command) int { maxWaitErrors, err := cmd.Flags().GetInt(flagName) if err != nil { @@ -27,6 +29,7 @@ func GetMaxWaitErrorsFlagValue(cmd *cobra.Command) int { return maxWaitErrors } +// WaitForCluster waits for a created cluster to be in a ready state. func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID, maxWaitErrors int) error { const timeoutSleep = 30 * time.Second @@ -40,7 +43,7 @@ func WaitForCluster(client v1.ClusterServiceClient, clusterID *v1.ResourceByID, if err != nil { fmt.Fprintf(os.Stderr, "...error %s\n", err) - nErrors += 1 + nErrors++ if nErrors >= maxWaitErrors { return errors.New("too many errors while waiting") } From 7d5c8856d9e5d853f1a26f9609339a37dbd68dc0 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 18:01:18 +0100 Subject: [PATCH 12/14] Try suppress naming warnings from linter ``` Running [/home/runner/golangci-lint-2.10.1-linux-amd64/golangci-lint config path] in [/home/runner/work/infra/infra] ... Running [/home/runner/golangci-lint-2.10.1-linux-amd64/golangci-lint config verify] in [/home/runner/work/infra/infra] ... Running [/home/runner/golangci-lint-2.10.1-linux-amd64/golangci-lint run] in [/home/runner/work/infra/infra] ... Error: cmd/infractl/flavor/list/fancy.go:1:9: var-naming: avoid package names that conflict with Go standard library package names (revive) package list ^ Error: pkg/buildinfo/buildinfo.go:3:9: var-naming: avoid package names that conflict with Go standard library package names (revive) package buildinfo ^ Error: pkg/service/metrics/metrics.go:2:9: var-naming: avoid package names that conflict with Go standard library package names (revive) package metrics ^ 3 issues: * revive: 3 Error: issues found ``` https://github.com/stackrox/infra/actions/runs/22721785912/job/65885650995?pr=1768#step:4:40 I did not break those, I did not even touch them. Confirmed they started failing on no-op PR, see https://github.com/stackrox/infra/pull/1769 and https://github.com/stackrox/infra/actions/runs/22730204288/job/65916882489?pr=1769 Not sure why `master` wasn't affected. Suppressing. Used these docs https://golangci-lint.run/docs/linters/false-positives/#exclude-issues-by-path I tried `package blah //nolint:var-naming` but that did not work. I did not do ``` //nolint:var-naming package blah ``` because that would turn off the rule for the entire file. --- .golangci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 41a4418ad..fddbcd0bd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -92,6 +92,13 @@ linters: - linters: - revive text: avoid meaningless package names + - linters: + - revive + text: avoid package names that conflict with Go standard library package names + paths: + - cmd/infractl/flavor/list/ + - pkg/buildinfo/ + - pkg/service/metrics/ paths: - third_party$ - builtin$ From 2f1da5d721ec43d0c67709c253cc830b4b99438f Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 18:32:58 +0100 Subject: [PATCH 13/14] Try implement test for `wait` command I think the value of this test is nearly zero, but for consistency... `infractl artifacts` command goes without a test though. --- test/e2e/cluster/wait_test.go | 28 ++++++++++++++++++++++++++++ test/utils/mock/infractl.go | 8 ++++++++ 2 files changed, 36 insertions(+) create mode 100644 test/e2e/cluster/wait_test.go diff --git a/test/e2e/cluster/wait_test.go b/test/e2e/cluster/wait_test.go new file mode 100644 index 000000000..41dfbfdf5 --- /dev/null +++ b/test/e2e/cluster/wait_test.go @@ -0,0 +1,28 @@ +//go:build e2e +// +build e2e + +package cluster_test + +import ( + "testing" + + "github.com/stackrox/infra/test/utils" + "github.com/stackrox/infra/test/utils/mock" + "github.com/stretchr/testify/assert" +) + +func TestWait(t *testing.T) { + utils.CheckContext() + clusterID, err := mock.InfractlCreateCluster( + "test-simulate", + "--lifespan=60s", + ) + assert.NoError(t, err) + + err = mock.InfractlWait(clusterID) + assert.NoError(t, err) + + cluster, err := mock.InfractlGetCluster(clusterID) + assert.NoError(t, err) + assert.Equal(t, "READY", cluster.Status) +} diff --git a/test/utils/mock/infractl.go b/test/utils/mock/infractl.go index 24b928e84..ca10ad702 100644 --- a/test/utils/mock/infractl.go +++ b/test/utils/mock/infractl.go @@ -10,6 +10,7 @@ import ( infraClusterLifespan "github.com/stackrox/infra/cmd/infractl/cluster/lifespan" infraClusterList "github.com/stackrox/infra/cmd/infractl/cluster/list" infraClusterLogs "github.com/stackrox/infra/cmd/infractl/cluster/logs" + infraClusterWait "github.com/stackrox/infra/cmd/infractl/cluster/wait" infraFlavorGet "github.com/stackrox/infra/cmd/infractl/flavor/get" infraFlavorList "github.com/stackrox/infra/cmd/infractl/flavor/list" infraJanitorFind "github.com/stackrox/infra/cmd/infractl/janitor/find" @@ -107,6 +108,13 @@ func InfractlLogs(clusterID string) (*v1.LogsResponse, error) { return jsonData, nil } +// InfractlWait is a warpper for 'infractl wait '. +func InfractlWait(clusterID string) error { + infraWaitCmd := infraClusterWait.Command() + PrepareCommand(infraWaitCmd, false, clusterID) + return infraWaitCmd.Execute() +} + // InfractlWhoami is a wrapper for 'infractl whoami'. func InfractlWhoami() (string, error) { whoamiCmd := infraWhoami.Command() From 33243d66dbdabadd3613abe2661f51c2b982a733 Mon Sep 17 00:00:00 2001 From: Misha Sugakov Date: Thu, 5 Mar 2026 20:58:31 +0100 Subject: [PATCH 14/14] Self-review --- cmd/infractl/cluster/wait/fancy.go | 1 + cmd/infractl/common/wait.go | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/infractl/cluster/wait/fancy.go b/cmd/infractl/cluster/wait/fancy.go index fd6066327..3fb42b1b2 100644 --- a/cmd/infractl/cluster/wait/fancy.go +++ b/cmd/infractl/cluster/wait/fancy.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" ) +// prettyNoop does not output anything because 'wait' command does not need output. type prettyNoop struct { } diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go index 769c74337..f0863b88d 100644 --- a/cmd/infractl/common/wait.go +++ b/cmd/infractl/common/wait.go @@ -11,22 +11,22 @@ import ( ) const ( - flagName = "wait-max-errors" + maxWaitErrorsFlagName = "wait-max-errors" defaultMaxConsecutiveWaitErrors = 10 ) // AddMaxWaitErrorsFlag adds a flag definition to cmd. func AddMaxWaitErrorsFlag(cmd *cobra.Command) { - cmd.Flags().Int(flagName, defaultMaxConsecutiveWaitErrors, "maximum number of consecutive errors before giving up waiting") + cmd.Flags().Int(maxWaitErrorsFlagName, defaultMaxConsecutiveWaitErrors, "maximum number of consecutive errors before giving up waiting") } // GetMaxWaitErrorsFlagValue gets effective value of the flag after arguments are parsed. func GetMaxWaitErrorsFlagValue(cmd *cobra.Command) int { - maxWaitErrors, err := cmd.Flags().GetInt(flagName) + value, err := cmd.Flags().GetInt(maxWaitErrorsFlagName) if err != nil { panic(err) } - return maxWaitErrors + return value } // WaitForCluster waits for a created cluster to be in a ready state.