github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/appconfig/appconfig_controller.go (about)

     1  // Copyright (c) 2021, 2022, Oracle and/or its affiliates.
     2  // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
     3  
     4  package appconfig
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"time"
    10  
    11  	oamv1 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
    12  	"github.com/verrazzano/verrazzano/application-operator/controllers/clusters"
    13  	vznav "github.com/verrazzano/verrazzano/application-operator/controllers/navigation"
    14  	"github.com/verrazzano/verrazzano/application-operator/metricsexporter"
    15  	"github.com/verrazzano/verrazzano/pkg/constants"
    16  	vzctrl "github.com/verrazzano/verrazzano/pkg/controller"
    17  	vzlog "github.com/verrazzano/verrazzano/pkg/log"
    18  	vzlog2 "github.com/verrazzano/verrazzano/pkg/log/vzlog"
    19  	vzstring "github.com/verrazzano/verrazzano/pkg/string"
    20  	"go.uber.org/zap"
    21  	appsv1 "k8s.io/api/apps/v1"
    22  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    23  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	ctrl "sigs.k8s.io/controller-runtime"
    27  	"sigs.k8s.io/controller-runtime/pkg/client"
    28  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    29  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    30  )
    31  
    32  type Reconciler struct {
    33  	client.Client
    34  	Log    *zap.SugaredLogger
    35  	Scheme *runtime.Scheme
    36  }
    37  
    38  const (
    39  	finalizerName  = "appconfig.finalizers.verrazzano.io"
    40  	controllerName = "appconfig"
    41  )
    42  
    43  // SetupWithManager registers our controller with the manager
    44  func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
    45  	return ctrl.NewControllerManagedBy(mgr).
    46  		For(&oamv1.ApplicationConfiguration{}).
    47  		Complete(r)
    48  }
    49  
    50  // Reconcile checks restart version annotations on an ApplicationConfiguration and
    51  // restarts applications as needed. When applications are restarted, the previous restart
    52  // version annotation value is updated.
    53  func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    54  
    55  	// We do not want any resource to get reconciled if it is in namespace kube-system
    56  	// This is due to a bug found in OKE, it should not affect functionality of any vz operators
    57  	// If this is the case then return success
    58  	counterMetricObject, errorCounterMetricObject, reconcileDurationMetricObject, zapLogForMetrics, err := metricsexporter.ExposeControllerMetrics(controllerName, metricsexporter.AppconfigReconcileCounter, metricsexporter.AppconfigReconcileError, metricsexporter.AppconfigReconcileDuration)
    59  	if err != nil {
    60  		return ctrl.Result{}, err
    61  	}
    62  	reconcileDurationMetricObject.TimerStart()
    63  	defer reconcileDurationMetricObject.TimerStop()
    64  
    65  	if req.Namespace == constants.KubeSystem {
    66  		log := zap.S().With(vzlog.FieldResourceNamespace, req.Namespace, vzlog.FieldResourceName, req.Name, vzlog.FieldController, controllerName)
    67  		log.Infof("Application configuration resource %v should not be reconciled in kube-system namespace, ignoring", req.NamespacedName)
    68  		return reconcile.Result{}, nil
    69  	}
    70  	if ctx == nil {
    71  		ctx = context.Background()
    72  	}
    73  
    74  	var appConfig oamv1.ApplicationConfiguration
    75  	if err := r.Client.Get(ctx, req.NamespacedName, &appConfig); err != nil {
    76  		return clusters.IgnoreNotFoundWithLog(err, zap.S())
    77  	}
    78  	log, err := clusters.GetResourceLogger("applicationconfiguration", req.NamespacedName, &appConfig)
    79  	if err != nil {
    80  		errorCounterMetricObject.Inc(zapLogForMetrics, err)
    81  		log.Errorf("Failed to create controller logger for application configuration resource: %v", err)
    82  		return clusters.NewRequeueWithDelay(), nil
    83  	}
    84  	log.Oncef("Reconciling application configuration resource %v, generation %v", req.NamespacedName, appConfig.Generation)
    85  
    86  	res, err := r.doReconcile(ctx, &appConfig, log)
    87  	if clusters.ShouldRequeue(res) {
    88  		return res, nil
    89  	}
    90  	// Never return an error since it has already been logged and we don't want the
    91  	// controller runtime to log again (with stack trace).  Just re-queue if there is an error.
    92  	if err != nil {
    93  		errorCounterMetricObject.Inc(zapLogForMetrics, err)
    94  		return clusters.NewRequeueWithDelay(), nil
    95  	}
    96  
    97  	// The Verrazzano resource has been reconciled.
    98  	log.Oncef("Finished reconciling application configuration %v", req.NamespacedName)
    99  	counterMetricObject.Inc(zapLogForMetrics, err)
   100  	return ctrl.Result{}, nil
   101  }
   102  
   103  // doReconcile performs the reconciliation operations for the application configuration
   104  func (r *Reconciler) doReconcile(ctx context.Context, appConfig *oamv1.ApplicationConfiguration, log vzlog2.VerrazzanoLogger) (ctrl.Result, error) {
   105  	// the logic to delete cert/secret is moved to the ingress trait finalizer
   106  	// but there could be apps deployed by older version of Verrazzano that are stuck being deleted, with finalizer
   107  	// remove the finalizer
   108  	if isAppConfigBeingDeleted(appConfig) {
   109  		log.Debugf("Deleting application configuration %v", appConfig)
   110  		if err := r.removeFinalizerIfRequired(ctx, appConfig, log); err != nil {
   111  			return vzctrl.NewRequeueWithDelay(2, 3, time.Second), nil
   112  		}
   113  		return reconcile.Result{}, nil
   114  	}
   115  
   116  	// get the user-specified restart version - if it's missing then there's nothing to do here
   117  	restartVersion, ok := appConfig.Annotations[constants.RestartVersionAnnotation]
   118  	if !ok || len(restartVersion) == 0 {
   119  		log.Debug("No restart version annotation found, nothing to do")
   120  		return reconcile.Result{}, nil
   121  	}
   122  
   123  	// restart all workloads in the appconfig
   124  	log.Debugf("Setting restart version %s for workloads in application %s", restartVersion, appConfig.Name)
   125  	for _, wlStatus := range appConfig.Status.Workloads {
   126  		if err := r.restartComponent(ctx, appConfig.Namespace, wlStatus, restartVersion, log); err != nil {
   127  			return vzctrl.NewRequeueWithDelay(2, 3, time.Second), nil
   128  		}
   129  	}
   130  	log.Debug("Successfully reconciled ApplicationConfiguration")
   131  	return reconcile.Result{}, nil
   132  }
   133  
   134  // removeFinalizerIfRequired removes the finalizer from the application configuration if required
   135  // The finalizer is only removed if the application configuration is being deleted and the finalizer had been added
   136  func (r *Reconciler) removeFinalizerIfRequired(ctx context.Context, appConfig *oamv1.ApplicationConfiguration, log vzlog2.VerrazzanoLogger) error {
   137  	if !appConfig.DeletionTimestamp.IsZero() && vzstring.SliceContainsString(appConfig.Finalizers, finalizerName) {
   138  		appName := vznav.GetNamespacedNameFromObjectMeta(appConfig.ObjectMeta)
   139  		log.Debugf("Removing finalizer from application configuration %s", appName)
   140  		appConfig.Finalizers = vzstring.RemoveStringFromSlice(appConfig.Finalizers, finalizerName)
   141  		err := r.Update(ctx, appConfig)
   142  		return vzlog.ConflictWithLog(fmt.Sprintf("Failed to remove finalizer from application configuration %s", appName), err, zap.S())
   143  	}
   144  	return nil
   145  }
   146  
   147  func (r *Reconciler) restartComponent(ctx context.Context, wlNamespace string, wlStatus oamv1.WorkloadStatus, restartVersion string, log vzlog2.VerrazzanoLogger) error {
   148  	// Get the workload as an unstructured object
   149  	var wlName = wlStatus.Reference.Name
   150  	var workload unstructured.Unstructured
   151  	workload.SetAPIVersion(wlStatus.Reference.APIVersion)
   152  	workload.SetKind(wlStatus.Reference.Kind)
   153  	err := r.Client.Get(ctx, types.NamespacedName{Name: wlName, Namespace: wlNamespace}, &workload)
   154  	if err != nil {
   155  		log.Errorf("Failed getting workload component %s in namespace %s with restart-version %s: %v", wlName, wlNamespace, restartVersion, err)
   156  		return err
   157  	}
   158  	// Set the annotation based on the workload kind
   159  	switch workload.GetKind() {
   160  	case constants.VerrazzanoCoherenceWorkloadKind:
   161  		log.Debugf("Setting Coherence workload %s restart-version", wlName)
   162  		return updateRestartVersion(ctx, r.Client, &workload, restartVersion, log)
   163  	case constants.VerrazzanoWebLogicWorkloadKind:
   164  		log.Debugf("Setting WebLogic workload %s restart-version", wlName)
   165  		return updateRestartVersion(ctx, r.Client, &workload, restartVersion, log)
   166  	case constants.VerrazzanoHelidonWorkloadKind:
   167  		log.Debugf("Setting Helidon workload %s restart-version", wlName)
   168  		return updateRestartVersion(ctx, r.Client, &workload, restartVersion, log)
   169  	case constants.ContainerizedWorkloadKind:
   170  		log.Debugf("Setting Containerized workload %s restart-version", wlName)
   171  		return updateRestartVersion(ctx, r.Client, &workload, restartVersion, log)
   172  	case constants.DeploymentWorkloadKind:
   173  		log.Debugf("Setting Deployment workload %s restart-version", wlName)
   174  		return r.restartDeployment(ctx, restartVersion, wlName, wlNamespace, log)
   175  	case constants.StatefulSetWorkloadKind:
   176  		log.Debugf("Setting StatefulSet workload %s restart-version", wlName)
   177  		return r.restartStatefulSet(ctx, restartVersion, wlName, wlNamespace, log)
   178  	case constants.DaemonSetWorkloadKind:
   179  		log.Debugf("Setting DaemonSet workload %s restart-version", wlName)
   180  		return r.restartDaemonSet(ctx, restartVersion, wlName, wlNamespace, log)
   181  	default:
   182  		log.Debugf("Skip marking restart-version for %s of kind %s in namespace %s", workload.GetName(), workload.GetKind(), wlNamespace)
   183  	}
   184  	return nil
   185  }
   186  
   187  func (r *Reconciler) restartDeployment(ctx context.Context, restartVersion, name, namespace string, log vzlog2.VerrazzanoLogger) error {
   188  	var deployment = appsv1.Deployment{}
   189  	deploymentKey := types.NamespacedName{Name: name, Namespace: namespace}
   190  	if err := r.Get(ctx, deploymentKey, &deployment); err != nil {
   191  		if k8serrors.IsNotFound(err) {
   192  			log.Debugf("Can not find deployment %s in namespace %s", name, namespace)
   193  		} else {
   194  			log.Errorf("Failed to obtain deployment %s in namespace %s: %v", name, namespace, err)
   195  			return err
   196  		}
   197  	}
   198  	log.Debugf("Marking deployment %s in namespace %s with restart-version %s", name, namespace, restartVersion)
   199  	return DoRestartDeployment(ctx, r.Client, restartVersion, &deployment, log)
   200  }
   201  
   202  func (r *Reconciler) restartStatefulSet(ctx context.Context, restartVersion, name, namespace string, log vzlog2.VerrazzanoLogger) error {
   203  	var statefulSet = appsv1.StatefulSet{}
   204  	statefulSetKey := types.NamespacedName{Name: name, Namespace: namespace}
   205  	if err := r.Get(ctx, statefulSetKey, &statefulSet); err != nil {
   206  		if k8serrors.IsNotFound(err) {
   207  			log.Debugf("Can not find statefulSet %s in namespace %s", name, namespace)
   208  		} else {
   209  			log.Errorf("Failed to obtain statefulSet %s in namespace %s: %v", name, namespace, err)
   210  			return err
   211  		}
   212  	}
   213  	log.Debugf("Marking statefulSet %s in namespace %s with restart-version %s", name, namespace, restartVersion)
   214  	return DoRestartStatefulSet(ctx, r.Client, restartVersion, &statefulSet, log)
   215  }
   216  
   217  func (r *Reconciler) restartDaemonSet(ctx context.Context, restartVersion, name, namespace string, log vzlog2.VerrazzanoLogger) error {
   218  	var daemonSet = appsv1.DaemonSet{}
   219  	daemonSetKey := types.NamespacedName{Name: name, Namespace: namespace}
   220  	if err := r.Get(ctx, daemonSetKey, &daemonSet); err != nil {
   221  		if k8serrors.IsNotFound(err) {
   222  			log.Debugf("Can not find daemonSet %s in namespace %s", name, namespace)
   223  		} else {
   224  			log.Errorf("Failed to obtain daemonSet %s in namespace %s: %v", name, namespace, err)
   225  			return err
   226  		}
   227  	}
   228  	log.Debugf("Marking daemonSet %s in namespace %s with restart-version %s", name, namespace, restartVersion)
   229  	return DoRestartDaemonSet(ctx, r.Client, restartVersion, &daemonSet, log)
   230  }
   231  
   232  func DoRestartDeployment(ctx context.Context, client client.Client, restartVersion string, deployment *appsv1.Deployment, log vzlog2.VerrazzanoLogger) error {
   233  	if deployment.Spec.Paused {
   234  		return fmt.Errorf("deployment %s can't be restarted because it is paused", deployment.Name)
   235  	}
   236  	log.Debugf("The deployment %s/%s restart version is set to %s", deployment.Namespace, deployment.Name, restartVersion)
   237  	_, err := controllerutil.CreateOrUpdate(ctx, client, deployment, func() error {
   238  		if len(restartVersion) > 0 {
   239  			if deployment.Spec.Template.ObjectMeta.Annotations == nil {
   240  				deployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
   241  			}
   242  			deployment.Spec.Template.ObjectMeta.Annotations[constants.RestartVersionAnnotation] = restartVersion
   243  		}
   244  		return nil
   245  	})
   246  	return vzlog.ConflictWithLog(fmt.Sprintf("Failed updating deployment %s/%s", deployment.Namespace, deployment.Name), err, zap.S())
   247  }
   248  
   249  func DoRestartStatefulSet(ctx context.Context, client client.Client, restartVersion string, statefulSet *appsv1.StatefulSet, log vzlog2.VerrazzanoLogger) error {
   250  	log.Debugf("The statefulSet %s/%s restart version is set to %s", statefulSet.Namespace, statefulSet.Name, restartVersion)
   251  	_, err := controllerutil.CreateOrUpdate(ctx, client, statefulSet, func() error {
   252  		if len(restartVersion) > 0 {
   253  			if statefulSet.Spec.Template.ObjectMeta.Annotations == nil {
   254  				statefulSet.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
   255  			}
   256  			statefulSet.Spec.Template.ObjectMeta.Annotations[constants.RestartVersionAnnotation] = restartVersion
   257  		}
   258  		return nil
   259  	})
   260  	return vzlog.ConflictWithLog(fmt.Sprintf("Conflict updating statefulSet %s/%s:", statefulSet.Namespace, statefulSet.Name), err, zap.S())
   261  }
   262  
   263  func DoRestartDaemonSet(ctx context.Context, client client.Client, restartVersion string, daemonSet *appsv1.DaemonSet, log vzlog2.VerrazzanoLogger) error {
   264  	log.Debugf("The daemonSet %s/%s restart version is set to %s", daemonSet.Namespace, daemonSet.Name, restartVersion)
   265  	_, err := controllerutil.CreateOrUpdate(ctx, client, daemonSet, func() error {
   266  		if len(restartVersion) > 0 {
   267  			if daemonSet.Spec.Template.ObjectMeta.Annotations == nil {
   268  				daemonSet.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
   269  			}
   270  			daemonSet.Spec.Template.ObjectMeta.Annotations[constants.RestartVersionAnnotation] = restartVersion
   271  		}
   272  		return nil
   273  	})
   274  	return vzlog.ConflictWithLog(fmt.Sprintf("Conflict updating daemonSet %s/%s:", daemonSet.Namespace, daemonSet.Name), err, zap.S())
   275  }
   276  
   277  // Update the workload annotation with the restart version. This will cause the workload to be restarted if the version changed
   278  func updateRestartVersion(ctx context.Context, client client.Client, u *unstructured.Unstructured, restartVersion string, log vzlog2.VerrazzanoLogger) error {
   279  	const metadataField = "metadata"
   280  	var metaAnnotationFields = []string{metadataField, "annotations"}
   281  
   282  	log.Debugf("Setting workload %s restartVersion to %s", u.GetName(), restartVersion)
   283  	_, err := controllerutil.CreateOrUpdate(ctx, client, u, func() error {
   284  		annotations, found, err := unstructured.NestedStringMap(u.Object, metaAnnotationFields...)
   285  		if err != nil {
   286  			log.Errorf("Failed getting NestedStringMap for workload %s: %v", u.GetName(), err)
   287  			return err
   288  		}
   289  		if !found {
   290  			annotations = map[string]string{}
   291  		}
   292  		annotations[constants.RestartVersionAnnotation] = restartVersion
   293  		err = unstructured.SetNestedStringMap(u.Object, annotations, metaAnnotationFields...)
   294  		if err != nil {
   295  			log.Errorf("Failed setting NestedStringMap for workload %s: %v", u.GetName(), err)
   296  			return err
   297  		}
   298  		return nil
   299  	})
   300  	err = vzlog.ConflictWithLog(fmt.Sprintf("Failed to update restart version for workload %s/%s", u.GetNamespace(), u.GetName()), err, zap.S())
   301  	return err
   302  }
   303  
   304  // isAppConfigBeingDeleted determines if the app config is in the process of being deleted.
   305  // This is done checking for a non-nil deletion timestamp.
   306  func isAppConfigBeingDeleted(appConfig *oamv1.ApplicationConfiguration) bool {
   307  	return appConfig != nil && appConfig.GetDeletionTimestamp() != nil
   308  }