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 }