diff --git a/Gopkg.lock b/Gopkg.lock index e39c7f3059..5cfaa9e8ec 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1301,6 +1301,7 @@ "k8s.io/apimachinery/pkg/api/meta", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured", + "k8s.io/apimachinery/pkg/fields", "k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/runtime", "k8s.io/apimachinery/pkg/runtime/schema", diff --git a/pkg/apis/rollouts/v1alpha1/types.go b/pkg/apis/rollouts/v1alpha1/types.go index f00afa0d1b..ba8445456d 100644 --- a/pkg/apis/rollouts/v1alpha1/types.go +++ b/pkg/apis/rollouts/v1alpha1/types.go @@ -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"` diff --git a/pkg/kubectl-argo-rollouts/cmd/cmd.go b/pkg/kubectl-argo-rollouts/cmd/cmd.go index d726a6d6ae..7a4c9f36dd 100644 --- a/pkg/kubectl-argo-rollouts/cmd/cmd.go +++ b/pkg/kubectl-argo-rollouts/cmd/cmd.go @@ -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" @@ -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)) diff --git a/pkg/kubectl-argo-rollouts/cmd/list/list.go b/pkg/kubectl-argo-rollouts/cmd/list/list.go new file mode 100644 index 0000000000..51b5fa5051 --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/list/list.go @@ -0,0 +1,181 @@ +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 list` 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 && !o.watch { + 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 + retries := 0 +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 { + // if we get here, it means an error on the watch. try to re-establish the watch + watchIf.Stop() + newWatchIf, err := rolloutIf.Watch(opts) + if err != nil { + if retries > 5 { + return err + } + o.Log.Warn(err) + // this sleep prevents a hot-loop in the event there is a persistent error + time.Sleep(time.Second) + retries++ + } else { + watchIf = newWatchIf + retries = 0 + } + 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 +} diff --git a/pkg/kubectl-argo-rollouts/cmd/list/list_test.go b/pkg/kubectl-argo-rollouts/cmd/list/list_test.go new file mode 100644 index 0000000000..940d32fbf0 --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/list/list_test.go @@ -0,0 +1,206 @@ +package list + +import ( + "bytes" + "errors" + "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: "can-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: "bg-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 +can-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 +bg-guestbook BlueGreen 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 can-guestbook Canary Progressing 1/3 10 1/4 5 3 2 +`, "\n") + assert.Equal(t, expectedOut, stdout) +} + +func TestListWithWatch(t *testing.T) { + can1 := newCanaryRollout() + bg := newBlueGreenRollout() + can1copy := can1.DeepCopy() + can2 := newCanaryRollout() + can2.Status.AvailableReplicas = 3 + + tf, o := options.NewFakeArgoRolloutsOptions(can1, bg) + o.RESTClientGetter = tf.WithNamespace("test") + defer tf.Cleanup() + cmd := NewCmdList(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + + fakeClient := o.RolloutsClient.(*fakeroclient.Clientset) + fakeClient.WatchReactionChain = nil + watcher := watch.NewFakeWithChanSize(10, false) + + watcher.Add(can1) + watcher.Add(bg) + watcher.Add(can1copy) + watcher.Add(can2) + watcher.Stop() + callCount := 0 + fakeClient.AddWatchReactor("*", func(action kubetesting.Action) (handled bool, ret watch.Interface, err error) { + if callCount > 0 { + return true, nil, errors.New("intentional error") + } + callCount++ + return true, watcher, nil + }) + + cmd.SetArgs([]string{"--watch"}) + err := cmd.Execute() + assert.Error(t, err) + assert.Equal(t, "intentional error", err.Error()) + + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + + expectedOut := strings.TrimPrefix(` +NAME STRATEGY STATUS STEP SET-WEIGHT READY DESIRED UP-TO-DATE AVAILABLE +can-guestbook Canary Progressing 1/3 10 1/4 5 3 2 +bg-guestbook BlueGreen Progressing - - 1/4 5 3 2 +can-guestbook Canary Progressing 1/3 10 1/4 5 3 3 +`, "\n") + assert.Equal(t, expectedOut, stdout) + + assert.Contains(t, stderr, "intentional error") +} diff --git a/pkg/kubectl-argo-rollouts/cmd/list/rollloutinfo.go b/pkg/kubectl-argo-rollouts/cmd/list/rollloutinfo.go new file mode 100644 index 0000000000..6fa2bd981b --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/list/rollloutinfo.go @@ -0,0 +1,138 @@ +package list + +import ( + "fmt" + "strconv" + "time" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + replicasetutil "github.com/argoproj/argo-rollouts/utils/replicaset" +) + +const ( + headerFmtString = "NAME\tSTRATEGY\tSTATUS\tSTEP\tSET-WEIGHT\tREADY\tDESIRED\tUP-TO-DATE\tAVAILABLE\n" + // column values are padded to be roughly equal to the character lengths of the headers, which + // gives a greater chance of visual table alignment. Some exceptions are made we anticipate + // longer values (e.g. Progressing for status) + columnFmtString = "%-10s\t%-9s\t%-12s\t%-4s\t%-10s\t%-5s\t%-7d\t%-10d\t%-9d" +) + +// rolloutInfo contains the columns which are printed as part of a list command +type rolloutInfo struct { + namespace string + name string + strategy string + status string + step string + setWeight string + readyCurrent string + desired int32 + upToDate int32 + available int32 +} + +// rolloutInfoKey is used as a map key to get a rolloutInfo by namespace/name +type rolloutInfoKey struct { + ns string + n string +} + +func newRolloutInfo(ro v1alpha1.Rollout) rolloutInfo { + ri := rolloutInfo{} + ri.name = ro.Name + ri.namespace = ro.Namespace + ri.strategy = "unknown" + ri.step = "-" + ri.setWeight = "-" + + if ro.Spec.Strategy.CanaryStrategy != nil { + ri.strategy = "Canary" + if ro.Status.CurrentStepIndex != nil && len(ro.Spec.Strategy.CanaryStrategy.Steps) > 0 { + ri.step = fmt.Sprintf("%d/%d", *ro.Status.CurrentStepIndex, len(ro.Spec.Strategy.CanaryStrategy.Steps)) + } + // NOTE that this is desired weight, not the actual current weight + ri.setWeight = strconv.Itoa(int(replicasetutil.GetCurrentSetWeight(&ro))) + + // TODO(jessesuen) in the future, we want to calculate the actual weight + // if ro.Status.AvailableReplicas == 0 { + // ri.weight = "0" + // } else { + // ri.weight = fmt.Sprintf("%d", (ro.Status.UpdatedReplicas*100)/ro.Status.AvailableReplicas) + // } + } else if ro.Spec.Strategy.BlueGreenStrategy != nil { + ri.strategy = "BlueGreen" + } + ri.status = rolloutStatus(&ro) + + ri.desired = 1 + if ro.Spec.Replicas != nil { + ri.desired = *ro.Spec.Replicas + } + ri.readyCurrent = fmt.Sprintf("%d/%d", ro.Status.ReadyReplicas, ro.Status.Replicas) + ri.upToDate = ro.Status.UpdatedReplicas + ri.available = ro.Status.AvailableReplicas + return ri +} + +func (ri *rolloutInfo) key() rolloutInfoKey { + return rolloutInfoKey{ + ns: ri.namespace, + n: ri.name, + } +} + +func (ri *rolloutInfo) String(timestamp, namespace bool) string { + fmtString := columnFmtString + args := []interface{}{ri.name, ri.strategy, ri.status, ri.step, ri.setWeight, ri.readyCurrent, ri.desired, ri.upToDate, ri.available} + if namespace { + fmtString = "%-9s\t" + fmtString + args = append([]interface{}{ri.namespace}, args...) + } + if timestamp { + fmtString = "%-20s\t" + fmtString + timestampStr := time.Now().UTC().Truncate(time.Second).Format("2006-01-02T15:04:05Z") + args = append([]interface{}{timestampStr}, args...) + } + return fmt.Sprintf(fmtString, args...) +} + +// rolloutStatus returns a status string to print in the STATUS column +func rolloutStatus(ro *v1alpha1.Rollout) string { + for _, condition := range ro.Status.Conditions { + if condition.Type == v1alpha1.InvalidSpec { + return string(condition.Type) + } + if condition.Type == v1alpha1.RolloutProgressing && condition.Reason == "ProgressDeadlineExceeded" { + return "Degraded" + } + } + if ro.Spec.Paused { + return "Paused" + } + if ro.Status.UpdatedReplicas < ro.Status.Replicas { + // more replicas need to be updated + return "Progressing" + } + if ro.Status.Replicas > ro.Status.UpdatedReplicas { + // old replicas are pending termination + return "Progressing" + } + if ro.Status.AvailableReplicas < ro.Status.UpdatedReplicas { + // updated replicas are still becoming available + return "Progressing" + } + if ro.Spec.Strategy.BlueGreenStrategy != nil { + if ro.Status.BlueGreen.ActiveSelector != "" && ro.Status.BlueGreen.ActiveSelector == ro.Status.CurrentPodHash { + return "Healthy" + } + // service cutover pending + return "Progressing" + } else if ro.Spec.Strategy.CanaryStrategy != nil { + if ro.Status.Canary.StableRS != "" && ro.Status.Canary.StableRS == ro.Status.CurrentPodHash { + return "Healthy" + } + // Waiting for rollout to finish steps + return "Progressing" + } + return "Unknown" +} diff --git a/pkg/kubectl-argo-rollouts/cmd/list/rolloutinfo_test.go b/pkg/kubectl-argo-rollouts/cmd/list/rolloutinfo_test.go new file mode 100644 index 0000000000..f138512bb7 --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/list/rolloutinfo_test.go @@ -0,0 +1,108 @@ +package list + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" +) + +func TestRolloutStatusDegraded(t *testing.T) { + ro := newCanaryRollout() + ro.Status.Conditions = append(ro.Status.Conditions, v1alpha1.RolloutCondition{ + Type: v1alpha1.RolloutProgressing, + Reason: "ProgressDeadlineExceeded", + }) + assert.Equal(t, "Degraded", rolloutStatus(ro)) +} + +func TestRolloutStatusInvalidSpec(t *testing.T) { + ro := newCanaryRollout() + ro.Status.Conditions = append(ro.Status.Conditions, v1alpha1.RolloutCondition{ + Type: v1alpha1.InvalidSpec, + }) + assert.Equal(t, string(v1alpha1.InvalidSpec), rolloutStatus(ro)) +} + +func TestRolloutStatusPaused(t *testing.T) { + ro := newCanaryRollout() + ro.Spec.Paused = true + assert.Equal(t, "Paused", rolloutStatus(ro)) +} + +func TestRolloutStatusProgressing(t *testing.T) { + { + ro := newCanaryRollout() + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newCanaryRollout() + ro.Status.UpdatedReplicas = 1 + ro.Status.Replicas = 2 + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newCanaryRollout() + ro.Status.UpdatedReplicas = 2 + ro.Status.Replicas = 1 + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newCanaryRollout() + ro.Status.AvailableReplicas = 1 + ro.Status.UpdatedReplicas = 2 + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newCanaryRollout() + ro.Status.AvailableReplicas = 1 + ro.Status.UpdatedReplicas = 2 + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newCanaryRollout() + ro.Status.Canary.StableRS = "" + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newCanaryRollout() + ro.Status.Canary.StableRS = "abc1234" + ro.Status.CurrentPodHash = "def5678" + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newBlueGreenRollout() + ro.Status.BlueGreen.ActiveSelector = "" + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } + { + ro := newBlueGreenRollout() + ro.Status.BlueGreen.ActiveSelector = "abc1234" + ro.Status.CurrentPodHash = "def5678" + assert.Equal(t, "Progressing", rolloutStatus(ro)) + } +} + +func TestRolloutStatusHealthy(t *testing.T) { + { + ro := newCanaryRollout() + ro.Status.Replicas = 1 + ro.Status.UpdatedReplicas = 1 + ro.Status.AvailableReplicas = 1 + ro.Status.ReadyReplicas = 1 + ro.Status.Canary.StableRS = "abc1234" + ro.Status.CurrentPodHash = "abc1234" + assert.Equal(t, "Healthy", rolloutStatus(ro)) + } + { + ro := newBlueGreenRollout() + ro.Status.Replicas = 1 + ro.Status.UpdatedReplicas = 1 + ro.Status.AvailableReplicas = 1 + ro.Status.ReadyReplicas = 1 + ro.Status.BlueGreen.ActiveSelector = "abc1234" + ro.Status.CurrentPodHash = "abc1234" + assert.Equal(t, "Healthy", rolloutStatus(ro)) + } +}