From 447bc08639d17e0f9f3afde9dae1240a6cad1406 Mon Sep 17 00:00:00 2001 From: hongming Date: Wed, 19 Mar 2025 12:31:33 +0800 Subject: [PATCH] feat: add resource protection webhook (#2168) Signed-off-by: hongming --- cmd/controller-manager/app/server.go | 2 + .../templates/post-patch-system-ns-job.yaml | 1 + config/ks-core/templates/webhook.yaml | 45 +++++++++++++++ config/ks-core/templates/workspace.yaml | 2 + pkg/constants/constants.go | 2 + .../resource_protection_webhook.go | 56 +++++++++++++++++++ 6 files changed, 108 insertions(+) create mode 100644 pkg/controller/resourceprotection/resource_protection_webhook.go diff --git a/cmd/controller-manager/app/server.go b/cmd/controller-manager/app/server.go index a28e2eefc..c9406cdac 100644 --- a/cmd/controller-manager/app/server.go +++ b/cmd/controller-manager/app/server.go @@ -40,6 +40,7 @@ import ( "kubesphere.io/kubesphere/pkg/controller/loginrecord" "kubesphere.io/kubesphere/pkg/controller/namespace" "kubesphere.io/kubesphere/pkg/controller/quota" + "kubesphere.io/kubesphere/pkg/controller/resourceprotection" "kubesphere.io/kubesphere/pkg/controller/role" "kubesphere.io/kubesphere/pkg/controller/rolebinding" "kubesphere.io/kubesphere/pkg/controller/roletemplate" @@ -120,6 +121,7 @@ func init() { // kubectl runtime.Must(controller.Register(&kubectl.Reconciler{})) runtime.Must(controller.Register(&serviceaccounttoken.Reconciler{})) + runtime.Must(controller.Register(&resourceprotection.Webhook{})) } func NewControllerManagerCommand() *cobra.Command { diff --git a/config/ks-core/templates/post-patch-system-ns-job.yaml b/config/ks-core/templates/post-patch-system-ns-job.yaml index bdc0586c4..9a357148d 100644 --- a/config/ks-core/templates/post-patch-system-ns-job.yaml +++ b/config/ks-core/templates/post-patch-system-ns-job.yaml @@ -26,6 +26,7 @@ spec: do kubectl label ns $ns kubesphere.io/workspace=system-workspace kubectl label ns $ns kubesphere.io/managed=true + kubectl label ns $ns kubesphere.io/protected-resource=true done kubectl get ns -l 'kubesphere.io/workspace,!kubesphere.io/managed' --no-headers -o custom-columns=NAME:.metadata.name | \ xargs -I {} kubectl label ns {} kubesphere.io/managed=true diff --git a/config/ks-core/templates/webhook.yaml b/config/ks-core/templates/webhook.yaml index 91077f954..426444a51 100644 --- a/config/ks-core/templates/webhook.yaml +++ b/config/ks-core/templates/webhook.yaml @@ -360,6 +360,51 @@ webhooks: sideEffects: None timeoutSeconds: 30 +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: protector.kubesphere.io +webhooks: + - admissionReviewVersions: + - v1 + clientConfig: + caBundle: {{ b64enc $ca.Cert | quote }} + service: + name: ks-controller-manager + namespace: {{ .Release.Namespace }} + path: /resource-protector + port: 443 + failurePolicy: Ignore + matchPolicy: Exact + name: protector.kubesphere.io + namespaceSelector: {} + objectSelector: + matchExpressions: + - key: kubesphere.io/protected-resource + operator: Exists + rules: + - apiGroups: + - "" + apiVersions: + - v1 + operations: + - DELETE + resources: + - namespaces + scope: '*' + - apiGroups: + - "tenant.kubesphere.io" + apiVersions: + - v1beta1 + operations: + - DELETE + resources: + - workspacetemplates + scope: '*' + sideEffects: None + timeoutSeconds: 30 + {{- if eq (include "multicluster.role" .) "host" }} --- apiVersion: admissionregistration.k8s.io/v1 diff --git a/config/ks-core/templates/workspace.yaml b/config/ks-core/templates/workspace.yaml index c89941710..e3b236dad 100644 --- a/config/ks-core/templates/workspace.yaml +++ b/config/ks-core/templates/workspace.yaml @@ -2,6 +2,8 @@ apiVersion: tenant.kubesphere.io/v1beta1 kind: WorkspaceTemplate metadata: + labels: + kubesphere.io/protected-resource: 'true' annotations: kubesphere.io/creator: admin kubesphere.io/description: "system-workspace is a built-in workspace automatically created by KubeSphere. It contains all system components to run KubeSphere." diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index aece012f9..a1af0c82c 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -8,6 +8,7 @@ package constants import corev1 "k8s.io/api/core/v1" const ( + SystemWorkspace = "system-workspace" KubeSystemNamespace = "kube-system" KubeSphereNamespace = "kubesphere-system" KubeSphereAPIServerName = "ks-apiserver" @@ -15,6 +16,7 @@ const ( KubeSphereConfigMapDataKey = "kubesphere.yaml" KubectlPodNamePrefix = "ks-managed-kubectl" + ProtectedResourceLabel = "kubesphere.io/protected-resource" WorkspaceLabelKey = "kubesphere.io/workspace" DisplayNameAnnotationKey = "kubesphere.io/alias-name" DescriptionAnnotationKey = "kubesphere.io/description" diff --git a/pkg/controller/resourceprotection/resource_protection_webhook.go b/pkg/controller/resourceprotection/resource_protection_webhook.go new file mode 100644 index 000000000..99a45f932 --- /dev/null +++ b/pkg/controller/resourceprotection/resource_protection_webhook.go @@ -0,0 +1,56 @@ +package resourceprotection + +import ( + "context" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "kubesphere.io/kubesphere/pkg/constants" + kscontroller "kubesphere.io/kubesphere/pkg/controller" +) + +const webhookName = "resource-protection-webhook" + +func (w *Webhook) Name() string { + return webhookName +} + +type Webhook struct { + client.Client +} + +func (w *Webhook) SetupWithManager(mgr *kscontroller.Manager) error { + w.Client = mgr.GetClient() + mgr.GetWebhookServer().Register("/resource-protector", &webhook.Admission{Handler: w}) + return nil +} + +func (w *Webhook) Handle(ctx context.Context, req admission.Request) admission.Response { + if req.Operation == admissionv1.Delete { + gvr := req.RequestResource + gvk, err := w.RESTMapper().KindFor(schema.GroupVersionResource{ + Group: gvr.Group, + Version: gvr.Version, + Resource: gvr.Resource, + }) + if err != nil { + return webhook.Errored(http.StatusInternalServerError, err) + } + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + if err = w.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, obj); err != nil { + return webhook.Errored(http.StatusInternalServerError, err) + } + + if obj.GetLabels()[constants.ProtectedResourceLabel] == "true" { + return webhook.Denied("this resource may not be deleted") + } + } + return admission.Allowed("") +}