diff --git a/pkg/kubectl-argo-rollouts/cmd/cmd.go b/pkg/kubectl-argo-rollouts/cmd/cmd.go index 7426b3c1b2..f6762b96c1 100644 --- a/pkg/kubectl-argo-rollouts/cmd/cmd.go +++ b/pkg/kubectl-argo-rollouts/cmd/cmd.go @@ -13,6 +13,7 @@ import ( "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/restart" "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/retry" "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/set" + "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/status" "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/terminate" "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/undo" "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/version" @@ -64,5 +65,6 @@ func NewCmdArgoRollouts(o *options.ArgoRolloutsOptions) *cobra.Command { cmd.AddCommand(terminate.NewCmdTerminate(o)) cmd.AddCommand(set.NewCmdSet(o)) cmd.AddCommand(undo.NewCmdUndo(o)) + cmd.AddCommand(status.NewCmdStatus(o)) return cmd } diff --git a/pkg/kubectl-argo-rollouts/cmd/status/status.go b/pkg/kubectl-argo-rollouts/cmd/status/status.go new file mode 100644 index 0000000000..f92f231e65 --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/status/status.go @@ -0,0 +1,122 @@ +package status + +import ( + "context" + "fmt" + "time" + + "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/info" + "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/options" + "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/viewcontroller" + "github.com/spf13/cobra" +) + +const ( + statusLong = `Watch rollout until it finishes or the timeout is exceeded. Returns success if +the rollout is healthy upon completion and an error otherwise.` + statusExample = ` + # Watch the rollout until it succeeds + %[1]s status guestbook + + # Watch the rollout until it succeeds, fail if it takes more than 60 seconds + %[1]s status --timeout 60 guestbook + ` +) + +type StatusOptions struct { + Watch bool + Timeout int64 + + options.ArgoRolloutsOptions +} + +// NewCmdStatus returns a new instance of a `rollouts status` command +func NewCmdStatus(o *options.ArgoRolloutsOptions) *cobra.Command { + statusOptions := StatusOptions{ + ArgoRolloutsOptions: *o, + } + + var cmd = &cobra.Command{ + Use: "status ROLLOUT_NAME", + Short: "Show the status of a rollout", + Long: statusLong, + Example: o.Example(statusExample), + SilenceUsage: true, + RunE: func(c *cobra.Command, args []string) error { + if len(args) != 1 { + return o.UsageErr(c) + } + name := args[0] + controller := viewcontroller.NewRolloutViewController(o.Namespace(), name, statusOptions.KubeClientset(), statusOptions.RolloutsClientset()) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + controller.Start(ctx) + + ri, err := controller.GetRolloutInfo() + if err != nil { + return err + } + + if !statusOptions.Watch { + fmt.Fprintln(o.Out, ri.Status) + } else { + rolloutUpdates := make(chan *info.RolloutInfo) + defer close(rolloutUpdates) + controller.RegisterCallback(func(roInfo *info.RolloutInfo) { + rolloutUpdates <- roInfo + }) + go statusOptions.WatchStatus(ctx.Done(), cancel, statusOptions.Timeout, rolloutUpdates) + controller.Run(ctx) + + finalRi, err := controller.GetRolloutInfo() + if err != nil { + return err + } + + if finalRi.Status == "Degraded" { + return fmt.Errorf("The rollout is in a degraded state with message: %s", finalRi.Message) + } else if finalRi.Status != "Healthy" { + return fmt.Errorf("Rollout progress exceeded timeout") + } + } + + return nil + }, + } + cmd.Flags().BoolVarP(&statusOptions.Watch, "watch", "w", true, "Watch the status of the rollout until it's done") + cmd.Flags().Int64VarP(&statusOptions.Timeout, "timeout", "t", 0, "The length of time in seconds to watch before giving up, zero means wait forever") + return cmd +} + +func (o *StatusOptions) WatchStatus(stopCh <-chan struct{}, cancelFunc context.CancelFunc, timeoutSeconds int64, rolloutUpdates chan *info.RolloutInfo) { + timeout := make(chan bool) + var roInfo *info.RolloutInfo + var preventFlicker time.Time + + if timeoutSeconds != 0 { + go func() { + time.Sleep(time.Duration(timeoutSeconds) * time.Second) + timeout <- true + }() + } + + for { + select { + case roInfo = <-rolloutUpdates: + if roInfo != nil && roInfo.Status == "Healthy" || roInfo.Status == "Degraded" { + fmt.Fprintln(o.Out, roInfo.Status) + cancelFunc() + return + } + if roInfo != nil && time.Now().After(preventFlicker.Add(200*time.Millisecond)) { + fmt.Fprintf(o.Out, "%s - %s\n", roInfo.Status, roInfo.Message) + preventFlicker = time.Now() + } + case <-stopCh: + return + case <-timeout: + cancelFunc() + return + } + } +} diff --git a/pkg/kubectl-argo-rollouts/cmd/status/status_test.go b/pkg/kubectl-argo-rollouts/cmd/status/status_test.go new file mode 100644 index 0000000000..bb8130b0e0 --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/status/status_test.go @@ -0,0 +1,143 @@ +package status + +import ( + "bytes" + "testing" + + "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/info/testdata" + options "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/options/fake" + "github.com/stretchr/testify/assert" +) + +const noWatch = "--watch=false" + +func TestStatusUsage(t *testing.T) { + tf, o := options.NewFakeArgoRolloutsOptions() + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{}) + err := cmd.Execute() + + assert.Error(t, err) +} + +func TestStatusRolloutNotFound(t *testing.T) { + tf, o := options.NewFakeArgoRolloutsOptions() + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"does-not-exist", noWatch}) + err := cmd.Execute() + + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Empty(t, stdout) + assert.Equal(t, "Error: rollout.argoproj.io \"does-not-exist\" not found\n", stderr) +} + +func TestWatchStatusRolloutNotFound(t *testing.T) { + tf, o := options.NewFakeArgoRolloutsOptions() + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"does-not-exist"}) + err := cmd.Execute() + + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Empty(t, stdout) + assert.Equal(t, "Error: rollout.argoproj.io \"does-not-exist\" not found\n", stderr) +} + +func TestStatusBlueGreenRollout(t *testing.T) { + rolloutObjs := testdata.NewBlueGreenRollout() + + tf, o := options.NewFakeArgoRolloutsOptions(rolloutObjs.AllObjects()...) + o.RESTClientGetter = tf.WithNamespace(rolloutObjs.Rollouts[0].Namespace) + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{rolloutObjs.Rollouts[0].Name, noWatch}) + err := cmd.Execute() + + assert.NoError(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, "Paused\n", stdout) + assert.Empty(t, stderr) +} + +func TestStatusInvalidRollout(t *testing.T) { + rolloutObjs := testdata.NewInvalidRollout() + + tf, o := options.NewFakeArgoRolloutsOptions(rolloutObjs.AllObjects()...) + o.RESTClientGetter = tf.WithNamespace(rolloutObjs.Rollouts[0].Namespace) + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{rolloutObjs.Rollouts[0].Name, noWatch}) + err := cmd.Execute() + + assert.NoError(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, "Degraded\n", stdout) + assert.Empty(t, stderr) +} + +func TestStatusAbortedRollout(t *testing.T) { + rolloutObjs := testdata.NewAbortedRollout() + + tf, o := options.NewFakeArgoRolloutsOptions(rolloutObjs.AllObjects()...) + o.RESTClientGetter = tf.WithNamespace(rolloutObjs.Rollouts[0].Namespace) + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{rolloutObjs.Rollouts[0].Name, noWatch}) + err := cmd.Execute() + + assert.NoError(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, "Degraded\n", stdout) + assert.Empty(t, stderr) +} + +func TestWatchAbortedRollout(t *testing.T) { + rolloutObjs := testdata.NewAbortedRollout() + + tf, o := options.NewFakeArgoRolloutsOptions(rolloutObjs.AllObjects()...) + o.RESTClientGetter = tf.WithNamespace(rolloutObjs.Rollouts[0].Namespace) + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{rolloutObjs.Rollouts[0].Name}) + err := cmd.Execute() + + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, "Degraded\n", stdout) + assert.Equal(t, "Error: The rollout is in a degraded state with message: RolloutAborted: metric \"web\" assessed Failed due to failed (1) > failureLimit (0)\n", stderr) +} + +func TestWatchTimeoutRollout(t *testing.T) { + rolloutObjs := testdata.NewBlueGreenRollout() + + tf, o := options.NewFakeArgoRolloutsOptions(rolloutObjs.AllObjects()...) + o.RESTClientGetter = tf.WithNamespace(rolloutObjs.Rollouts[0].Namespace) + defer tf.Cleanup() + cmd := NewCmdStatus(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{rolloutObjs.Rollouts[0].Name, "--timeout=1"}) + err := cmd.Execute() + + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, "Paused - BlueGreenPause\n", stdout) + assert.Equal(t, "Error: Rollout progress exceeded timeout\n", stderr) +}