github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/webhooks/metrics-binding-labeler-pod.go (about)

     1  // Copyright (c) 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  	"strings"
    12  
    13  	"github.com/gertd/go-pluralize"
    14  	"github.com/verrazzano/verrazzano/application-operator/constants"
    15  	"github.com/verrazzano/verrazzano/application-operator/controllers"
    16  	"github.com/verrazzano/verrazzano/application-operator/metricsexporter"
    17  	vzlog "github.com/verrazzano/verrazzano/pkg/log"
    18  	"go.uber.org/zap"
    19  	corev1 "k8s.io/api/core/v1"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  	"k8s.io/client-go/dynamic"
    24  	"sigs.k8s.io/controller-runtime/pkg/client"
    25  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    26  )
    27  
    28  const (
    29  	MetricsBindingLabelerPodPath = "/metrics-binding-labeler-pod"
    30  
    31  	PrometheusPortAnnotation   = "prometheus.io/port"
    32  	PrometheusPathAnnotation   = "prometheus.io/path"
    33  	PrometheusScrapeAnnotation = "prometheus.io/scrape"
    34  
    35  	PrometheusPortDefault   = "8080"
    36  	PrometheusPathDefault   = "/metrics"
    37  	PrometheusScrapeDefault = "true"
    38  )
    39  
    40  // LabelerPodWebhook type for the mutating webhook
    41  type LabelerPodWebhook struct {
    42  	client.Client
    43  	Decoder       *admission.Decoder
    44  	DynamicClient dynamic.Interface
    45  }
    46  
    47  // Handle is the handler for the mutating webhook
    48  func (a *LabelerPodWebhook) Handle(ctx context.Context, req admission.Request) admission.Response {
    49  	log := zap.S().With(vzlog.FieldResourceNamespace, req.Namespace, vzlog.FieldResourceName, req.Name, vzlog.FieldWebhook, "metrics-binding-labeler-pod")
    50  	log.Debug("metrics-binding-labeler-pod webhook called")
    51  	durationMetricHandle, err := metricsexporter.GetDurationMetric(metricsexporter.LabelerPodHandleDuration)
    52  	if err != nil {
    53  		return admission.Response{}
    54  	}
    55  	counterMetricHandle, err := metricsexporter.GetSimpleCounterMetric(metricsexporter.LabelerPodHandleCounter)
    56  	if err != nil {
    57  		return admission.Response{}
    58  	}
    59  	durationMetricHandle.TimerStart()
    60  	defer durationMetricHandle.TimerStop()
    61  	counterMetricHandle.Inc(zap.S(), err)
    62  	return a.handlePodResource(req, log)
    63  }
    64  
    65  // InjectDecoder injects the decoder.
    66  func (a *LabelerPodWebhook) InjectDecoder(d *admission.Decoder) error {
    67  	a.Decoder = d
    68  	return nil
    69  }
    70  
    71  // handlePodResource decodes the admission request for a pod resource into a Pod struct
    72  // and then processes the pod resource
    73  func (a *LabelerPodWebhook) handlePodResource(req admission.Request, log *zap.SugaredLogger) admission.Response {
    74  	pod := &corev1.Pod{}
    75  	err := a.Decoder.Decode(req, pod)
    76  	if err != nil {
    77  		log.Errorf("Failed decoding object in admission request: %v", err)
    78  		return admission.Errored(http.StatusBadRequest, err)
    79  	}
    80  
    81  	var workloadLabel string
    82  
    83  	// Get the workload resource for the given pod if there are owner references
    84  	if len(pod.OwnerReferences) != 0 {
    85  		workloads, err := a.getWorkloadResource(nil, req.Namespace, pod.OwnerReferences, log)
    86  		if err != nil {
    87  			return admission.Errored(http.StatusInternalServerError, err)
    88  		}
    89  		for _, workload := range workloads {
    90  			// If we have an owner ref that is an OAM ApplicationConfiguration resource then we don't want
    91  			// to label the pod to have the app.verrazzano.io/workload label
    92  			group, _ := controllers.ConvertAPIVersionToGroupAndVersion(workload.GetAPIVersion())
    93  			if workload.GetKind() == "ApplicationConfiguration" && group == "core.oam.dev" {
    94  				return admission.Allowed(constants.StatusReasonSuccess)
    95  			}
    96  		}
    97  		if len(workloads) > 1 {
    98  			err = fmt.Errorf("multiple workload resources found for %s, Verrazzano metrics cannot be enabled", pod.Name)
    99  			log.Errorf("Failed identifying workload resource: %v", err)
   100  			return admission.Errored(http.StatusInternalServerError, err)
   101  		}
   102  		workloadLabel = generateMetricsBindingName(workloads[0].GetName(), workloads[0].GetAPIVersion(), workloads[0].GetKind())
   103  	} else {
   104  		workloadLabel = generateMetricsBindingName(pod.Name, pod.APIVersion, pod.Kind)
   105  	}
   106  
   107  	// Set the app.verrazzano.io/workload to identify the Prometheus config scrape target
   108  	labels := pod.GetLabels()
   109  	if labels == nil {
   110  		labels = make(map[string]string)
   111  	}
   112  	labels[constants.MetricsWorkloadLabel] = workloadLabel
   113  	pod.SetLabels(labels)
   114  	log.Infof("Setting pod label %s to %s", constants.MetricsWorkloadLabel, workloadLabel)
   115  
   116  	// Set the Prometheus annotations if not present
   117  	a.setPrometheusAnnotations(pod, log)
   118  
   119  	marshaledPodResource, err := json.Marshal(pod)
   120  	if err != nil {
   121  		log.Errorf("Failed marshalling pod resource: %v", err)
   122  		return admission.Errored(http.StatusInternalServerError, err)
   123  	}
   124  	return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPodResource)
   125  }
   126  
   127  // getWorkloadResource traverses a nested array of owner references and returns a list of resources
   128  // that have no owner references.  Most likely, the list will have only one resource
   129  func (a *LabelerPodWebhook) getWorkloadResource(resources []*unstructured.Unstructured, namespace string, ownerRefs []metav1.OwnerReference, log *zap.SugaredLogger) ([]*unstructured.Unstructured, error) {
   130  	for _, ownerRef := range ownerRefs {
   131  		group, version := controllers.ConvertAPIVersionToGroupAndVersion(ownerRef.APIVersion)
   132  		resource := schema.GroupVersionResource{
   133  			Group:    group,
   134  			Version:  version,
   135  			Resource: pluralize.NewClient().Plural(strings.ToLower(ownerRef.Kind)),
   136  		}
   137  
   138  		// The Coherence resource has the same singular and plural values.  Force singular for Coherence.
   139  		// Note: Coherence seems to be an outlier.
   140  		if resource.Resource == "coherences" {
   141  			resource.Resource = "coherence"
   142  		}
   143  
   144  		unst, err := a.DynamicClient.Resource(resource).Namespace(namespace).Get(context.TODO(), ownerRef.Name, metav1.GetOptions{})
   145  		if err != nil {
   146  			log.Errorf("Failed getting the Dynamic API: %v", err)
   147  			return nil, err
   148  		}
   149  
   150  		if len(unst.GetOwnerReferences()) == 0 {
   151  			resources = append(resources, unst)
   152  		} else {
   153  			resources, err = a.getWorkloadResource(resources, namespace, unst.GetOwnerReferences(), log)
   154  			if err != nil {
   155  				return nil, err
   156  			}
   157  		}
   158  	}
   159  
   160  	return resources, nil
   161  }
   162  
   163  func (a *LabelerPodWebhook) setPrometheusAnnotations(pod *corev1.Pod, log *zap.SugaredLogger) {
   164  	log.Debug("Setting Prometheus annotations for workload pod")
   165  	podAnnotations := pod.GetAnnotations()
   166  	if podAnnotations == nil {
   167  		podAnnotations = map[string]string{}
   168  		pod.Annotations = podAnnotations
   169  	}
   170  
   171  	// Set port default if not present
   172  	if _, ok := podAnnotations[PrometheusPortAnnotation]; !ok {
   173  		pod.Annotations[PrometheusPortAnnotation] = PrometheusPortDefault
   174  	}
   175  	// Set path default if not present
   176  	if _, ok := podAnnotations[PrometheusPathAnnotation]; !ok {
   177  		pod.Annotations[PrometheusPathAnnotation] = PrometheusPathDefault
   178  	}
   179  	// Set scrape default if not present
   180  	if _, ok := podAnnotations[PrometheusScrapeAnnotation]; !ok {
   181  		pod.Annotations[PrometheusScrapeAnnotation] = PrometheusScrapeDefault
   182  	}
   183  }