diff --git a/references/cli/workflow.go b/references/cli/workflow.go index ee67df8a5f8..c047e046177 100644 --- a/references/cli/workflow.go +++ b/references/cli/workflow.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/spf13/cobra" + k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" @@ -45,6 +46,7 @@ func NewWorkflowCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Comma NewWorkflowResumeCommand(c, ioStreams), NewWorkflowTerminateCommand(c, ioStreams), NewWorkflowRestartCommand(c, ioStreams), + NewWorkflowRollbackCommand(c, ioStreams), ) return cmd } @@ -223,6 +225,47 @@ func NewWorkflowRestartCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra return cmd } +// NewWorkflowRollbackCommand create workflow rollback command +func NewWorkflowRollbackCommand(c common.Args, ioStream cmdutil.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "rollback", + Short: "Rollback an application workflow to the latest revision", + Long: "Rollback an application workflow to the latest revision", + Example: "vela workflow rollback ", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("must specify application name") + } + namespace, err := GetFlagNamespaceOrEnv(cmd, c) + if err != nil { + return err + } + app, err := appfile.LoadApplication(namespace, args[0], c) + if err != nil { + return err + } + if app.Spec.Workflow == nil { + return fmt.Errorf("the application must have workflow") + } + if app.Status.Workflow != nil && !app.Status.Workflow.Terminated && !app.Status.Workflow.Suspend && !app.Status.Workflow.Finished { + return fmt.Errorf("can not rollback a running workflow") + } + kubecli, err := c.GetClient() + if err != nil { + return err + } + + err = rollbackWorkflow(kubecli, app) + if err != nil { + return err + } + return nil + }, + } + addNamespaceArg(cmd) + return cmd +} + func suspendWorkflow(kubecli client.Client, app *v1beta1.Application) error { // set the workflow suspend to true app.Status.Workflow.Suspend = true @@ -270,3 +313,22 @@ func restartWorkflow(kubecli client.Client, app *v1beta1.Application) error { fmt.Printf("Successfully restart workflow: %s\n", app.Name) return nil } + +func rollbackWorkflow(kubecli client.Client, app *v1beta1.Application) error { + if app.Status.LatestRevision == nil || app.Status.LatestRevision.Name == "" { + return fmt.Errorf("the latest revision is not set: %s", app.Name) + } + // get the last revision + revision := &v1beta1.ApplicationRevision{} + if err := kubecli.Get(context.TODO(), k8stypes.NamespacedName{Name: app.Status.LatestRevision.Name, Namespace: app.Namespace}, revision); err != nil { + return fmt.Errorf("failed to get the latest revision: %w", err) + } + + app.Spec = revision.Spec.Application.Spec + if err := kubecli.Status().Update(context.TODO(), app); err != nil { + return err + } + + fmt.Printf("Successfully rollback workflow to the latest revision: %s\n", app.Name) + return nil +} diff --git a/references/cli/workflow_test.go b/references/cli/workflow_test.go index 4d8edd28019..6624c81a107 100644 --- a/references/cli/workflow_test.go +++ b/references/cli/workflow_test.go @@ -429,3 +429,137 @@ func TestWorkflowRestart(t *testing.T) { }) } } + +func TestWorkflowRollback(t *testing.T) { + c := initArgs() + ioStream := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} + ctx := context.TODO() + + testCases := map[string]struct { + app *v1beta1.Application + revision *v1beta1.ApplicationRevision + expectedErr error + }{ + "no app name specified": { + expectedErr: fmt.Errorf("must specify application name"), + }, + "no workflow in app": { + app: &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-workflow", + Namespace: "default", + }, + }, + expectedErr: fmt.Errorf("the application must have workflow"), + }, + "workflow running": { + app: &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "workflow-not-running", + Namespace: "default", + }, + Spec: workflowSpec, + Status: common.AppStatus{ + Workflow: &common.WorkflowStatus{ + Suspend: false, + Terminated: false, + Finished: false, + }, + }, + }, + expectedErr: fmt.Errorf("can not rollback a running workflow"), + }, + "invalid revision": { + app: &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-revision", + Namespace: "default", + }, + Spec: workflowSpec, + Status: common.AppStatus{ + Workflow: &common.WorkflowStatus{ + Suspend: true, + }, + }, + }, + expectedErr: fmt.Errorf("the latest revision is not set: invalid-revision"), + }, + "rollback successfully": { + app: &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "workflow", + Namespace: "test", + }, + Spec: workflowSpec, + Status: common.AppStatus{ + LatestRevision: &common.Revision{ + Name: "revision-v1", + }, + Workflow: &common.WorkflowStatus{ + Terminated: true, + }, + }, + }, + revision: &v1beta1.ApplicationRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "revision-v1", + Namespace: "test", + }, + Spec: v1beta1.ApplicationRevisionSpec{ + Application: v1beta1.Application{ + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "revision-component", + Type: "worker", + Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)}, + }}, + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + r := require.New(t) + cmd := NewWorkflowRollbackCommand(c, ioStream) + initCommand(cmd) + + if tc.app != nil { + err := c.Client.Create(ctx, tc.app) + r.NoError(err) + + if tc.app.Namespace != corev1.NamespaceDefault { + err := c.Client.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.app.Namespace, + }, + }) + r.NoError(err) + cmd.SetArgs([]string{tc.app.Name, "-n", tc.app.Namespace}) + } else { + cmd.SetArgs([]string{tc.app.Name}) + } + } + if tc.revision != nil { + err := c.Client.Create(ctx, tc.revision) + r.NoError(err) + } + err := cmd.Execute() + if tc.expectedErr != nil { + r.Equal(tc.expectedErr, err) + return + } + r.NoError(err) + + wf := &v1beta1.Application{} + err = c.Client.Get(ctx, types.NamespacedName{ + Namespace: tc.app.Namespace, + Name: tc.app.Name, + }, wf) + r.NoError(err) + r.Equal(wf.Spec.Components[0].Name, "revision-component") + }) + } +}