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  }