diff --git a/internal/controller/etcdcluster_controller.go b/internal/controller/etcdcluster_controller.go index e63b1a9f..bda0fa6a 100644 --- a/internal/controller/etcdcluster_controller.go +++ b/internal/controller/etcdcluster_controller.go @@ -144,6 +144,10 @@ func (r *EtcdClusterReconciler) ensureClusterObjects( if err := r.ensureClusterStatefulSet(ctx, cluster); err != nil { return err } + // 3. create or update ClusterIP Service + if err := r.ensureClusterClientService(ctx, cluster); err != nil { + return err + } return nil } @@ -196,6 +200,51 @@ func (r *EtcdClusterReconciler) ensureClusterService(ctx context.Context, cluste return nil } +func (r *EtcdClusterReconciler) ensureClusterClientService(ctx context.Context, cluster *etcdaenixiov1alpha1.EtcdCluster) error { + svc := &corev1.Service{} + err := r.Get(ctx, client.ObjectKey{ + Namespace: cluster.Namespace, + Name: r.getClientServiceName(cluster), + }, svc) + // Service exists, skip creation + if err == nil { + return nil + } + if !errors.IsNotFound(err) { + return fmt.Errorf("cannot get cluster client service: %w", err) + } + + svc = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.getClientServiceName(cluster), + Namespace: cluster.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/name": "etcd", + "app.kubernetes.io/instance": cluster.Name, + "app.kubernetes.io/managed-by": "etcd-operator", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Name: "client", TargetPort: intstr.FromInt32(2379), Port: 2379, Protocol: corev1.ProtocolTCP}, + }, + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{ + "app.kubernetes.io/name": "etcd", + "app.kubernetes.io/instance": cluster.Name, + "app.kubernetes.io/managed-by": "etcd-operator", + }, + }, + } + if err = ctrl.SetControllerReference(cluster, svc, r.Scheme); err != nil { + return fmt.Errorf("cannot set controller reference: %w", err) + } + if err = r.Create(ctx, svc); err != nil { + return fmt.Errorf("cannot create cluster client service: %w", err) + } + return nil +} + // ensureClusterStateConfigMap creates or updates cluster state configmap. func (r *EtcdClusterReconciler) ensureClusterStateConfigMap( ctx context.Context, cluster *etcdaenixiov1alpha1.EtcdCluster, isClusterInitialized bool) error { @@ -421,6 +470,10 @@ func (r *EtcdClusterReconciler) getClusterStateConfigMapName(cluster *etcdaenixi return cluster.Name + "-cluster-state" } +func (r *EtcdClusterReconciler) getClientServiceName(cluster *etcdaenixiov1alpha1.EtcdCluster) string { + return cluster.Name + "-client" +} + // updateClusterState patches status condition in cluster using merge by Type func (r *EtcdClusterReconciler) updateClusterState(cluster *etcdaenixiov1alpha1.EtcdCluster, state metav1.Condition) { if initIdx := slices.IndexFunc(cluster.Status.Conditions, func(condition metav1.Condition) bool { diff --git a/internal/controller/etcdcluster_controller_test.go b/internal/controller/etcdcluster_controller_test.go index 660e2a52..70aa8234 100644 --- a/internal/controller/etcdcluster_controller_test.go +++ b/internal/controller/etcdcluster_controller_test.go @@ -110,12 +110,21 @@ var _ = Describe("EtcdCluster Controller", func() { // check that Service is created svc := &v1.Service{} err = k8sClient.Get(ctx, typeNamespacedName, svc) - Expect(err).NotTo(HaveOccurred(), "cluster headless Service state should exist") + Expect(err).NotTo(HaveOccurred(), "cluster headless Service should exist") Expect(svc.Spec.ClusterIP).To(Equal("None"), "cluster Service should be headless") // check that StatefulSet is created sts := &appsv1.StatefulSet{} err = k8sClient.Get(ctx, typeNamespacedName, sts) Expect(err).NotTo(HaveOccurred(), "cluster statefulset should exist") + // check that Service is created + svc = &v1.Service{} + clientSvcName := types.NamespacedName{ + Namespace: typeNamespacedName.Namespace, + Name: controllerReconciler.getClientServiceName(etcdcluster), + } + err = k8sClient.Get(ctx, clientSvcName, svc) + Expect(err).NotTo(HaveOccurred(), "cluster client Service should exist") + Expect(svc.Spec.ClusterIP).NotTo(Equal("None"), "cluster client Service should NOT be headless") }) It("should successfully reconcile the resource twice and mark as ready", func() {