github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/webhooks/metrics-binding-updater-workload.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 webhooks
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"net/http"
    11  	"reflect"
    12  	"strings"
    13  
    14  	vzapp "github.com/verrazzano/verrazzano/application-operator/apis/app/v1alpha1"
    15  	"github.com/verrazzano/verrazzano/application-operator/constants"
    16  	"github.com/verrazzano/verrazzano/application-operator/controllers/workloadselector"
    17  	"github.com/verrazzano/verrazzano/application-operator/metricsexporter"
    18  	vzconst "github.com/verrazzano/verrazzano/pkg/constants"
    19  	vzlog "github.com/verrazzano/verrazzano/pkg/log"
    20  	"go.uber.org/zap"
    21  	corev1 "k8s.io/api/core/v1"
    22  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    25  	"k8s.io/apimachinery/pkg/types"
    26  	"k8s.io/client-go/kubernetes"
    27  	"sigs.k8s.io/controller-runtime/pkg/client"
    28  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    29  )
    30  
    31  const (
    32  	MetricsAnnotation                   = "app.verrazzano.io/metrics"
    33  	MetricsBindingGeneratorWorkloadPath = "/metrics-binding-generator-workload"
    34  )
    35  
    36  // WorkloadWebhook type for the mutating webhook
    37  type WorkloadWebhook struct {
    38  	client.Client
    39  	Decoder    *admission.Decoder
    40  	KubeClient kubernetes.Interface
    41  }
    42  
    43  // Handle - handler for the mutating webhook
    44  func (a *WorkloadWebhook) Handle(ctx context.Context, req admission.Request) admission.Response {
    45  	log := zap.S().With(vzlog.FieldResourceNamespace, req.Namespace, vzlog.FieldResourceName, req.Name, vzlog.FieldWebhook, "metrics-binding-generator-workload")
    46  	log.Debugf("group: %s, version: %s, kind: %s", req.Kind.Group, req.Kind.Version, req.Kind.Kind)
    47  	durationMetricHandle, err := metricsexporter.GetDurationMetric(metricsexporter.BindingUpdaterHandleDuration)
    48  	if err != nil {
    49  		return admission.Response{}
    50  	}
    51  	counterMetricHandle, err := metricsexporter.GetSimpleCounterMetric(metricsexporter.BindingUpdaterHandleCounter)
    52  	if err != nil {
    53  		return admission.Response{}
    54  	}
    55  	durationMetricHandle.TimerStart()
    56  	defer durationMetricHandle.TimerStop()
    57  	counterMetricHandle.Inc(zap.S(), err)
    58  	return a.handleWorkloadResource(ctx, req, log)
    59  }
    60  
    61  // InjectDecoder injects the decoder.
    62  func (a *WorkloadWebhook) InjectDecoder(d *admission.Decoder) error {
    63  	a.Decoder = d
    64  	return nil
    65  }
    66  
    67  // handleWorkloadResource decodes the admission request for a workload resource into an unstructured
    68  // and then processes workload resource
    69  func (a *WorkloadWebhook) handleWorkloadResource(ctx context.Context, req admission.Request, log *zap.SugaredLogger) admission.Response {
    70  	unst := &unstructured.Unstructured{}
    71  	if err := a.Decoder.Decode(req, unst); err != nil {
    72  		log.Errorf("Failed decoding object in admission request: %v", err)
    73  		return admission.Errored(http.StatusBadRequest, err)
    74  	}
    75  
    76  	// Do not handle any workload resources that have owner references.
    77  	// NOTE: this will be revisited.
    78  	if len(unst.GetOwnerReferences()) != 0 {
    79  		return admission.Allowed(constants.StatusReasonSuccess)
    80  	}
    81  
    82  	// Handle legacy metrics annotations only for _existing_ workloads i.e. if a MetricsBinding
    83  	// already exists
    84  	var existingMetricsBinding *vzapp.MetricsBinding
    85  	var err error
    86  	if existingMetricsBinding, err = a.GetLegacyMetricsBinding(ctx, unst); err != nil {
    87  		log.Errorf("Failed trying to retrieve legacy MetricsBinding for %s workload %s/%s: %v", unst.GetKind(), unst.GetNamespace(), unst.GetName(), err)
    88  		return admission.Errored(http.StatusInternalServerError, err)
    89  	}
    90  
    91  	if existingMetricsBinding == nil {
    92  		// No MetricsBinding exists to be migrated - this is likely a newer app that has not been
    93  		// processed by Verrazzano versions earlier than 1.4
    94  		return admission.Allowed(constants.StatusReasonSuccess)
    95  	}
    96  
    97  	// If we got here, this is a pre-Verrazzano 1.4 application - process the annotations and
    98  	// update the existing metrics binding accordingly as before
    99  	// If "none" is specified on workload for annotation "app.verrazzano.io/metrics" then this workload has opted out of metrics.
   100  	if metricsTemplateAnnotation, ok := unst.GetAnnotations()[MetricsAnnotation]; ok {
   101  		if strings.ToLower(metricsTemplateAnnotation) == "none" {
   102  			log.Infof("%s is set to none in the workload - opting out of metrics", MetricsAnnotation)
   103  			return admission.Allowed(constants.StatusReasonSuccess)
   104  		}
   105  	}
   106  
   107  	// Get the workload Namespace for annotation processing
   108  	workloadNamespace := &corev1.Namespace{}
   109  	if err = a.Client.Get(context.TODO(), types.NamespacedName{Name: unst.GetNamespace()}, workloadNamespace); err != nil {
   110  		log.Errorf("Failed getting workload namespace %s: %v", unst.GetNamespace(), err)
   111  		return admission.Errored(http.StatusInternalServerError, err)
   112  	}
   113  
   114  	// If "none" is specified on namespace for annotation "app.verrazzano.io/metrics" then this namespace has opted out of metrics.
   115  	if metricsTemplateAnnotation, ok := workloadNamespace.GetAnnotations()[MetricsAnnotation]; ok {
   116  		if strings.ToLower(metricsTemplateAnnotation) == "none" {
   117  			log.Infof("%s is set to none in the namespace - opting out of metrics", MetricsAnnotation)
   118  			return admission.Allowed(constants.StatusReasonSuccess)
   119  		}
   120  	}
   121  
   122  	// Get the metrics template from annotation or workload selector
   123  	metricsTemplate, err := a.getMetricsTemplate(ctx, unst, workloadNamespace, log)
   124  	if err != nil {
   125  		return admission.Errored(http.StatusInternalServerError, err)
   126  	}
   127  
   128  	// Metrics template handling - update the metrics binding as needed
   129  
   130  	// Workload resource specifies a valid metrics template or we found one above
   131  	// We use that metrics template to update the existing metrics binding resource. We won't
   132  	// create new MetricsBindings as of Verrazzano 1.4 but we will honor settings for existing apps
   133  	if err = a.updateMetricBinding(ctx, unst, metricsTemplate, existingMetricsBinding, log); err != nil {
   134  		return admission.Errored(http.StatusInternalServerError, err)
   135  	}
   136  
   137  	marshaledWorkloadResource, err := json.Marshal(unst)
   138  	if err != nil {
   139  		log.Errorf("Failed marshalling workload resource: %v", err)
   140  		return admission.Errored(http.StatusInternalServerError, err)
   141  	}
   142  	return admission.PatchResponseFromRaw(req.Object.Raw, marshaledWorkloadResource)
   143  }
   144  
   145  // getMetricsTemplate processes the app.verrazzano.io/metrics annotation and gets the metrics
   146  // template, if specified. Otherwise it finds the matching metrics template based on workload selector
   147  func (a *WorkloadWebhook) getMetricsTemplate(ctx context.Context, unst *unstructured.Unstructured, workloadNamespace *corev1.Namespace, log *zap.SugaredLogger) (*vzapp.MetricsTemplate, error) {
   148  	metricsTemplate, err := a.processMetricsAnnotation(unst, workloadNamespace, log)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	if metricsTemplate == nil {
   154  		// Workload resource does not specify a metrics template.
   155  		// Look for a matching metrics template workload whose workload selector matches.
   156  		// First, check the namespace of the workload resource and then check the verrazzano-system namespace
   157  		// NOTE: use the first match for now
   158  		// var metricsTemplate *vzapp.MetricsTemplate
   159  		metricsTemplate, err = a.findMatchingTemplate(ctx, unst, unst.GetNamespace(), log)
   160  		if err != nil {
   161  			return nil, err
   162  		}
   163  		if metricsTemplate == nil {
   164  			template, err := a.findMatchingTemplate(ctx, unst, constants.VerrazzanoSystemNamespace, log)
   165  			if err != nil {
   166  				return nil, err
   167  			}
   168  			metricsTemplate = template
   169  		}
   170  	}
   171  	return metricsTemplate, nil
   172  }
   173  
   174  // GetLegacyMetricsBinding returns the existing MetricsBinding (legacy resource) for the given
   175  // workload - nil if it does not exist.
   176  func (a *WorkloadWebhook) GetLegacyMetricsBinding(ctx context.Context, unst *unstructured.Unstructured) (*vzapp.MetricsBinding, error) {
   177  	metricsBindingName := generateMetricsBindingName(unst.GetName(), unst.GetAPIVersion(), unst.GetKind())
   178  	metricsBindingKey := types.NamespacedName{Namespace: unst.GetNamespace(), Name: metricsBindingName}
   179  	metricsBinding := vzapp.MetricsBinding{}
   180  	err := a.Client.Get(ctx, metricsBindingKey, &metricsBinding)
   181  	if apierrors.IsNotFound(err) {
   182  		return nil, nil
   183  	}
   184  	return &metricsBinding, err
   185  }
   186  
   187  // processMetricsAnnotation checks the workload resource for the "app.verrazzano.io/metrics" annotation and returns the
   188  // metrics template referenced in the annotation
   189  func (a *WorkloadWebhook) processMetricsAnnotation(unst *unstructured.Unstructured, workloadNamespace *corev1.Namespace, log *zap.SugaredLogger) (*vzapp.MetricsTemplate, error) {
   190  	// Check workload, then namespace for annotation
   191  	metricsTemplate, ok := unst.GetAnnotations()[MetricsAnnotation]
   192  	if !ok {
   193  		metricsTemplate, ok = workloadNamespace.GetAnnotations()[MetricsAnnotation]
   194  		if !ok {
   195  			return nil, nil
   196  		}
   197  	}
   198  
   199  	// Look for the metrics template in the namespace of the workload resource
   200  	template := &vzapp.MetricsTemplate{}
   201  	namespacedName := types.NamespacedName{Namespace: unst.GetNamespace(), Name: metricsTemplate}
   202  	err := a.Client.Get(context.TODO(), namespacedName, template)
   203  	if err != nil {
   204  		// If we don't find the metrics template in the namespace of the workload resource then
   205  		// look in the verrazzano-system namespace
   206  		if apierrors.IsNotFound(err) {
   207  			namespacedName := types.NamespacedName{Namespace: constants.VerrazzanoSystemNamespace, Name: metricsTemplate}
   208  			err := a.Client.Get(context.TODO(), namespacedName, template)
   209  			if err != nil {
   210  				log.Errorf("Failed getting metrics template %s/%s: %v", constants.VerrazzanoSystemNamespace, metricsTemplate, err)
   211  				return nil, err
   212  			}
   213  			log.Infof("Found matching metrics template %s/%s", constants.VerrazzanoSystemNamespace, metricsTemplate)
   214  			return template, nil
   215  		}
   216  
   217  		log.Errorf("Failed getting metrics template %s/%s: %v", unst.GetNamespace(), metricsTemplate, err)
   218  		return nil, err
   219  	}
   220  
   221  	log.Infof("Found matching metrics template %s/%s", unst.GetNamespace(), metricsTemplate)
   222  	return template, nil
   223  }
   224  
   225  // updateMetricBinding updates an existing metricsBinding resource and
   226  // adds the apps.verrazzano.io/workload label to the workload resource
   227  func (a *WorkloadWebhook) updateMetricBinding(ctx context.Context, unst *unstructured.Unstructured, template *vzapp.MetricsTemplate, metricsBinding *vzapp.MetricsBinding, log *zap.SugaredLogger) error {
   228  	if template == nil {
   229  		// nothing to update
   230  		return nil
   231  	}
   232  	// When the Prometheus target config map was not specified in the metrics template then there is nothing to do.
   233  	if reflect.DeepEqual(template.Spec.PrometheusConfig.TargetConfigMap, vzapp.TargetConfigMap{}) {
   234  		log.Infof("Prometheus target config map %s/%s not specified", template.Namespace, template.Name)
   235  		return nil
   236  	}
   237  
   238  	// Only look for the config map if it's not the legacy one. The legacy VMI config map will no longer exist, and be replaced
   239  	// with the additional scrape configs secret in the MetricsBinding, so don't look for it.
   240  	if !isLegacyVmiPrometheusConfigMapName(vzapp.NamespaceName{
   241  		Namespace: template.Spec.PrometheusConfig.TargetConfigMap.Namespace, Name: template.Spec.PrometheusConfig.TargetConfigMap.Name}) {
   242  		_, err := a.KubeClient.CoreV1().ConfigMaps(template.Spec.PrometheusConfig.TargetConfigMap.Namespace).Get(ctx, template.Spec.PrometheusConfig.TargetConfigMap.Name, metav1.GetOptions{})
   243  		if err != nil {
   244  			log.Errorf("Failed getting Prometheus target config map %s/%s: %v", template.Namespace, template.Name, err)
   245  			return err
   246  		}
   247  	}
   248  
   249  	err := a.mutateMetricsBinding(metricsBinding, template, unst)
   250  	if err != nil {
   251  		log.Errorf("Failed mutating the metricsBinding resource: %v", err)
   252  		return err
   253  	}
   254  
   255  	err = a.Client.Update(ctx, metricsBinding)
   256  	if err != nil {
   257  		log.Errorf("Failed updating the metricsBinding resource: %v", err)
   258  		return err
   259  	}
   260  
   261  	// Set the app.verrazzano.io/workload to identify the Prometheus config scrape target
   262  	labels := unst.GetLabels()
   263  	if labels == nil {
   264  		labels = make(map[string]string)
   265  	}
   266  	labels[constants.MetricsWorkloadLabel] = metricsBinding.GetName()
   267  	unst.SetLabels(labels)
   268  
   269  	return nil
   270  }
   271  
   272  // mutateMetricsBinding mutates a metricsBinding resource based on the metrics template provided
   273  func (a *WorkloadWebhook) mutateMetricsBinding(metricsBinding *vzapp.MetricsBinding, template *vzapp.MetricsTemplate, unst *unstructured.Unstructured) error {
   274  	metricsBinding.Spec.MetricsTemplate.Namespace = template.Namespace
   275  	metricsBinding.Spec.MetricsTemplate.Name = template.Name
   276  	metricsBinding.Spec.PrometheusConfigMap.Namespace = template.Spec.PrometheusConfig.TargetConfigMap.Namespace
   277  	metricsBinding.Spec.PrometheusConfigMap.Name = template.Spec.PrometheusConfig.TargetConfigMap.Name
   278  	metricsBinding.Spec.Workload.Name = unst.GetName()
   279  	metricsBinding.Spec.Workload.TypeMeta = metav1.TypeMeta{APIVersion: unst.GetAPIVersion(), Kind: unst.GetKind()}
   280  
   281  	// If the config map specified is the legacy VMI prometheus config map, modify it to use
   282  	// the additionalScrapeConfigs config map for the Prometheus Operator
   283  	if isLegacyVmiPrometheusConfigMapName(metricsBinding.Spec.PrometheusConfigMap) {
   284  		metricsBinding.Spec.PrometheusConfigMap = vzapp.NamespaceName{}
   285  		metricsBinding.Spec.PrometheusConfigSecret = vzapp.SecretKey{
   286  			Namespace: vzconst.PrometheusOperatorNamespace,
   287  			Name:      vzconst.PromAdditionalScrapeConfigsSecretName,
   288  			Key:       vzconst.PromAdditionalScrapeConfigsSecretKey,
   289  		}
   290  	}
   291  
   292  	return nil
   293  }
   294  
   295  // isLegacyVmiPrometheusConfigMapName returns true if the given NamespaceName is that of the legacy
   296  // vmi system prometheus config map
   297  func isLegacyVmiPrometheusConfigMapName(configMapName vzapp.NamespaceName) bool {
   298  	return configMapName.Namespace == constants.VerrazzanoSystemNamespace &&
   299  		configMapName.Name == vzconst.VmiPromConfigName
   300  }
   301  
   302  // findMatchingTemplate returns a matching template for a given namespace
   303  func (a *WorkloadWebhook) findMatchingTemplate(ctx context.Context, unst *unstructured.Unstructured, namespace string, log *zap.SugaredLogger) (*vzapp.MetricsTemplate, error) {
   304  	// Get the list of metrics templates for the given namespace
   305  	templateList := &vzapp.MetricsTemplateList{}
   306  	err := a.Client.List(ctx, templateList, &client.ListOptions{Namespace: namespace})
   307  	if err != nil {
   308  		log.Errorf("Failed getting list of metrics templates in namespace %s: %v", namespace, err)
   309  		return nil, err
   310  	}
   311  
   312  	ws := &workloadselector.WorkloadSelector{
   313  		KubeClient: a.KubeClient,
   314  	}
   315  
   316  	// Iterate through the metrics template list and check if we find a matching template for the workload resource
   317  	for _, template := range templateList.Items {
   318  		// If the template workload selector was not specified then don't try to match this template
   319  		if reflect.DeepEqual(template.Spec.WorkloadSelector, vzapp.WorkloadSelector{}) {
   320  			log.Infof("Metrics template %s/%s workloadSelector not specified - no workload match checking performed", template.Namespace, template.Name)
   321  			continue
   322  		}
   323  		found, err := ws.DoesWorkloadMatch(unst,
   324  			&template.Spec.WorkloadSelector.NamespaceSelector,
   325  			&template.Spec.WorkloadSelector.ObjectSelector,
   326  			template.Spec.WorkloadSelector.APIGroups,
   327  			template.Spec.WorkloadSelector.APIVersions,
   328  			template.Spec.WorkloadSelector.Resources)
   329  		if err != nil {
   330  			log.Errorf("Failed looking for a matching metrics template: %v", err)
   331  			return nil, err
   332  		}
   333  		// Found a match, return the matching metrics template
   334  		if found {
   335  			log.Infof("Found matching metrics template %s/%s", namespace, template.Name)
   336  			return &template, nil
   337  		}
   338  	}
   339  
   340  	return nil, nil
   341  }
   342  
   343  // Generate the metricBindings name
   344  func generateMetricsBindingName(name string, apiVersion string, kind string) string {
   345  	return fmt.Sprintf("%s-%s-%s", name, strings.Replace(apiVersion, "/", "-", 1), strings.ToLower(kind))
   346  }