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  }