diff --git a/pkg/kubectl-argo-rollouts/cmd/cmd.go b/pkg/kubectl-argo-rollouts/cmd/cmd.go index baa754bba5..fb72a82e35 100644 --- a/pkg/kubectl-argo-rollouts/cmd/cmd.go +++ b/pkg/kubectl-argo-rollouts/cmd/cmd.go @@ -7,7 +7,7 @@ import ( "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/cmd/get" "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/promote" "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/version" @@ -20,7 +20,7 @@ const ( %[1]s pause guestbook # Resume the guestbook rollout - %[1]s resume guestbook + %[1]s promote guestbook ` ) @@ -38,7 +38,7 @@ func NewCmdArgoRollouts(o *options.ArgoRolloutsOptions) *cobra.Command { cmd.AddCommand(get.NewCmdGet(o)) cmd.AddCommand(list.NewCmdList(o)) cmd.AddCommand(pause.NewCmdPause(o)) - cmd.AddCommand(resume.NewCmdResume(o)) + cmd.AddCommand(promote.NewCmdPromote(o)) cmd.AddCommand(version.NewCmdVersion(o)) cmd.AddCommand(abort.NewCmdAbort(o)) cmd.AddCommand(retry.NewCmdRetry(o)) diff --git a/pkg/kubectl-argo-rollouts/cmd/promote/promote.go b/pkg/kubectl-argo-rollouts/cmd/promote/promote.go new file mode 100644 index 0000000000..2f7d4fe97e --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/promote/promote.go @@ -0,0 +1,102 @@ +package promote + +import ( + "fmt" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + + "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/options" + replicasetutil "github.com/argoproj/argo-rollouts/utils/replicaset" +) + +const ( + example = ` + # Promote a rollout with the canary strategy + %[1]s promote guestbook [--skip-steps] [--skip-current-step] +` + setCurrentStepIndex = `{ + "status": { + "currentStepIndex": %d + } +}` + + unpausePatch = `{ + "spec": { + "paused": false + }, + "status": { + "pauseConditions": null + } +}` + useBothSkipFlagsError = "Cannot use skip-current-step and skip-all-steps flags at the same time" + skipFlagsWithBlueGreenError = "Cannot skip steps of a bluegreen rollout. Run without a flags" + skipFlagWithNoStepCanaryError = "Cannot skip steps of a rollout without steps" +) + +// NewCmdPromote returns a new instance of an `rollouts promote` command +func NewCmdPromote(o *options.ArgoRolloutsOptions) *cobra.Command { + var ( + skipCurrentStep = false + skipAllSteps = false + ) + var cmd = &cobra.Command{ + Use: "promote ROLLOUT", + Short: "promote a rollout", + Example: o.Example(example), + SilenceUsage: true, + RunE: func(c *cobra.Command, args []string) error { + if len(args) != 1 { + return o.UsageErr(c) + } + if skipCurrentStep && skipAllSteps { + return fmt.Errorf(useBothSkipFlagsError) + } + name := args[0] + rolloutIf := o.RolloutsClientset().ArgoprojV1alpha1().Rollouts(o.Namespace()) + ro, err := rolloutIf.Get(name, metav1.GetOptions{}) + if err != nil { + return err + } + if skipCurrentStep || skipAllSteps { + if ro.Spec.Strategy.BlueGreen != nil { + return fmt.Errorf(skipFlagsWithBlueGreenError) + } + if ro.Spec.Strategy.Canary != nil && len(ro.Spec.Strategy.Canary.Steps) == 0 { + return fmt.Errorf(skipFlagWithNoStepCanaryError) + } + } + patch := getPatch(ro, skipCurrentStep, skipAllSteps) + ro, err = rolloutIf.Patch(name, types.MergePatchType, patch) + if err != nil { + return err + } + fmt.Fprintf(o.Out, "rollout '%s' promoted\n", ro.Name) + return nil + }, + } + o.AddKubectlFlags(cmd) + cmd.Flags().BoolVarP(&skipCurrentStep, "skip-current-step", "c", false, "Skip current step") + cmd.Flags().BoolVarP(&skipAllSteps, "skip-all-steps", "a", false, "Skip remaining steps") + + return cmd +} + +func getPatch(rollout *v1alpha1.Rollout, skipCurrentStep, skipAllStep bool) []byte { + switch { + case skipCurrentStep: + _, index := replicasetutil.GetCurrentCanaryStep(rollout) + // At this point, the controller knows that the rollout is a canary with steps and GetCurrentCanaryStep returns 0 if + // the index is not set in the rollout + if *index < int32(len(rollout.Spec.Strategy.Canary.Steps)) { + *index++ + } + return []byte(fmt.Sprintf(setCurrentStepIndex, *index)) + case skipAllStep: + return []byte(fmt.Sprintf(setCurrentStepIndex, len(rollout.Spec.Strategy.Canary.Steps))) + default: + return []byte(unpausePatch) + } +} diff --git a/pkg/kubectl-argo-rollouts/cmd/promote/promote_test.go b/pkg/kubectl-argo-rollouts/cmd/promote/promote_test.go new file mode 100644 index 0000000000..442f146e25 --- /dev/null +++ b/pkg/kubectl-argo-rollouts/cmd/promote/promote_test.go @@ -0,0 +1,342 @@ +package promote + +import ( + "bytes" + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + 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 TestPromoteCmdUsage(t *testing.T) { + tf, o := options.NewFakeArgoRolloutsOptions() + defer tf.Cleanup() + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{}) + err := cmd.Execute() + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Empty(t, stdout) + assert.Contains(t, stderr, "Usage:") + assert.Contains(t, stderr, "promote ROLLOUT") +} + +func TestPromoteUseBothSkipFlagError(t *testing.T) { + tf, o := options.NewFakeArgoRolloutsOptions() + defer tf.Cleanup() + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook", "--skip-current-step", "--skip-all-steps"}) + err := cmd.Execute() + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Empty(t, stdout) + assert.Contains(t, stderr, useBothSkipFlagsError) +} +func TestPromoteSkipFlagOnBlueGreenError(t *testing.T) { + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "guestbook", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Strategy: v1alpha1.RolloutStrategy{ + BlueGreen: &v1alpha1.BlueGreenStrategy{}, + }, + }, + } + tf, o := options.NewFakeArgoRolloutsOptions(&ro) + defer tf.Cleanup() + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook", "-a"}) + err := cmd.Execute() + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Empty(t, stdout) + assert.Contains(t, stderr, skipFlagsWithBlueGreenError) +} +func TestPromoteSkipFlagOnNoStepCanaryError(t *testing.T) { + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "guestbook", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Strategy: v1alpha1.RolloutStrategy{ + Canary: &v1alpha1.CanaryStrategy{}, + }, + }, + } + tf, o := options.NewFakeArgoRolloutsOptions(&ro) + defer tf.Cleanup() + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook", "-c"}) + err := cmd.Execute() + assert.Error(t, err) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Empty(t, stdout) + assert.Contains(t, stderr, skipFlagWithNoStepCanaryError) +} + +func TestPromoteCmdSuccesSkipAllSteps(t *testing.T) { + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "guestbook", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Strategy: v1alpha1.RolloutStrategy{ + Canary: &v1alpha1.CanaryStrategy{ + Steps: []v1alpha1.CanaryStep{ + { + SetWeight: pointer.Int32Ptr(1), + }, + { + SetWeight: pointer.Int32Ptr(2), + }, + }, + }, + }, + }, + } + + tf, o := options.NewFakeArgoRolloutsOptions(&ro) + defer tf.Cleanup() + fakeClient := o.RolloutsClient.(*fakeroclient.Clientset) + fakeClient.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + if patchAction, ok := action.(kubetesting.PatchAction); ok { + patchRo := v1alpha1.Rollout{} + err := json.Unmarshal(patchAction.GetPatch(), &patchRo) + if err != nil { + panic(err) + } + ro.Status.CurrentStepIndex = patchRo.Status.CurrentStepIndex + } + return true, &ro, nil + }) + + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook", "-a"}) + + err := cmd.Execute() + assert.Nil(t, err) + assert.Equal(t, int32(2), *ro.Status.CurrentStepIndex) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, stdout, "rollout 'guestbook' promoted\n") + assert.Empty(t, stderr) +} + +func TestPromoteCmdSuccesFirstStep(t *testing.T) { + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "guestbook", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Strategy: v1alpha1.RolloutStrategy{ + Canary: &v1alpha1.CanaryStrategy{ + Steps: []v1alpha1.CanaryStep{ + { + SetWeight: pointer.Int32Ptr(1), + }, + { + SetWeight: pointer.Int32Ptr(2), + }, + }, + }, + }, + }, + } + + tf, o := options.NewFakeArgoRolloutsOptions(&ro) + defer tf.Cleanup() + fakeClient := o.RolloutsClient.(*fakeroclient.Clientset) + fakeClient.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + if patchAction, ok := action.(kubetesting.PatchAction); ok { + patchRo := v1alpha1.Rollout{} + err := json.Unmarshal(patchAction.GetPatch(), &patchRo) + if err != nil { + panic(err) + } + ro.Status.CurrentStepIndex = patchRo.Status.CurrentStepIndex + } + return true, &ro, nil + }) + + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook", "-c"}) + + err := cmd.Execute() + assert.Nil(t, err) + assert.Equal(t, int32(1), *ro.Status.CurrentStepIndex) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, stdout, "rollout 'guestbook' promoted\n") + assert.Empty(t, stderr) +} + +func TestPromoteCmdSuccessDoNotGoPastLastStep(t *testing.T) { + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "guestbook", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Strategy: v1alpha1.RolloutStrategy{ + Canary: &v1alpha1.CanaryStrategy{ + Steps: []v1alpha1.CanaryStep{ + { + SetWeight: pointer.Int32Ptr(1), + }, + { + SetWeight: pointer.Int32Ptr(2), + }, + }, + }, + }, + }, + Status: v1alpha1.RolloutStatus{ + CurrentStepIndex: pointer.Int32Ptr(2), + }, + } + + tf, o := options.NewFakeArgoRolloutsOptions(&ro) + defer tf.Cleanup() + fakeClient := o.RolloutsClient.(*fakeroclient.Clientset) + fakeClient.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + if patchAction, ok := action.(kubetesting.PatchAction); ok { + patchRo := v1alpha1.Rollout{} + err := json.Unmarshal(patchAction.GetPatch(), &patchRo) + if err != nil { + panic(err) + } + ro.Status.CurrentStepIndex = patchRo.Status.CurrentStepIndex + } + return true, &ro, nil + }) + + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook", "-c"}) + + err := cmd.Execute() + assert.Nil(t, err) + assert.Equal(t, int32(2), *ro.Status.CurrentStepIndex) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, stdout, "rollout 'guestbook' promoted\n") + assert.Empty(t, stderr) +} + +func TestPromoteCmdSuccessUnpause(t *testing.T) { + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "guestbook", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Paused: true, + }, + Status: v1alpha1.RolloutStatus{ + PauseConditions: []v1alpha1.PauseCondition{{ + Reason: v1alpha1.PauseReasonCanaryPauseStep, + }}, + ControllerPause: true, + }, + } + + tf, o := options.NewFakeArgoRolloutsOptions(&ro) + defer tf.Cleanup() + fakeClient := o.RolloutsClient.(*fakeroclient.Clientset) + fakeClient.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + if patchAction, ok := action.(kubetesting.PatchAction); ok { + if string(patchAction.GetPatch()) == unpausePatch { + ro.Status.PauseConditions = nil + ro.Spec.Paused = false + } + } + return true, &ro, nil + }) + + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook"}) + err := cmd.Execute() + assert.Nil(t, err) + + assert.Nil(t, ro.Status.PauseConditions) + assert.False(t, ro.Spec.Paused) + assert.True(t, ro.Status.ControllerPause) + stdout := o.Out.(*bytes.Buffer).String() + stderr := o.ErrOut.(*bytes.Buffer).String() + assert.Equal(t, stdout, "rollout 'guestbook' promoted\n") + assert.Empty(t, stderr) +} + +func TestPromoteCmdPatchError(t *testing.T) { + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "guestbook", + Namespace: metav1.NamespaceDefault, + }, + Spec: v1alpha1.RolloutSpec{ + Paused: true, + }, + Status: v1alpha1.RolloutStatus{ + PauseConditions: []v1alpha1.PauseCondition{{ + Reason: v1alpha1.PauseReasonCanaryPauseStep, + }}, + ControllerPause: true, + }, + } + + tf, o := options.NewFakeArgoRolloutsOptions(&ro) + defer tf.Cleanup() + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"guestbook"}) + fakeClient := o.RolloutsClient.(*fakeroclient.Clientset) + fakeClient.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, fmt.Errorf("Intentional Error") + }) + + 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: Intentional Error\n", stderr) +} + +func TestPromoteCmdNotFoundError(t *testing.T) { + tf, o := options.NewFakeArgoRolloutsOptions(&v1alpha1.Rollout{}) + defer tf.Cleanup() + cmd := NewCmdPromote(o) + cmd.PersistentPreRunE = o.PersistentPreRunE + cmd.SetArgs([]string{"doesnotexist"}) + 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: rollouts.argoproj.io \"doesnotexist\" not found\n", stderr) +} diff --git a/pkg/kubectl-argo-rollouts/cmd/resume/resume.go b/pkg/kubectl-argo-rollouts/cmd/resume/resume.go deleted file mode 100644 index 7d861560b0..0000000000 --- a/pkg/kubectl-argo-rollouts/cmd/resume/resume.go +++ /dev/null @@ -1,51 +0,0 @@ -package resume - -import ( - "fmt" - - "github.com/spf13/cobra" - types "k8s.io/apimachinery/pkg/types" - - "github.com/argoproj/argo-rollouts/pkg/kubectl-argo-rollouts/options" -) - -const ( - example = ` - # Resume a rollout - %[1]s resume guestbook -` - unpausePatch = `{ - "spec": { - "paused": false - }, - "status": { - "pauseConditions": null - } -}` -) - -// NewCmdResume returns a new instance of an `rollouts resume` command -func NewCmdResume(o *options.ArgoRolloutsOptions) *cobra.Command { - var cmd = &cobra.Command{ - Use: "resume ROLLOUT", - Short: "Resume a rollout", - Example: o.Example(example), - SilenceUsage: true, - RunE: func(c *cobra.Command, args []string) error { - if len(args) == 0 { - return o.UsageErr(c) - } - rolloutIf := o.RolloutsClientset().ArgoprojV1alpha1().Rollouts(o.Namespace()) - for _, name := range args { - ro, err := rolloutIf.Patch(name, types.MergePatchType, []byte(unpausePatch)) - if err != nil { - return err - } - fmt.Fprintf(o.Out, "rollout '%s' resumed\n", ro.Name) - } - return nil - }, - } - o.AddKubectlFlags(cmd) - return cmd -} diff --git a/pkg/kubectl-argo-rollouts/cmd/resume/resume_test.go b/pkg/kubectl-argo-rollouts/cmd/resume/resume_test.go deleted file mode 100644 index 6e262fae25..0000000000 --- a/pkg/kubectl-argo-rollouts/cmd/resume/resume_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package resume - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - kubetesting "k8s.io/client-go/testing" - - "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 TestResumeCmdUsage(t *testing.T) { - tf, o := options.NewFakeArgoRolloutsOptions() - defer tf.Cleanup() - cmd := NewCmdResume(o) - cmd.PersistentPreRunE = o.PersistentPreRunE - cmd.SetArgs([]string{}) - err := cmd.Execute() - assert.Error(t, err) - stdout := o.Out.(*bytes.Buffer).String() - stderr := o.ErrOut.(*bytes.Buffer).String() - assert.Empty(t, stdout) - assert.Contains(t, stderr, "Usage:") - assert.Contains(t, stderr, "resume ROLLOUT") -} - -func TestResumeCmdSuccess(t *testing.T) { - ro := v1alpha1.Rollout{ - ObjectMeta: metav1.ObjectMeta{ - Name: "guestbook", - Namespace: metav1.NamespaceDefault, - }, - Spec: v1alpha1.RolloutSpec{ - Paused: true, - }, - Status: v1alpha1.RolloutStatus{ - PauseConditions: []v1alpha1.PauseCondition{{ - Reason: v1alpha1.PauseReasonCanaryPauseStep, - }}, - ControllerPause: true, - }, - } - - tf, o := options.NewFakeArgoRolloutsOptions(&ro) - defer tf.Cleanup() - fakeClient := o.RolloutsClient.(*fakeroclient.Clientset) - fakeClient.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { - if patchAction, ok := action.(kubetesting.PatchAction); ok { - if string(patchAction.GetPatch()) == unpausePatch { - ro.Status.PauseConditions = nil - ro.Spec.Paused = false - } - } - return true, &ro, nil - }) - - cmd := NewCmdResume(o) - cmd.PersistentPreRunE = o.PersistentPreRunE - cmd.SetArgs([]string{"guestbook"}) - err := cmd.Execute() - assert.Nil(t, err) - - assert.Nil(t, ro.Status.PauseConditions) - assert.False(t, ro.Spec.Paused) - assert.True(t, ro.Status.ControllerPause) - stdout := o.Out.(*bytes.Buffer).String() - stderr := o.ErrOut.(*bytes.Buffer).String() - assert.Equal(t, stdout, "rollout 'guestbook' resumed\n") - assert.Empty(t, stderr) -} - -func TestResumeCmdError(t *testing.T) { - tf, o := options.NewFakeArgoRolloutsOptions(&v1alpha1.Rollout{}) - defer tf.Cleanup() - cmd := NewCmdResume(o) - cmd.PersistentPreRunE = o.PersistentPreRunE - cmd.SetArgs([]string{"doesnotexist"}) - 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: rollouts.argoproj.io \"doesnotexist\" not found\n", stderr) -}