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$ diff --git a/cmd/infractl/cluster/create/command.go b/cmd/infractl/cluster/create/command.go index c231cea2e..28b9e255d 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" @@ -65,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") + 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") @@ -82,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 := common.GetMaxWaitErrorsFlagValue(cmd) noSlack, _ := cmd.Flags().GetBool("no-slack") slackDM, _ := cmd.Flags().GetBool("slack-me") downloadDir, _ := cmd.Flags().GetString("download-dir") @@ -129,7 +129,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, maxWaitErrors); err != nil { return nil, err } if downloadDir != "" { @@ -161,36 +161,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() { diff --git a/cmd/infractl/cluster/wait/command.go b/cmd/infractl/cluster/wait/command.go new file mode 100644 index 000000000..871ce7c73 --- /dev/null +++ b/cmd/infractl/cluster/wait/command.go @@ -0,0 +1,49 @@ +// Package wait implements the infractl wait command. +package wait + +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 = `Wait for the "example-s3maj" cluster to become ready. +$ infractl wait example-s3maj` + +// Command defines the handler for infractl wait. +func Command() *cobra.Command { + // $ 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 { + if args[0] == "" { + return errors.New("no cluster ID given") + } + return utils.ValidateClusterName(args[0]) +} + +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 prettyNoop{}, err +} diff --git a/cmd/infractl/cluster/wait/fancy.go b/cmd/infractl/cluster/wait/fancy.go new file mode 100644 index 000000000..3fb42b1b2 --- /dev/null +++ b/cmd/infractl/cluster/wait/fancy.go @@ -0,0 +1,18 @@ +package wait + +import ( + "github.com/spf13/cobra" +) + +// prettyNoop does not output anything because 'wait' command does not need output. +type prettyNoop struct { +} + +func (p prettyNoop) PrettyPrint(cmd *cobra.Command) { + cmd.Printf("\n") +} + +func (p prettyNoop) PrettyJSONPrint(cmd *cobra.Command) error { + cmd.Printf("{}\n") + return nil +} diff --git a/cmd/infractl/common/wait.go b/cmd/infractl/common/wait.go new file mode 100644 index 000000000..f0863b88d --- /dev/null +++ b/cmd/infractl/common/wait.go @@ -0,0 +1,66 @@ +package common + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" + v1 "github.com/stackrox/infra/generated/api/v1" +) + +const ( + maxWaitErrorsFlagName = "wait-max-errors" + defaultMaxConsecutiveWaitErrors = 10 +) + +// AddMaxWaitErrorsFlag adds a flag definition to cmd. +func AddMaxWaitErrorsFlag(cmd *cobra.Command) { + 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 { + value, err := cmd.Flags().GetInt(maxWaitErrorsFlagName) + if err != nil { + panic(err) + } + return value +} + +// 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 + + 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) + nErrors++ + if nErrors >= maxWaitErrors { + 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) + } +} 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(), ) 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()