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 }