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 }