github.com/kubevela/workflow@v0.6.0/controllers/backup_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 "errors" 22 "reflect" 23 "sort" 24 25 kerrors "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 ctrl "sigs.k8s.io/controller-runtime" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 "sigs.k8s.io/controller-runtime/pkg/controller" 31 ctrlEvent "sigs.k8s.io/controller-runtime/pkg/event" 32 "sigs.k8s.io/controller-runtime/pkg/predicate" 33 34 monitorContext "github.com/kubevela/pkg/monitor/context" 35 36 "github.com/kubevela/workflow/api/v1alpha1" 37 "github.com/kubevela/workflow/pkg/backup" 38 "github.com/kubevela/workflow/pkg/types" 39 ) 40 41 // BackupReconciler reconciles a WorkflowRun object 42 type BackupReconciler struct { 43 client.Client 44 Scheme *runtime.Scheme 45 ControllerVersion string 46 BackupArgs 47 Args 48 } 49 50 // BackupArgs is the args for backup 51 type BackupArgs struct { 52 Persister backup.PersistWorkflowRecord 53 BackupStrategy string 54 IgnoreStrategy string 55 GroupByLabel string 56 CleanOnBackup bool 57 } 58 59 const ( 60 // StrategyIgnoreLatestFailed is the backup strategy to ignore the latest failed workflowrun 61 StrategyIgnoreLatestFailed string = "IgnoreLatestFailedRecord" 62 // StrategyBackupFinishedRecord is the backup strategy to backup all finished workflowrun 63 StrategyBackupFinishedRecord string = "BackupFinishedRecord" 64 ) 65 66 // Reconcile reconciles the WorkflowRun object 67 // +kubebuilder:rbac:groups=core.oam.dev,resources=workflowruns,verbs=get;list;watch;create;update;patch;delete 68 // +kubebuilder:rbac:groups=core.oam.dev,resources=workflowruns/status,verbs=get;update;patch 69 // +kubebuilder:rbac:groups=core.oam.dev,resources=workflowruns/finalizers,verbs=update 70 func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 71 ctx, cancel := context.WithTimeout(ctx, ReconcileTimeout) 72 defer cancel() 73 74 ctx = types.SetNamespaceInCtx(ctx, req.Namespace) 75 76 logCtx := monitorContext.NewTraceContext(ctx, "").AddTag("workflowrun", req.String()) 77 logCtx.Info("Start backup workflow record") 78 defer logCtx.Commit("End backup workflow record") 79 run := new(v1alpha1.WorkflowRun) 80 if err := r.Get(ctx, client.ObjectKey{ 81 Name: req.Name, 82 Namespace: req.Namespace, 83 }, run); err != nil { 84 if !kerrors.IsNotFound(err) { 85 logCtx.Error(err, "get workflowrun") 86 return ctrl.Result{}, err 87 } 88 return ctrl.Result{}, client.IgnoreNotFound(err) 89 } 90 91 if !r.matchControllerRequirement(run) { 92 logCtx.Info("skip workflowrun: not match the controller requirement of workflowrun") 93 return ctrl.Result{}, nil 94 } 95 96 if !run.Status.Finished { 97 logCtx.Info("WorkflowRun is not finished, skip reconcile") 98 return ctrl.Result{}, nil 99 } 100 101 switch r.BackupStrategy { 102 case StrategyBackupFinishedRecord: 103 if r.IgnoreStrategy == StrategyIgnoreLatestFailed { 104 latest, failedList, err := isLatestFailedRecord(ctx, r.Client, run, r.GroupByLabel) 105 if err != nil { 106 logCtx.Error(err, "failed to list workflowrun record") 107 return ctrl.Result{}, err 108 } 109 if !latest { 110 if err := r.backup(logCtx, r.Client, run); err != nil { 111 logCtx.Error(err, "failed to backup workflowrun", "workflowrun", run.Name) 112 return ctrl.Result{}, err 113 } 114 } 115 if len(failedList) > 1 { 116 for _, item := range failedList[1:] { 117 if err := r.backup(logCtx, r.Client, &item); err != nil { 118 logCtx.Error(err, "failed to backup workflowrun", "workflowrun", run.Name) 119 return ctrl.Result{}, err 120 } 121 } 122 } 123 break 124 } 125 if err := r.backup(logCtx, r.Client, run); err != nil { 126 logCtx.Error(err, "failed to backup workflowrun", "workflowrun", run.Name) 127 return ctrl.Result{}, err 128 } 129 default: 130 err := errors.New("unknown backup strategy") 131 logCtx.Error(err, "invalid strategy", "strategy", r.BackupStrategy) 132 return ctrl.Result{}, err 133 } 134 135 return ctrl.Result{}, nil 136 } 137 138 func (r *BackupReconciler) backup(ctx monitorContext.Context, cli client.Client, run *v1alpha1.WorkflowRun) error { 139 if r.Persister != nil { 140 if err := r.Persister.Store(ctx, run); err != nil { 141 return err 142 } 143 } 144 if r.CleanOnBackup { 145 if err := cli.Delete(ctx, run); err != nil && !kerrors.IsNotFound(err) { 146 return err 147 } 148 } 149 ctx.Info("Successfully backup workflowrun", "workflowrun", run.Name) 150 return nil 151 } 152 153 func (r *BackupReconciler) matchControllerRequirement(wr *v1alpha1.WorkflowRun) bool { 154 if wr.Annotations != nil { 155 if requireVersion, ok := wr.Annotations[types.AnnotationControllerRequirement]; ok { 156 return requireVersion == r.ControllerVersion 157 } 158 } 159 if r.IgnoreWorkflowWithoutControllerRequirement { 160 return false 161 } 162 return true 163 } 164 165 // SetupWithManager sets up the controller with the Manager. 166 func (r *BackupReconciler) SetupWithManager(mgr ctrl.Manager) error { 167 return ctrl.NewControllerManagedBy(mgr). 168 WithOptions(controller.Options{ 169 MaxConcurrentReconciles: r.ConcurrentReconciles, 170 }). 171 WithEventFilter(predicate.Funcs{ 172 // filter the changes in workflow status 173 // let workflow handle its reconcile 174 UpdateFunc: func(e ctrlEvent.UpdateEvent) bool { 175 new := e.ObjectNew.DeepCopyObject().(*v1alpha1.WorkflowRun) 176 old := e.ObjectOld.DeepCopyObject().(*v1alpha1.WorkflowRun) 177 // if the workflow is not finished, skip the reconcile 178 if !new.Status.Finished { 179 return false 180 } 181 182 return !reflect.DeepEqual(old, new) 183 }, 184 CreateFunc: func(e ctrlEvent.CreateEvent) bool { 185 run := e.Object.DeepCopyObject().(*v1alpha1.WorkflowRun) 186 return run.Status.Finished 187 }, 188 }). 189 For(&v1alpha1.WorkflowRun{}). 190 Complete(r) 191 } 192 193 func isLatestFailedRecord(ctx context.Context, cli client.Client, run *v1alpha1.WorkflowRun, groupByLabel string) (bool, []v1alpha1.WorkflowRun, error) { 194 if run.Status.Phase != v1alpha1.WorkflowStateFailed { 195 return false, nil, nil 196 } 197 runs := &v1alpha1.WorkflowRunList{} 198 listOpt := &client.ListOptions{} 199 if groupByLabel != "" && run.Labels != nil && run.Labels[groupByLabel] != "" { 200 labels := &metav1.LabelSelector{ 201 MatchLabels: map[string]string{ 202 groupByLabel: run.Labels[groupByLabel], 203 }, 204 } 205 selector, err := metav1.LabelSelectorAsSelector(labels) 206 if err != nil { 207 return false, nil, err 208 } 209 listOpt = &client.ListOptions{LabelSelector: selector} 210 } 211 if err := cli.List(ctx, runs, listOpt); err != nil { 212 return false, nil, err 213 } 214 sort.Sort(runs) 215 failedRecord := make([]v1alpha1.WorkflowRun, 0) 216 for _, item := range runs.Items { 217 if item.Status.Phase == v1alpha1.WorkflowStateFailed { 218 failedRecord = append(failedRecord, item) 219 } 220 } 221 if len(failedRecord) > 0 { 222 latest := failedRecord[0] 223 return latest.Name == run.Name && latest.Namespace == run.Namespace, failedRecord, nil 224 } 225 return false, nil, nil 226 }