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  }