Skip to content

Commit

Permalink
feat: Implement rollout status command. Fixes #596 (#1001)
Browse files Browse the repository at this point in the history
Signed-off-by: Pedro Arvela <[email protected]>
  • Loading branch information
Pedro Arvela authored Mar 24, 2021
1 parent bff46e1 commit 92c1b16
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 0 deletions.
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)
}

0 comments on commit 92c1b16

Please sign in to comment.