From d48065f0860acf108fceb8ae96adddb0339249c6 Mon Sep 17 00:00:00 2001 From: Hanbo Li Date: Wed, 10 Mar 2021 09:36:32 +0800 Subject: [PATCH] add webhook for clusterpropagationpolicy and propagationpolicy (#201) Signed-off-by: lihanbo --- artifacts/deploy/webhook-configuration.yaml | 28 +++++++++ cmd/webhook/app/webhook.go | 3 + pkg/util/helper/policy.go | 43 +++++++++++++ .../clusterpropagationpolicy/mutating.go | 48 +++++++++++++++ .../clusterpropagationpolicy/validating.go | 61 +++++++++++++++++++ pkg/webhook/propagationpolicy/mutating.go | 14 +---- pkg/webhook/propagationpolicy/validating.go | 29 +++++---- 7 files changed, 201 insertions(+), 25 deletions(-) create mode 100644 pkg/util/helper/policy.go create mode 100644 pkg/webhook/clusterpropagationpolicy/mutating.go create mode 100644 pkg/webhook/clusterpropagationpolicy/validating.go diff --git a/artifacts/deploy/webhook-configuration.yaml b/artifacts/deploy/webhook-configuration.yaml index 92e64c1b3..58e2052f6 100644 --- a/artifacts/deploy/webhook-configuration.yaml +++ b/artifacts/deploy/webhook-configuration.yaml @@ -19,6 +19,20 @@ webhooks: sideEffects: None admissionReviewVersions: ["v1beta1"] timeoutSeconds: 3 + - name: clusterpropagationpolicy.karmada.io + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["policy.karmada.io"] + apiVersions: ["*"] + resources: ["clusterpropagationpolicies"] + scope: "Cluster" + clientConfig: + url: https://karmada-webhook.karmada-system.svc:443/mutate-clusterpropagationpolicy + caBundle: {{caBundle}} + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: ["v1beta1"] + timeoutSeconds: 3 - name: overridepolicy.karmada.io rules: - operations: ["CREATE", "UPDATE"] @@ -69,3 +83,17 @@ webhooks: sideEffects: None admissionReviewVersions: ["v1beta1"] timeoutSeconds: 3 + - name: clusterpropagationpolicy.karmada.io + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["policy.karmada.io"] + apiVersions: ["*"] + resources: ["clusterpropagationpolicies"] + scope: "Cluster" + clientConfig: + url: https://karmada-webhook.karmada-system.svc:443/validate-clusterpropagationpolicy + caBundle: {{caBundle}} + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: ["v1beta1"] + timeoutSeconds: 3 diff --git a/cmd/webhook/app/webhook.go b/cmd/webhook/app/webhook.go index 16237b641..1dd8b8c61 100644 --- a/cmd/webhook/app/webhook.go +++ b/cmd/webhook/app/webhook.go @@ -16,6 +16,7 @@ import ( "github.com/karmada-io/karmada/cmd/webhook/app/options" "github.com/karmada-io/karmada/pkg/util/gclient" "github.com/karmada-io/karmada/pkg/webhook/cluster" + "github.com/karmada-io/karmada/pkg/webhook/clusterpropagationpolicy" "github.com/karmada-io/karmada/pkg/webhook/overridepolicy" "github.com/karmada-io/karmada/pkg/webhook/propagationpolicy" ) @@ -69,6 +70,8 @@ func Run(opts *options.Options, stopChan <-chan struct{}) error { hookServer.Register("/validate-cluster", &webhook.Admission{Handler: &cluster.ValidatingAdmission{}}) hookServer.Register("/mutate-propagationpolicy", &webhook.Admission{Handler: &propagationpolicy.MutatingAdmission{}}) hookServer.Register("/validate-propagationpolicy", &webhook.Admission{Handler: &propagationpolicy.ValidatingAdmission{}}) + hookServer.Register("/mutate-clusterpropagationpolicy", &webhook.Admission{Handler: &clusterpropagationpolicy.MutatingAdmission{}}) + hookServer.Register("/validate-clusterpropagationpolicy", &webhook.Admission{Handler: &clusterpropagationpolicy.ValidatingAdmission{}}) hookServer.Register("/mutate-overridepolicy", &webhook.Admission{Handler: &overridepolicy.MutatingAdmission{}}) hookServer.WebhookMux.Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{})) diff --git a/pkg/util/helper/policy.go b/pkg/util/helper/policy.go new file mode 100644 index 000000000..67dd7fbc9 --- /dev/null +++ b/pkg/util/helper/policy.go @@ -0,0 +1,43 @@ +package helper + +import ( + "fmt" + + "k8s.io/klog/v2" + + policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" +) + +// DenyReasonResourceSelectorsModify constructs a reason indicating that modify ResourceSelectors is not allowed. +const DenyReasonResourceSelectorsModify = "modify ResourceSelectors is forbidden" + +// SetDefaultSpreadConstraints set default spread constraints if both 'SpreadByField' and 'SpreadByLabel' not set. +func SetDefaultSpreadConstraints(spreadConstraints []policyv1alpha1.SpreadConstraint) { + for i := range spreadConstraints { + if len(spreadConstraints[i].SpreadByLabel) == 0 && len(spreadConstraints[i].SpreadByField) == 0 { + klog.Infof("Setting default SpreadByField with %s", policyv1alpha1.SpreadByFieldCluster) + spreadConstraints[i].SpreadByField = policyv1alpha1.SpreadByFieldCluster + } + + if spreadConstraints[i].MinGroups == 0 { + klog.Infof("Setting default MinGroups to 1") + spreadConstraints[i].MinGroups = 1 + } + } +} + +// ValidateSpreadConstraint tests if the constraints is valid. +func ValidateSpreadConstraint(spreadConstraints []policyv1alpha1.SpreadConstraint) error { + // SpreadByField and SpreadByLabel should not co-exist + for _, constraint := range spreadConstraints { + if len(constraint.SpreadByField) > 0 && len(constraint.SpreadByLabel) > 0 { + return fmt.Errorf("invalid constraints: SpreadByLabel(%s) should not co-exist with spreadByField(%s)", constraint.SpreadByLabel, constraint.SpreadByField) + } + + // If MaxGroups provided, it should greater or equal than MinGroups. + if constraint.MaxGroups > 0 && constraint.MaxGroups < constraint.MinGroups { + return fmt.Errorf("maxGroups(%d) lower than minGroups(%d) is not allowed", constraint.MaxGroups, constraint.MinGroups) + } + } + return nil +} diff --git a/pkg/webhook/clusterpropagationpolicy/mutating.go b/pkg/webhook/clusterpropagationpolicy/mutating.go new file mode 100644 index 000000000..e6f53ab23 --- /dev/null +++ b/pkg/webhook/clusterpropagationpolicy/mutating.go @@ -0,0 +1,48 @@ +package clusterpropagationpolicy + +import ( + "context" + "encoding/json" + "net/http" + + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" + "github.com/karmada-io/karmada/pkg/util/helper" +) + +// MutatingAdmission mutates API request if necessary. +type MutatingAdmission struct { + decoder *admission.Decoder +} + +// Check if our MutatingAdmission implements necessary interface +var _ admission.Handler = &MutatingAdmission{} +var _ admission.DecoderInjector = &MutatingAdmission{} + +// Handle yields a response to an AdmissionRequest. +func (a *MutatingAdmission) Handle(ctx context.Context, req admission.Request) admission.Response { + policy := &policyv1alpha1.ClusterPropagationPolicy{} + + err := a.decoder.Decode(req, policy) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + // Set default spread constraints if both 'SpreadByField' and 'SpreadByLabel' not set. + helper.SetDefaultSpreadConstraints(policy.Spec.Placement.SpreadConstraints) + + marshaledBytes, err := json.Marshal(policy) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + return admission.PatchResponseFromRaw(req.Object.Raw, marshaledBytes) +} + +// InjectDecoder implements admission.DecoderInjector interface. +// A decoder will be automatically injected. +func (a *MutatingAdmission) InjectDecoder(d *admission.Decoder) error { + a.decoder = d + return nil +} diff --git a/pkg/webhook/clusterpropagationpolicy/validating.go b/pkg/webhook/clusterpropagationpolicy/validating.go new file mode 100644 index 000000000..521c79280 --- /dev/null +++ b/pkg/webhook/clusterpropagationpolicy/validating.go @@ -0,0 +1,61 @@ +package clusterpropagationpolicy + +import ( + "context" + "net/http" + + "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" + "github.com/karmada-io/karmada/pkg/util/helper" +) + +// ValidatingAdmission validates ClusterPropagationPolicy object when creating/updating/deleting. +type ValidatingAdmission struct { + decoder *admission.Decoder +} + +// Check if our ValidatingAdmission implements necessary interface +var _ admission.Handler = &ValidatingAdmission{} +var _ admission.DecoderInjector = &ValidatingAdmission{} + +// Handle implements admission.Handler interface. +// It yields a response to an AdmissionRequest. +func (v *ValidatingAdmission) Handle(ctx context.Context, req admission.Request) admission.Response { + policy := &policyv1alpha1.ClusterPropagationPolicy{} + + err := v.decoder.Decode(req, policy) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + klog.V(2).Infof("Validating ClusterPropagationPolicy(%s) for request: %s", policy.Name, req.Operation) + + if req.Operation == v1beta1.Update { + oldPolicy := &policyv1alpha1.ClusterPropagationPolicy{} + err := v.decoder.DecodeRaw(req.OldObject, oldPolicy) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + if !equality.Semantic.DeepEqual(policy.Spec.ResourceSelectors, oldPolicy.Spec.ResourceSelectors) { + klog.Info(helper.DenyReasonResourceSelectorsModify) + return admission.Denied(helper.DenyReasonResourceSelectorsModify) + } + } + + if err := helper.ValidateSpreadConstraint(policy.Spec.Placement.SpreadConstraints); err != nil { + klog.Info(err) + return admission.Denied(err.Error()) + } + + return admission.Allowed("") +} + +// InjectDecoder implements admission.DecoderInjector interface. +// A decoder will be automatically injected. +func (v *ValidatingAdmission) InjectDecoder(d *admission.Decoder) error { + v.decoder = d + return nil +} diff --git a/pkg/webhook/propagationpolicy/mutating.go b/pkg/webhook/propagationpolicy/mutating.go index 186315353..9f8b8da64 100644 --- a/pkg/webhook/propagationpolicy/mutating.go +++ b/pkg/webhook/propagationpolicy/mutating.go @@ -9,6 +9,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" + "github.com/karmada-io/karmada/pkg/util/helper" ) // MutatingAdmission mutates API request if necessary. @@ -38,18 +39,7 @@ func (a *MutatingAdmission) Handle(ctx context.Context, req admission.Request) a } // Set default spread constraints if both 'SpreadByField' and 'SpreadByLabel' not set. - spreadConstraints := policy.Spec.Placement.SpreadConstraints - for i := range spreadConstraints { - if len(spreadConstraints[i].SpreadByLabel) == 0 && len(spreadConstraints[i].SpreadByField) == 0 { - klog.Infof("Setting default SpreadByField with %s", policyv1alpha1.SpreadByFieldCluster) - spreadConstraints[i].SpreadByField = policyv1alpha1.SpreadByFieldCluster - } - - if spreadConstraints[i].MinGroups == 0 { - klog.Infof("Setting default MinGroups to 1") - spreadConstraints[i].MinGroups = 1 - } - } + helper.SetDefaultSpreadConstraints(policy.Spec.Placement.SpreadConstraints) marshaledBytes, err := json.Marshal(policy) if err != nil { diff --git a/pkg/webhook/propagationpolicy/validating.go b/pkg/webhook/propagationpolicy/validating.go index 72a071fc6..b27662223 100644 --- a/pkg/webhook/propagationpolicy/validating.go +++ b/pkg/webhook/propagationpolicy/validating.go @@ -2,13 +2,15 @@ package propagationpolicy import ( "context" - "fmt" "net/http" + "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/api/equality" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" + "github.com/karmada-io/karmada/pkg/util/helper" ) // ValidatingAdmission validates PropagationPolicy object when creating/updating/deleting. @@ -31,20 +33,21 @@ func (v *ValidatingAdmission) Handle(ctx context.Context, req admission.Request) } klog.V(2).Infof("Validating PropagationPolicy(%s/%s) for request: %s", policy.Namespace, policy.Name, req.Operation) - // SpreadByField and SpreadByLabel should not co-exist - for _, constraint := range policy.Spec.Placement.SpreadConstraints { - if len(constraint.SpreadByField) > 0 && len(constraint.SpreadByLabel) > 0 { - errMsg := fmt.Sprintf("invalid constraints: SpreadByLabel(%s) should not co-exist with spreadByField(%s)", constraint.SpreadByLabel, constraint.SpreadByField) - klog.Info(errMsg) - return admission.Denied(errMsg) + if req.Operation == v1beta1.Update { + oldPolicy := &policyv1alpha1.PropagationPolicy{} + err := v.decoder.DecodeRaw(req.OldObject, oldPolicy) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) } + if !equality.Semantic.DeepEqual(policy.Spec.ResourceSelectors, oldPolicy.Spec.ResourceSelectors) { + klog.Info(helper.DenyReasonResourceSelectorsModify) + return admission.Denied(helper.DenyReasonResourceSelectorsModify) + } + } - // If MaxGroups provided, it should greater or equal than MinGroups. - if constraint.MaxGroups > 0 && constraint.MaxGroups < constraint.MinGroups { - errMsg := fmt.Sprintf("maxGroups(%d) lower than minGroups(%d) is not allowed", constraint.MaxGroups, constraint.MinGroups) - klog.Info(errMsg) - return admission.Denied(errMsg) - } + if err := helper.ValidateSpreadConstraint(policy.Spec.Placement.SpreadConstraints); err != nil { + klog.Info(err) + return admission.Denied(err.Error()) } return admission.Allowed("")