-
Notifications
You must be signed in to change notification settings - Fork 880
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement list command in kubectl plugin
- Loading branch information
Showing
5 changed files
with
501 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.