Skip to content

Commit d6e430c

Browse files
sergeyshevchilya-hontarau
authored andcommitted
feat: Refactor resource generation (aenix-io#84)
Closes aenix-io#55
1 parent 2d34e62 commit d6e430c

File tree

8 files changed

+415
-4
lines changed

8 files changed

+415
-4
lines changed

internal/controller/etcdcluster_controller_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ var _ = Describe("EtcdCluster Controller", func() {
119119
svc = &v1.Service{}
120120
clientSvcName := types.NamespacedName{
121121
Namespace: typeNamespacedName.Namespace,
122-
Name: controllerReconciler.getClientServiceName(etcdcluster),
122+
Name: factory.GetClientServiceName(etcdcluster),
123123
}
124124
err = k8sClient.Get(ctx, clientSvcName, svc)
125125
Expect(err).NotTo(HaveOccurred(), "cluster client Service should exist")

internal/controller/factory/builders.go

+41-1
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import (
2121
"fmt"
2222

2323
appsv1 "k8s.io/api/apps/v1"
24+
corev1 "k8s.io/api/core/v1"
2425
"k8s.io/apimachinery/pkg/api/errors"
2526
"k8s.io/apimachinery/pkg/labels"
2627
"k8s.io/apimachinery/pkg/types"
2728
"sigs.k8s.io/controller-runtime/pkg/client"
2829
"sigs.k8s.io/controller-runtime/pkg/log"
2930
)
3031

31-
func reconcileSTS(ctx context.Context, rclient client.Client, crdName string, sts *appsv1.StatefulSet) error {
32+
func reconcileStatefulSet(ctx context.Context, rclient client.Client, crdName string, sts *appsv1.StatefulSet) error {
3233
logger := log.FromContext(ctx)
3334

3435
currentSts := &appsv1.StatefulSet{}
@@ -47,3 +48,42 @@ func reconcileSTS(ctx context.Context, rclient client.Client, crdName string, st
4748
sts.Status = currentSts.Status
4849
return rclient.Update(ctx, sts)
4950
}
51+
52+
func reconcileConfigMap(ctx context.Context, rclient client.Client, crdName string, configMap *corev1.ConfigMap) error {
53+
logger := log.FromContext(ctx)
54+
55+
currentConfigMap := &corev1.ConfigMap{}
56+
err := rclient.Get(ctx, types.NamespacedName{Namespace: configMap.Namespace, Name: configMap.Name}, currentConfigMap)
57+
if err != nil {
58+
if errors.IsNotFound(err) {
59+
logger.V(2).Info("creating new configMap", "cm_name", configMap.Name, "crd_object", crdName)
60+
return rclient.Create(ctx, configMap)
61+
}
62+
return fmt.Errorf("cannot get existing configMap: %s, for crd_object: %s, err: %w", configMap.Name, crdName, err)
63+
}
64+
configMap.Annotations = labels.Merge(currentConfigMap.Annotations, configMap.Annotations)
65+
if configMap.ResourceVersion != "" {
66+
configMap.ResourceVersion = currentConfigMap.ResourceVersion
67+
}
68+
return rclient.Update(ctx, configMap)
69+
}
70+
71+
func reconcileService(ctx context.Context, rclient client.Client, crdName string, svc *corev1.Service) error {
72+
logger := log.FromContext(ctx)
73+
74+
currentSvc := &corev1.Service{}
75+
err := rclient.Get(ctx, types.NamespacedName{Namespace: svc.Namespace, Name: svc.Name}, currentSvc)
76+
if err != nil {
77+
if errors.IsNotFound(err) {
78+
logger.V(2).Info("creating new service", "svc_name", svc.Name, "crd_object", crdName)
79+
return rclient.Create(ctx, svc)
80+
}
81+
return fmt.Errorf("cannot get existing service: %s, for crd_object: %s, err: %w", svc.Name, crdName, err)
82+
}
83+
svc.Annotations = labels.Merge(currentSvc.Annotations, svc.Annotations)
84+
if svc.ResourceVersion != "" {
85+
svc.ResourceVersion = currentSvc.ResourceVersion
86+
}
87+
svc.Status = currentSvc.Status
88+
return rclient.Update(ctx, svc)
89+
}

internal/controller/factory/configMap.go

+54-1
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,61 @@ limitations under the License.
1616

1717
package factory
1818

19-
import etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
19+
import (
20+
"context"
21+
"fmt"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
ctrl "sigs.k8s.io/controller-runtime"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
29+
etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
30+
)
2031

2132
func GetClusterStateConfigMapName(cluster *etcdaenixiov1alpha1.EtcdCluster) string {
2233
return cluster.Name + "-cluster-state"
2334
}
35+
36+
func CreateOrUpdateClusterStateConfigMap(
37+
ctx context.Context,
38+
cluster *etcdaenixiov1alpha1.EtcdCluster,
39+
isClusterReady bool,
40+
rclient client.Client,
41+
rscheme *runtime.Scheme,
42+
) error {
43+
initialCluster := ""
44+
for i := int32(0); i < *cluster.Spec.Replicas; i++ {
45+
if i > 0 {
46+
initialCluster += ","
47+
}
48+
initialCluster += fmt.Sprintf("%s-%d=https://%s-%d.%s.%s.svc:2380",
49+
cluster.Name, i,
50+
cluster.Name, i, cluster.Name, cluster.Namespace,
51+
)
52+
}
53+
54+
configMap := &corev1.ConfigMap{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Namespace: cluster.Namespace,
57+
Name: GetClusterStateConfigMapName(cluster),
58+
},
59+
Data: map[string]string{
60+
"ETCD_INITIAL_CLUSTER_STATE": "new",
61+
"ETCD_INITIAL_CLUSTER": initialCluster,
62+
"ETCD_INITIAL_CLUSTER_TOKEN": cluster.Name + "-" + cluster.Namespace,
63+
},
64+
}
65+
66+
if isClusterReady {
67+
// update cluster state to existing
68+
configMap.Data["ETCD_INITIAL_CLUSTER_STATE"] = "existing"
69+
}
70+
71+
if err := ctrl.SetControllerReference(cluster, configMap, rscheme); err != nil {
72+
return fmt.Errorf("cannot set controller reference: %w", err)
73+
}
74+
75+
return reconcileConfigMap(ctx, rclient, cluster.Name, configMap)
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Copyright 2024 The etcd-operator Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package factory
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/runtime"
23+
"k8s.io/utils/ptr"
24+
25+
. "github.com/onsi/ginkgo/v2"
26+
. "github.com/onsi/gomega"
27+
corev1 "k8s.io/api/core/v1"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/types"
30+
31+
etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
32+
)
33+
34+
var _ = Describe("CreateOrUpdateClusterStateConfigMap handlers", func() {
35+
Context("When ensuring a configMap", func() {
36+
const resourceName = "test-resource"
37+
38+
ctx := context.Background()
39+
40+
etcdcluster := &etcdaenixiov1alpha1.EtcdCluster{
41+
ObjectMeta: metav1.ObjectMeta{
42+
Name: resourceName,
43+
Namespace: "default",
44+
UID: "test-uid",
45+
},
46+
Spec: etcdaenixiov1alpha1.EtcdClusterSpec{
47+
Replicas: ptr.To(int32(3)),
48+
},
49+
}
50+
typeNamespacedName := types.NamespacedName{
51+
Name: GetClusterStateConfigMapName(etcdcluster),
52+
Namespace: "default",
53+
}
54+
55+
It("should successfully ensure the configmap", func() {
56+
cm := &corev1.ConfigMap{}
57+
58+
By("creating the configmap for initial cluster")
59+
err := CreateOrUpdateClusterStateConfigMap(ctx, etcdcluster, false, k8sClient, k8sClient.Scheme())
60+
Expect(err).NotTo(HaveOccurred())
61+
62+
err = k8sClient.Get(ctx, typeNamespacedName, cm)
63+
cmUid := cm.UID
64+
Expect(err).NotTo(HaveOccurred())
65+
Expect(cm.Data["ETCD_INITIAL_CLUSTER_STATE"]).To(Equal("new"))
66+
67+
By("updating the configmap for initialized cluster")
68+
err = CreateOrUpdateClusterStateConfigMap(ctx, etcdcluster, true, k8sClient, k8sClient.Scheme())
69+
Expect(err).NotTo(HaveOccurred())
70+
71+
err = k8sClient.Get(ctx, typeNamespacedName, cm)
72+
Expect(err).NotTo(HaveOccurred())
73+
Expect(cm.Data["ETCD_INITIAL_CLUSTER_STATE"]).To(Equal("existing"))
74+
// Check that we are updating the same configmap
75+
Expect(cm.UID).To(Equal(cmUid))
76+
77+
By("deleting the configmap")
78+
79+
Expect(k8sClient.Delete(ctx, cm)).To(Succeed())
80+
})
81+
82+
It("should fail on creating the configMap with invalid owner reference", func() {
83+
etcdcluster := etcdcluster.DeepCopy()
84+
emptyScheme := runtime.NewScheme()
85+
86+
err := CreateOrUpdateClusterStateConfigMap(ctx, etcdcluster, false, k8sClient, emptyScheme)
87+
Expect(err).To(HaveOccurred())
88+
})
89+
})
90+
})

internal/controller/factory/statefulset.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ func CreateOrUpdateStatefulSet(
198198
return fmt.Errorf("cannot set controller reference: %w", err)
199199
}
200200

201-
return reconcileSTS(ctx, rclient, cluster.Name, statefulSet)
201+
return reconcileStatefulSet(ctx, rclient, cluster.Name, statefulSet)
202202
}
203203

204204
func generateEtcdCommand(cluster *etcdaenixiov1alpha1.EtcdCluster) []string {

internal/controller/factory/statefulset_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ var _ = Describe("CreateOrUpdateStatefulSet handler", func() {
6969
It("should successfully create the statefulset with filled spec", func() {
7070
By("Creating the statefulset")
7171
etcdcluster := etcdcluster.DeepCopy()
72+
etcdcluster.Spec.Storage = etcdaenixiov1alpha1.StorageSpec{
73+
VolumeClaimTemplate: etcdaenixiov1alpha1.EmbeddedPersistentVolumeClaim{
74+
EmbeddedObjectMetadata: etcdaenixiov1alpha1.EmbeddedObjectMetadata{
75+
Name: "etcd-data",
76+
},
77+
Spec: v1.PersistentVolumeClaimSpec{
78+
AccessModes: []v1.PersistentVolumeAccessMode{
79+
v1.ReadWriteOnce,
80+
},
81+
Resources: v1.VolumeResourceRequirements{
82+
Requests: v1.ResourceList{
83+
v1.ResourceStorage: resource.MustParse("1Gi"),
84+
},
85+
},
86+
},
87+
Status: v1.PersistentVolumeClaimStatus{},
88+
},
89+
}
7290
etcdcluster.Spec.PodSpec = etcdaenixiov1alpha1.PodSpec{
7391
Resources: v1.ResourceRequirements{
7492
Requests: v1.ResourceList{

internal/controller/factory/svc.go

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Copyright 2024 The etcd-operator Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package factory
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/util/intstr"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
30+
etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
31+
)
32+
33+
func GetClientServiceName(cluster *etcdaenixiov1alpha1.EtcdCluster) string {
34+
return fmt.Sprintf("%s-client", cluster.Name)
35+
}
36+
37+
func CreateOrUpdateClusterService(
38+
ctx context.Context,
39+
cluster *etcdaenixiov1alpha1.EtcdCluster,
40+
rclient client.Client,
41+
rscheme *runtime.Scheme,
42+
) error {
43+
svc := &corev1.Service{
44+
ObjectMeta: metav1.ObjectMeta{
45+
Name: cluster.Name,
46+
Namespace: cluster.Namespace,
47+
Labels: map[string]string{
48+
"app.kubernetes.io/name": "etcd",
49+
"app.kubernetes.io/instance": cluster.Name,
50+
"app.kubernetes.io/managed-by": "etcd-operator",
51+
},
52+
},
53+
Spec: corev1.ServiceSpec{
54+
Ports: []corev1.ServicePort{
55+
{Name: "peer", TargetPort: intstr.FromInt32(2380), Port: 2380, Protocol: corev1.ProtocolTCP},
56+
{Name: "client", TargetPort: intstr.FromInt32(2379), Port: 2379, Protocol: corev1.ProtocolTCP},
57+
},
58+
Type: corev1.ServiceTypeClusterIP,
59+
ClusterIP: "None",
60+
Selector: map[string]string{
61+
"app.kubernetes.io/name": "etcd",
62+
"app.kubernetes.io/instance": cluster.Name,
63+
"app.kubernetes.io/managed-by": "etcd-operator",
64+
},
65+
PublishNotReadyAddresses: true,
66+
},
67+
}
68+
69+
if err := ctrl.SetControllerReference(cluster, svc, rscheme); err != nil {
70+
return fmt.Errorf("cannot set controller reference: %w", err)
71+
}
72+
73+
return reconcileService(ctx, rclient, cluster.Name, svc)
74+
}
75+
76+
func CreateOrUpdateClientService(
77+
ctx context.Context,
78+
cluster *etcdaenixiov1alpha1.EtcdCluster,
79+
rclient client.Client,
80+
rscheme *runtime.Scheme,
81+
) error {
82+
svc := &corev1.Service{
83+
ObjectMeta: metav1.ObjectMeta{
84+
Name: GetClientServiceName(cluster),
85+
Namespace: cluster.Namespace,
86+
Labels: map[string]string{
87+
"app.kubernetes.io/name": "etcd",
88+
"app.kubernetes.io/instance": cluster.Name,
89+
"app.kubernetes.io/managed-by": "etcd-operator",
90+
},
91+
},
92+
Spec: corev1.ServiceSpec{
93+
Ports: []corev1.ServicePort{
94+
{Name: "client", TargetPort: intstr.FromInt32(2379), Port: 2379, Protocol: corev1.ProtocolTCP},
95+
},
96+
Type: corev1.ServiceTypeClusterIP,
97+
Selector: map[string]string{
98+
"app.kubernetes.io/name": "etcd",
99+
"app.kubernetes.io/instance": cluster.Name,
100+
"app.kubernetes.io/managed-by": "etcd-operator",
101+
},
102+
},
103+
}
104+
105+
if err := ctrl.SetControllerReference(cluster, svc, rscheme); err != nil {
106+
return fmt.Errorf("cannot set controller reference: %w", err)
107+
}
108+
109+
return reconcileService(ctx, rclient, cluster.Name, svc)
110+
}

0 commit comments

Comments
 (0)