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 2024/09/06 16:41:11 UTC

(incubator-seata-k8s) branch master updated: feature: add volume cleanup behaviour and optimize setup defaults procedure (#21)

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 f629513  feature: add volume cleanup behaviour and optimize setup defaults procedure (#21)
f629513 is described below

commit f629513199e554027f9cc95fc3dc9c706dafcc22
Author: Xiangkun Yin <pt...@apache.org>
AuthorDate: Sat Sep 7 00:41:07 2024 +0800

    feature: add volume cleanup behaviour and optimize setup defaults procedure (#21)
---
 README.md                                          |  18 +-
 README.zh.md                                       |  20 +-
 api/v1alpha1/seataserver_types.go                  |  86 ++++++-
 api/v1alpha1/zz_generated.deepcopy.go              |  34 +--
 .../operator.seata.apache.org_seataservers.yaml    | 283 ++++++++++++++++-----
 controllers/seataserver_controller.go              | 120 ++++++++-
 deploy/seata-server-cluster.yaml                   |   2 +
 pkg/seata/generators.go                            |  10 +-
 pkg/utils/utils.go                                 |  38 +++
 9 files changed, 500 insertions(+), 111 deletions(-)

diff --git a/README.md b/README.md
index 2cd44ad..4f1970c 100644
--- a/README.md
+++ b/README.md
@@ -39,12 +39,14 @@ To deploy Seata Server using the Operator method, follow these steps:
      serviceName: seata-server-cluster
      replicas: 3
      image: seataio/seata-server:latest
-     store:
-       resources:
-         requests:
-           storage: 5Gi
+     persistence:
+   	  volumeReclaimPolicy: Retain
+       spec:
+         resources:
+           requests:
+           	storage: 5Gi
    ```
-
+   
    For the example above, if everything is correct, the controller will deploy 3 StatefulSet resources and a Headless Service to the cluster. You can access the Seata Server cluster in the cluster through `seata-server-0.seata-server-cluster.default.svc`.
 
 ### Reference
@@ -61,9 +63,11 @@ For CRD details, you can visit [operator.seata.apache.org_seataservers.yaml](con
 
 5. `resources`: Used to define container resource requirements.
 
-6. `store.resources`: Used to define mounted storage resource requirements.
+6. `persistence.spec`: Used to define mounted storage resource requirements.
+
+7. `persistence.volumeReclaimPolicy`: Used to control volume reclaim behavior, possible choices include `Retain` or `Delete`, which infer retain volumes or delete volumes after deletion respectively.
 
-7. `env`: Environment variables passed to the container. You can use this field to define Seata Server configuration. For example:
+8. `env`: Environment variables passed to the container. You can use this field to define Seata Server configuration. For example:
 
    ```yaml
    apiVersion: operator.seata.apache.org/v1alpha1
diff --git a/README.zh.md b/README.zh.md
index 21a68c7..21c9c7c 100755
--- a/README.zh.md
+++ b/README.zh.md
@@ -43,13 +43,15 @@ https://github.com/seata/seata-docker
      serviceName: seata-server-cluster
      replicas: 3
      image: seataio/seata-server:latest
-     store:
-       resources:
-         requests:
-           storage: 5Gi
+     persistence:
+   	  volumeReclaimPolicy: Retain
+       spec:
+         resources:
+           requests:
+           	storage: 5Gi
    
    ```
-
+   
    对于上面这个 CR 的例子而言,如果一切正常的话,controller 将会部署 3 个 StatefulSet 资源和一个 Headless Service 到集群中;在集群中你可以通过 seata-server-0.seata-server-cluster.default.svc 对 Seata Server 集群进行访问。
 
 ### Reference
@@ -66,9 +68,11 @@ https://github.com/seata/seata-docker
 
 5. `resources`: 用于定义容器的资源要求
 
-6. `store.resources`: 用于定义挂载的存储资源要求
+6. `persistence.spec`: 用于定义挂载的存储资源要求
+
+7. `persistence.volumeReclaimPolicy`: 用于控制存储回收行为,允许的选项有 `Retain` 或者 `Delete`,分别代表了在 CR 删除之后保存存储卷或删除存储卷
 
-7. `env`: 传递给容器的环境变量,可以通过此字段去定义 Seata Server 的配置,比如:
+8. `env`: 传递给容器的环境变量,可以通过此字段去定义 Seata Server 的配置,比如:
 
    ```yaml
    apiVersion: operator.seata.apache.org/v1alpha1
@@ -99,7 +103,7 @@ https://github.com/seata/seata-docker
    data:
      password: seata
    ```
-   
+
    
 
 ### For Developer
diff --git a/api/v1alpha1/seataserver_types.go b/api/v1alpha1/seataserver_types.go
index 1c4a761..265c5c2 100644
--- a/api/v1alpha1/seataserver_types.go
+++ b/api/v1alpha1/seataserver_types.go
@@ -19,11 +19,14 @@ package v1alpha1
 
 import (
 	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
-// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
+const (
+	DefaultSeataSessionStorageVolumeSize = "5Gi"
+	DefaultSeataServerImage              = "seataio/seata-server:latest"
+)
 
 // SeataServerSpec defines the desired state of SeataServer
 type SeataServerSpec struct {
@@ -35,13 +38,32 @@ type SeataServerSpec struct {
 	Replicas int32 `json:"replicas"`
 
 	// +kubebuilder:validation:Optional
-	// +kubebuilder:default=seata-server
+	// +kubebuilder:default=seata-server-cluster
 	ServiceName string `json:"serviceName"`
 
 	// +kubebuilder:validation:Optional
 	Ports Ports `json:"ports,omitempty"`
 
-	Store Store `json:"store"`
+	// +kubebuilder:validation:Optional
+	Persistence Persistence `json:"persistence,omitempty"`
+}
+
+func (s *SeataServerSpec) withDefaults() (changed bool) {
+	if s.ContainerName == "" {
+		s.ContainerName = "seata-server"
+		changed = true
+	}
+	if s.ServiceName == "" {
+		s.ServiceName = "seata-server-cluster"
+		changed = true
+	}
+	if s.Image == "" {
+		s.Image = DefaultSeataServerImage
+		changed = true
+	}
+	changed = s.Ports.withDefaults() || changed
+	changed = s.Persistence.withDefaults() || changed
+	return changed
 }
 
 // SeataServerStatus defines the observed state of SeataServer
@@ -63,6 +85,10 @@ type SeataServer struct {
 	Status SeataServerStatus `json:"status,omitempty"`
 }
 
+func (s *SeataServer) WithDefaults() bool {
+	return s.Spec.withDefaults()
+}
+
 //+kubebuilder:object:root=true
 
 // SeataServerList contains a list of SeataServer
@@ -76,7 +102,9 @@ type ContainerSpec struct {
 	// +kubebuilder:validation:Optional
 	// +kubebuilder:default=seata-server
 	ContainerName string `json:"containerName"`
-	Image         string `json:"image"`
+	// +kubebuilder:validation:Optional
+	// +kubebuilder:default="seataio/seata-server:latest"
+	Image string `json:"image"`
 	// +kubebuilder:validation:Optional
 	Env []apiv1.EnvVar `json:"env"`
 	// +kubebuilder:validation:Optional
@@ -95,8 +123,52 @@ type Ports struct {
 	RaftPort int32 `json:"raftPort"`
 }
 
-type Store struct {
-	Resources apiv1.ResourceRequirements `json:"resources"`
+func (p *Ports) withDefaults() bool {
+	if *p == (Ports{}) {
+		p.ConsolePort = 7091
+		p.ServicePort = 8091
+		p.RaftPort = 9091
+		return true
+	}
+	return false
+}
+
+type Persistence struct {
+	// VolumeReclaimPolicy is a seata operator configuration. If it's set to Delete,
+	// the corresponding PVCs will be deleted by the operator when seata server cluster is deleted.
+	// The default value is Retain.
+	// +kubebuilder:validation:Enum="Delete";"Retain"
+	VolumeReclaimPolicy VolumeReclaimPolicy `json:"volumeReclaimPolicy,omitempty"`
+	// PersistentVolumeClaimSpec is the spec to describe PVC for the container
+	// This field is optional. If no PVC is specified default persistent volume
+	// will get created.
+	PersistentVolumeClaimSpec apiv1.PersistentVolumeClaimSpec `json:"spec,omitempty"`
+}
+
+type VolumeReclaimPolicy string
+
+const (
+	VolumeReclaimPolicyRetain VolumeReclaimPolicy = "Retain"
+	VolumeReclaimPolicyDelete VolumeReclaimPolicy = "Delete"
+)
+
+func (p *Persistence) withDefaults() (changed bool) {
+	if p.VolumeReclaimPolicy != VolumeReclaimPolicyDelete && p.VolumeReclaimPolicy != VolumeReclaimPolicyRetain {
+		changed = true
+		p.VolumeReclaimPolicy = VolumeReclaimPolicyRetain
+	}
+	p.PersistentVolumeClaimSpec.AccessModes = []apiv1.PersistentVolumeAccessMode{
+		apiv1.ReadWriteOnce,
+	}
+
+	storage, _ := p.PersistentVolumeClaimSpec.Resources.Requests["storage"]
+	if storage.IsZero() {
+		p.PersistentVolumeClaimSpec.Resources.Requests = apiv1.ResourceList{
+			apiv1.ResourceStorage: resource.MustParse(DefaultSeataSessionStorageVolumeSize),
+		}
+		changed = true
+	}
+	return changed
 }
 
 func init() {
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 54ee94f..3c328ca 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -255,6 +255,22 @@ func (in *MultiCluster) DeepCopy() *MultiCluster {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Persistence) DeepCopyInto(out *Persistence) {
+	*out = *in
+	in.PersistentVolumeClaimSpec.DeepCopyInto(&out.PersistentVolumeClaimSpec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Persistence.
+func (in *Persistence) DeepCopy() *Persistence {
+	if in == nil {
+		return nil
+	}
+	out := new(Persistence)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Ports) DeepCopyInto(out *Ports) {
 	*out = *in
@@ -349,7 +365,7 @@ func (in *SeataServerSpec) DeepCopyInto(out *SeataServerSpec) {
 	*out = *in
 	in.ContainerSpec.DeepCopyInto(&out.ContainerSpec)
 	out.Ports = in.Ports
-	in.Store.DeepCopyInto(&out.Store)
+	in.Persistence.DeepCopyInto(&out.Persistence)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeataServerSpec.
@@ -432,19 +448,3 @@ func (in *ServiceAccount) DeepCopy() *ServiceAccount {
 	in.DeepCopyInto(out)
 	return out
 }
-
-// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
-func (in *Store) DeepCopyInto(out *Store) {
-	*out = *in
-	in.Resources.DeepCopyInto(&out.Resources)
-}
-
-// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Store.
-func (in *Store) DeepCopy() *Store {
-	if in == nil {
-		return nil
-	}
-	out := new(Store)
-	in.DeepCopyInto(out)
-	return out
-}
diff --git a/config/crd/bases/operator.seata.apache.org_seataservers.yaml b/config/crd/bases/operator.seata.apache.org_seataservers.yaml
index 9d3bbf1..88fc5b2 100644
--- a/config/crd/bases/operator.seata.apache.org_seataservers.yaml
+++ b/config/crd/bases/operator.seata.apache.org_seataservers.yaml
@@ -146,7 +146,230 @@ spec:
                   type: object
                 type: array
               image:
+                default: seataio/seata-server:latest
                 type: string
+              persistence:
+                properties:
+                  spec:
+                    description: PersistentVolumeClaimSpec is the spec to describe
+                      PVC for the container This field is optional. If no PVC is specified
+                      default persistent volume will get created.
+                    properties:
+                      accessModes:
+                        description: 'accessModes contains the desired access modes
+                          the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1'
+                        items:
+                          type: string
+                        type: array
+                      dataSource:
+                        description: 'dataSource field can be used to specify either:
+                          * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot)
+                          * An existing PVC (PersistentVolumeClaim) If the provisioner
+                          or an external controller can support the specified data
+                          source, it will create a new volume based on the contents
+                          of the specified data source. When the AnyVolumeDataSource
+                          feature gate is enabled, dataSource contents will be copied
+                          to dataSourceRef, and dataSourceRef contents will be copied
+                          to dataSource when dataSourceRef.namespace is not specified.
+                          If the namespace is specified, then dataSourceRef will not
+                          be copied to dataSource.'
+                        properties:
+                          apiGroup:
+                            description: APIGroup is the group for the resource being
+                              referenced. If APIGroup is not specified, the specified
+                              Kind must be in the core API group. For any other third-party
+                              types, APIGroup is required.
+                            type: string
+                          kind:
+                            description: Kind is the type of resource being referenced
+                            type: string
+                          name:
+                            description: Name is the name of resource being referenced
+                            type: string
+                        required:
+                        - kind
+                        - name
+                        type: object
+                        x-kubernetes-map-type: atomic
+                      dataSourceRef:
+                        description: 'dataSourceRef specifies the object from which
+                          to populate the volume with data, if a non-empty volume
+                          is desired. This may be any object from a non-empty API
+                          group (non core object) or a PersistentVolumeClaim object.
+                          When this field is specified, volume binding will only succeed
+                          if the type of the specified object matches some installed
+                          volume populator or dynamic provisioner. This field will
+                          replace the functionality of the dataSource field and as
+                          such if both fields are non-empty, they must have the same
+                          value. For backwards compatibility, when namespace isn''t
+                          specified in dataSourceRef, both fields (dataSource and
+                          dataSourceRef) will be set to the same value automatically
+                          if one of them is empty and the other is non-empty. When
+                          namespace is specified in dataSourceRef, dataSource isn''t
+                          set to the same value and must be empty. There are three
+                          important differences between dataSource and dataSourceRef:
+                          * While dataSource only allows two specific types of objects,
+                          dataSourceRef allows any non-core object, as well as PersistentVolumeClaim
+                          objects. * While dataSource ignores disallowed values (dropping
+                          them), dataSourceRef preserves all values, and generates
+                          an error if a disallowed value is specified. * While dataSource
+                          only allows local objects, dataSourceRef allows objects
+                          in any namespaces. (Beta) Using this field requires the
+                          AnyVolumeDataSource feature gate to be enabled. (Alpha)
+                          Using the namespace field of dataSourceRef requires the
+                          CrossNamespaceVolumeDataSource feature gate to be enabled.'
+                        properties:
+                          apiGroup:
+                            description: APIGroup is the group for the resource being
+                              referenced. If APIGroup is not specified, the specified
+                              Kind must be in the core API group. For any other third-party
+                              types, APIGroup is required.
+                            type: string
+                          kind:
+                            description: Kind is the type of resource being referenced
+                            type: string
+                          name:
+                            description: Name is the name of resource being referenced
+                            type: string
+                          namespace:
+                            description: Namespace is the namespace of resource being
+                              referenced Note that when a namespace is specified,
+                              a gateway.networking.k8s.io/ReferenceGrant object is
+                              required in the referent namespace to allow that namespace's
+                              owner to accept the reference. See the ReferenceGrant
+                              documentation for details. (Alpha) This field requires
+                              the CrossNamespaceVolumeDataSource feature gate to be
+                              enabled.
+                            type: string
+                        required:
+                        - kind
+                        - name
+                        type: object
+                      resources:
+                        description: 'resources represents the minimum resources the
+                          volume should have. If RecoverVolumeExpansionFailure feature
+                          is enabled users are allowed to specify resource requirements
+                          that are lower than previous value but must still be higher
+                          than capacity recorded in the status field of the claim.
+                          More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources'
+                        properties:
+                          claims:
+                            description: "Claims lists the names of resources, defined
+                              in spec.resourceClaims, that are used by this container.
+                              \n This is an alpha field and requires enabling the
+                              DynamicResourceAllocation feature gate. \n This field
+                              is immutable. It can only be set for containers."
+                            items:
+                              description: ResourceClaim references one entry in PodSpec.ResourceClaims.
+                              properties:
+                                name:
+                                  description: Name must match the name of one entry
+                                    in pod.spec.resourceClaims of the Pod where this
+                                    field is used. It makes that resource available
+                                    inside a container.
+                                  type: string
+                              required:
+                              - name
+                              type: object
+                            type: array
+                            x-kubernetes-list-map-keys:
+                            - name
+                            x-kubernetes-list-type: map
+                          limits:
+                            additionalProperties:
+                              anyOf:
+                              - type: integer
+                              - type: string
+                              pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                              x-kubernetes-int-or-string: true
+                            description: 'Limits describes the maximum amount of compute
+                              resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                            type: object
+                          requests:
+                            additionalProperties:
+                              anyOf:
+                              - type: integer
+                              - type: string
+                              pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                              x-kubernetes-int-or-string: true
+                            description: 'Requests describes the minimum amount of
+                              compute resources required. If Requests is omitted for
+                              a container, it defaults to Limits if that is explicitly
+                              specified, otherwise to an implementation-defined value.
+                              Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                            type: object
+                        type: object
+                      selector:
+                        description: selector is a label query over volumes to consider
+                          for binding.
+                        properties:
+                          matchExpressions:
+                            description: matchExpressions is a list of label selector
+                              requirements. The requirements are ANDed.
+                            items:
+                              description: A label selector requirement is a selector
+                                that contains values, a key, and an operator that
+                                relates the key and values.
+                              properties:
+                                key:
+                                  description: key is the label key that the selector
+                                    applies to.
+                                  type: string
+                                operator:
+                                  description: operator represents a key's relationship
+                                    to a set of values. Valid operators are In, NotIn,
+                                    Exists and DoesNotExist.
+                                  type: string
+                                values:
+                                  description: values is an array of string values.
+                                    If the operator is In or NotIn, the values array
+                                    must be non-empty. If the operator is Exists or
+                                    DoesNotExist, the values array must be empty.
+                                    This array is replaced during a strategic merge
+                                    patch.
+                                  items:
+                                    type: string
+                                  type: array
+                              required:
+                              - key
+                              - operator
+                              type: object
+                            type: array
+                          matchLabels:
+                            additionalProperties:
+                              type: string
+                            description: matchLabels is a map of {key,value} pairs.
+                              A single {key,value} in the matchLabels map is equivalent
+                              to an element of matchExpressions, whose key field is
+                              "key", the operator is "In", and the values array contains
+                              only "value". The requirements are ANDed.
+                            type: object
+                        type: object
+                        x-kubernetes-map-type: atomic
+                      storageClassName:
+                        description: 'storageClassName is the name of the StorageClass
+                          required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1'
+                        type: string
+                      volumeMode:
+                        description: volumeMode defines what type of volume is required
+                          by the claim. Value of Filesystem is implied when not included
+                          in claim spec.
+                        type: string
+                      volumeName:
+                        description: volumeName is the binding reference to the PersistentVolume
+                          backing this claim.
+                        type: string
+                    type: object
+                  volumeReclaimPolicy:
+                    description: VolumeReclaimPolicy is a seata operator configuration.
+                      If it's set to Delete, the corresponding PVCs will be deleted
+                      by the operator when seata server cluster is deleted. The default
+                      value is Retain.
+                    enum:
+                    - Delete
+                    - Retain
+                    type: string
+                type: object
               ports:
                 properties:
                   consolePort:
@@ -216,66 +439,8 @@ spec:
                     type: object
                 type: object
               serviceName:
-                default: seata-server
+                default: seata-server-cluster
                 type: string
-              store:
-                properties:
-                  resources:
-                    description: ResourceRequirements describes the compute resource
-                      requirements.
-                    properties:
-                      claims:
-                        description: "Claims lists the names of resources, defined
-                          in spec.resourceClaims, that are used by this container.
-                          \n This is an alpha field and requires enabling the DynamicResourceAllocation
-                          feature gate. \n This field is immutable. It can only be
-                          set for containers."
-                        items:
-                          description: ResourceClaim references one entry in PodSpec.ResourceClaims.
-                          properties:
-                            name:
-                              description: Name must match the name of one entry in
-                                pod.spec.resourceClaims of the Pod where this field
-                                is used. It makes that resource available inside a
-                                container.
-                              type: string
-                          required:
-                          - name
-                          type: object
-                        type: array
-                        x-kubernetes-list-map-keys:
-                        - name
-                        x-kubernetes-list-type: map
-                      limits:
-                        additionalProperties:
-                          anyOf:
-                          - type: integer
-                          - type: string
-                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                          x-kubernetes-int-or-string: true
-                        description: 'Limits describes the maximum amount of compute
-                          resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
-                        type: object
-                      requests:
-                        additionalProperties:
-                          anyOf:
-                          - type: integer
-                          - type: string
-                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
-                          x-kubernetes-int-or-string: true
-                        description: 'Requests describes the minimum amount of compute
-                          resources required. If Requests is omitted for a container,
-                          it defaults to Limits if that is explicitly specified, otherwise
-                          to an implementation-defined value. Requests cannot exceed
-                          Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
-                        type: object
-                    type: object
-                required:
-                - resources
-                type: object
-            required:
-            - image
-            - store
             type: object
           status:
             description: SeataServerStatus defines the observed state of SeataServer
diff --git a/controllers/seataserver_controller.go b/controllers/seataserver_controller.go
index 63142c4..0e83928 100644
--- a/controllers/seataserver_controller.go
+++ b/controllers/seataserver_controller.go
@@ -21,9 +21,11 @@ import (
 	"context"
 	"fmt"
 	"github.com/apache/seata-k8s/pkg/seata"
+	"github.com/apache/seata-k8s/pkg/utils"
 	appsv1 "k8s.io/api/apps/v1"
 	apiv1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -78,11 +80,21 @@ func (r *SeataServerReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 		logger.Error(err, fmt.Sprintf("Failed to get resource SeataServer(%v)", req.NamespacedName))
 		return ctrl.Result{}, err
 	}
-	setupDefaults(s)
+
+	changed := s.WithDefaults()
+	if changed {
+		logger.Info(fmt.Sprintf("Setting default values for SeataServer(%v)", req.NamespacedName))
+		if err := r.Client.Update(ctx, s); err != nil {
+			logger.Error(err, fmt.Sprintf("Failed to update resource SeataServer(%v)", req.NamespacedName))
+			return reconcile.Result{}, err
+		}
+		return reconcile.Result{Requeue: true}, nil
+	}
 
 	for _, fun := range []reconcileFun{
 		r.reconcileHeadlessService,
 		r.reconcileStatefulSet,
+		r.reconcileFinalizers,
 	} {
 		if err := fun(ctx, s); err != nil {
 			return reconcile.Result{}, err
@@ -211,6 +223,104 @@ func (r *SeataServerReconciler) updateStatefulSet(ctx context.Context, s *seatav
 	return r.Client.Status().Update(ctx, s)
 }
 
+func (r *SeataServerReconciler) reconcileFinalizers(ctx context.Context, instance *seatav1alpha1.SeataServer) (err error) {
+	if instance.Spec.Persistence.VolumeReclaimPolicy != seatav1alpha1.VolumeReclaimPolicyDelete {
+		return nil
+	}
+	if instance.DeletionTimestamp.IsZero() {
+		if !utils.ContainsString(instance.ObjectMeta.Finalizers, utils.SeataFinalizer) {
+			instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, utils.SeataFinalizer)
+			if err = r.Client.Update(ctx, instance); err != nil {
+				return err
+			}
+		}
+		return r.cleanupOrphanPVCs(ctx, instance)
+	} else {
+		if utils.ContainsString(instance.ObjectMeta.Finalizers, utils.SeataFinalizer) {
+			if err = r.cleanUpAllPVCs(ctx, instance); err != nil {
+				return err
+			}
+			instance.ObjectMeta.Finalizers = utils.RemoveString(instance.ObjectMeta.Finalizers, utils.SeataFinalizer)
+			if err = r.Client.Update(ctx, instance); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (r *SeataServerReconciler) cleanupOrphanPVCs(ctx context.Context, s *seatav1alpha1.SeataServer) (err error) {
+	logger := log.FromContext(ctx)
+	// this check should make sure we do not delete the PVCs before the STS has scaled down
+	if s.Status.ReadyReplicas == s.Spec.Replicas {
+		pvcCount, err := r.getPVCCount(ctx, s)
+		if err != nil {
+			return err
+		}
+		logger.Info(fmt.Sprintf("cleanupOrphanPVCs with PVC count %d and ReadyReplicas count %d", pvcCount, s.Status.ReadyReplicas))
+		if pvcCount > int(s.Spec.Replicas) {
+			pvcList, err := r.getPVCList(ctx, s)
+			if err != nil {
+				return err
+			}
+			for _, pvcItem := range pvcList.Items {
+				// delete only Orphan PVCs
+				if utils.IsPVCOrphan(pvcItem.Name, s.Spec.Replicas) {
+					r.deletePVC(ctx, pvcItem)
+				}
+			}
+		}
+	}
+	return nil
+}
+func (r *SeataServerReconciler) getPVCCount(ctx context.Context, s *seatav1alpha1.SeataServer) (pvcCount int, err error) {
+	pvcList, err := r.getPVCList(ctx, s)
+	if err != nil {
+		return -1, err
+	}
+	pvcCount = len(pvcList.Items)
+	return pvcCount, nil
+}
+
+func (r *SeataServerReconciler) getPVCList(ctx context.Context, s *seatav1alpha1.SeataServer) (pvList apiv1.PersistentVolumeClaimList, err error) {
+	selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
+		MatchLabels: map[string]string{"app": s.GetName(), "uid": string(s.UID)},
+	})
+	pvclistOps := &client.ListOptions{
+		Namespace:     s.Namespace,
+		LabelSelector: selector,
+	}
+	pvcList := &apiv1.PersistentVolumeClaimList{}
+	err = r.Client.List(ctx, pvcList, pvclistOps)
+	return *pvcList, err
+}
+
+func (r *SeataServerReconciler) cleanUpAllPVCs(ctx context.Context, s *seatav1alpha1.SeataServer) (err error) {
+	pvcList, err := r.getPVCList(ctx, s)
+	if err != nil {
+		return err
+	}
+	for _, pvcItem := range pvcList.Items {
+		r.deletePVC(ctx, pvcItem)
+	}
+	return nil
+}
+
+func (r *SeataServerReconciler) deletePVC(ctx context.Context, pvcItem apiv1.PersistentVolumeClaim) {
+	logger := log.FromContext(ctx)
+	pvcDelete := &apiv1.PersistentVolumeClaim{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      pvcItem.Name,
+			Namespace: pvcItem.Namespace,
+		},
+	}
+	logger.Info(fmt.Sprintf("Deleting PVC with name %s", pvcItem.Name))
+	err := r.Client.Delete(ctx, pvcDelete)
+	if err != nil {
+		logger.Error(err, fmt.Sprintf("Error deleting PVC with name %s", pvcDelete))
+	}
+}
+
 // SetupWithManager sets up the controller with the Manager.
 func (r *SeataServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return ctrl.NewControllerManagedBy(mgr).
@@ -220,11 +330,3 @@ func (r *SeataServerReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		WithEventFilter(predicate.GenerationChangedPredicate{}).
 		Complete(r)
 }
-
-func setupDefaults(s *seatav1alpha1.SeataServer) {
-	if s.Spec.Ports == (seatav1alpha1.Ports{}) {
-		s.Spec.Ports.ConsolePort = 7091
-		s.Spec.Ports.ServicePort = 8091
-		s.Spec.Ports.RaftPort = 9091
-	}
-}
diff --git a/deploy/seata-server-cluster.yaml b/deploy/seata-server-cluster.yaml
index db81345..1ae362e 100644
--- a/deploy/seata-server-cluster.yaml
+++ b/deploy/seata-server-cluster.yaml
@@ -7,6 +7,8 @@ spec:
   serviceName: seata-server-cluster
   replicas: 1
   image: seataio/seata-server:latest
+  persistence:
+    volumeReclaimPolicy: Delete
   store:
     resources:
       requests:
diff --git a/pkg/seata/generators.go b/pkg/seata/generators.go
index 21addc6..12b3413 100644
--- a/pkg/seata/generators.go
+++ b/pkg/seata/generators.go
@@ -68,6 +68,8 @@ while True:
 	exit(0)
 `
 
+const pvcName = "seata-store"
+
 func MakeStatefulSet(s *seatav1alpha1.SeataServer) *appsv1.StatefulSet {
 	labels := makeLabels(s.Name)
 	statefulSet := &appsv1.StatefulSet{
@@ -82,11 +84,11 @@ func MakeStatefulSet(s *seatav1alpha1.SeataServer) *appsv1.StatefulSet {
 			ServiceName: s.Spec.ServiceName,
 			Template:    apiv1.PodTemplateSpec{ObjectMeta: metav1.ObjectMeta{Labels: labels}},
 			VolumeClaimTemplates: []apiv1.PersistentVolumeClaim{{
-				ObjectMeta: metav1.ObjectMeta{Name: "seata-store"},
-				Spec: apiv1.PersistentVolumeClaimSpec{
-					AccessModes: []apiv1.PersistentVolumeAccessMode{"ReadWriteOnce"},
-					Resources:   s.Spec.Store.Resources,
+				ObjectMeta: metav1.ObjectMeta{
+					Name:   pvcName,
+					Labels: map[string]string{"app": s.GetName(), "uid": string(s.UID)},
 				},
+				Spec: s.Spec.Persistence.PersistentVolumeClaimSpec,
 			}},
 		},
 	}
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 8418b46..8049380 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -3,9 +3,14 @@ package utils
 import (
 	"fmt"
 	seatav1alpha1 "github.com/apache/seata-k8s/api/v1alpha1"
+	"strconv"
 	"strings"
 )
 
+const (
+	SeataFinalizer = "cleanUpSeataPVC"
+)
+
 func ConcatRaftServerAddress(s *seatav1alpha1.SeataServer) string {
 	var addrBuilder strings.Builder
 	for i := int32(0); i < s.Spec.Replicas; i++ {
@@ -17,3 +22,36 @@ func ConcatRaftServerAddress(s *seatav1alpha1.SeataServer) string {
 	addr = addr[:len(addr)-1]
 	return addr
 }
+
+func ContainsString(slice []string, str string) bool {
+	for _, item := range slice {
+		if item == str {
+			return true
+		}
+	}
+	return false
+}
+
+func RemoveString(slice []string, str string) (result []string) {
+	for _, item := range slice {
+		if item == str {
+			continue
+		}
+		result = append(result, item)
+	}
+	return result
+}
+
+func IsPVCOrphan(pvcName string, replicas int32) bool {
+	index := strings.LastIndexAny(pvcName, "-")
+	if index == -1 {
+		return false
+	}
+
+	ordinal, err := strconv.Atoi(pvcName[index+1:])
+	if err != nil {
+		return false
+	}
+
+	return int32(ordinal) >= replicas
+}


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