Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement rollout status command. Fixes #596 #1001

Merged
merged 11 commits into from
Mar 24, 2021
2 changes: 2 additions & 0 deletions pkg/kubectl-argo-rollouts/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
122 changes: 122 additions & 0 deletions pkg/kubectl-argo-rollouts/cmd/status/status.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
143 changes: 143 additions & 0 deletions pkg/kubectl-argo-rollouts/cmd/status/status_test.go
Original file line number Diff line number Diff line change
@@ -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)
}