github.com/redhat-appstudio/release-service@v0.0.0-20240507143925-083712697924/api/v1alpha1/webhooks/author/webhook.go (about)

     1  /*
     2  Copyright 2022.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package author
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"github.com/redhat-appstudio/release-service/api/v1alpha1"
    24  	"net/http"
    25  	ctrl "sigs.k8s.io/controller-runtime"
    26  	ctrlWebhook "sigs.k8s.io/controller-runtime/pkg/webhook"
    27  	"strings"
    28  
    29  	"github.com/go-logr/logr"
    30  	"github.com/pkg/errors"
    31  	"github.com/redhat-appstudio/release-service/metadata"
    32  	admissionv1 "k8s.io/api/admission/v1"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    35  )
    36  
    37  // Webhook describes the data structure for the author webhook
    38  type Webhook struct {
    39  	client client.Client
    40  	log    logr.Logger
    41  }
    42  
    43  // Handle creates an admission response for Release and ReleasePlan requests.
    44  func (w *Webhook) Handle(ctx context.Context, req admission.Request) admission.Response {
    45  	switch req.Kind.Kind {
    46  	case "Release":
    47  		return w.handleRelease(req)
    48  	case "ReleasePlan":
    49  		return w.handleReleasePlan(req)
    50  	default:
    51  		return admission.Errored(http.StatusInternalServerError,
    52  			fmt.Errorf("webhook tried to handle an unsupported resource: %s", req.Kind.Kind))
    53  	}
    54  }
    55  
    56  // +kubebuilder:webhook:path=/mutate-appstudio-redhat-com-v1alpha1-author,mutating=true,failurePolicy=fail,sideEffects=None,groups=appstudio.redhat.com,resources=releases;releaseplans,verbs=create;update,versions=v1alpha1,name=mauthor.kb.io,admissionReviewVersions=v1
    57  
    58  // Register registers the webhook with the passed manager and log.
    59  func (w *Webhook) Register(mgr ctrl.Manager, log *logr.Logger) error {
    60  	w.client = mgr.GetClient()
    61  	w.log = log.WithName("author")
    62  
    63  	mgr.GetWebhookServer().Register("/mutate-appstudio-redhat-com-v1alpha1-author", &ctrlWebhook.Admission{Handler: w})
    64  
    65  	return nil
    66  }
    67  
    68  // handleRelease takes an incoming admission request and returns an admission response. Create requests
    69  // add an author label with the current user. Update requests are rejected if the author label is being
    70  // modified. All other requests are accepted without action.
    71  func (w *Webhook) handleRelease(req admission.Request) admission.Response {
    72  	release := &v1alpha1.Release{}
    73  	err := json.Unmarshal(req.Object.Raw, release)
    74  	if err != nil {
    75  		return admission.Errored(http.StatusBadRequest, errors.Wrap(err, "error decoding object"))
    76  	}
    77  
    78  	switch req.AdmissionRequest.Operation {
    79  	case admissionv1.Create:
    80  		if release.GetLabels()[metadata.AutomatedLabel] != "true" {
    81  			w.setAuthorLabel(req.UserInfo.Username, release)
    82  		}
    83  
    84  		return w.patchResponse(req.Object.Raw, release)
    85  	case admissionv1.Update:
    86  		oldRelease := &v1alpha1.Release{}
    87  		err := json.Unmarshal(req.OldObject.Raw, oldRelease)
    88  		if err != nil {
    89  			return admission.Errored(http.StatusBadRequest, errors.Wrap(err, "error decoding object"))
    90  		}
    91  
    92  		if release.GetLabels()[metadata.AuthorLabel] != oldRelease.GetLabels()[metadata.AuthorLabel] {
    93  			return admission.Errored(http.StatusBadRequest, errors.New("release author label cannnot be updated"))
    94  		}
    95  	}
    96  	return admission.Allowed("Success")
    97  }
    98  
    99  // handleReleasePlan takes an incoming admission request and returns an admission response. If the
   100  // attribution label is set to true, the current user is set as the author. If the attribution label
   101  // is false, the author label is removed. The only exception is if the attribution label remains true
   102  // during an update and the author value is not modified, the previous author label remains.
   103  func (w *Webhook) handleReleasePlan(req admission.Request) admission.Response {
   104  	releasePlan := &v1alpha1.ReleasePlan{}
   105  	err := json.Unmarshal(req.Object.Raw, releasePlan)
   106  	if err != nil {
   107  		return admission.Errored(http.StatusBadRequest, errors.Wrap(err, "error decoding object"))
   108  	}
   109  	// Author label should not exist in any case if attribution is not true
   110  	if releasePlan.GetLabels()[metadata.AttributionLabel] != "true" {
   111  		delete(releasePlan.GetLabels(), metadata.AuthorLabel)
   112  	}
   113  
   114  	switch req.AdmissionRequest.Operation {
   115  	case admissionv1.Create:
   116  		if releasePlan.GetLabels()[metadata.AttributionLabel] == "true" {
   117  			w.setAuthorLabel(req.UserInfo.Username, releasePlan)
   118  		}
   119  	case admissionv1.Update:
   120  		oldReleasePlan := &v1alpha1.ReleasePlan{}
   121  		err := json.Unmarshal(req.OldObject.Raw, oldReleasePlan)
   122  		if err != nil {
   123  			return admission.Errored(http.StatusBadRequest, errors.Wrap(err, "error decoding object"))
   124  		}
   125  
   126  		if releasePlan.GetLabels()[metadata.AttributionLabel] == "true" {
   127  			author := releasePlan.GetLabels()[metadata.AuthorLabel]
   128  
   129  			if oldReleasePlan.GetLabels()[metadata.AttributionLabel] != "true" || author == w.sanitizeLabelValue(req.UserInfo.Username) {
   130  				w.setAuthorLabel(req.UserInfo.Username, releasePlan)
   131  			} else {
   132  				// Preserve previous author if the new author does not match the user making the change
   133  				w.setAuthorLabel(oldReleasePlan.GetLabels()[metadata.AuthorLabel], releasePlan)
   134  			}
   135  		}
   136  	}
   137  
   138  	return w.patchResponse(req.Object.Raw, releasePlan)
   139  }
   140  
   141  // patchResponse returns an admission response that patches the passed raw object to be the passed object.
   142  func (w *Webhook) patchResponse(raw []byte, object client.Object) admission.Response {
   143  	marshalledObject, err := json.Marshal(object)
   144  	if err != nil {
   145  		return admission.Errored(http.StatusInternalServerError, errors.Wrap(err, "error encoding object"))
   146  	}
   147  
   148  	return admission.PatchResponseFromRaw(raw, marshalledObject)
   149  }
   150  
   151  // setAuthorLabel returns the passed object with the author label added.
   152  func (w *Webhook) setAuthorLabel(username string, obj client.Object) {
   153  	labels := make(map[string]string)
   154  	if obj.GetLabels() != nil {
   155  		labels = obj.GetLabels()
   156  	}
   157  
   158  	labels[metadata.AuthorLabel] = w.sanitizeLabelValue(username)
   159  	obj.SetLabels(labels)
   160  }
   161  
   162  // sanitizeLabelValue takes a username and returns it in a form appropriate to use as a label value.
   163  func (w *Webhook) sanitizeLabelValue(username string) string {
   164  	author := strings.Replace(username, ":", "_", -1) // Colons disallowed in labels
   165  
   166  	if len(author) > metadata.MaxLabelLength {
   167  		author = string(author)[0:metadata.MaxLabelLength]
   168  	}
   169  
   170  	return author
   171  }