/* * This file is part of the CDI project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Copyright 2019 Red Hat, Inc. * */ package webhooks import ( "crypto/rsa" "encoding/json" "fmt" "io/ioutil" "net/http" "time" "github.com/appscode/jsonpatch" admissionv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" cdiv1alpha1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1alpha1" cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1beta1" cdiclient "kubevirt.io/containerized-data-importer/pkg/client/clientset/versioned" "kubevirt.io/containerized-data-importer/pkg/common" "kubevirt.io/containerized-data-importer/pkg/token" ) // Admitter is the interface implemented by admission webhooks type Admitter interface { Admit(admissionv1.AdmissionReview) *admissionv1.AdmissionResponse } type admissionHandler struct { a Admitter } // NewDataVolumeValidatingWebhook creates a new DataVolumeValidation webhook func NewDataVolumeValidatingWebhook(k8sClient kubernetes.Interface, cdiClient cdiclient.Interface) http.Handler { return newAdmissionHandler(&dataVolumeValidatingWebhook{k8sClient: k8sClient, cdiClient: cdiClient}) } // NewDataVolumeMutatingWebhook creates a new DataVolumeMutation webhook func NewDataVolumeMutatingWebhook(k8sClient kubernetes.Interface, cdiClient cdiclient.Interface, key *rsa.PrivateKey) http.Handler { generator := newCloneTokenGenerator(key) return newAdmissionHandler(&dataVolumeMutatingWebhook{k8sClient: k8sClient, cdiClient: cdiClient, tokenGenerator: generator, proxy: &sarProxy{client: k8sClient}}) } // NewCDIValidatingWebhook creates a new CDI validating webhook func NewCDIValidatingWebhook(client cdiclient.Interface) http.Handler { return newAdmissionHandler(&cdiValidatingWebhook{client: client}) } // NewObjectTransferValidatingWebhook creates a new ObjectTransfer validating webhook func NewObjectTransferValidatingWebhook(k8sClient kubernetes.Interface, cdiClient cdiclient.Interface) http.Handler { return newAdmissionHandler(&objectTransferValidatingWebhook{k8sClient: k8sClient, cdiClient: cdiClient}) } func newCloneTokenGenerator(key *rsa.PrivateKey) token.Generator { return token.NewGenerator(common.CloneTokenIssuer, key, 5*time.Minute) } func newAdmissionHandler(a Admitter) http.Handler { return &admissionHandler{a: a} } func (h *admissionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var body []byte if r.Body != nil { if data, err := ioutil.ReadAll(r.Body); err == nil { body = data } } // verify the content type is accurate contentType := r.Header.Get("Content-Type") if contentType != "application/json" { klog.Errorf("contentType=%s, expect application/json", contentType) return } klog.V(2).Info(fmt.Sprintf("handling request: %s", body)) // The AdmissionReview that was sent to the webhook requestedAdmissionReview := admissionv1.AdmissionReview{} // The AdmissionReview that will be returned responseAdmissionReview := admissionv1.AdmissionReview{ TypeMeta: metav1.TypeMeta{ APIVersion: admissionv1.SchemeGroupVersion.String(), Kind: "AdmissionReview", }, } deserializer := codecs.UniversalDeserializer() if _, _, err := deserializer.Decode(body, nil, &requestedAdmissionReview); err != nil { klog.Error(err) responseAdmissionReview.Response = toAdmissionResponseError(err) } else { if requestedAdmissionReview.Request == nil { responseAdmissionReview.Response = toAdmissionResponseError(fmt.Errorf("AdmissionReview.Request is nil")) } else { // pass to Admitter responseAdmissionReview.Response = h.a.Admit(requestedAdmissionReview) } } // Return the same UID if requestedAdmissionReview.Request != nil { responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID } // Match request's APIVersion for backwards compatibility with v1beta1 if requestedAdmissionReview.APIVersion != "" { responseAdmissionReview.APIVersion = requestedAdmissionReview.APIVersion } klog.V(2).Info(fmt.Sprintf("sending response: %v", responseAdmissionReview.Response)) respBytes, err := json.Marshal(responseAdmissionReview) if err != nil { klog.Error(err) } if _, err := w.Write(respBytes); err != nil { klog.Error(err) } } func toRejectedAdmissionResponse(causes []metav1.StatusCause) *admissionv1.AdmissionResponse { globalMessage := "" for _, cause := range causes { globalMessage = fmt.Sprintf("%s %s", globalMessage, cause.Message) } return &admissionv1.AdmissionResponse{ Result: &metav1.Status{ Message: globalMessage, Code: http.StatusUnprocessableEntity, Details: &metav1.StatusDetails{ Causes: causes, }, }, } } func toAdmissionResponseError(err error) *admissionv1.AdmissionResponse { return &admissionv1.AdmissionResponse{ Result: &metav1.Status{ Message: err.Error(), Code: http.StatusBadRequest, }, } } func allowedAdmissionResponse() *admissionv1.AdmissionResponse { return &admissionv1.AdmissionResponse{ Allowed: true, } } func validateDataVolumeResource(ar admissionv1.AdmissionReview) error { resources := []metav1.GroupVersionResource{ { Group: cdiv1.SchemeGroupVersion.Group, Version: cdiv1.SchemeGroupVersion.Version, Resource: "datavolumes", }, { Group: cdiv1alpha1.SchemeGroupVersion.Group, Version: cdiv1alpha1.SchemeGroupVersion.Version, Resource: "datavolumes", }, } for _, resource := range resources { if ar.Request.Resource == resource { return nil } } klog.Errorf("resource is %s but request is: %s", resources[0], ar.Request.Resource) return fmt.Errorf("expect resource to be '%s'", resources[0].Resource) } func toPatchResponse(original, current interface{}) *admissionv1.AdmissionResponse { patchType := admissionv1.PatchTypeJSONPatch ob, err := json.Marshal(original) if err != nil { return toAdmissionResponseError(err) } cb, err := json.Marshal(current) if err != nil { return toAdmissionResponseError(err) } patches, err := jsonpatch.CreatePatch(ob, cb) if err != nil { toAdmissionResponseError(err) } pb, err := json.Marshal(patches) if err != nil { return toAdmissionResponseError(err) } klog.V(3).Infof("sending patches\n%s", pb) return &admissionv1.AdmissionResponse{ Allowed: true, Patch: pb, PatchType: &patchType, } }