diff --git a/README.md b/README.md index 983abe1..0391667 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,21 @@ For CRD details, you can visit [operator.seata.apache.org_seataservers.yaml](con requests: storage: 5Gi env: - console.user.username: seata - console.user.password: seata + - name: console.user.username + value: seata + - name: console.user.password + valueFrom: + secretKeyRef: + name: seata + key: password + --- + apiVersion: v1 + kind: Secret + metadata: + name: seata + type: Opaque + data: + password: seata ``` ## Method 2: Example without Using Operator diff --git a/README.zh.md b/README.zh.md index 86ffc87..fface34 100755 --- a/README.zh.md +++ b/README.zh.md @@ -98,8 +98,21 @@ https://github.com/seata/seata-docker requests: storage: 5Gi env: - console.user.username: seata - console.user.password: seata + - name: console.user.username + value: seata + - name: console.user.password + valueFrom: + secretKeyRef: + name: seata + key: password + --- + apiVersion: v1 + kind: Secret + metadata: + name: seata + type: Opaque + data: + password: seata ``` diff --git a/api/v1alpha1/seataserver_types.go b/api/v1alpha1/seataserver_types.go index cee9f53..1c4a761 100644 --- a/api/v1alpha1/seataserver_types.go +++ b/api/v1alpha1/seataserver_types.go @@ -78,7 +78,7 @@ type ContainerSpec struct { ContainerName string `json:"containerName"` Image string `json:"image"` // +kubebuilder:validation:Optional - Env map[string]string `json:"env"` + Env []apiv1.EnvVar `json:"env"` // +kubebuilder:validation:Optional Resources apiv1.ResourceRequirements `json:"resources"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 35277bb..54ee94f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -192,9 +192,9 @@ func (in *ContainerSpec) DeepCopyInto(out *ContainerSpec) { *out = *in if in.Env != nil { in, out := &in.Env, &out.Env - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) } } in.Resources.DeepCopyInto(&out.Resources) diff --git a/config/crd/bases/operator.seata.apache.org_seataservers.yaml b/config/crd/bases/operator.seata.apache.org_seataservers.yaml index 2c6883c..9d3bbf1 100644 --- a/config/crd/bases/operator.seata.apache.org_seataservers.yaml +++ b/config/crd/bases/operator.seata.apache.org_seataservers.yaml @@ -38,9 +38,113 @@ spec: default: seata-server type: string env: - additionalProperties: - type: string - type: object + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: Name of the environment variable. Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) are expanded using + the previously defined environment variables in the container + and any service environment variables. If a variable cannot + be resolved, the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows for escaping + the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the + string literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists or + not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. Cannot + be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, + status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array image: type: string ports: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f2b3a78..0ab68b1 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,22 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch - apiGroups: - apps resources: diff --git a/controllers/seataserver_controller.go b/controllers/seataserver_controller.go index a73f184..899da36 100644 --- a/controllers/seataserver_controller.go +++ b/controllers/seataserver_controller.go @@ -54,6 +54,8 @@ const RequeueSeconds = 10 //+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -183,7 +185,22 @@ func (r *SeataServerReconciler) updateStatefulSet(ctx context.Context, s *seatav s.Status.Synchronized = false } if readySize == newSize && !s.Status.Synchronized { - if err = seata.SyncRaftCluster(ctx, s); err != nil { + username, password := "seata", "seata" + for _, env := range s.Spec.Env { + if env.Name == "console.user.username" { + username, err = seata.FetchEnvVar(ctx, r.Client, s, env) + if err != nil { + logger.Error(err, "Failed to fetch Env console.user.username") + } + } + if env.Name == "console.user.password" { + password, err = seata.FetchEnvVar(ctx, r.Client, s, env) + if err != nil { + logger.Error(err, "Failed to fetch Env console.user.password") + } + } + } + if err = seata.SyncRaftCluster(ctx, s, username, password); err != nil { logger.Error(err, "Failed to synchronize the raft cluster") s.Status.Synchronized = false } else { diff --git a/deploy/seata-server-cluster.yaml b/deploy/seata-server-cluster.yaml index 4499c7a..db81345 100644 --- a/deploy/seata-server-cluster.yaml +++ b/deploy/seata-server-cluster.yaml @@ -5,9 +5,25 @@ metadata: namespace: default spec: serviceName: seata-server-cluster - replicas: 2 - image: seataio/seata-server:2.0.0 + replicas: 1 + image: seataio/seata-server:latest store: resources: requests: storage: 5Gi + env: + - name: console.user.username + value: seata + - name: console.user.password + valueFrom: + secretKeyRef: + name: seata + key: password +--- +apiVersion: v1 +kind: Secret +metadata: + name: seata +type: Opaque +data: + password: MTIzNDU2 diff --git a/pkg/seata/fetchers.go b/pkg/seata/fetchers.go new file mode 100644 index 0000000..26bca69 --- /dev/null +++ b/pkg/seata/fetchers.go @@ -0,0 +1,70 @@ +package seata + +import ( + "context" + "fmt" + seatav1alpha1 "github.com/apache/seata-k8s/api/v1alpha1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func FetchEnvVar(ctx context.Context, c client.Client, cr *seatav1alpha1.SeataServer, envVar v1.EnvVar) (string, error) { + if envVar.ValueFrom == nil { + return envVar.Value, nil + } + + // Inspired by kubelet#makeEnvironmentVariables, determine the final values of variables. + // See https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L694-L806 + var result string + switch { + case envVar.ValueFrom.ConfigMapKeyRef != nil: + cm := envVar.ValueFrom.ConfigMapKeyRef + name := cm.Name + key := cm.Key + optional := cm.Optional != nil && *cm.Optional + + configMap := &v1.ConfigMap{} + err := c.Get(ctx, types.NamespacedName{Name: name, Namespace: cr.Namespace}, configMap) + if err != nil { + if errors.IsNotFound(err) && optional { + // ignore error when marked optional + return result, nil + } + return result, err + } + runtimeVal, ok := configMap.Data[key] + if !ok { + if optional { + return result, nil + } + return result, fmt.Errorf("couldn't find key %v in ConfigMap %v/%v", key, cr.Namespace, name) + } + result = runtimeVal + case envVar.ValueFrom.SecretKeyRef != nil: + s := envVar.ValueFrom.SecretKeyRef + name := s.Name + key := s.Key + optional := s.Optional != nil && *s.Optional + secret := &v1.Secret{} + err := c.Get(ctx, types.NamespacedName{Name: name, Namespace: cr.Namespace}, secret) + if err != nil { + if errors.IsNotFound(err) && optional { + // ignore error when marked optional + return result, nil + } + return result, err + } + runtimeValBytes, ok := secret.Data[key] + if !ok { + if optional { + return result, nil + } + return result, fmt.Errorf("couldn't find key %v in Secret %v/%v", key, cr.Namespace, name) + } + runtimeVal := string(runtimeValBytes) + result = runtimeVal + } + return result, nil +} diff --git a/pkg/seata/generators.go b/pkg/seata/generators.go index 76f2385..21addc6 100644 --- a/pkg/seata/generators.go +++ b/pkg/seata/generators.go @@ -137,8 +137,8 @@ func MakeStatefulSet(s *seatav1alpha1.SeataServer) *appsv1.StatefulSet { addr := utils.ConcatRaftServerAddress(s) envs = append(envs, apiv1.EnvVar{Name: "server.raft.serverAddr", Value: addr}) - for k, v := range s.Spec.Env { - envs = append(envs, apiv1.EnvVar{Name: k, Value: v}) + for _, env := range s.Spec.Env { + envs = append(envs, env) } container.Env = envs diff --git a/pkg/seata/synchronizers.go b/pkg/seata/synchronizers.go index 71c0da2..6a12c33 100644 --- a/pkg/seata/synchronizers.go +++ b/pkg/seata/synchronizers.go @@ -33,17 +33,9 @@ type rspData struct { Success bool `json:"success"` } -func changeCluster(s *seatav1alpha1.SeataServer, i int32) error { +func changeCluster(s *seatav1alpha1.SeataServer, i int32, username string, password string) error { client := http.Client{} host := fmt.Sprintf("%s-%d.%s.%s.svc:%d", s.Name, i, s.Spec.ServiceName, s.Namespace, s.Spec.Ports.ConsolePort) - username, ok := s.Spec.Env["console.user.username"] - if !ok { - username = "seata" - } - password, ok := s.Spec.Env["console.user.password"] - if !ok { - password = "seata" - } values := map[string]string{"username": username, "password": password} jsonValue, _ := json.Marshal(values) @@ -100,7 +92,7 @@ func changeCluster(s *seatav1alpha1.SeataServer, i int32) error { return nil } -func SyncRaftCluster(ctx context.Context, s *seatav1alpha1.SeataServer) error { +func SyncRaftCluster(ctx context.Context, s *seatav1alpha1.SeataServer, username string, password string) error { logger := log.FromContext(ctx) group, childContext := errgroup.WithContext(ctx) @@ -111,7 +103,7 @@ func SyncRaftCluster(ctx context.Context, s *seatav1alpha1.SeataServer) error { case <-childContext.Done(): return nil default: - err := changeCluster(s, finalI) + err := changeCluster(s, finalI, username, password) if err != nil { logger.Error(err, fmt.Sprintf("fail to SyncRaftCluster at %d-th pod", finalI)) }