github.com/kubevela/workflow@v0.6.0/controllers/workflowrun_controller.go (about)

     1  /*
     2  Copyright 2022 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 controllers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  	"time"
    24  
    25  	"github.com/crossplane/crossplane-runtime/pkg/event"
    26  	"github.com/pkg/errors"
    27  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	k8stypes "k8s.io/apimachinery/pkg/types"
    31  	"k8s.io/apiserver/pkg/util/feature"
    32  	ctrl "sigs.k8s.io/controller-runtime"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  	"sigs.k8s.io/controller-runtime/pkg/controller"
    35  	ctrlEvent "sigs.k8s.io/controller-runtime/pkg/event"
    36  	ctrlHandler "sigs.k8s.io/controller-runtime/pkg/handler"
    37  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    38  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    39  	"sigs.k8s.io/controller-runtime/pkg/source"
    40  
    41  	triggerv1alpha1 "github.com/kubevela/kube-trigger/api/v1alpha1"
    42  	monitorContext "github.com/kubevela/pkg/monitor/context"
    43  
    44  	"github.com/kubevela/workflow/api/condition"
    45  	"github.com/kubevela/workflow/api/v1alpha1"
    46  	wfContext "github.com/kubevela/workflow/pkg/context"
    47  	"github.com/kubevela/workflow/pkg/cue/packages"
    48  	"github.com/kubevela/workflow/pkg/executor"
    49  	"github.com/kubevela/workflow/pkg/features"
    50  	"github.com/kubevela/workflow/pkg/generator"
    51  	"github.com/kubevela/workflow/pkg/monitor/metrics"
    52  	"github.com/kubevela/workflow/pkg/types"
    53  )
    54  
    55  // Args args used by controller
    56  type Args struct {
    57  	// ConcurrentReconciles is the concurrent reconcile number of the controller
    58  	ConcurrentReconciles int
    59  	// IgnoreWorkflowWithoutControllerRequirement indicates that workflow controller will not process the workflowrun without 'workflowrun.oam.dev/controller-version-require' annotation.
    60  	IgnoreWorkflowWithoutControllerRequirement bool
    61  	// PackageDiscover discover the packages
    62  	PackageDiscover *packages.PackageDiscover
    63  }
    64  
    65  // WorkflowRunReconciler reconciles a WorkflowRun object
    66  type WorkflowRunReconciler struct {
    67  	client.Client
    68  	Scheme            *runtime.Scheme
    69  	Recorder          event.Recorder
    70  	ControllerVersion string
    71  	Args
    72  }
    73  
    74  type workflowRunPatcher struct {
    75  	client.Client
    76  	run *v1alpha1.WorkflowRun
    77  }
    78  
    79  var (
    80  	// ReconcileTimeout timeout for controller to reconcile
    81  	ReconcileTimeout = time.Minute * 3
    82  )
    83  
    84  // Reconcile reconciles the WorkflowRun object
    85  // +kubebuilder:rbac:groups=core.oam.dev,resources=workflowruns,verbs=get;list;watch;create;update;patch;delete
    86  // +kubebuilder:rbac:groups=core.oam.dev,resources=workflowruns/status,verbs=get;update;patch
    87  // +kubebuilder:rbac:groups=core.oam.dev,resources=workflowruns/finalizers,verbs=update
    88  func (r *WorkflowRunReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    89  	ctx, cancel := context.WithTimeout(ctx, ReconcileTimeout)
    90  	defer cancel()
    91  
    92  	ctx = types.SetNamespaceInCtx(ctx, req.Namespace)
    93  
    94  	logCtx := monitorContext.NewTraceContext(ctx, "").AddTag("workflowrun", req.String())
    95  	logCtx.Info("Start reconcile workflowrun")
    96  	defer logCtx.Commit("End reconcile workflowrun")
    97  	run := new(v1alpha1.WorkflowRun)
    98  	if err := r.Get(ctx, client.ObjectKey{
    99  		Name:      req.Name,
   100  		Namespace: req.Namespace,
   101  	}, run); err != nil {
   102  		if !kerrors.IsNotFound(err) {
   103  			logCtx.Error(err, "get workflowrun")
   104  			return ctrl.Result{}, err
   105  		}
   106  		return ctrl.Result{}, client.IgnoreNotFound(err)
   107  	}
   108  
   109  	if !r.matchControllerRequirement(run) {
   110  		logCtx.Info("skip workflowrun: not match the controller requirement of workflowrun")
   111  		return ctrl.Result{}, nil
   112  	}
   113  
   114  	timeReporter := timeReconcile(run)
   115  	defer timeReporter()
   116  
   117  	if run.Status.Finished {
   118  		logCtx.Info("WorkflowRun is finished, skip reconcile")
   119  		return ctrl.Result{}, nil
   120  	}
   121  
   122  	instance, err := generator.GenerateWorkflowInstance(ctx, r.Client, run)
   123  	if err != nil {
   124  		logCtx.Error(err, "[generate workflow instance]")
   125  		r.Recorder.Event(run, event.Warning(v1alpha1.ReasonGenerate, errors.WithMessage(err, v1alpha1.MessageFailedGenerate)))
   126  		run.Status.Phase = v1alpha1.WorkflowStateInitializing
   127  		return r.endWithNegativeCondition(logCtx, run, condition.ErrorCondition(v1alpha1.WorkflowRunConditionType, err))
   128  	}
   129  	isUpdate := instance.Status.Message != ""
   130  
   131  	runners, err := generator.GenerateRunners(logCtx, instance, types.StepGeneratorOptions{
   132  		PackageDiscover: r.PackageDiscover,
   133  		Client:          r.Client,
   134  	})
   135  	if err != nil {
   136  		logCtx.Error(err, "[generate runners]")
   137  		r.Recorder.Event(run, event.Warning(v1alpha1.ReasonGenerate, errors.WithMessage(err, v1alpha1.MessageFailedGenerate)))
   138  		run.Status.Phase = v1alpha1.WorkflowStateInitializing
   139  		return r.endWithNegativeCondition(logCtx, run, condition.ErrorCondition(v1alpha1.WorkflowRunConditionType, err))
   140  	}
   141  
   142  	patcher := &workflowRunPatcher{
   143  		Client: r.Client,
   144  		run:    run,
   145  	}
   146  	executor := executor.New(instance, r.Client, patcher.patchStatus)
   147  	state, err := executor.ExecuteRunners(logCtx, runners)
   148  	if err != nil {
   149  		logCtx.Error(err, "[execute runners]")
   150  		r.Recorder.Event(run, event.Warning(v1alpha1.ReasonExecute, errors.WithMessage(err, v1alpha1.MessageFailedExecute)))
   151  		run.Status.Phase = v1alpha1.WorkflowStateExecuting
   152  		return r.endWithNegativeCondition(logCtx, run, condition.ErrorCondition(v1alpha1.WorkflowRunConditionType, err))
   153  	}
   154  	isUpdate = isUpdate && instance.Status.Message == ""
   155  	run.Status = instance.Status
   156  	run.Status.Phase = state
   157  	switch state {
   158  	case v1alpha1.WorkflowStateSuspending:
   159  		logCtx.Info("Workflow return state=Suspend")
   160  		if duration := executor.GetSuspendBackoffWaitTime(); duration > 0 {
   161  			return ctrl.Result{RequeueAfter: duration}, patcher.patchStatus(logCtx, &run.Status, isUpdate)
   162  		}
   163  		return ctrl.Result{}, patcher.patchStatus(logCtx, &run.Status, isUpdate)
   164  	case v1alpha1.WorkflowStateFailed:
   165  		logCtx.Info("Workflow return state=Failed")
   166  		r.doWorkflowFinish(run)
   167  		r.Recorder.Event(run, event.Normal(v1alpha1.ReasonExecute, v1alpha1.MessageFailed))
   168  		return ctrl.Result{}, patcher.patchStatus(logCtx, &run.Status, isUpdate)
   169  	case v1alpha1.WorkflowStateTerminated:
   170  		logCtx.Info("Workflow return state=Terminated")
   171  		r.doWorkflowFinish(run)
   172  		r.Recorder.Event(run, event.Normal(v1alpha1.ReasonExecute, v1alpha1.MessageTerminated))
   173  		return ctrl.Result{}, patcher.patchStatus(logCtx, &run.Status, isUpdate)
   174  	case v1alpha1.WorkflowStateExecuting:
   175  		logCtx.Info("Workflow return state=Executing")
   176  		return ctrl.Result{RequeueAfter: executor.GetBackoffWaitTime()}, patcher.patchStatus(logCtx, &run.Status, isUpdate)
   177  	case v1alpha1.WorkflowStateSucceeded:
   178  		logCtx.Info("Workflow return state=Succeeded")
   179  		r.doWorkflowFinish(run)
   180  		run.Status.SetConditions(condition.ReadyCondition(v1alpha1.WorkflowRunConditionType))
   181  		r.Recorder.Event(run, event.Normal(v1alpha1.ReasonExecute, v1alpha1.MessageSuccessfully))
   182  		return ctrl.Result{}, patcher.patchStatus(logCtx, &run.Status, isUpdate)
   183  	case v1alpha1.WorkflowStateSkipped:
   184  		logCtx.Info("Skip this reconcile")
   185  		return ctrl.Result{RequeueAfter: executor.GetBackoffWaitTime()}, nil
   186  	}
   187  
   188  	return ctrl.Result{}, nil
   189  }
   190  
   191  func (r *WorkflowRunReconciler) matchControllerRequirement(wr *v1alpha1.WorkflowRun) bool {
   192  	if wr.Annotations != nil {
   193  		if requireVersion, ok := wr.Annotations[types.AnnotationControllerRequirement]; ok {
   194  			return requireVersion == r.ControllerVersion
   195  		}
   196  	}
   197  	if r.IgnoreWorkflowWithoutControllerRequirement {
   198  		return false
   199  	}
   200  	return true
   201  }
   202  
   203  // SetupWithManager sets up the controller with the Manager.
   204  func (r *WorkflowRunReconciler) SetupWithManager(mgr ctrl.Manager) error {
   205  	builder := ctrl.NewControllerManagedBy(mgr)
   206  	if feature.DefaultMutableFeatureGate.Enabled(features.EnableWatchEventListener) {
   207  		builder = builder.Watches(&source.Kind{
   208  			Type: &triggerv1alpha1.EventListener{},
   209  		}, ctrlHandler.EnqueueRequestsFromMapFunc(findObjectForEventListener))
   210  	}
   211  	return builder.
   212  		WithOptions(controller.Options{
   213  			MaxConcurrentReconciles: r.ConcurrentReconciles,
   214  		}).
   215  		WithEventFilter(predicate.Funcs{
   216  			// filter the changes in workflow status
   217  			// let workflow handle its reconcile
   218  			UpdateFunc: func(e ctrlEvent.UpdateEvent) bool {
   219  				new, isNewWR := e.ObjectNew.DeepCopyObject().(*v1alpha1.WorkflowRun)
   220  				old, isOldWR := e.ObjectOld.DeepCopyObject().(*v1alpha1.WorkflowRun)
   221  
   222  				// if the object is a event listener, reconcile the controller
   223  				if !isNewWR || !isOldWR {
   224  					return true
   225  				}
   226  
   227  				// if the workflow is finished, skip the reconcile
   228  				if new.Status.Finished {
   229  					return false
   230  				}
   231  
   232  				// filter managedFields changes
   233  				old.ManagedFields = nil
   234  				new.ManagedFields = nil
   235  
   236  				// filter resourceVersion changes
   237  				old.ResourceVersion = new.ResourceVersion
   238  
   239  				// if the generation is changed, return true to let the controller handle it
   240  				if old.Generation != new.Generation {
   241  					return true
   242  				}
   243  
   244  				// ignore the changes in step status
   245  				old.Status.Steps = new.Status.Steps
   246  
   247  				return !reflect.DeepEqual(old, new)
   248  			},
   249  			CreateFunc: func(e ctrlEvent.CreateEvent) bool {
   250  				return true
   251  			},
   252  		}).
   253  		For(&v1alpha1.WorkflowRun{}).
   254  		Complete(r)
   255  }
   256  
   257  func (r *WorkflowRunReconciler) endWithNegativeCondition(ctx context.Context, wr *v1alpha1.WorkflowRun, condition condition.Condition) (ctrl.Result, error) {
   258  	wr.SetConditions(condition)
   259  	if err := r.Status().Patch(ctx, wr, client.Merge); err != nil {
   260  		executor.StepStatusCache.Store(fmt.Sprintf("%s-%s", wr.Name, wr.Namespace), -1)
   261  		return ctrl.Result{}, errors.WithMessage(err, "failed to patch workflowrun status")
   262  	}
   263  	return ctrl.Result{}, fmt.Errorf("reconcile WorkflowRun error, msg: %s", condition.Message)
   264  }
   265  
   266  func (r *workflowRunPatcher) patchStatus(ctx context.Context, status *v1alpha1.WorkflowRunStatus, isUpdate bool) error {
   267  	r.run.Status = *status
   268  	wr := r.run
   269  	if isUpdate {
   270  		if err := r.Status().Update(ctx, wr); err != nil {
   271  			executor.StepStatusCache.Store(fmt.Sprintf("%s-%s", wr.Name, wr.Namespace), -1)
   272  			return errors.WithMessage(err, "failed to update workflowrun status")
   273  		}
   274  		return nil
   275  	}
   276  	if err := r.Status().Patch(ctx, wr, client.Merge); err != nil {
   277  		executor.StepStatusCache.Store(fmt.Sprintf("%s-%s", wr.Name, wr.Namespace), -1)
   278  		return errors.WithMessage(err, "failed to patch workflowrun status")
   279  	}
   280  	return nil
   281  }
   282  
   283  func (r *WorkflowRunReconciler) doWorkflowFinish(wr *v1alpha1.WorkflowRun) {
   284  	wr.Status.Finished = true
   285  	wr.Status.EndTime = metav1.Now()
   286  	metrics.WorkflowRunFinishedTimeHistogram.WithLabelValues(string(wr.Status.Phase)).Observe(wr.Status.EndTime.Sub(wr.Status.StartTime.Time).Seconds())
   287  	executor.StepStatusCache.Delete(fmt.Sprintf("%s-%s", wr.Name, wr.Namespace))
   288  	wfContext.CleanupMemoryStore(wr.Name, wr.Namespace)
   289  }
   290  
   291  func timeReconcile(wr *v1alpha1.WorkflowRun) func() {
   292  	t := time.Now()
   293  	beginPhase := string(wr.Status.Phase)
   294  	return func() {
   295  		v := time.Since(t).Seconds()
   296  		metrics.WorkflowRunReconcileTimeHistogram.WithLabelValues(beginPhase, string(wr.Status.Phase)).Observe(v)
   297  	}
   298  }
   299  
   300  func findObjectForEventListener(object client.Object) []reconcile.Request {
   301  	return []reconcile.Request{{
   302  		NamespacedName: k8stypes.NamespacedName{Name: object.GetName(), Namespace: object.GetNamespace()},
   303  	}}
   304  }