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 }