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