github.com/oam-dev/kubevela@v1.9.11/pkg/workflow/operation/operation.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package operation
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  
    24  	"github.com/pkg/errors"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	"k8s.io/client-go/util/retry"
    29  	"sigs.k8s.io/controller-runtime/pkg/client"
    30  
    31  	workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
    32  	wfTypes "github.com/kubevela/workflow/pkg/types"
    33  	wfUtils "github.com/kubevela/workflow/pkg/utils"
    34  
    35  	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    36  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    37  	"github.com/oam-dev/kubevela/pkg/appfile"
    38  	"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/application"
    39  	"github.com/oam-dev/kubevela/pkg/controller/utils"
    40  	"github.com/oam-dev/kubevela/pkg/oam"
    41  	"github.com/oam-dev/kubevela/pkg/resourcetracker"
    42  	"github.com/oam-dev/kubevela/pkg/rollout"
    43  	kubevelaapp "github.com/oam-dev/kubevela/pkg/utils/app"
    44  	errors3 "github.com/oam-dev/kubevela/pkg/utils/errors"
    45  )
    46  
    47  // NewApplicationWorkflowOperator get an workflow operator with k8sClient, ioWriter(optional, useful for cli) and application
    48  func NewApplicationWorkflowOperator(cli client.Client, w io.Writer, app *v1beta1.Application) wfUtils.WorkflowOperator {
    49  	return appWorkflowOperator{
    50  		cli:          cli,
    51  		outputWriter: w,
    52  		application:  app,
    53  	}
    54  }
    55  
    56  // NewApplicationWorkflowStepOperator get an workflow step operator with k8sClient, ioWriter(optional, useful for cli) and application
    57  func NewApplicationWorkflowStepOperator(cli client.Client, w io.Writer, app *v1beta1.Application) wfUtils.WorkflowStepOperator {
    58  	return appWorkflowStepOperator{
    59  		cli:          cli,
    60  		outputWriter: w,
    61  		application:  app,
    62  	}
    63  }
    64  
    65  type appWorkflowOperator struct {
    66  	cli          client.Client
    67  	outputWriter io.Writer
    68  	application  *v1beta1.Application
    69  }
    70  
    71  type appWorkflowStepOperator struct {
    72  	cli          client.Client
    73  	outputWriter io.Writer
    74  	application  *v1beta1.Application
    75  }
    76  
    77  // Suspend a running workflow
    78  func (wo appWorkflowOperator) Suspend(ctx context.Context) error {
    79  	app := wo.application
    80  	if app.Status.Workflow == nil {
    81  		return fmt.Errorf("the workflow in application is not running")
    82  	}
    83  	var err error
    84  	if err = rollout.SuspendRollout(ctx, wo.cli, app, wo.outputWriter); err != nil {
    85  		return err
    86  	}
    87  	if err := SuspendWorkflow(ctx, wo.cli, app, ""); err != nil {
    88  		return err
    89  	}
    90  	return writeOutputF(wo.outputWriter, "Successfully suspend workflow: %s\n", app.Name)
    91  }
    92  
    93  // Suspend a suspending workflow
    94  func (wo appWorkflowStepOperator) Suspend(ctx context.Context, step string) error {
    95  	if step == "" {
    96  		return fmt.Errorf("step can not be empty")
    97  	}
    98  	app := wo.application
    99  	if app.Status.Workflow == nil {
   100  		return fmt.Errorf("the workflow in application is not running")
   101  	}
   102  	if app.Status.Workflow.Terminated {
   103  		return fmt.Errorf("can not suspend a terminated workflow")
   104  	}
   105  
   106  	if err := SuspendWorkflow(ctx, wo.cli, app, step); err != nil {
   107  		return err
   108  	}
   109  	return writeOutputF(wo.outputWriter, "Successfully suspend workflow %s from step %s \n", app.Name, step)
   110  }
   111  
   112  // SuspendWorkflow suspend workflow
   113  func SuspendWorkflow(ctx context.Context, kubecli client.Client, app *v1beta1.Application, stepName string) error {
   114  	app.Status.Workflow.Suspend = true
   115  	steps := app.Status.Workflow.Steps
   116  	found := stepName == ""
   117  
   118  	for i, step := range steps {
   119  		for j, sub := range step.SubStepsStatus {
   120  			if sub.Phase != workflowv1alpha1.WorkflowStepPhaseRunning {
   121  				continue
   122  			}
   123  			if stepName == "" {
   124  				wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseSuspending)
   125  			} else if stepName == sub.Name {
   126  				wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseSuspending)
   127  				found = true
   128  				break
   129  			}
   130  		}
   131  		if step.Phase != workflowv1alpha1.WorkflowStepPhaseRunning {
   132  			continue
   133  		}
   134  		if stepName == "" {
   135  			wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseSuspending)
   136  		} else if stepName == step.Name {
   137  			wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseSuspending)
   138  			found = true
   139  			break
   140  		}
   141  	}
   142  	if !found {
   143  		return fmt.Errorf("can not find step %s", stepName)
   144  	}
   145  	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   146  		return kubecli.Status().Patch(ctx, app, client.Merge)
   147  	}); err != nil {
   148  		return err
   149  	}
   150  	return nil
   151  }
   152  
   153  // Resume a suspending workflow
   154  func (wo appWorkflowOperator) Resume(ctx context.Context) error {
   155  	app := wo.application
   156  	if app.Status.Workflow == nil {
   157  		return fmt.Errorf("the workflow in application is not running")
   158  	}
   159  	if app.Status.Workflow.Terminated {
   160  		return fmt.Errorf("can not resume a terminated workflow")
   161  	}
   162  
   163  	var rolloutResumed bool
   164  	var err error
   165  
   166  	if rolloutResumed, err = rollout.ResumeRollout(ctx, wo.cli, app, wo.outputWriter); err != nil {
   167  		return err
   168  	}
   169  	if !rolloutResumed && !app.Status.Workflow.Suspend {
   170  		return writeOutputF(wo.outputWriter, "workflow %s is not suspended.\n", app.Name)
   171  	}
   172  
   173  	if app.Status.Workflow.Suspend {
   174  		if err = ResumeWorkflow(ctx, wo.cli, app, ""); err != nil {
   175  			return err
   176  		}
   177  	}
   178  	return writeOutputF(wo.outputWriter, "Successfully resume workflow: %s\n", app.Name)
   179  }
   180  
   181  // Resume a suspending workflow
   182  func (wo appWorkflowStepOperator) Resume(ctx context.Context, step string) error {
   183  	if step == "" {
   184  		return fmt.Errorf("step can not be empty")
   185  	}
   186  	app := wo.application
   187  	if app.Status.Workflow == nil {
   188  		return fmt.Errorf("the workflow in application is not running")
   189  	}
   190  	if app.Status.Workflow.Terminated {
   191  		return fmt.Errorf("can not resume a terminated workflow")
   192  	}
   193  
   194  	if !app.Status.Workflow.Suspend {
   195  		return writeOutputF(wo.outputWriter, "workflow %s is not suspended.\n", app.Name)
   196  	}
   197  
   198  	if app.Status.Workflow.Suspend {
   199  		if err := ResumeWorkflow(ctx, wo.cli, app, step); err != nil {
   200  			return err
   201  		}
   202  	}
   203  	return writeOutputF(wo.outputWriter, "Successfully resume workflow %s from step %s \n", app.Name, step)
   204  }
   205  
   206  // ResumeWorkflow resume workflow
   207  func ResumeWorkflow(ctx context.Context, kubecli client.Client, app *v1beta1.Application, stepName string) error {
   208  	app.Status.Workflow.Suspend = false
   209  	steps := app.Status.Workflow.Steps
   210  	found := stepName == ""
   211  
   212  	for i, step := range steps {
   213  		for j, sub := range step.SubStepsStatus {
   214  			if sub.Phase != workflowv1alpha1.WorkflowStepPhaseSuspending {
   215  				continue
   216  			}
   217  			if stepName == "" {
   218  				wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseRunning)
   219  			} else if stepName == sub.Name {
   220  				wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseRunning)
   221  				found = true
   222  				break
   223  			}
   224  		}
   225  		if step.Phase != workflowv1alpha1.WorkflowStepPhaseSuspending {
   226  			continue
   227  		}
   228  		if stepName == "" {
   229  			wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseRunning)
   230  		} else if stepName == step.Name {
   231  			wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseRunning)
   232  			found = true
   233  			break
   234  		}
   235  	}
   236  
   237  	if !found {
   238  		return fmt.Errorf("can not find step %s", stepName)
   239  	}
   240  	if err := kubecli.Status().Patch(ctx, app, client.Merge); err != nil {
   241  		return err
   242  	}
   243  	return nil
   244  }
   245  
   246  // Rollback a running in middle state workflow.
   247  // nolint
   248  func (wo appWorkflowOperator) Rollback(ctx context.Context) error {
   249  	app := wo.application
   250  	if app.Status.Workflow != nil && !app.Status.Workflow.Terminated && !app.Status.Workflow.Suspend && !app.Status.Workflow.Finished {
   251  		return fmt.Errorf("can not rollback a running workflow")
   252  	}
   253  	if oam.GetPublishVersion(app) == "" {
   254  		if app.Status.LatestRevision == nil || app.Status.LatestRevision.Name == "" {
   255  			return fmt.Errorf("the latest revision is not set: %s", app.Name)
   256  		}
   257  		// get the last revision
   258  		revision := &v1beta1.ApplicationRevision{}
   259  		if err := wo.cli.Get(ctx, types.NamespacedName{Name: app.Status.LatestRevision.Name, Namespace: app.Namespace}, revision); err != nil {
   260  			return fmt.Errorf("failed to get the latest revision: %w", err)
   261  		}
   262  
   263  		app.Spec = revision.Spec.Application.Spec
   264  		if err := wo.cli.Status().Update(ctx, app); err != nil {
   265  			return err
   266  		}
   267  
   268  		fmt.Printf("Successfully rollback workflow to the latest revision: %s\n", app.Name)
   269  		return nil
   270  	}
   271  
   272  	appRevs, err := application.GetSortedAppRevisions(ctx, wo.cli, app.Name, app.Namespace)
   273  	if err != nil {
   274  		return errors.Wrapf(err, "failed to list revisions for application %s/%s", app.Namespace, app.Name)
   275  	}
   276  
   277  	// find succeeded revision to rollback
   278  	var rev *v1beta1.ApplicationRevision
   279  	var outdatedRev []*v1beta1.ApplicationRevision
   280  	for i := range appRevs {
   281  		candidate := appRevs[len(appRevs)-i-1]
   282  		_rev := candidate.DeepCopy()
   283  		if !candidate.Status.Succeeded || oam.GetPublishVersion(_rev) == "" {
   284  			outdatedRev = append(outdatedRev, _rev)
   285  			continue
   286  		}
   287  		rev = _rev
   288  		break
   289  	}
   290  	if rev == nil {
   291  		return errors.Errorf("failed to find previous succeeded revision for application %s/%s", app.Namespace, app.Name)
   292  	}
   293  	publishVersion := oam.GetPublishVersion(rev)
   294  	revisionNumber, err := utils.ExtractRevision(rev.Name)
   295  	if err != nil {
   296  		return errors.Wrapf(err, "failed to extract revision number from revision %s", rev.Name)
   297  	}
   298  	_, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, wo.cli, app)
   299  	if err != nil {
   300  		return errors.Wrapf(err, "failed to list resource trackers for application %s/%s", app.Namespace, app.Name)
   301  	}
   302  	var matchRT *v1beta1.ResourceTracker
   303  	for _, rt := range append(historyRTs, currentRT) {
   304  		if rt == nil {
   305  			continue
   306  		}
   307  		labels := rt.GetLabels()
   308  		if labels != nil && labels[oam.LabelAppRevision] == rev.Name {
   309  			matchRT = rt.DeepCopy()
   310  		}
   311  	}
   312  	if matchRT == nil {
   313  		return errors.Errorf("cannot find resource tracker for previous revision %s, unable to rollback", rev.Name)
   314  	}
   315  	if matchRT.DeletionTimestamp != nil {
   316  		return errors.Errorf("previous revision %s is being recycled, unable to rollback", rev.Name)
   317  	}
   318  	err = wo.writeOutput("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n")
   319  	if err != nil {
   320  		return err
   321  	}
   322  	appKey := client.ObjectKeyFromObject(app)
   323  	// rollback application spec and freeze
   324  	controllerRequirement, err := kubevelaapp.FreezeApplication(ctx, wo.cli, app, func() {
   325  		app.Spec = rev.Spec.Application.Spec
   326  		oam.SetPublishVersion(app, publishVersion)
   327  	})
   328  	if err != nil {
   329  		return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
   330  	}
   331  	err = writeOutputF(wo.outputWriter, "Application spec rollback successfully.\n")
   332  	if err != nil {
   333  		return err
   334  	}
   335  	// rollback application status
   336  	if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   337  		if err = wo.cli.Get(ctx, appKey, app); err != nil {
   338  			return err
   339  		}
   340  		app.Status.Workflow = rev.Status.Workflow
   341  		app.Status.Services = []common.ApplicationComponentStatus{}
   342  		app.Status.AppliedResources = []common.ClusterObjectReference{}
   343  		for _, rsc := range matchRT.Spec.ManagedResources {
   344  			app.Status.AppliedResources = append(app.Status.AppliedResources, rsc.ClusterObjectReference)
   345  		}
   346  		app.Status.LatestRevision = &common.Revision{
   347  			Name:         rev.Name,
   348  			Revision:     int64(revisionNumber),
   349  			RevisionHash: rev.GetLabels()[oam.LabelAppRevisionHash],
   350  		}
   351  		return wo.cli.Status().Update(ctx, app)
   352  	}); err != nil {
   353  		return errors.Wrapf(err, "failed to rollback application status to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
   354  	}
   355  
   356  	err = writeOutputF(wo.outputWriter, "Application status rollback successfully.\n")
   357  	if err != nil {
   358  		return err
   359  	}
   360  	// update resource tracker generation
   361  	matchRTKey := client.ObjectKeyFromObject(matchRT)
   362  	if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   363  		if err = wo.cli.Get(ctx, matchRTKey, matchRT); err != nil {
   364  			return err
   365  		}
   366  		matchRT.Spec.ApplicationGeneration = app.Generation
   367  		return wo.cli.Update(ctx, matchRT)
   368  	}); err != nil {
   369  		return errors.Wrapf(err, "failed to update application generation in resource tracker")
   370  	}
   371  
   372  	// unfreeze application
   373  	if err = kubevelaapp.UnfreezeApplication(ctx, wo.cli, app, nil, controllerRequirement); err != nil {
   374  		return errors.Wrapf(err, "failed to resume application to restart")
   375  	}
   376  
   377  	rollback, err := rollout.RollbackRollout(ctx, wo.cli, app, wo.outputWriter)
   378  	if err != nil {
   379  		return err
   380  	}
   381  
   382  	if rollback {
   383  		err = writeOutputF(wo.outputWriter, "Successfully rollback app.\n")
   384  		if err != nil {
   385  			return err
   386  		}
   387  	}
   388  
   389  	// clean up outdated revisions
   390  	var errs errors3.ErrorList
   391  	for _, _rev := range outdatedRev {
   392  		if err = wo.cli.Delete(ctx, _rev); err != nil {
   393  			errs = append(errs, err)
   394  		}
   395  	}
   396  	if errs.HasError() {
   397  		return errors.Wrapf(errs, "failed to clean up outdated revisions")
   398  	}
   399  
   400  	err = writeOutputF(wo.outputWriter, "Application outdated revision cleaned up.\n")
   401  	if err != nil {
   402  		return err
   403  	}
   404  	return nil
   405  }
   406  
   407  // Restart a terminated or finished workflow.
   408  func (wo appWorkflowOperator) Restart(ctx context.Context) error {
   409  	app := wo.application
   410  	status := app.Status.Workflow
   411  	if status == nil {
   412  		return fmt.Errorf("the workflow in application is not running")
   413  	}
   414  	// reset the workflow status to restart the workflow
   415  	app.Status.Workflow = nil
   416  
   417  	if err := wo.cli.Status().Update(ctx, app); err != nil {
   418  		return err
   419  	}
   420  
   421  	return writeOutputF(wo.outputWriter, "Successfully restart workflow: %s\n", app.Name)
   422  }
   423  
   424  // Restart a terminated or finished workflow.
   425  func (wo appWorkflowStepOperator) Restart(ctx context.Context, step string) error {
   426  	if step == "" {
   427  		return fmt.Errorf("step can not be empty")
   428  	}
   429  	app := wo.application
   430  	status := app.Status.Workflow
   431  	if status == nil {
   432  		return fmt.Errorf("the workflow in application is not running")
   433  	}
   434  	status.Terminated = false
   435  	status.Suspend = false
   436  	status.Finished = false
   437  	if !status.EndTime.IsZero() {
   438  		status.EndTime = metav1.Time{}
   439  	}
   440  	var cm *corev1.ConfigMap
   441  	if status.ContextBackend != nil {
   442  		if err := wo.cli.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: status.ContextBackend.Name}, cm); err != nil {
   443  			return err
   444  		}
   445  	}
   446  	appParser := appfile.NewApplicationParser(wo.cli, nil)
   447  	appFile, err := appParser.GenerateAppFile(ctx, app)
   448  	if err != nil {
   449  		return fmt.Errorf("failed to parse appfile: %w", err)
   450  	}
   451  	stepStatus, cm, err := wfUtils.CleanStatusFromStep(appFile.WorkflowSteps, status.Steps, *appFile.WorkflowMode, cm, step)
   452  	if err != nil {
   453  		return err
   454  	}
   455  	status.Steps = stepStatus
   456  	if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   457  		return wo.cli.Status().Update(ctx, app)
   458  	}); err != nil {
   459  		return err
   460  	}
   461  	if cm != nil {
   462  		if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   463  			return wo.cli.Update(ctx, cm)
   464  		}); err != nil {
   465  			return err
   466  		}
   467  	}
   468  	return writeOutputF(wo.outputWriter, "Successfully restart workflow %s from step %s\n", app.Name, step)
   469  }
   470  
   471  func (wo appWorkflowOperator) Terminate(ctx context.Context) error {
   472  	app := wo.application
   473  	if err := TerminateWorkflow(ctx, wo.cli, app); err != nil {
   474  		return err
   475  	}
   476  
   477  	return writeOutputF(wo.outputWriter, "Successfully terminate workflow: %s\n", app.Name)
   478  }
   479  
   480  // TerminateWorkflow terminate workflow
   481  func TerminateWorkflow(ctx context.Context, kubecli client.Client, app *v1beta1.Application) error {
   482  	// set the workflow terminated to true
   483  	app.Status.Workflow.Terminated = true
   484  	// set the workflow suspend to false
   485  	app.Status.Workflow.Suspend = false
   486  	steps := app.Status.Workflow.Steps
   487  	for i, step := range steps {
   488  		switch step.Phase {
   489  		case workflowv1alpha1.WorkflowStepPhaseFailed:
   490  			if step.Reason != wfTypes.StatusReasonFailedAfterRetries && step.Reason != wfTypes.StatusReasonTimeout {
   491  				steps[i].Reason = wfTypes.StatusReasonTerminate
   492  			}
   493  		case workflowv1alpha1.WorkflowStepPhaseRunning, workflowv1alpha1.WorkflowStepPhaseSuspending:
   494  			steps[i].Phase = workflowv1alpha1.WorkflowStepPhaseFailed
   495  			steps[i].Reason = wfTypes.StatusReasonTerminate
   496  		default:
   497  		}
   498  		for j, sub := range step.SubStepsStatus {
   499  			switch sub.Phase {
   500  			case workflowv1alpha1.WorkflowStepPhaseFailed:
   501  				if sub.Reason != wfTypes.StatusReasonFailedAfterRetries && sub.Reason != wfTypes.StatusReasonTimeout {
   502  					steps[i].SubStepsStatus[j].Reason = wfTypes.StatusReasonTerminate
   503  				}
   504  			case workflowv1alpha1.WorkflowStepPhaseRunning, workflowv1alpha1.WorkflowStepPhaseSuspending:
   505  				steps[i].SubStepsStatus[j].Phase = workflowv1alpha1.WorkflowStepPhaseFailed
   506  				steps[i].SubStepsStatus[j].Reason = wfTypes.StatusReasonTerminate
   507  			default:
   508  			}
   509  		}
   510  	}
   511  
   512  	if err := kubecli.Status().Patch(ctx, app, client.Merge); err != nil {
   513  		return err
   514  	}
   515  	return nil
   516  }
   517  
   518  func (wo appWorkflowOperator) writeOutput(str string) error {
   519  	if wo.outputWriter == nil {
   520  		return nil
   521  	}
   522  	_, err := wo.outputWriter.Write([]byte(str))
   523  	return err
   524  }
   525  
   526  func writeOutputF(outputWriter io.Writer, format string, a ...interface{}) error {
   527  	if outputWriter == nil {
   528  		return nil
   529  	}
   530  	_, err := fmt.Fprintf(outputWriter, format, a...)
   531  	return err
   532  }