package webhook import ( "encoding/json" "fmt" "net/http" workv1 "github.com/open-cluster-management/api/work/v1" admissionv1beta1 "k8s.io/api/admission/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" "k8s.io/klog" ) var manifestLimit = 10 // ManifestWorkAdmissionHook will validate the creating/updating manifestwork request. type ManifestWorkAdmissionHook struct{} // ValidatingResource is called by generic-admission-server on startup to register the returned REST resource through which the // webhook is accessed by the kube apiserver. func (a *ManifestWorkAdmissionHook) ValidatingResource() (plural schema.GroupVersionResource, singular string) { return schema.GroupVersionResource{ Group: "admission.work.open-cluster-management.io", Version: "v1", Resource: "manifestworkvalidators", }, "manifestworkvalidators" } // Validate is called by generic-admission-server when the registered REST resource above is called with an admission request. func (a *ManifestWorkAdmissionHook) Validate(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { klog.V(4).Infof("validate %q operation for object %q", admissionSpec.Operation, admissionSpec.Object) status := &admissionv1beta1.AdmissionResponse{} // only validate the request for manifestwork if admissionSpec.Resource.Group != "work.open-cluster-management.io" || admissionSpec.Resource.Resource != "manifestworks" { status.Allowed = true return status } switch admissionSpec.Operation { case admissionv1beta1.Create: return a.validateRequest(admissionSpec) case admissionv1beta1.Update: return a.validateRequest(admissionSpec) default: status.Allowed = true return status } } // Initialize is called by generic-admission-server on startup to setup initialization that managedclusters webhook needs. func (a *ManifestWorkAdmissionHook) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { return nil } // validateCreateRequest validates create managed cluster operation func (a *ManifestWorkAdmissionHook) validateRequest(request *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { status := &admissionv1beta1.AdmissionResponse{} // validate ManagedCluster object firstly err := a.validateManifestWorkObj(request.Object) if err != nil { status.Allowed = false status.Result = &metav1.Status{ Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest, Message: err.Error(), } return status } status.Allowed = true return status } // validateManagedClusterObj validates the fileds of ManagedCluster object func (a *ManifestWorkAdmissionHook) validateManifestWorkObj(requestObj runtime.RawExtension) error { work := &workv1.ManifestWork{} if err := json.Unmarshal(requestObj.Raw, work); err != nil { return err } if len(work.Spec.Workload.Manifests) == 0 { return fmt.Errorf("manifests should not be empty") } if len(work.Spec.Workload.Manifests) > manifestLimit { return fmt.Errorf("number of manifests should not be larger than %d", manifestLimit) } for _, manifest := range work.Spec.Workload.Manifests { err := a.validateManifest(manifest.Raw) if err != nil { return err } } return nil } func (a *ManifestWorkAdmissionHook) validateManifest(manifest []byte) error { // If the manifest cannot be decoded, return err unstructuredObj := &unstructured.Unstructured{} err := unstructuredObj.UnmarshalJSON(manifest) if err != nil { return err } // The object must have name specified, generateName is not allowed in manifestwork if unstructuredObj.GetName() == "" { return fmt.Errorf("name must be set in manifest") } if unstructuredObj.GetGenerateName() != "" { return fmt.Errorf("generateName must not be set in manifest") } return nil }