Skip to content

Commit

Permalink
Implement list command in kubectl plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
jessesuen committed Oct 16, 2019
1 parent 55894e6 commit 166d90a
Show file tree
Hide file tree
Showing 5 changed files with 501 additions and 0 deletions.
1 change: 1 addition & 0 deletions pkg/apis/rollouts/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const (
)

// RolloutStrategy defines strategy to apply during next rollout
// TODO(jessesuen): rename field names to match json tags to remove api violations
type RolloutStrategy struct {
// +optional
BlueGreenStrategy *BlueGreenStrategy `json:"blueGreen,omitempty"`
Expand Down
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 @@ -3,6 +3,7 @@ package cmd
import (
"github.com/spf13/cobra"

"github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/list"
"github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/pause"
"github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/resume"
"github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/version"
Expand Down Expand Up @@ -30,6 +31,7 @@ func NewCmdArgoRollouts(o *options.ArgoRolloutsOptions) *cobra.Command {
return o.UsageErr(c)
},
}
cmd.AddCommand(list.NewCmdList(o))
cmd.AddCommand(pause.NewCmdPause(o))
cmd.AddCommand(resume.NewCmdResume(o))
cmd.AddCommand(version.NewCmdVersion(o))
Expand Down
174 changes: 174 additions & 0 deletions pkg/kubectl-argo-rollouts/cmd/list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package list

import (
"context"
"fmt"
"text/tabwriter"
"time"

"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"

"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
argoprojv1alpha1 "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/typed/rollouts/v1alpha1"
"github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/options"
)

const (
example = `
# List rollouts
%[1]s list
# List rollouts from all namespaces
%[1]s list --all-namespaces
# List rollouts and watch for changes
%[1]s list --watch
`
)

type ListOptions struct {
name string
allNamespaces bool
watch bool
timestamps bool

options.ArgoRolloutsOptions
}

// NewCmdList returns a new instance of an `rollouts resume` command
func NewCmdList(o *options.ArgoRolloutsOptions) *cobra.Command {
listOptions := ListOptions{
ArgoRolloutsOptions: *o,
}

var cmd = &cobra.Command{
Use: "list",
Short: "List rollouts",
Example: o.Example(example),
SilenceUsage: true,
RunE: func(c *cobra.Command, args []string) error {
var namespace string
if listOptions.allNamespaces {
namespace = metav1.NamespaceAll
} else {
namespace = o.Namespace()
}
rolloutIf := o.RolloutsClientset().ArgoprojV1alpha1().Rollouts(namespace)
opts := listOptions.ListOptions()
rolloutList, err := rolloutIf.List(opts)
if err != nil {
return err
}
err = listOptions.PrintRolloutTable(rolloutList)
if err != nil {
return err
}
if listOptions.watch {
ctx := context.Background()
err = listOptions.PrintRolloutUpdates(ctx, rolloutIf, rolloutList)
if err != nil {
return err
}
}
return nil
},
}
o.AddKubectlFlags(cmd)
cmd.Flags().StringVar(&listOptions.name, "name", "", "Only show rollout with specified name")
cmd.Flags().BoolVar(&listOptions.allNamespaces, "all-namespaces", false, "Include all namespaces")
cmd.Flags().BoolVarP(&listOptions.watch, "watch", "w", false, "Watch for changes")
cmd.Flags().BoolVar(&listOptions.timestamps, "timestamps", false, "Print timestamps on updates")
return cmd
}

// ListOptions returns a metav1.ListOptions based on user supplied flags
func (o *ListOptions) ListOptions() metav1.ListOptions {
opts := metav1.ListOptions{}
if o.name != "" {
nameSelector := fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", o.name))
opts.FieldSelector = nameSelector.String()
}
return opts
}

// PrintRolloutTable prints rollouts in table format
func (o *ListOptions) PrintRolloutTable(roList *v1alpha1.RolloutList) error {
if len(roList.Items) == 0 {
fmt.Fprintln(o.ErrOut, "No resources found.")
return nil
}
w := tabwriter.NewWriter(o.Out, 0, 0, 2, ' ', 0)
headerStr := headerFmtString
if o.allNamespaces {
headerStr = "NAMESPACE\t" + headerStr
}
if o.timestamps {
headerStr = "TIMESTAMP\t" + headerStr
}
fmt.Fprintf(w, headerStr)
for _, ro := range roList.Items {
roLine := newRolloutInfo(ro)
fmt.Fprintln(w, roLine.String(o.timestamps, o.allNamespaces))
}
_ = w.Flush()
return nil
}

// PrintRolloutUpdates watches for changes to rollouts and prints the updates
func (o *ListOptions) PrintRolloutUpdates(ctx context.Context, rolloutIf argoprojv1alpha1.RolloutInterface, roList *v1alpha1.RolloutList) error {
w := tabwriter.NewWriter(o.Out, 0, 0, 2, ' ', 0)

opts := o.ListOptions()
opts.ResourceVersion = roList.ListMeta.ResourceVersion
watchIf, err := rolloutIf.Watch(opts)
if err != nil {
return err
}
// ticker is used to flush the tabwriter every few moments so that table is aligned when there
// are a flood of results in the watch channel
ticker := time.NewTicker(500 * time.Millisecond)

// prevLines remembers the most recent rollout lines we printed, so that we only print new lines
// when they have have changed in a meaningful way
prevLines := make(map[rolloutInfoKey]rolloutInfo)
for _, ro := range roList.Items {
roLine := newRolloutInfo(ro)
prevLines[roLine.key()] = roLine
}

var ro *v1alpha1.Rollout
L:
for {
select {
case next := <-watchIf.ResultChan():
ro, _ = next.Object.(*v1alpha1.Rollout)
case <-ticker.C:
_ = w.Flush()
continue
case <-ctx.Done():
break L
}
if ro == nil {
watchIf.Stop()
newWatchIf, err := rolloutIf.Watch(opts)
if err != nil {
o.Log.Warn(err)
// this sleep prevents a hot-loop in the event there is a persistent error
time.Sleep(time.Second)
} else {
watchIf = newWatchIf
}
continue
}
opts.ResourceVersion = ro.ObjectMeta.ResourceVersion
roLine := newRolloutInfo(*ro)
if prevLine, ok := prevLines[roLine.key()]; !ok || prevLine != roLine {
fmt.Fprintln(w, roLine.String(o.timestamps, o.allNamespaces))
prevLines[roLine.key()] = roLine
}
}
watchIf.Stop()
return nil
}
186 changes: 186 additions & 0 deletions pkg/kubectl-argo-rollouts/cmd/list/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package list

import (
"bytes"
"strings"
"testing"
"time"

"github.com/bouk/monkey"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
kubetesting "k8s.io/client-go/testing"
"k8s.io/utils/pointer"

"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
fakeroclient "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake"
options "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/options/fake"
)

func newCanaryRollout() *v1alpha1.Rollout {
return &v1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{
Name: "guestbook",
Namespace: "test",
},
Spec: v1alpha1.RolloutSpec{
Replicas: pointer.Int32Ptr(5),
Strategy: v1alpha1.RolloutStrategy{
CanaryStrategy: &v1alpha1.CanaryStrategy{
Steps: []v1alpha1.CanaryStep{
{
SetWeight: pointer.Int32Ptr(10),
},
{
Pause: &v1alpha1.RolloutPause{
Duration: pointer.Int32Ptr(60),
},
},
{
SetWeight: pointer.Int32Ptr(20),
},
},
},
},
},
Status: v1alpha1.RolloutStatus{
CurrentStepIndex: pointer.Int32Ptr(1),
Replicas: 4,
ReadyReplicas: 1,
UpdatedReplicas: 3,
AvailableReplicas: 2,
},
}
}

func newBlueGreenRollout() *v1alpha1.Rollout {
return &v1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{
Name: "guestbook",
Namespace: "test",
},
Spec: v1alpha1.RolloutSpec{
Replicas: pointer.Int32Ptr(5),
Strategy: v1alpha1.RolloutStrategy{
BlueGreenStrategy: &v1alpha1.BlueGreenStrategy{},
},
},
Status: v1alpha1.RolloutStatus{
CurrentStepIndex: pointer.Int32Ptr(1),
Replicas: 4,
ReadyReplicas: 1,
UpdatedReplicas: 3,
AvailableReplicas: 2,
},
}
}

func TestListNoResources(t *testing.T) {
tf, o := options.NewFakeArgoRolloutsOptions()
defer tf.Cleanup()
cmd := NewCmdList(o)
cmd.PersistentPreRunE = o.PersistentPreRunE
cmd.SetArgs([]string{})
err := cmd.Execute()
assert.NoError(t, err)
stdout := o.Out.(*bytes.Buffer).String()
stderr := o.ErrOut.(*bytes.Buffer).String()
assert.Empty(t, stdout)
assert.Equal(t, "No resources found.\n", stderr)
}

func TestListCanaryRollout(t *testing.T) {
ro := newCanaryRollout()
tf, o := options.NewFakeArgoRolloutsOptions(ro)
o.RESTClientGetter = tf.WithNamespace("test")
defer tf.Cleanup()
cmd := NewCmdList(o)
cmd.PersistentPreRunE = o.PersistentPreRunE
cmd.SetArgs([]string{})
err := cmd.Execute()
assert.NoError(t, err)
stdout := o.Out.(*bytes.Buffer).String()
stderr := o.ErrOut.(*bytes.Buffer).String()
assert.Empty(t, stderr)
expectedOut := strings.TrimPrefix(`
NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE
guestbook canary Progressing 1/3 10 1/4 5 3 2
`, "\n")
assert.Equal(t, expectedOut, stdout)
}

func TestListBlueGreenResource(t *testing.T) {
ro := newBlueGreenRollout()
tf, o := options.NewFakeArgoRolloutsOptions(ro)
o.RESTClientGetter = tf.WithNamespace("test")
defer tf.Cleanup()
cmd := NewCmdList(o)
cmd.PersistentPreRunE = o.PersistentPreRunE
cmd.SetArgs([]string{})
err := cmd.Execute()
assert.NoError(t, err)
stdout := o.Out.(*bytes.Buffer).String()
stderr := o.ErrOut.(*bytes.Buffer).String()
assert.Empty(t, stderr)
expectedOut := strings.TrimPrefix(`
NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE
guestbook blue-green Progressing - - 1/4 5 3 2
`, "\n")
assert.Equal(t, expectedOut, stdout)
}

func TestListNamespaceAndTimestamp(t *testing.T) {
ro := newCanaryRollout()
tf, o := options.NewFakeArgoRolloutsOptions(ro)
o.RESTClientGetter = tf.WithNamespace("test")
defer tf.Cleanup()
cmd := NewCmdList(o)
cmd.PersistentPreRunE = o.PersistentPreRunE
cmd.SetArgs([]string{"--all-namespaces", "--timestamps"})

patch := monkey.Patch(time.Now, func() time.Time { return time.Time{} })
err := cmd.Execute()
patch.Unpatch()

assert.NoError(t, err)
stdout := o.Out.(*bytes.Buffer).String()
stderr := o.ErrOut.(*bytes.Buffer).String()
assert.Empty(t, stderr)
expectedOut := strings.TrimPrefix(`
TIMESTAMP NAMESPACE NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE
0001-01-01T00:00:00Z test guestbook canary Progressing 1/3 10 1/4 5 3 2
`, "\n")
assert.Equal(t, expectedOut, stdout)
}

func TestListWithWatch(t *testing.T) {
t.Skip()
can := newCanaryRollout()
tf, o := options.NewFakeArgoRolloutsOptions(can)
o.RESTClientGetter = tf.WithNamespace("test")
defer tf.Cleanup()
cmd := NewCmdList(o)
cmd.PersistentPreRunE = o.PersistentPreRunE

fakeClient := o.RolloutsClient.(*fakeroclient.Clientset)
fakeClient.ReactionChain = nil
fakeClient.WatchReactionChain = nil
watcher := watch.NewFakeWithChanSize(1, true)
fakeClient.AddWatchReactor("*", func(action kubetesting.Action) (handled bool, ret watch.Interface, err error) {
return true, watcher, nil
})

cmd.SetArgs([]string{"--watch"})
err := cmd.Execute()
assert.NoError(t, err)

stdout := o.Out.(*bytes.Buffer).String()
stderr := o.ErrOut.(*bytes.Buffer).String()
assert.Empty(t, stderr)
expectedOut := strings.TrimPrefix(`
NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE
guestbook canary Progressing 1/3 10 1/4 5 3 2
`, "\n")
assert.Equal(t, expectedOut, stdout)
}
Loading

0 comments on commit 166d90a

Please sign in to comment.