github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/helidonworkload/helidonworkload_controller.go (about) 1 // Copyright (c) 2021, 2023, 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 helidonworkload 5 6 import ( 7 "context" 8 "errors" 9 "reflect" 10 "strconv" 11 "strings" 12 13 "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" 14 vzapi "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1" 15 "github.com/verrazzano/verrazzano/application-operator/controllers/appconfig" 16 "github.com/verrazzano/verrazzano/application-operator/controllers/clusters" 17 "github.com/verrazzano/verrazzano/application-operator/controllers/metricstrait" 18 vznav "github.com/verrazzano/verrazzano/application-operator/controllers/navigation" 19 "github.com/verrazzano/verrazzano/application-operator/metricsexporter" 20 vzconst "github.com/verrazzano/verrazzano/pkg/constants" 21 vzlogInit "github.com/verrazzano/verrazzano/pkg/log" 22 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 23 24 "go.uber.org/zap" 25 appsv1 "k8s.io/api/apps/v1" 26 corev1 "k8s.io/api/core/v1" 27 k8serrors "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/labels" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/selection" 32 "k8s.io/apimachinery/pkg/types" 33 "k8s.io/apimachinery/pkg/util/intstr" 34 ctrl "sigs.k8s.io/controller-runtime" 35 "sigs.k8s.io/controller-runtime/pkg/builder" 36 "sigs.k8s.io/controller-runtime/pkg/client" 37 "sigs.k8s.io/controller-runtime/pkg/predicate" 38 "sigs.k8s.io/controller-runtime/pkg/reconcile" 39 "sigs.k8s.io/yaml" 40 ) 41 42 const ( 43 labelKey = "verrazzanohelidonworkloads.oam.verrazzano.io" 44 controllerName = "helidonworkload" 45 ) 46 47 var ( 48 deploymentKind = reflect.TypeOf(appsv1.Deployment{}).Name() 49 deploymentAPIVersion = appsv1.SchemeGroupVersion.String() 50 serviceKind = reflect.TypeOf(corev1.Service{}).Name() 51 serviceAPIVersion = corev1.SchemeGroupVersion.String() 52 ) 53 54 // Reconciler reconciles a VerrazzanoHelidonWorkload object 55 type Reconciler struct { 56 client.Client 57 Log *zap.SugaredLogger 58 Scheme *runtime.Scheme 59 Metrics *metricstrait.Reconciler 60 } 61 62 // SetupWithManager registers our controller with the manager 63 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { 64 return ctrl.NewControllerManagedBy(mgr). 65 For(&vzapi.VerrazzanoHelidonWorkload{}). 66 Owns(&appsv1.Deployment{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). 67 Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). 68 Complete(r) 69 } 70 71 // Reconcile reconciles a VerrazzanoHelidonWorkload resource. It fetches the embedded DeploymentSpec, mutates it to add 72 // scopes and traits, and then writes out the apps/Deployment (or deletes it if the workload is being deleted). 73 func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 74 if ctx == nil { 75 return ctrl.Result{}, errors.New("context cannot be nil") 76 } 77 78 // We do not want any resource to get reconciled if it is in namespace kube-system 79 // This is due to a bug found in OKE, it should not affect functionality of any vz operators 80 // If this is the case then return success 81 counterMetricObject, errorCounterMetricObject, reconcileDurationMetricObject, zapLogForMetrics, err := metricsexporter.ExposeControllerMetrics(controllerName, metricsexporter.HelidonReconcileCounter, metricsexporter.HelidonReconcileError, metricsexporter.HelidonReconcileDuration) 82 if err != nil { 83 return ctrl.Result{}, err 84 } 85 reconcileDurationMetricObject.TimerStart() 86 defer reconcileDurationMetricObject.TimerStop() 87 88 if req.Namespace == vzconst.KubeSystem { 89 log := zap.S().With(vzlogInit.FieldResourceNamespace, req.Namespace, vzlogInit.FieldResourceName, req.Name, vzlogInit.FieldController, controllerName) 90 log.Infof("Helidon workload resource %v should not be reconciled in kube-system namespace, ignoring", req.NamespacedName) 91 return reconcile.Result{}, nil 92 } 93 94 // Fetch the workload 95 var workload vzapi.VerrazzanoHelidonWorkload 96 if err := r.Get(ctx, req.NamespacedName, &workload); err != nil { 97 return clusters.IgnoreNotFoundWithLog(err, zap.S()) 98 } 99 log, err := clusters.GetResourceLogger("verrazzanohelidonworkload", req.NamespacedName, &workload) 100 if err != nil { 101 errorCounterMetricObject.Inc(zapLogForMetrics, err) 102 zap.S().Errorf("Failed to create controller logger for Helidon workload resource: %v", err) 103 return clusters.NewRequeueWithDelay(), nil 104 } 105 log.Oncef("Reconciling Helidon workload resource %v, generation %v", req.NamespacedName, workload.Generation) 106 107 res, err := r.doReconcile(ctx, workload, log) 108 if clusters.ShouldRequeue(res) { 109 return res, nil 110 } 111 // Never return an error since it has already been logged and we don't want the 112 // controller runtime to log again (with stack trace). Just re-queue if there is an error. 113 if err != nil { 114 errorCounterMetricObject.Inc(zapLogForMetrics, err) 115 return clusters.NewRequeueWithDelay(), nil 116 } 117 118 log.Oncef("Finished reconciling Helidon workload %v", req.NamespacedName) 119 counterMetricObject.Inc(zapLogForMetrics, err) 120 return ctrl.Result{}, nil 121 } 122 123 // doReconcile performs the reconciliation operations for the VerrazzanoHelidonWorkload 124 func (r *Reconciler) doReconcile(ctx context.Context, workload vzapi.VerrazzanoHelidonWorkload, log vzlog.VerrazzanoLogger) (ctrl.Result, error) { 125 // If required info is not available in workload, log error and return 126 if len(workload.Spec.DeploymentTemplate.Metadata.GetName()) == 0 { 127 err := errors.New("VerrazzanoHelidonWorkload is missing required spec.deploymentTemplate.metadata.name") 128 log.Errorf("Failed to get workload name: %v", err) 129 return reconcile.Result{Requeue: false}, err 130 } 131 132 // Unwrap the apps/DeploymentSpec and meta/ObjectMeta 133 deploy, err := r.convertWorkloadToDeployment(&workload, log) 134 if err != nil { 135 log.Errorf("Failed to convert workload to deployment: %v", err) 136 return reconcile.Result{}, err 137 } 138 // Attempt to get the existing deployment. This is used in the case where we don't want to update any resources 139 // which are defined by Verrazzano such as the Fluentd image used by logging. In this case we obtain the previous 140 // Fluentd image and set that on the new deployment. We also need to know if the deployment exists 141 // so that when we write out the deployment later, we will call update instead of create if the deployment exists. 142 var existingDeployment appsv1.Deployment 143 deploymentKey := types.NamespacedName{Name: workload.Spec.DeploymentTemplate.Metadata.GetName(), Namespace: workload.Namespace} 144 if err := r.Get(ctx, deploymentKey, &existingDeployment); err != nil { 145 if k8serrors.IsNotFound(err) { 146 log.Debug("No existing deployment found") 147 } else { 148 log.Errorf("Failed trying to obtain an existing deployment: %v", err) 149 return reconcile.Result{}, err 150 } 151 } 152 153 if err = r.addMetrics(ctx, log, workload.Namespace, &workload, deploy); err != nil { 154 return reconcile.Result{}, err 155 } 156 157 // set the controller reference so that we can watch this deployment and it will be deleted automatically 158 if err := ctrl.SetControllerReference(&workload, deploy, r.Scheme); err != nil { 159 return reconcile.Result{}, err 160 } 161 162 // server side apply, only the fields we set are touched 163 applyOpts := []client.PatchOption{client.ForceOwnership, client.FieldOwner(workload.GetUID())} 164 if err := r.Patch(ctx, deploy, client.Apply, applyOpts...); err != nil { 165 log.Errorf("Failed to apply a deployment: %v", err) 166 return reconcile.Result{}, err 167 } 168 169 // create a service for the workload 170 service, err := r.createServiceFromDeployment(&workload, deploy, log) 171 if err != nil { 172 log.Errorf("Failed to get service from a deployment: %v", err) 173 return reconcile.Result{}, err 174 } 175 // set the controller reference so that we can watch this service and it will be deleted automatically 176 if err = ctrl.SetControllerReference(&workload, service, r.Scheme); err != nil { 177 return reconcile.Result{}, err 178 } 179 180 // server side apply the service 181 if err := r.Patch(ctx, service, client.Apply, applyOpts...); err != nil { 182 log.Errorf("Failed to apply a service: %v", err) 183 return reconcile.Result{}, err 184 } 185 186 // write out restart-version in helidon deployment 187 if err = r.restartHelidon(ctx, workload.Annotations[vzconst.RestartVersionAnnotation], &workload, log); err != nil { 188 return reconcile.Result{}, err 189 } 190 191 // Prepare the list of resources to reference in status. 192 statusResources := []vzapi.QualifiedResourceRelation{ 193 { 194 APIVersion: deploy.GetObjectKind().GroupVersionKind().GroupVersion().String(), 195 Kind: deploy.GetObjectKind().GroupVersionKind().Kind, 196 Name: deploy.GetName(), 197 Namespace: deploy.GetNamespace(), 198 Role: "Deployment", 199 }, 200 { 201 APIVersion: service.GetObjectKind().GroupVersionKind().GroupVersion().String(), 202 Kind: service.GetObjectKind().GroupVersionKind().Kind, 203 Name: service.GetName(), 204 Namespace: service.GetNamespace(), 205 Role: "Service", 206 }, 207 } 208 209 if !vzapi.QualifiedResourceRelationSlicesEquivalent(statusResources, workload.Status.Resources) { 210 workload.Status.Resources = statusResources 211 if err := r.Status().Update(ctx, &workload); err != nil { 212 return reconcile.Result{}, err 213 } 214 } 215 216 return reconcile.Result{}, nil 217 } 218 219 // convertWorkloadToDeployment converts a VerrazzanoHelidonWorkload into a Deployment. 220 func (r *Reconciler) convertWorkloadToDeployment(workload *vzapi.VerrazzanoHelidonWorkload, log vzlog.VerrazzanoLogger) (*appsv1.Deployment, error) { 221 d := &appsv1.Deployment{ 222 TypeMeta: metav1.TypeMeta{ 223 Kind: deploymentKind, 224 APIVersion: deploymentAPIVersion, 225 }, 226 ObjectMeta: metav1.ObjectMeta{ 227 Name: workload.Spec.DeploymentTemplate.Metadata.GetName(), 228 // make sure the namespace is set to the namespace of the component 229 Namespace: workload.GetNamespace(), 230 }, 231 Spec: appsv1.DeploymentSpec{ 232 // setting label selector for pod that this deployment will manage 233 Selector: &metav1.LabelSelector{ 234 MatchLabels: workload.Spec.DeploymentTemplate.Selector.MatchLabels, 235 MatchExpressions: workload.Spec.DeploymentTemplate.Selector.MatchExpressions, 236 }, 237 }, 238 } 239 if d.Spec.Selector.MatchLabels == nil { 240 d.Spec.Selector.MatchLabels = make(map[string]string) 241 } 242 d.Spec.Selector.MatchLabels[labelKey] = string(workload.GetUID()) 243 // Set metadata on deployment from workload spec's metadata 244 d.ObjectMeta.SetLabels(workload.Spec.DeploymentTemplate.Metadata.GetLabels()) 245 d.ObjectMeta.SetAnnotations(workload.Spec.DeploymentTemplate.Metadata.GetAnnotations()) 246 // Set deployment strategy from workload spec 247 d.Spec.Strategy = workload.Spec.DeploymentTemplate.Strategy 248 // Set PodSpec on deployment's PodTemplate from workload spec 249 workload.Spec.DeploymentTemplate.PodSpec.DeepCopyInto(&d.Spec.Template.Spec) 250 // making sure pods have same label as selector on deployment 251 d.Spec.Template.ObjectMeta.SetLabels(d.Spec.Selector.MatchLabels) 252 253 // pass through label and annotation from the workload to the deployment 254 passLabelAndAnnotation(workload, d) 255 256 if y, err := yaml.Marshal(d); err != nil { 257 log.Errorf("Failed to convert deployment to yaml: %v", err) 258 log.Debugf("Deployment in json format: %s ", d) 259 } else { 260 log.Debugf("Deployment in yaml format: %s", string(y)) 261 } 262 263 return d, nil 264 } 265 266 // createServiceFromDeployment creates a service for the deployment 267 func (r *Reconciler) createServiceFromDeployment(workload *vzapi.VerrazzanoHelidonWorkload, deploy *appsv1.Deployment, log vzlog.VerrazzanoLogger) (*corev1.Service, error) { 268 // We don't add a Service if there are no containers for the Deployment. 269 // This should never happen in practice. 270 if len(deploy.Spec.Template.Spec.Containers) == 0 { 271 return &corev1.Service{}, nil 272 } 273 s := &corev1.Service{ 274 TypeMeta: metav1.TypeMeta{ 275 Kind: serviceKind, 276 APIVersion: serviceAPIVersion, 277 }, 278 ObjectMeta: workload.Spec.ServiceTemplate.Metadata, 279 Spec: workload.Spec.ServiceTemplate.ServiceSpec, 280 } 281 if s.GetName() == "" { 282 s.SetName(deploy.GetName()) 283 } 284 if s.GetNamespace() == "" { 285 s.SetNamespace(deploy.GetNamespace()) 286 287 } 288 if s.Labels == nil { 289 s.Labels = map[string]string{} 290 } 291 s.Labels[labelKey] = string(workload.GetUID()) 292 s.Labels[oam.LabelAppName] = deploy.ObjectMeta.Labels[oam.LabelAppName] 293 s.Labels[oam.LabelAppComponent] = deploy.ObjectMeta.Labels[oam.LabelAppComponent] 294 295 if s.Spec.Selector == nil { 296 s.Spec.Selector = deploy.Spec.Selector.MatchLabels 297 } 298 if s.Spec.Type == "" { 299 s.Spec.Type = corev1.ServiceTypeClusterIP 300 } 301 if s.Spec.Ports == nil { 302 for _, container := range deploy.Spec.Template.Spec.Containers { 303 if len(container.Ports) > 0 { 304 for _, port := range container.Ports { 305 // All ports within a ServiceSpec must have unique names. 306 // When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. 307 name := strings.ToLower(container.Name + "-" + strconv.FormatInt(int64(port.ContainerPort), 10)) 308 protocol := corev1.ProtocolTCP 309 if len(port.Protocol) > 0 { 310 protocol = port.Protocol 311 } 312 313 servicePort := corev1.ServicePort{ 314 Name: name, 315 Port: port.ContainerPort, 316 TargetPort: intstr.FromInt(int(port.ContainerPort)), 317 Protocol: protocol, 318 } 319 log.Debugf("Appending port %s to service", servicePort) 320 s.Spec.Ports = append(s.Spec.Ports, servicePort) 321 } 322 } 323 } 324 } 325 326 if y, err := yaml.Marshal(s); err != nil { 327 log.Errorf("Failed to convert service to yaml: %v", err) 328 log.Debugf("Service in json format: %s", s) 329 } else { 330 log.Debugf("Service in yaml format: %s", string(y)) 331 } 332 return s, nil 333 } 334 335 // passLabelAndAnnotation passes through labels and annotation objectMeta from the workload to the deployment object 336 func passLabelAndAnnotation(workload *vzapi.VerrazzanoHelidonWorkload, deploy *appsv1.Deployment) { 337 // set app-config labels on deployment metadata 338 deploy.SetLabels(mergeMapOverrideWithDest(workload.GetLabels(), deploy.GetLabels())) 339 // set app-config labels on deployment/podtemplate metadata 340 deploy.Spec.Template.SetLabels(mergeMapOverrideWithDest(workload.GetLabels(), deploy.Spec.Template.GetLabels())) 341 // set app-config annotation on deployment metadata 342 deploy.SetAnnotations(mergeMapOverrideWithDest(workload.GetAnnotations(), deploy.GetAnnotations())) 343 } 344 345 // mergeMapOverrideWithDest merges two could be nil maps. If any conflicts, override src with dst. 346 func mergeMapOverrideWithDest(src, dst map[string]string) map[string]string { 347 if src == nil && dst == nil { 348 return nil 349 } 350 r := make(map[string]string) 351 for k, v := range dst { 352 r[k] = v 353 } 354 for k, v := range src { 355 if _, exist := r[k]; !exist { 356 r[k] = v 357 } 358 } 359 return r 360 } 361 362 // addMetrics adds the labels and annotations needed for metrics to the Helidon resource annotations which are propagated to the individual Helidon pods. 363 func (r *Reconciler) addMetrics(ctx context.Context, log vzlog.VerrazzanoLogger, namespace string, workload *vzapi.VerrazzanoHelidonWorkload, helidon *appsv1.Deployment) error { 364 log.Debugf("Adding Metrics for workload: %s", workload.Name) 365 metricsTrait, err := vznav.MetricsTraitFromWorkloadLabels(ctx, r.Client, zap.S(), namespace, workload.ObjectMeta) 366 if err != nil { 367 return err 368 } 369 370 if metricsTrait == nil { 371 log.Debug("Workload has no associated MetricTrait, nothing to do") 372 return nil 373 } 374 log.Debugf("Found associated metrics trait for workload: %s : %s", workload.Name, metricsTrait.Name) 375 376 traitDefaults, err := r.Metrics.NewTraitDefaultsForGenericWorkload() 377 if err != nil { 378 log.Errorf("Failed to get default metric trait values: %v", err) 379 return err 380 } 381 382 if helidon.Spec.Template.Labels == nil { 383 helidon.Spec.Template.Labels = make(map[string]string) 384 } 385 386 if helidon.Spec.Template.Annotations == nil { 387 helidon.Spec.Template.Annotations = make(map[string]string) 388 } 389 390 labels := metricstrait.MutateLabels(metricsTrait, nil, helidon.Spec.Template.Labels) 391 annotations := metricstrait.MutateAnnotations(metricsTrait, traitDefaults, helidon.Spec.Template.Annotations) 392 393 finalLabels := mergeMapOverrideWithDest(helidon.Spec.Template.Labels, labels) 394 log.Debugf("Setting labels on %s: %v", workload.Name, finalLabels) 395 helidon.Spec.Template.Labels = finalLabels 396 finalAnnotations := mergeMapOverrideWithDest(helidon.Spec.Template.Annotations, annotations) 397 log.Debugf("Setting annotations on %s: %v", workload.Name, finalAnnotations) 398 helidon.Spec.Template.Annotations = finalAnnotations 399 400 return nil 401 } 402 403 func (r *Reconciler) restartHelidon(ctx context.Context, restartVersion string, workload *vzapi.VerrazzanoHelidonWorkload, log vzlog.VerrazzanoLogger) error { 404 if len(restartVersion) > 0 { 405 var deploymentList appsv1.DeploymentList 406 componentNameReq, _ := labels.NewRequirement(oam.LabelAppComponent, selection.Equals, []string{workload.ObjectMeta.Labels[oam.LabelAppComponent]}) 407 appNameReq, _ := labels.NewRequirement(oam.LabelAppName, selection.Equals, []string{workload.ObjectMeta.Labels[oam.LabelAppName]}) 408 selector := labels.NewSelector() 409 selector = selector.Add(*componentNameReq, *appNameReq) 410 err := r.Client.List(ctx, &deploymentList, &client.ListOptions{Namespace: workload.Namespace, LabelSelector: selector}) 411 if err != nil { 412 return err 413 } 414 for index := range deploymentList.Items { 415 deployment := &deploymentList.Items[index] 416 if err := appconfig.DoRestartDeployment(ctx, r.Client, restartVersion, deployment, log); err != nil { 417 return err 418 } 419 } 420 } 421 return nil 422 }