diff --git a/artifacts/deploy/webhook-configuration.yaml b/artifacts/deploy/webhook-configuration.yaml index ad6618c1814e..2db21903da83 100644 --- a/artifacts/deploy/webhook-configuration.yaml +++ b/artifacts/deploy/webhook-configuration.yaml @@ -237,3 +237,20 @@ webhooks: sideEffects: None admissionReviewVersions: [ "v1" ] timeoutSeconds: 3 + - name: resourcedeletionprotection.karmada.io + rules: + - operations: ["DELETE"] + apiGroups: ["*"] + apiVersions: ["*"] + resources: ["*"] + scope: "*" + clientConfig: + url: https://karmada-webhook.karmada-system.svc:443/validate-resourcedeletionprotection + caBundle: {{caBundle}} + objectSelector: + matchExpressions: + - { key: "resourcetemplate.karmada.io/deletion-protected", operator: "Exists" } + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: [ "v1" ] + timeoutSeconds: 3 diff --git a/charts/karmada/templates/_karmada_webhook_configuration.tpl b/charts/karmada/templates/_karmada_webhook_configuration.tpl index 423935486b6c..3c7cd1b0df73 100644 --- a/charts/karmada/templates/_karmada_webhook_configuration.tpl +++ b/charts/karmada/templates/_karmada_webhook_configuration.tpl @@ -213,4 +213,21 @@ webhooks: sideEffects: None admissionReviewVersions: [ "v1" ] timeoutSeconds: 3 + - name: resourcedeletionprotection.karmada.io + rules: + - operations: ["DELETE"] + apiGroups: ["*"] + apiVersions: ["*"] + resources: ["*"] + scope: "*" + clientConfig: + url: https://{{ $name }}-webhook.{{ $namespace }}.svc:443/validate-resourcedeletionprotection + {{- include "karmada.webhook.caBundle" . | nindent 6 }} + objectSelector: + matchExpressions: + - { key: "resourcetemplate.karmada.io/deletion-protected", operator: "Exists" } + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: [ "v1" ] + timeoutSeconds: 3 {{- end -}} diff --git a/cmd/webhook/app/webhook.go b/cmd/webhook/app/webhook.go index d61980112c57..2dcb29da984d 100644 --- a/cmd/webhook/app/webhook.go +++ b/cmd/webhook/app/webhook.go @@ -33,6 +33,7 @@ import ( "github.com/karmada-io/karmada/pkg/webhook/multiclusterservice" "github.com/karmada-io/karmada/pkg/webhook/overridepolicy" "github.com/karmada-io/karmada/pkg/webhook/propagationpolicy" + "github.com/karmada-io/karmada/pkg/webhook/resourcedeletionprotection" "github.com/karmada-io/karmada/pkg/webhook/resourceinterpretercustomization" "github.com/karmada-io/karmada/pkg/webhook/work" ) @@ -139,6 +140,7 @@ func Run(ctx context.Context, opts *options.Options) error { hookServer.Register("/validate-multiclusteringress", &webhook.Admission{Handler: &multiclusteringress.ValidatingAdmission{Decoder: decoder}}) hookServer.Register("/validate-multiclusterservice", &webhook.Admission{Handler: &multiclusterservice.ValidatingAdmission{Decoder: decoder}}) hookServer.Register("/mutate-federatedhpa", &webhook.Admission{Handler: &federatedhpa.MutatingAdmission{Decoder: decoder}}) + hookServer.Register("/validate-resourcedeletionprotection", &webhook.Admission{Handler: &resourcedeletionprotection.ValidatingAdmission{Decoder: decoder}}) hookServer.WebhookMux().Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{})) // blocks until the context is done. diff --git a/operator/pkg/karmadaresource/webhookconfiguration/manifests.go b/operator/pkg/karmadaresource/webhookconfiguration/manifests.go index abf12dd670f9..942519d1090b 100644 --- a/operator/pkg/karmadaresource/webhookconfiguration/manifests.go +++ b/operator/pkg/karmadaresource/webhookconfiguration/manifests.go @@ -245,5 +245,22 @@ webhooks: sideEffects: None admissionReviewVersions: [ "v1" ] timeoutSeconds: 3 + - name: resourcedeletionprotection.karmada.io + rules: + - operations: ["DELETE"] + apiGroups: ["*"] + apiVersions: ["*"] + resources: ["*"] + scope: "*" + clientConfig: + url: https://{{ .Service }}.{{ .Namespace }}.svc:443/validate-resourcedeletionprotection + caBundle: {{ .CaBundle }} + objectSelector: + matchExpressions: + - { key: "resourcetemplate.karmada.io/deletion-protected", operator: "Exists" } + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: [ "v1" ] + timeoutSeconds: 3 ` ) diff --git a/pkg/apis/work/v1alpha2/well_known_constants.go b/pkg/apis/work/v1alpha2/well_known_constants.go index 237a1f85a902..596208d6b5cd 100644 --- a/pkg/apis/work/v1alpha2/well_known_constants.go +++ b/pkg/apis/work/v1alpha2/well_known_constants.go @@ -94,6 +94,14 @@ const ( // E.g. "resourcetemplate.karmada.io/managed-annotations: bar,foo". // Note: the keys will be sorted in alphabetical order. ManagedAnnotation = "resourcetemplate.karmada.io/managed-annotations" + + // DeletionProtectionLabelKey If a user assigns the DeletionProtectionLabelKey label to a specific resource, + // and the value of this label is DeletionProtectionAlways, then deletion requests + // for this resource will be denied. + // In the current design, only the Value set to 'Always' will be protected, + // Additional options will be added here in the future. + DeletionProtectionLabelKey = "resourcetemplate.karmada.io/deletion-protected" + DeletionProtectionAlways = "Always" ) // Define eviction reasons. diff --git a/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go b/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go index ccccd6385434..786db9780dfe 100644 --- a/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go +++ b/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go @@ -256,6 +256,23 @@ webhooks: sideEffects: None admissionReviewVersions: [ "v1" ] timeoutSeconds: 3 + - name: resourcedeletionprotection.karmada.io + rules: + - operations: ["DELETE"] + apiGroups: ["*"] + apiVersions: ["*"] + resources: ["*"] + scope: "*" + clientConfig: + url: https://karmada-webhook.%[1]s.svc:443/validate-resourcedeletionprotection + caBundle: %[2]s + objectSelector: + matchExpressions: + - { key: "resourcetemplate.karmada.io/deletion-protected", operator: "Exists" } + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: [ "v1" ] + timeoutSeconds: 3 `, systemNamespace, caBundle) } diff --git a/pkg/webhook/cronfederatedhpa/validation.go b/pkg/webhook/cronfederatedhpa/validating.go similarity index 100% rename from pkg/webhook/cronfederatedhpa/validation.go rename to pkg/webhook/cronfederatedhpa/validating.go diff --git a/pkg/webhook/federatedhpa/validation.go b/pkg/webhook/federatedhpa/validating.go similarity index 100% rename from pkg/webhook/federatedhpa/validation.go rename to pkg/webhook/federatedhpa/validating.go diff --git a/pkg/webhook/resourcedeletionprotection/validating.go b/pkg/webhook/resourcedeletionprotection/validating.go new file mode 100755 index 000000000000..020e312b478c --- /dev/null +++ b/pkg/webhook/resourcedeletionprotection/validating.go @@ -0,0 +1,61 @@ +/* +Copyright 2023 The Karmada Authors. +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. +*/ + +package resourcedeletionprotection + +import ( + "context" + "fmt" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" +) + +// ValidatingAdmission validates resource templates to ensure those protected resources are not delectable. +type ValidatingAdmission struct { + Decoder *admission.Decoder +} + +// Check if our ValidatingAdmission implements necessary interface +var _ admission.Handler = &ValidatingAdmission{} + +// Handle implements admission.Handler interface. +// It yields a response to an AdmissionRequest. +func (v *ValidatingAdmission) Handle(_ context.Context, req admission.Request) admission.Response { + if req.Operation != admissionv1.Delete { + // We only care about the Delete operation. + return admission.Allowed("") + } + + // Parse the uncertain type resource object + obj := &unstructured.Unstructured{} + if err := v.Decoder.DecodeRaw(req.OldObject, obj); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + klog.V(2).Infof("Validating ResourceDeletionProtection for resource: Kind:%s Name:%s Namespace:%s", req.Kind.Kind, obj.GetName(), obj.GetNamespace()) + + if value, ok := obj.GetLabels()[workv1alpha2.DeletionProtectionLabelKey]; ok { + // In normal, requests will be processed here. + // Only v1alpha2.DeletionProtectionAlways value will be denied + if value == workv1alpha2.DeletionProtectionAlways { + return admission.Denied(fmt.Sprintf("This resource is protected, please make sure to remove the label: %s", workv1alpha2.DeletionProtectionLabelKey)) + } + } + return admission.Allowed("") +}