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 }