diff --git a/internal/controller/etcdcluster_controller.go b/internal/controller/etcdcluster_controller.go index 03eaec9..334878f 100644 --- a/internal/controller/etcdcluster_controller.go +++ b/internal/controller/etcdcluster_controller.go @@ -130,7 +130,7 @@ func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) // update sts pod template (and only pod template) if it doesn't match desired state if !state.statefulSetPodSpecCorrect() { // TODO: needs implementing - desiredSts := factory.TemplateStatefulSet() // TODO: needs implementing + desiredSts := factory.TemplateStatefulSet(state.instance) // TODO: needs implementing state.statefulSet.Spec.Template.Spec = desiredSts.Spec.Template.Spec return ctrl.Result{}, r.patchOrCreateObject(ctx, &state.statefulSet) } diff --git a/internal/controller/factory/statefulset.go b/internal/controller/factory/statefulset.go index 202419b..54bfcd9 100644 --- a/internal/controller/factory/statefulset.go +++ b/internal/controller/factory/statefulset.go @@ -31,8 +31,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/aenix-io/etcd-operator/api/v1alpha1" etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" - "github.com/aenix-io/etcd-operator/internal/k8sutils" "github.com/aenix-io/etcd-operator/internal/log" ) @@ -42,8 +42,90 @@ const ( ) // TODO! -func TemplateStatefulSet() *appsv1.StatefulSet { - panic("not yet implemented") +func TemplateStatefulSet(c *v1alpha1.EtcdCluster) *appsv1.StatefulSet { + labels := func(c *v1alpha1.EtcdCluster) map[string]string { + return map[string]string{ + "app.kubernetes.io/name": "etcd", + "app.kubernetes.io/instance": c.Name, + "app.kubernetes.io/managed-by": "etcd-operator", + } + } + volumeClaimTemplates := func(c *v1alpha1.EtcdCluster) []corev1.PersistentVolumeClaim { + if c.Spec.Storage.EmptyDir != nil { + return nil + } + return []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{Name: "data"}, + Spec: c.Spec.Storage.VolumeClaimTemplate.Spec, + }, + } + } + s := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.Name, + Namespace: c.Namespace, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: c.Spec.Replicas, + PodManagementPolicy: appsv1.ParallelPodManagement, + ServiceName: fmt.Sprintf("%s-headless", c.Name), + Selector: &metav1.LabelSelector{ + MatchLabels: labels(c), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels(c), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: etcdContainerName, + Image: etcdaenixiov1alpha1.DefaultEtcdImage, + Args: generateEtcdArgs(c), + Ports: []corev1.ContainerPort{ + {Name: "metrics", ContainerPort: 2381}, + {Name: "peer", ContainerPort: 2380}, + {Name: "client", ContainerPort: 2379}, + }, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-cluster-state", c.Name), + }, + }, + }, + }, + Env: []corev1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: "POD_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + }, + StartupProbe: getStartupProbe(), + LivenessProbe: getLivenessProbe(), + ReadinessProbe: getReadinessProbe(), + VolumeMounts: generateVolumeMounts(c), + }}, + Volumes: generateVolumes(c), + }, + }, + VolumeClaimTemplates: volumeClaimTemplates(c), + }, + } + return s } func PodLabels(cluster *etcdaenixiov1alpha1.EtcdCluster) map[string]string { @@ -63,62 +145,27 @@ func CreateOrUpdateStatefulSet( cluster *etcdaenixiov1alpha1.EtcdCluster, rclient client.Client, ) error { - podMetadata := metav1.ObjectMeta{ - Labels: PodLabels(cluster), - } - - if cluster.Spec.PodTemplate.Annotations != nil { - podMetadata.Annotations = cluster.Spec.PodTemplate.Annotations - } - - volumeClaimTemplates := make([]corev1.PersistentVolumeClaim, 0) - if cluster.Spec.Storage.EmptyDir == nil { - volumeClaimTemplates = append(volumeClaimTemplates, corev1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: GetPVCName(cluster), - Labels: cluster.Spec.Storage.VolumeClaimTemplate.Labels, - Annotations: cluster.Spec.Storage.VolumeClaimTemplate.Annotations, - }, - Spec: cluster.Spec.Storage.VolumeClaimTemplate.Spec, - Status: cluster.Spec.Storage.VolumeClaimTemplate.Status, - }) - } + /* + // This is kept as an example of how override pod spec is merged with base pod spec + // for the future, when we get round to restoring this feature + basePodSpec := corev1.PodSpec{ + Containers: []corev1.Container{generateContainer(cluster)}, + Volumes: volumes, + } + if cluster.Spec.PodTemplate.Spec.Containers == nil { + cluster.Spec.PodTemplate.Spec.Containers = make([]corev1.Container, 0) + } + finalPodSpec, err := k8sutils.StrategicMerge(basePodSpec, cluster.Spec.PodTemplate.Spec) + if err != nil { + return fmt.Errorf("cannot strategic-merge base podspec with podTemplate.spec: %w", err) + } + */ - volumes := generateVolumes(cluster) + statefulSet := TemplateStatefulSet(cluster) - basePodSpec := corev1.PodSpec{ - Containers: []corev1.Container{generateContainer(cluster)}, - Volumes: volumes, - } - if cluster.Spec.PodTemplate.Spec.Containers == nil { - cluster.Spec.PodTemplate.Spec.Containers = make([]corev1.Container, 0) - } - finalPodSpec, err := k8sutils.StrategicMerge(basePodSpec, cluster.Spec.PodTemplate.Spec) - if err != nil { - return fmt.Errorf("cannot strategic-merge base podspec with podTemplate.spec: %w", err) - } - - statefulSet := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cluster.Namespace, - Name: cluster.Name, - }, - Spec: appsv1.StatefulSetSpec{ - // initialize static fields that cannot be changed across updates. - Replicas: cluster.Spec.Replicas, - ServiceName: GetHeadlessServiceName(cluster), - PodManagementPolicy: appsv1.ParallelPodManagement, - Selector: &metav1.LabelSelector{ - MatchLabels: NewLabelsBuilder().WithName().WithInstance(cluster.Name).WithManagedBy(), - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: podMetadata, - Spec: finalPodSpec, - }, - VolumeClaimTemplates: volumeClaimTemplates, - }, - } - ctx, err = contextWithGVK(ctx, statefulSet, rclient.Scheme()) + // This line used to be an assignment rather than a declaration and redefining + // the surrounding function's arguments looks really really wrong + ctx, err := contextWithGVK(ctx, statefulSet, rclient.Scheme()) if err != nil { return err } @@ -134,31 +181,17 @@ func CreateOrUpdateStatefulSet( func generateVolumes(cluster *etcdaenixiov1alpha1.EtcdCluster) []corev1.Volume { volumes := []corev1.Volume{} - var dataVolumeSource corev1.VolumeSource - if cluster.Spec.Storage.EmptyDir != nil { - dataVolumeSource = corev1.VolumeSource{EmptyDir: cluster.Spec.Storage.EmptyDir} - } else { - dataVolumeSource = corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: GetPVCName(cluster), - }, - } - } - - volumes = append( - volumes, - - corev1.Volume{ + volumes = append(volumes, corev1.Volume{ Name: "data", - VolumeSource: dataVolumeSource, - }, - ) + VolumeSource: corev1.VolumeSource{EmptyDir: cluster.Spec.Storage.EmptyDir}, + }) + } - if cluster.Spec.Security != nil && cluster.Spec.Security.TLS.PeerSecret != "" { - volumes = append(volumes, - []corev1.Volume{ - { + if cluster.Spec.Security != nil { + if cluster.Spec.Security.TLS.PeerSecret != "" { + volumes = append(volumes, + corev1.Volume{ Name: "peer-trusted-ca-certificate", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ @@ -166,7 +199,7 @@ func generateVolumes(cluster *etcdaenixiov1alpha1.EtcdCluster) []corev1.Volume { }, }, }, - { + corev1.Volume{ Name: "peer-certificate", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ @@ -174,35 +207,30 @@ func generateVolumes(cluster *etcdaenixiov1alpha1.EtcdCluster) []corev1.Volume { }, }, }, - }...) - } + ) + } - if cluster.Spec.Security != nil && cluster.Spec.Security.TLS.ServerSecret != "" { - volumes = append(volumes, - []corev1.Volume{ - { - Name: "server-certificate", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: cluster.Spec.Security.TLS.ServerSecret, - }, + if cluster.Spec.Security.TLS.ServerSecret != "" { + volumes = append(volumes, corev1.Volume{ + Name: "server-certificate", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: cluster.Spec.Security.TLS.ServerSecret, }, }, - }...) - } + }) + } - if cluster.Spec.Security != nil && cluster.Spec.Security.TLS.ClientSecret != "" { - volumes = append(volumes, - []corev1.Volume{ - { - Name: "client-trusted-ca-certificate", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: cluster.Spec.Security.TLS.ClientTrustedCASecret, - }, + if cluster.Spec.Security.TLS.ClientSecret != "" { + volumes = append(volumes, corev1.Volume{ + Name: "client-trusted-ca-certificate", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: cluster.Spec.Security.TLS.ClientTrustedCASecret, }, }, - }...) + }) + } } return volumes @@ -219,51 +247,42 @@ func generateVolumeMounts(cluster *etcdaenixiov1alpha1.EtcdCluster) []corev1.Vol MountPath: "/var/run/etcd", }) - if cluster.Spec.Security != nil && cluster.Spec.Security.TLS.PeerSecret != "" { - volumeMounts = append(volumeMounts, []corev1.VolumeMount{ - { - Name: "peer-trusted-ca-certificate", - ReadOnly: true, - MountPath: "/etc/etcd/pki/peer/ca", - }, - { - Name: "peer-certificate", - ReadOnly: true, - MountPath: "/etc/etcd/pki/peer/cert", - }, - }...) - } + if cluster.Spec.Security != nil { + if cluster.Spec.Security.TLS.PeerSecret != "" { + volumeMounts = append(volumeMounts, + corev1.VolumeMount{ + Name: "peer-trusted-ca-certificate", + ReadOnly: true, + MountPath: "/etc/etcd/pki/peer/ca", + }, + corev1.VolumeMount{ + Name: "peer-certificate", + ReadOnly: true, + MountPath: "/etc/etcd/pki/peer/cert", + }, + ) + } - if cluster.Spec.Security != nil && cluster.Spec.Security.TLS.ServerSecret != "" { - volumeMounts = append(volumeMounts, []corev1.VolumeMount{ - { + if cluster.Spec.Security.TLS.ServerSecret != "" { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: "server-certificate", ReadOnly: true, MountPath: "/etc/etcd/pki/server/cert", - }, - }...) - } - - if cluster.Spec.Security != nil && cluster.Spec.Security.TLS.ClientSecret != "" { + }) + } - volumeMounts = append(volumeMounts, []corev1.VolumeMount{ - { + if cluster.Spec.Security.TLS.ClientSecret != "" { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: "client-trusted-ca-certificate", ReadOnly: true, MountPath: "/etc/etcd/pki/client/ca", - }, - }...) + }) + } } return volumeMounts } -func generateEtcdCommand() []string { - return []string{ - "etcd", - } -} - func generateEtcdArgs(cluster *etcdaenixiov1alpha1.EtcdCluster) []string { args := []string{} @@ -355,54 +374,6 @@ func generateEtcdArgs(cluster *etcdaenixiov1alpha1.EtcdCluster) []string { return args } -func generateContainer(cluster *etcdaenixiov1alpha1.EtcdCluster) corev1.Container { - podEnv := []corev1.EnvVar{ - { - Name: "POD_NAME", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.name", - }, - }, - }, - { - Name: "POD_NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - }, - } - - c := corev1.Container{} - c.Name = etcdContainerName - c.Image = etcdaenixiov1alpha1.DefaultEtcdImage - c.Command = generateEtcdCommand() - c.Args = generateEtcdArgs(cluster) - c.Ports = []corev1.ContainerPort{ - {Name: "peer", ContainerPort: 2380}, - {Name: "client", ContainerPort: 2379}, - } - clusterStateConfigMapName := GetClusterStateConfigMapName(cluster) - c.EnvFrom = []corev1.EnvFromSource{ - { - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: clusterStateConfigMapName, - }, - }, - }, - } - c.StartupProbe = getStartupProbe() - c.LivenessProbe = getLivenessProbe() - c.ReadinessProbe = getReadinessProbe() - c.Env = podEnv - c.VolumeMounts = generateVolumeMounts(cluster) - - return c -} - func getStartupProbe() *corev1.Probe { return &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ diff --git a/internal/controller/factory/statefulset_test.go b/internal/controller/factory/statefulset_test.go index 64d4660..9d0ad9c 100644 --- a/internal/controller/factory/statefulset_test.go +++ b/internal/controller/factory/statefulset_test.go @@ -179,10 +179,6 @@ var _ = Describe("CreateOrUpdateStatefulSet handler", func() { Expect(statefulSet.Spec.Template.ObjectMeta.Annotations).To(Equal(etcdcluster.Spec.PodTemplate.Annotations)) }) - By("Checking the command", func() { - Expect(statefulSet.Spec.Template.Spec.Containers[0].Command).To(Equal(generateEtcdCommand())) - }) - By("Checking the extraArgs", func() { Expect(statefulSet.Spec.Template.Spec.Containers[0].Args).To(Equal(generateEtcdArgs(&etcdcluster))) By("Checking args are sorted", func() {