You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@seata.apache.org by ji...@apache.org on 2025/12/04 02:17:05 UTC

(incubator-seata-k8s) branch master updated: feature: support ValidatingWebhookConfiguration (#42)

This is an automated email from the ASF dual-hosted git repository.

jimin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-k8s.git


The following commit(s) were added to refs/heads/master by this push:
     new 8bf4183  feature: support ValidatingWebhookConfiguration (#42)
8bf4183 is described below

commit 8bf4183d592f16ca4a5ba21147f13e3641eecf35
Author: jimin <sl...@163.com>
AuthorDate: Thu Dec 4 10:17:01 2025 +0800

    feature: support ValidatingWebhookConfiguration (#42)
---
 helm/seata-server/templates/validatingwebhook.yaml |  87 +++++
 .../templates/webhook-certificate.yaml             |  77 ++++
 helm/seata-server/templates/webhook-rbac.yaml      | 105 +++++
 .../{values.yaml => values-webhook-enabled.yaml}   | 111 +++---
 helm/seata-server/values.yaml                      |  46 +++
 pkg/webhook/validating_webhook.go                  | 354 +++++++++++++++++
 pkg/webhook/validating_webhook_test.go             | 424 +++++++++++++++++++++
 7 files changed, 1148 insertions(+), 56 deletions(-)

diff --git a/helm/seata-server/templates/validatingwebhook.yaml b/helm/seata-server/templates/validatingwebhook.yaml
new file mode 100644
index 0000000..fa147ca
--- /dev/null
+++ b/helm/seata-server/templates/validatingwebhook.yaml
@@ -0,0 +1,87 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+{{- if and .Values.webhook.enabled (eq .Values.seataServer.mode "cluster") }}
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  name: {{ include "seata-server.fullname" . }}-validating-webhook
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+webhooks:
+  # Webhook for validating SeataServer creation and updates
+  - name: seataserver.operator.seata.apache.org
+    clientConfig:
+      service:
+        name: {{ include "seata-server.fullname" . }}-webhook
+        namespace: {{ .Release.Namespace }}
+        path: /validate-operator-seata-apache-org-v1-seataserver
+        port: {{ .Values.webhook.port }}
+      caBundle: {{ .Values.webhook.caBundle | b64enc }}
+    rules:
+      - operations: ["CREATE", "UPDATE"]
+        apiGroups: ["operator.seata.apache.org"]
+        apiVersions: ["v1", "v1alpha1"]
+        resources: ["seataservers"]
+        scope: "*"
+    admissionReviewVersions: ["v1"]
+    sideEffects: None
+    timeoutSeconds: 10
+    failurePolicy: {{ .Values.webhook.failurePolicy }}
+    namespaceSelector:
+      matchLabels:
+        {{- toYaml .Values.webhook.namespaceSelector.matchLabels | nindent 8 }}
+    {{- if .Values.webhook.namespaceSelector.matchExpressions }}
+      matchExpressions:
+        {{- toYaml .Values.webhook.namespaceSelector.matchExpressions | nindent 8 }}
+    {{- end }}
+    objectSelector: {}
+    reinvocationPolicy: {{ .Values.webhook.reinvocationPolicy }}
+
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+spec:
+  ports:
+    - name: webhook
+      port: {{ .Values.webhook.port }}
+      protocol: TCP
+      targetPort: {{ .Values.webhook.containerPort }}
+  selector:
+    {{- include "seata-server.selectorLabels" . | nindent 4 }}
+  type: ClusterIP
+
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+
+{{- end }}
+
diff --git a/helm/seata-server/templates/webhook-certificate.yaml b/helm/seata-server/templates/webhook-certificate.yaml
new file mode 100644
index 0000000..525d559
--- /dev/null
+++ b/helm/seata-server/templates/webhook-certificate.yaml
@@ -0,0 +1,77 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+{{- if and .Values.webhook.enabled (eq .Values.seataServer.mode "cluster") }}
+{{- if .Values.webhook.certManager.enabled }}
+# Self-signed certificate issuer for webhook
+apiVersion: cert-manager.io/v1
+kind: Issuer
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook-selfsigned-issuer
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+spec:
+  selfSigned: {}
+
+---
+# Certificate for webhook TLS
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook-certs
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+spec:
+  secretName: {{ include "seata-server.fullname" . }}-webhook-certs
+  duration: 2160h # 90 days
+  renewBefore: 720h # 30 days
+  commonName: {{ include "seata-server.fullname" . }}-webhook.{{ .Release.Namespace }}.svc
+  dnsNames:
+    - {{ include "seata-server.fullname" . }}-webhook
+    - {{ include "seata-server.fullname" . }}-webhook.{{ .Release.Namespace }}
+    - {{ include "seata-server.fullname" . }}-webhook.{{ .Release.Namespace }}.svc
+    - {{ include "seata-server.fullname" . }}-webhook.{{ .Release.Namespace }}.svc.cluster.local
+  issuerRef:
+    name: {{ include "seata-server.fullname" . }}-webhook-selfsigned-issuer
+    kind: Issuer
+
+{{- else }}
+# Self-signed certificate (generated manually or via Job)
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook-certs
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+type: kubernetes.io/tls
+data:
+  {{- if .Values.webhook.tls.crt }}
+  tls.crt: {{ .Values.webhook.tls.crt }}
+  {{- end }}
+  {{- if .Values.webhook.tls.key }}
+  tls.key: {{ .Values.webhook.tls.key }}
+  {{- end }}
+
+{{- end }}
+{{- end }}
+
diff --git a/helm/seata-server/templates/webhook-rbac.yaml b/helm/seata-server/templates/webhook-rbac.yaml
new file mode 100644
index 0000000..f332b18
--- /dev/null
+++ b/helm/seata-server/templates/webhook-rbac.yaml
@@ -0,0 +1,105 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+#
+
+{{- if and .Values.webhook.enabled (eq .Values.seataServer.mode "cluster") }}
+# Webhook ClusterRole for SeataServer validation
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+rules:
+  # Allow reading SeataServer resources
+  - apiGroups: ["operator.seata.apache.org"]
+    resources: ["seataservers"]
+    verbs: ["get", "list", "watch"]
+  # Allow reading SeataServer status
+  - apiGroups: ["operator.seata.apache.org"]
+    resources: ["seataservers/status"]
+    verbs: ["get"]
+  # Allow reading core API resources needed for validation
+  - apiGroups: [""]
+    resources: ["services", "persistentvolumeclaims", "configmaps"]
+    verbs: ["get", "list"]
+  # Allow reading storage class information
+  - apiGroups: ["storage.k8s.io"]
+    resources: ["storageclasses"]
+    verbs: ["get", "list"]
+
+---
+# Webhook ClusterRoleBinding for SeataServer validation
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: {{ include "seata-server.fullname" . }}-webhook
+subjects:
+  - kind: ServiceAccount
+    name: {{ include "seata-server.fullname" . }}-webhook
+    namespace: {{ .Release.Namespace }}
+
+---
+# Role for webhook to manage its own resources in the namespace
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+rules:
+  # Allow reading secrets (for webhook certificates)
+  - apiGroups: [""]
+    resources: ["secrets"]
+    verbs: ["get", "list", "watch"]
+    resourceNames: 
+      - {{ include "seata-server.fullname" . }}-webhook-certs
+  # Allow reading configmaps
+  - apiGroups: [""]
+    resources: ["configmaps"]
+    verbs: ["get", "list", "watch"]
+
+---
+# RoleBinding for webhook namespace-scoped permissions
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ include "seata-server.fullname" . }}-webhook
+  namespace: {{ .Release.Namespace }}
+  labels:
+    {{- include "seata-server.labels" . | nindent 4 }}
+    app.kubernetes.io/component: webhook
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ include "seata-server.fullname" . }}-webhook
+subjects:
+  - kind: ServiceAccount
+    name: {{ include "seata-server.fullname" . }}-webhook
+    namespace: {{ .Release.Namespace }}
+
+{{- end }}
+
diff --git a/helm/seata-server/values.yaml b/helm/seata-server/values-webhook-enabled.yaml
similarity index 62%
copy from helm/seata-server/values.yaml
copy to helm/seata-server/values-webhook-enabled.yaml
index c9bbc67..e977e38 100644
--- a/helm/seata-server/values.yaml
+++ b/helm/seata-server/values-webhook-enabled.yaml
@@ -15,32 +15,23 @@
 # limitations under the License.
 #
 
-# Default values for seata-server Helm chart
+# Example values for deploying Seata Server with ValidatingWebhook enabled
+# Usage: helm install seata-server ./seata-server -f values-webhook-enabled.yaml
 
-# Global configuration
 global:
   namespace: default
-  
-# Seata Server configuration
+
 seataServer:
-  # Deployment name
   name: seata-server
-  
-  # Deployment mode: cluster or standalone
-  # cluster: Use SeataServer CRD (requires seata-k8s operator)
-  # standalone: Use Deployment directly
   mode: cluster
   
-  # Image configuration
   image:
     repository: apache/seata-server
     tag: latest
     pullPolicy: IfNotPresent
   
-  # Replica configuration
   replicas: 3
   
-  # Service configuration
   service:
     name: seata-server-cluster
     type: ClusterIP
@@ -61,7 +52,6 @@ seataServer:
         protocol: TCP
         name: raft
   
-  # NodePort Service (for standalone mode)
   nodePort:
     enabled: false
     type: NodePort
@@ -69,7 +59,6 @@ seataServer:
       service: 30091
       console: 30092
   
-  # Resource configuration
   resources:
     requests:
       cpu: 100m
@@ -78,24 +67,19 @@ seataServer:
       cpu: 500m
       memory: 512Mi
   
-  # Persistence configuration
   persistence:
     enabled: true
-    storageClassName: ""  # Leave empty to use default
+    storageClassName: ""
     accessMode: ReadWriteOnce
     size: 5Gi
-    # Volume reclaim policy: Delete or Retain
     reclaimPolicy: Retain
   
-  # Ports configuration
   ports:
     console: 7091
     service: 8091
     raft: 9091
   
-  # Environment variables
   env:
-    # Console credentials
     - name: console.user.username
       value: seata
     - name: console.user.password
@@ -103,7 +87,6 @@ seataServer:
         secretKeyRef:
           name: seata-credentials
           key: password
-    # Store configuration
     - name: store.mode
       value: raft
     - name: server.port
@@ -111,48 +94,26 @@ seataServer:
     - name: server.servicePort
       value: "8091"
 
-# Secret configuration
 secret:
-  # Create secret automatically
   create: true
-  # Secret name
   name: seata-credentials
-  # Password value (base64 encoded in Kubernetes)
   password: seata123456
 
-# PodSecurityPolicy configuration
 podSecurityPolicy:
   enabled: false
 
-# Network Policy configuration
 networkPolicy:
   enabled: false
 
-# Ingress configuration
 ingress:
   enabled: false
-  className: ""
-  annotations: {}
-    # kubernetes.io/ingress.class: nginx
-    # cert-manager.io/cluster-issuer: letsencrypt-prod
-  hosts:
-    - host: seata.example.com
-      paths:
-        - path: /
-          pathType: Prefix
-  tls: []
-    # - secretName: seata-tls
-    #   hosts:
-    #     - seata.example.com
-
-# Monitoring and logging
+
 monitoring:
   enabled: false
   serviceMonitor:
     enabled: false
     interval: 30s
 
-# Autoscaling configuration (for standalone mode)
 autoscaling:
   enabled: false
   minReplicas: 2
@@ -160,40 +121,78 @@ autoscaling:
   targetCPUUtilizationPercentage: 80
   targetMemoryUtilizationPercentage: 80
 
-# Pod Disruption Budget configuration
 podDisruptionBudget:
   enabled: false
   minAvailable: 1
-  # maxUnavailable: 1
 
-# Node affinity configuration
 nodeAffinity:
   enabled: false
-  requiredDuringSchedulingIgnoredDuringExecution: []
-  preferredDuringSchedulingIgnoredDuringExecution: []
 
-# Pod affinity configuration
 podAffinity:
   enabled: false
 
-# Pod anti-affinity configuration
 podAntiAffinity:
   enabled: false
-  type: preferred  # preferred or required
+  type: preferred
   topologyKey: kubernetes.io/hostname
 
-# Tolerations
 tolerations: []
 
-# Node selector
 nodeSelector: {}
 
-# Labels
 labels:
   app: seata-server
   version: v1
 
-# Annotations
 annotations:
   operator.seata.apache.org/managed: "true"
 
+# ============================================================================
+# Webhook Configuration - Enabled with cert-manager
+# ============================================================================
+webhook:
+  # Enable validating webhook for SeataServer resources
+  enabled: true
+  
+  # Webhook port
+  port: 8443
+  
+  # Webhook container port
+  containerPort: 8443
+  
+  # Failure policy: Fail or Ignore
+  # Fail: reject the request if webhook fails (more strict)
+  # Ignore: allow the request if webhook fails (more lenient)
+  failurePolicy: Fail
+  
+  # Reinvocation policy: Never or IfNeeded
+  reinvocationPolicy: Never
+  
+  # Namespace selector for webhook
+  # Only validate SeataServer resources in namespaces with matching labels
+  namespaceSelector:
+    matchLabels:
+      seata-webhook: "enabled"
+    matchExpressions: []
+      # Example: match namespaces with specific label
+      # - key: seata-validation
+      #   operator: In
+      #   values: ["enabled"]
+  
+  # Certificate management with cert-manager (recommended)
+  certManager:
+    enabled: true
+  
+  # TLS certificate and key (only used when certManager.enabled=false)
+  # Generate using:
+  #   openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
+  # Encode using:
+  #   cat cert.pem | base64 | tr -d '\n'
+  tls:
+    crt: ""
+    key: ""
+  
+  # CA Bundle (base64 encoded)
+  # This is the CA certificate that signed the webhook TLS certificate
+  caBundle: ""
+
diff --git a/helm/seata-server/values.yaml b/helm/seata-server/values.yaml
index c9bbc67..07c39b9 100644
--- a/helm/seata-server/values.yaml
+++ b/helm/seata-server/values.yaml
@@ -197,3 +197,49 @@ labels:
 annotations:
   operator.seata.apache.org/managed: "true"
 
+# Webhook configuration for SeataServer validation (cluster mode only)
+webhook:
+  # Enable validating webhook for SeataServer resources
+  enabled: true
+  
+  # Webhook port
+  port: 8443
+  
+  # Webhook container port
+  containerPort: 8443
+  
+  # Failure policy: Fail or Ignore
+  # Fail: reject the request if webhook fails
+  # Ignore: allow the request if webhook fails
+  failurePolicy: Fail
+  
+  # Reinvocation policy: Never or IfNeeded
+  reinvocationPolicy: Never
+  
+  # Namespace selector for webhook
+  # Only validate SeataServer resources in selected namespaces
+  namespaceSelector:
+    matchLabels:
+      seata-webhook: "enabled"
+    matchExpressions: []
+      # - key: seata-validation
+      #   operator: In
+      #   values: ["enabled"]
+  
+  # Certificate management
+  certManager:
+    # Use cert-manager for automatic certificate management
+    enabled: false
+  
+  # TLS certificate and key (base64 encoded)
+  # Only used when certManager.enabled is false
+  # Generate using: openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
+  # Then base64 encode: base64 < cert.pem | base64 -d
+  tls:
+    crt: ""  # base64 encoded certificate
+    key: ""  # base64 encoded private key
+  
+  # CA Bundle (base64 encoded)
+  # This should be the base64 encoded CA certificate used to sign the webhook server certificate
+  caBundle: ""
+
diff --git a/pkg/webhook/validating_webhook.go b/pkg/webhook/validating_webhook.go
new file mode 100644
index 0000000..6ada5c8
--- /dev/null
+++ b/pkg/webhook/validating_webhook.go
@@ -0,0 +1,354 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 webhook
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"regexp"
+	"strings"
+
+	admissionv1 "k8s.io/api/admission/v1"
+	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+
+	seatav1 "github.com/apache/seata-k8s/api/v1"
+)
+
+// ValidatingWebhookHandler handles validation webhook requests
+type ValidatingWebhookHandler struct {
+	decoder runtime.Decoder
+}
+
+// NewValidatingWebhookHandler creates a new ValidatingWebhookHandler
+func NewValidatingWebhookHandler(decoder runtime.Decoder) *ValidatingWebhookHandler {
+	return &ValidatingWebhookHandler{
+		decoder: decoder,
+	}
+}
+
+// HandleSeataServerValidation handles SeataServer validation webhook requests
+func (h *ValidatingWebhookHandler) HandleSeataServerValidation(w http.ResponseWriter, r *http.Request) {
+	// Read admission review from request body
+	body, err := io.ReadAll(r.Body)
+	if err != nil {
+		http.Error(w, fmt.Sprintf("failed to read request body: %v", err), http.StatusBadRequest)
+		return
+	}
+
+	admissionReview := admissionv1.AdmissionReview{}
+	if err := json.Unmarshal(body, &admissionReview); err != nil {
+		http.Error(w, fmt.Sprintf("failed to unmarshal admission review: %v", err), http.StatusBadRequest)
+		return
+	}
+
+	// Validate the AdmissionRequest
+	if admissionReview.Request == nil {
+		http.Error(w, "admission request is nil", http.StatusBadRequest)
+		return
+	}
+
+	// Parse the SeataServer object
+	seataServer := &seatav1.SeataServer{}
+	if err := json.Unmarshal(admissionReview.Request.Object.Raw, seataServer); err != nil {
+		w.Header().Set("Content-Type", "application/json")
+		json.NewEncoder(w).Encode(admissionv1.AdmissionReview{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: "admission.k8s.io/v1",
+				Kind:       "AdmissionReview",
+			},
+			Response: &admissionv1.AdmissionResponse{
+				UID:     admissionReview.Request.UID,
+				Allowed: false,
+				Result: &metav1.Status{
+					Status:  metav1.StatusFailure,
+					Code:    http.StatusBadRequest,
+					Message: fmt.Sprintf("failed to unmarshal SeataServer: %v", err),
+				},
+			},
+		})
+		return
+	}
+
+	// Perform validation
+	validationErrors := ValidateSeataServer(seataServer)
+
+	// Build admission response
+	response := admissionv1.AdmissionReview{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "admission.k8s.io/v1",
+			Kind:       "AdmissionReview",
+		},
+		Response: &admissionv1.AdmissionResponse{
+			UID: admissionReview.Request.UID,
+		},
+	}
+
+	if len(validationErrors) == 0 {
+		response.Response.Allowed = true
+	} else {
+		response.Response.Allowed = false
+		response.Response.Result = &metav1.Status{
+			Status:  metav1.StatusFailure,
+			Code:    http.StatusUnprocessableEntity,
+			Message: strings.Join(validationErrors, "; "),
+		}
+	}
+
+	// Write response
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(response)
+}
+
+// ValidateSeataServer performs validation on SeataServer resource
+func ValidateSeataServer(server *seatav1.SeataServer) []string {
+	var errors []string
+
+	if server == nil {
+		return append(errors, "SeataServer object is nil")
+	}
+
+	// Validate service name
+	if err := validateServiceName(server.Spec.ServiceName); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	// Validate replicas
+	if err := validateReplicas(server.Spec.Replicas); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	// Validate image
+	if err := validateImage(server.Spec.Image); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	// Validate ports
+	if err := validatePorts(server.Spec.Ports); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	// Validate storage configuration
+	if err := validateStorage(&server.Spec.Persistence); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	// Validate resources
+	if err := validateResourceRequirements(server.Spec.Resources); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	// Validate volume reclaim policy
+	if err := validateVolumeReclaimPolicy(server.Spec.Persistence.VolumeReclaimPolicy); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	// Validate environment variables
+	if err := validateEnvironmentVariables(server.Spec.Env); err != nil {
+		errors = append(errors, err.Error())
+	}
+
+	return errors
+}
+
+// validateServiceName validates the service name
+func validateServiceName(name string) error {
+	if name == "" {
+		return fmt.Errorf("serviceName must not be empty")
+	}
+
+	// DNS-1035 label validation
+	dnsRegex := regexp.MustCompile(`^[a-z]([-a-z0-9]*[a-z0-9])?$`)
+	if !dnsRegex.MatchString(name) {
+		return fmt.Errorf("serviceName must be a valid DNS-1035 label (lowercase alphanumeric and hyphens, must start with letter)")
+	}
+
+	if len(name) > 63 {
+		return fmt.Errorf("serviceName must be no more than 63 characters")
+	}
+
+	return nil
+}
+
+// validateReplicas validates the number of replicas
+func validateReplicas(replicas int32) error {
+	if replicas < 1 {
+		return fmt.Errorf("replicas must be at least 1")
+	}
+
+	if replicas > 1000 {
+		return fmt.Errorf("replicas must not exceed 1000")
+	}
+
+	return nil
+}
+
+// validateImage validates the Docker image reference
+func validateImage(image string) error {
+	if image == "" {
+		return fmt.Errorf("image must not be empty")
+	}
+
+	// Basic Docker image reference validation
+	// Format: [REGISTRY_HOST[:REGISTRY_PORT]/]NAME[:TAG][@DIGEST]
+	imageRegex := regexp.MustCompile(`^([a-z0-9]+(?:[._-][a-z0-9]+)*/)*[a-z0-9]+(?:[._-][a-z0-9]+)*(?::[a-zA-Z0-9_][a-zA-Z0-9._-]*)?(?:@[a-zA-Z0-9:]+)?$`)
+	if !imageRegex.MatchString(image) {
+		return fmt.Errorf("image must be a valid Docker image reference")
+	}
+
+	return nil
+}
+
+// validatePorts validates port numbers
+func validatePorts(ports seatav1.Ports) error {
+	// Validate port numbers
+	if ports.ServicePort < 1 || ports.ServicePort > 65535 {
+		return fmt.Errorf("servicePort must be between 1 and 65535")
+	}
+
+	if ports.ConsolePort < 1 || ports.ConsolePort > 65535 {
+		return fmt.Errorf("consolePort must be between 1 and 65535")
+	}
+
+	if ports.RaftPort < 1 || ports.RaftPort > 65535 {
+		return fmt.Errorf("raftPort must be between 1 and 65535")
+	}
+
+	// Check for port conflicts
+	portMap := make(map[int32]string)
+	portMap[ports.ServicePort] = "servicePort"
+	portMap[ports.ConsolePort] = "consolePort"
+	portMap[ports.RaftPort] = "raftPort"
+
+	if len(portMap) < 3 {
+		return fmt.Errorf("servicePort, consolePort, and raftPort must be different")
+	}
+
+	return nil
+}
+
+// validateStorage validates storage configuration
+func validateStorage(persistence *seatav1.Persistence) error {
+	if persistence == nil {
+		return nil
+	}
+
+	spec := persistence.PersistentVolumeClaimSpec
+	if spec.Resources.Requests == nil {
+		return fmt.Errorf("storage must be specified in persistence.spec.resources.requests")
+	}
+
+	storage, exists := spec.Resources.Requests["storage"]
+	if !exists {
+		return fmt.Errorf("storage must be specified in persistence.spec.resources.requests")
+	}
+
+	// Check storage size range (1Gi to 1000Gi)
+	minStorage := resource.MustParse("1Gi")
+	maxStorage := resource.MustParse("1000Gi")
+
+	if storage.Cmp(minStorage) < 0 {
+		return fmt.Errorf("storage size must be at least 1Gi")
+	}
+
+	if storage.Cmp(maxStorage) > 0 {
+		return fmt.Errorf("storage size must not exceed 1000Gi")
+	}
+
+	return nil
+}
+
+// validateResourceRequirements validates CPU and memory resources
+func validateResourceRequirements(resources apiv1.ResourceRequirements) error {
+	// Validate requests
+	for resource, quantity := range resources.Requests {
+		if err := validateResourceQuantity(string(resource), quantity); err != nil {
+			return err
+		}
+	}
+
+	// Validate limits
+	for resource, quantity := range resources.Limits {
+		if err := validateResourceQuantity(string(resource), quantity); err != nil {
+			return err
+		}
+	}
+
+	// Ensure limits are greater than or equal to requests
+	cpuRequest := resources.Requests["cpu"]
+	cpuLimit := resources.Limits["cpu"]
+	if !cpuRequest.IsZero() && !cpuLimit.IsZero() && cpuLimit.Cmp(cpuRequest) < 0 {
+		return fmt.Errorf("cpu limit must be greater than or equal to cpu request")
+	}
+
+	memRequest := resources.Requests["memory"]
+	memLimit := resources.Limits["memory"]
+	if !memRequest.IsZero() && !memLimit.IsZero() && memLimit.Cmp(memRequest) < 0 {
+		return fmt.Errorf("memory limit must be greater than or equal to memory request")
+	}
+
+	return nil
+}
+
+// validateResourceQuantity validates a single resource quantity
+func validateResourceQuantity(resourceName string, quantity resource.Quantity) error {
+	if quantity.IsZero() {
+		return nil
+	}
+
+	// Check for negative values
+	if quantity.Sign() < 0 {
+		return fmt.Errorf("%s must be non-negative", resourceName)
+	}
+
+	return nil
+}
+
+// validateVolumeReclaimPolicy validates the volume reclaim policy
+func validateVolumeReclaimPolicy(policy seatav1.VolumeReclaimPolicy) error {
+	if policy == "" {
+		return nil // Empty is valid, will use default
+	}
+
+	if policy != seatav1.VolumeReclaimPolicyRetain && policy != seatav1.VolumeReclaimPolicyDelete {
+		return fmt.Errorf("volumeReclaimPolicy must be 'Retain' or 'Delete'")
+	}
+
+	return nil
+}
+
+// validateEnvironmentVariables validates environment variables
+func validateEnvironmentVariables(envVars []apiv1.EnvVar) error {
+	validNameRegex := regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
+
+	for _, env := range envVars {
+		if !validNameRegex.MatchString(env.Name) {
+			return fmt.Errorf("environment variable name '%s' is invalid (must start with letter or underscore, contain only alphanumeric and underscores)", env.Name)
+		}
+
+		if len(env.Name) > 250 {
+			return fmt.Errorf("environment variable name '%s' is too long (max 250 characters)", env.Name)
+		}
+	}
+
+	return nil
+}
diff --git a/pkg/webhook/validating_webhook_test.go b/pkg/webhook/validating_webhook_test.go
new file mode 100644
index 0000000..12ff24d
--- /dev/null
+++ b/pkg/webhook/validating_webhook_test.go
@@ -0,0 +1,424 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 webhook
+
+import (
+	"testing"
+
+	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	seatav1 "github.com/apache/seata-k8s/api/v1"
+)
+
+func TestValidateServiceName(t *testing.T) {
+	testCases := []struct {
+		name    string
+		service string
+		valid   bool
+	}{
+		{
+			name:    "valid lowercase name",
+			service: "seata-server",
+			valid:   true,
+		},
+		{
+			name:    "valid single character",
+			service: "s",
+			valid:   true,
+		},
+		{
+			name:    "empty service name",
+			service: "",
+			valid:   false,
+		},
+		{
+			name:    "uppercase characters",
+			service: "Seata-Server",
+			valid:   false,
+		},
+		{
+			name:    "starts with hyphen",
+			service: "-seata-server",
+			valid:   false,
+		},
+		{
+			name:    "ends with hyphen",
+			service: "seata-server-",
+			valid:   false,
+		},
+		{
+			name:    "too long name",
+			service: "seata-server-with-very-long-name-that-exceeds-sixty-three-characters-limit",
+			valid:   false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := validateServiceName(tc.service)
+			if (err == nil) != tc.valid {
+				t.Errorf("expected valid=%v, got error=%v", tc.valid, err)
+			}
+		})
+	}
+}
+
+func TestValidateReplicas(t *testing.T) {
+	testCases := []struct {
+		name     string
+		replicas int32
+		valid    bool
+	}{
+		{
+			name:     "valid minimum",
+			replicas: 1,
+			valid:    true,
+		},
+		{
+			name:     "valid middle",
+			replicas: 3,
+			valid:    true,
+		},
+		{
+			name:     "valid maximum",
+			replicas: 1000,
+			valid:    true,
+		},
+		{
+			name:     "zero replicas",
+			replicas: 0,
+			valid:    false,
+		},
+		{
+			name:     "negative replicas",
+			replicas: -1,
+			valid:    false,
+		},
+		{
+			name:     "exceeds maximum",
+			replicas: 1001,
+			valid:    false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := validateReplicas(tc.replicas)
+			if (err == nil) != tc.valid {
+				t.Errorf("expected valid=%v, got error=%v", tc.valid, err)
+			}
+		})
+	}
+}
+
+func TestValidateImage(t *testing.T) {
+	testCases := []struct {
+		name  string
+		image string
+		valid bool
+	}{
+		{
+			name:  "valid simple image",
+			image: "apache/seata-server:latest",
+			valid: true,
+		},
+		{
+			name:  "valid with registry",
+			image: "docker.io/apache/seata-server:latest",
+			valid: true,
+		},
+		{
+			name:  "valid without tag",
+			image: "apache/seata-server",
+			valid: true,
+		},
+		{
+			name:  "empty image",
+			image: "",
+			valid: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := validateImage(tc.image)
+			if (err == nil) != tc.valid {
+				t.Errorf("expected valid=%v, got error=%v", tc.valid, err)
+			}
+		})
+	}
+}
+
+func TestValidatePorts(t *testing.T) {
+	testCases := []struct {
+		name  string
+		ports seatav1.Ports
+		valid bool
+	}{
+		{
+			name: "valid default ports",
+			ports: seatav1.Ports{
+				ServicePort: 8091,
+				ConsolePort: 7091,
+				RaftPort:    9091,
+			},
+			valid: true,
+		},
+		{
+			name: "invalid service port zero",
+			ports: seatav1.Ports{
+				ServicePort: 0,
+				ConsolePort: 7091,
+				RaftPort:    9091,
+			},
+			valid: false,
+		},
+		{
+			name: "invalid port exceeds max",
+			ports: seatav1.Ports{
+				ServicePort: 65536,
+				ConsolePort: 7091,
+				RaftPort:    9091,
+			},
+			valid: false,
+		},
+		{
+			name: "duplicate ports",
+			ports: seatav1.Ports{
+				ServicePort: 8091,
+				ConsolePort: 8091,
+				RaftPort:    9091,
+			},
+			valid: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := validatePorts(tc.ports)
+			if (err == nil) != tc.valid {
+				t.Errorf("expected valid=%v, got error=%v", tc.valid, err)
+			}
+		})
+	}
+}
+
+func TestValidateStorage(t *testing.T) {
+	testCases := []struct {
+		name        string
+		persistence *seatav1.Persistence
+		valid       bool
+	}{
+		{
+			name: "valid storage",
+			persistence: &seatav1.Persistence{
+				PersistentVolumeClaimSpec: apiv1.PersistentVolumeClaimSpec{
+					Resources: apiv1.ResourceRequirements{
+						Requests: apiv1.ResourceList{
+							apiv1.ResourceStorage: resource.MustParse("5Gi"),
+						},
+					},
+				},
+			},
+			valid: true,
+		},
+		{
+			name: "storage too small",
+			persistence: &seatav1.Persistence{
+				PersistentVolumeClaimSpec: apiv1.PersistentVolumeClaimSpec{
+					Resources: apiv1.ResourceRequirements{
+						Requests: apiv1.ResourceList{
+							apiv1.ResourceStorage: resource.MustParse("512Mi"),
+						},
+					},
+				},
+			},
+			valid: false,
+		},
+		{
+			name: "storage too large",
+			persistence: &seatav1.Persistence{
+				PersistentVolumeClaimSpec: apiv1.PersistentVolumeClaimSpec{
+					Resources: apiv1.ResourceRequirements{
+						Requests: apiv1.ResourceList{
+							apiv1.ResourceStorage: resource.MustParse("2000Gi"),
+						},
+					},
+				},
+			},
+			valid: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := validateStorage(tc.persistence)
+			if (err == nil) != tc.valid {
+				t.Errorf("expected valid=%v, got error=%v", tc.valid, err)
+			}
+		})
+	}
+}
+
+func TestValidateVolumeReclaimPolicy(t *testing.T) {
+	testCases := []struct {
+		name   string
+		policy seatav1.VolumeReclaimPolicy
+		valid  bool
+	}{
+		{
+			name:   "valid Retain",
+			policy: seatav1.VolumeReclaimPolicyRetain,
+			valid:  true,
+		},
+		{
+			name:   "valid Delete",
+			policy: seatav1.VolumeReclaimPolicyDelete,
+			valid:  true,
+		},
+		{
+			name:   "empty policy",
+			policy: "",
+			valid:  true,
+		},
+		{
+			name:   "invalid policy",
+			policy: "Invalid",
+			valid:  false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := validateVolumeReclaimPolicy(tc.policy)
+			if (err == nil) != tc.valid {
+				t.Errorf("expected valid=%v, got error=%v", tc.valid, err)
+			}
+		})
+	}
+}
+
+func TestValidateEnvironmentVariables(t *testing.T) {
+	testCases := []struct {
+		name  string
+		envs  []apiv1.EnvVar
+		valid bool
+	}{
+		{
+			name: "valid environment variables",
+			envs: []apiv1.EnvVar{
+				{Name: "VAR_NAME", Value: "value"},
+				{Name: "var_name2", Value: "value"},
+				{Name: "_private", Value: "value"},
+			},
+			valid: true,
+		},
+		{
+			name: "invalid starts with number",
+			envs: []apiv1.EnvVar{
+				{Name: "1VAR", Value: "value"},
+			},
+			valid: false,
+		},
+		{
+			name: "invalid contains hyphen",
+			envs: []apiv1.EnvVar{
+				{Name: "VAR-NAME", Value: "value"},
+			},
+			valid: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := validateEnvironmentVariables(tc.envs)
+			if (err == nil) != tc.valid {
+				t.Errorf("expected valid=%v, got error=%v", tc.valid, err)
+			}
+		})
+	}
+}
+
+func TestValidateSeataServer(t *testing.T) {
+	testCases := []struct {
+		name   string
+		server *seatav1.SeataServer
+		valid  bool
+	}{
+		{
+			name: "valid SeataServer",
+			server: &seatav1.SeataServer{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "seata-server",
+					Namespace: "default",
+				},
+				Spec: seatav1.SeataServerSpec{
+					ContainerSpec: seatav1.ContainerSpec{
+						Image: "apache/seata-server:latest",
+					},
+					Replicas:    3,
+					ServiceName: "seata-server-cluster",
+					Ports: seatav1.Ports{
+						ServicePort: 8091,
+						ConsolePort: 7091,
+						RaftPort:    9091,
+					},
+					Persistence: seatav1.Persistence{
+						VolumeReclaimPolicy: seatav1.VolumeReclaimPolicyRetain,
+						PersistentVolumeClaimSpec: apiv1.PersistentVolumeClaimSpec{
+							Resources: apiv1.ResourceRequirements{
+								Requests: apiv1.ResourceList{
+									apiv1.ResourceStorage: resource.MustParse("5Gi"),
+								},
+							},
+						},
+					},
+				},
+			},
+			valid: true,
+		},
+		{
+			name: "invalid empty service name",
+			server: &seatav1.SeataServer{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "seata-server",
+					Namespace: "default",
+				},
+				Spec: seatav1.SeataServerSpec{
+					ContainerSpec: seatav1.ContainerSpec{
+						Image: "apache/seata-server:latest",
+					},
+					Replicas:    3,
+					ServiceName: "",
+				},
+			},
+			valid: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			errors := ValidateSeataServer(tc.server)
+			if (len(errors) == 0) != tc.valid {
+				t.Errorf("expected valid=%v, got errors=%v", tc.valid, errors)
+			}
+		})
+	}
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For additional commands, e-mail: notifications-help@seata.apache.org