github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/caasadmission/handler.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package caasadmission 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "net/http" 11 "strings" 12 13 "github.com/juju/errors" 14 admission "k8s.io/api/admission/v1beta1" 15 meta "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/runtime" 17 "k8s.io/apimachinery/pkg/runtime/schema" 18 "k8s.io/apimachinery/pkg/runtime/serializer" 19 "k8s.io/apimachinery/pkg/types" 20 21 providerconst "github.com/juju/juju/caas/kubernetes/provider/constants" 22 providerutils "github.com/juju/juju/caas/kubernetes/provider/utils" 23 ) 24 25 type patchOperation struct { 26 Op string `json:"op"` 27 Path string `json:"path"` 28 Value interface{} `json:"value,omitempty"` 29 } 30 31 type RBACMapper interface { 32 // AppNameForServiceAccount fetches the juju application name associated 33 // with a given kubernetes service account UID. If no result is found 34 // errors.NotFound is returned. All other errors should be considered 35 // internal to the interface operation. 36 AppNameForServiceAccount(types.UID) (string, error) 37 } 38 39 const ( 40 ExpectedContentType = "application/json" 41 HeaderContentType = "Content-Type" 42 addOp = "add" 43 replaceOp = "replace" 44 ) 45 46 var ( 47 AdmissionGVK = schema.GroupVersionKind{ 48 Group: admission.SchemeGroupVersion.Group, 49 Version: admission.SchemeGroupVersion.Version, 50 Kind: "AdmissionReview", 51 } 52 ) 53 54 func admissionHandler(logger Logger, rbacMapper RBACMapper, legacyLabels bool) http.Handler { 55 codecFactory := serializer.NewCodecFactory(runtime.NewScheme()) 56 57 return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 58 data, err := io.ReadAll(req.Body) 59 if err != nil { 60 logger.Errorf("digesting admission request body: %v", err) 61 http.Error(res, fmt.Sprintf("%s: reading request body", 62 http.StatusText(http.StatusInternalServerError)), http.StatusInternalServerError) 63 return 64 } 65 66 if len(data) == 0 { 67 http.Error(res, fmt.Sprintf("%s: empty request body", 68 http.StatusText(http.StatusBadRequest)), http.StatusBadRequest) 69 return 70 } 71 72 if req.Header.Get(HeaderContentType) != ExpectedContentType { 73 http.Error(res, fmt.Sprintf("%s: supported content types = [%s]", 74 http.StatusText(http.StatusUnsupportedMediaType), 75 ExpectedContentType), http.StatusUnsupportedMediaType) 76 return 77 } 78 79 finalise := func(review *admission.AdmissionReview, response *admission.AdmissionResponse) { 80 var uid types.UID 81 if review != nil && review.Request != nil { 82 uid = review.Request.UID 83 } 84 response.UID = uid 85 86 body, err := json.Marshal(admission.AdmissionReview{ 87 Response: response, 88 }) 89 if err != nil { 90 logger.Errorf("marshaling admission request response body: %v", err) 91 http.Error(res, fmt.Sprintf("%s: building response body", 92 http.StatusText(http.StatusInternalServerError)), http.StatusInternalServerError) 93 } 94 if _, err := res.Write(body); err != nil { 95 logger.Errorf("writing admission request response body: %v", err) 96 } 97 } 98 99 admissionReview := &admission.AdmissionReview{} 100 obj, _, err := codecFactory.UniversalDecoder().Decode(data, nil, admissionReview) 101 if err != nil { 102 finalise(admissionReview, errToAdmissionResponse(err)) 103 return 104 } 105 106 var ok bool 107 if admissionReview, ok = obj.(*admission.AdmissionReview); !ok { 108 finalise(admissionReview, 109 errToAdmissionResponse(errors.NewNotValid(nil, "converting admission request"))) 110 return 111 } 112 113 logger.Debugf("received admission request for %s of %s in namespace %s", 114 admissionReview.Request.Name, 115 admissionReview.Request.Kind, 116 admissionReview.Request.Namespace, 117 ) 118 119 reviewResponse := &admission.AdmissionResponse{ 120 Allowed: true, 121 } 122 123 for _, ignoreObjKind := range admissionObjectIgnores { 124 if compareAPIGroupVersionKind(ignoreObjKind, admissionReview.Request.Kind) { 125 logger.Debugf("ignoring admission request for gvk %s", ignoreObjKind) 126 finalise(admissionReview, reviewResponse) 127 return 128 } 129 } 130 131 appName, err := rbacMapper.AppNameForServiceAccount( 132 types.UID(admissionReview.Request.UserInfo.UID)) 133 if err != nil && !errors.IsNotFound(err) { 134 http.Error(res, fmt.Sprintf( 135 "could not determine if admission request belongs to juju: %v", err, 136 ), 137 http.StatusInternalServerError) 138 return 139 } else if errors.IsNotFound(err) { 140 finalise(admissionReview, reviewResponse) 141 return 142 } 143 144 metaObj := struct { 145 meta.ObjectMeta `json:"metadata,omitempty"` 146 }{} 147 148 err = json.Unmarshal(admissionReview.Request.Object.Raw, &metaObj) 149 if err != nil { 150 http.Error(res, 151 fmt.Sprintf("unmarshalling admission object from json: %v", err), 152 http.StatusInternalServerError) 153 return 154 } 155 156 patchJSON, err := json.Marshal( 157 patchForLabels(metaObj.Labels, appName, legacyLabels)) 158 if err != nil { 159 http.Error(res, 160 fmt.Sprintf("marshalling patch object to json: %v", err), 161 http.StatusInternalServerError) 162 return 163 } 164 165 patchType := admission.PatchTypeJSONPatch 166 reviewResponse.Patch = patchJSON 167 reviewResponse.PatchType = &patchType 168 finalise(admissionReview, reviewResponse) 169 }) 170 } 171 172 func compareGroupVersionKind(a, b *schema.GroupVersionKind) bool { 173 if a == nil || b == nil { 174 return false 175 } 176 return a.Group == b.Group && a.Version == b.Version && a.Kind == b.Kind 177 } 178 179 func errToAdmissionResponse(err error) *admission.AdmissionResponse { 180 return &admission.AdmissionResponse{ 181 Allowed: false, 182 Result: &meta.Status{ 183 Message: err.Error(), 184 }, 185 } 186 } 187 188 func patchEscape(s string) string { 189 r := strings.Replace(s, "~", "~0", -1) 190 r = strings.Replace(r, "/", "~1", -1) 191 return r 192 } 193 194 func patchForLabels( 195 labels map[string]string, 196 appName string, 197 legacyLabels bool) []patchOperation { 198 patches := []patchOperation{} 199 200 neededLabels := providerutils.LabelForKeyValue( 201 providerconst.LabelJujuAppCreatedBy, appName) 202 203 if len(labels) == 0 { 204 patches = append(patches, patchOperation{ 205 Op: addOp, 206 Path: "/metadata/labels", 207 Value: map[string]string{}, 208 }) 209 } 210 211 for k, v := range neededLabels { 212 if extVal, found := labels[k]; found && extVal != v { 213 patches = append(patches, patchOperation{ 214 Op: replaceOp, 215 Path: fmt.Sprintf("/metadata/labels/%s", patchEscape(k)), 216 Value: patchEscape(v), 217 }) 218 } else if !found { 219 patches = append(patches, patchOperation{ 220 Op: addOp, 221 Path: fmt.Sprintf("/metadata/labels/%s", patchEscape(k)), 222 Value: patchEscape(v), 223 }) 224 } 225 } 226 227 return patches 228 }