diff --git a/control-plane-operator/controllers/hostedcontrolplane/manifests/etcd.go b/control-plane-operator/controllers/hostedcontrolplane/manifests/etcd.go index 8d70f9c838f..6901f2d4555 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/manifests/etcd.go +++ b/control-plane-operator/controllers/hostedcontrolplane/manifests/etcd.go @@ -91,6 +91,33 @@ func EtcdDefragControllerServiceAccount(ns string) *corev1.ServiceAccount { } } +func EtcdServiceAccount(ns string) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd", + Namespace: ns, + }, + } +} + +func EtcdSelfRegisterRole(ns string) *rbacv1.Role { + return &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd-self-register", + Namespace: ns, + }, + } +} + +func EtcdSelfRegisterRoleBinding(ns string) *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd-self-register", + Namespace: ns, + }, + } +} + func EtcdBackupServiceAccount(hcpNamespace string) *corev1.ServiceAccount { return &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml index 44d35df32f1..ac44d424b8f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml @@ -22,6 +22,15 @@ status: - group: "" kind: Service name: etcd-discovery + - group: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register + - group: rbac.authorization.k8s.io + kind: RoleBinding + name: etcd-self-register + - group: "" + kind: ServiceAccount + name: etcd - group: policy kind: PodDisruptionBudget name: etcd diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml new file mode 100644 index 00000000000..3e97f78628b --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +rules: +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml new file mode 100644 index 00000000000..f8cada01fe4 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register +subjects: +- kind: ServiceAccount + name: etcd + namespace: hcp-namespace diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml new file mode 100644 index 00000000000..00f053fdf28 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +imagePullSecrets: +- name: pull-secret +kind: ServiceAccount +metadata: + name: etcd + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml index ba0322c7603..46353bf2b68 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/AROSwift/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml @@ -270,7 +270,7 @@ spec: initContainers: - args: - -c - - exec control-plane-operator resolve-dns ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc + - exec control-plane-operator resolve-dns --self-register ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc command: - /bin/bash env: @@ -279,6 +279,11 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP image: controlplane-operator imagePullPolicy: IfNotPresent name: ensure-dns @@ -351,6 +356,7 @@ spec: priorityClassName: hypershift-etcd restartPolicy: Always schedulerName: default-scheduler + serviceAccountName: etcd tolerations: - effect: NoSchedule key: hypershift.openshift.io/control-plane diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml index 44d35df32f1..ac44d424b8f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml @@ -22,6 +22,15 @@ status: - group: "" kind: Service name: etcd-discovery + - group: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register + - group: rbac.authorization.k8s.io + kind: RoleBinding + name: etcd-self-register + - group: "" + kind: ServiceAccount + name: etcd - group: policy kind: PodDisruptionBudget name: etcd diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml new file mode 100644 index 00000000000..3e97f78628b --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +rules: +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml new file mode 100644 index 00000000000..f8cada01fe4 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register +subjects: +- kind: ServiceAccount + name: etcd + namespace: hcp-namespace diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml new file mode 100644 index 00000000000..00f053fdf28 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +imagePullSecrets: +- name: pull-secret +kind: ServiceAccount +metadata: + name: etcd + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml index 902857d72a0..7d2d81bee1e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/GCP/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml @@ -285,7 +285,7 @@ spec: initContainers: - args: - -c - - exec control-plane-operator resolve-dns ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc + - exec control-plane-operator resolve-dns --self-register ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc command: - /bin/bash env: @@ -294,6 +294,11 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP image: controlplane-operator imagePullPolicy: IfNotPresent name: ensure-dns @@ -378,6 +383,7 @@ spec: priorityClassName: hypershift-etcd restartPolicy: Always schedulerName: default-scheduler + serviceAccountName: etcd tolerations: - effect: NoSchedule key: hypershift.openshift.io/control-plane diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml index 44d35df32f1..ac44d424b8f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml @@ -22,6 +22,15 @@ status: - group: "" kind: Service name: etcd-discovery + - group: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register + - group: rbac.authorization.k8s.io + kind: RoleBinding + name: etcd-self-register + - group: "" + kind: ServiceAccount + name: etcd - group: policy kind: PodDisruptionBudget name: etcd diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml new file mode 100644 index 00000000000..3e97f78628b --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +rules: +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml new file mode 100644 index 00000000000..f8cada01fe4 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register +subjects: +- kind: ServiceAccount + name: etcd + namespace: hcp-namespace diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml new file mode 100644 index 00000000000..00f053fdf28 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +imagePullSecrets: +- name: pull-secret +kind: ServiceAccount +metadata: + name: etcd + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml index ba0322c7603..46353bf2b68 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/IBMCloud/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml @@ -270,7 +270,7 @@ spec: initContainers: - args: - -c - - exec control-plane-operator resolve-dns ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc + - exec control-plane-operator resolve-dns --self-register ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc command: - /bin/bash env: @@ -279,6 +279,11 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP image: controlplane-operator imagePullPolicy: IfNotPresent name: ensure-dns @@ -351,6 +356,7 @@ spec: priorityClassName: hypershift-etcd restartPolicy: Always schedulerName: default-scheduler + serviceAccountName: etcd tolerations: - effect: NoSchedule key: hypershift.openshift.io/control-plane diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml index 44d35df32f1..ac44d424b8f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml @@ -22,6 +22,15 @@ status: - group: "" kind: Service name: etcd-discovery + - group: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register + - group: rbac.authorization.k8s.io + kind: RoleBinding + name: etcd-self-register + - group: "" + kind: ServiceAccount + name: etcd - group: policy kind: PodDisruptionBudget name: etcd diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml new file mode 100644 index 00000000000..3e97f78628b --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +rules: +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml new file mode 100644 index 00000000000..f8cada01fe4 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register +subjects: +- kind: ServiceAccount + name: etcd + namespace: hcp-namespace diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml new file mode 100644 index 00000000000..00f053fdf28 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +imagePullSecrets: +- name: pull-secret +kind: ServiceAccount +metadata: + name: etcd + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml index ba0322c7603..46353bf2b68 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/TechPreviewNoUpgrade/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml @@ -270,7 +270,7 @@ spec: initContainers: - args: - -c - - exec control-plane-operator resolve-dns ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc + - exec control-plane-operator resolve-dns --self-register ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc command: - /bin/bash env: @@ -279,6 +279,11 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP image: controlplane-operator imagePullPolicy: IfNotPresent name: ensure-dns @@ -351,6 +356,7 @@ spec: priorityClassName: hypershift-etcd restartPolicy: Always schedulerName: default-scheduler + serviceAccountName: etcd tolerations: - effect: NoSchedule key: hypershift.openshift.io/control-plane diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml index 44d35df32f1..ac44d424b8f 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_controlplanecomponent.yaml @@ -22,6 +22,15 @@ status: - group: "" kind: Service name: etcd-discovery + - group: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register + - group: rbac.authorization.k8s.io + kind: RoleBinding + name: etcd-self-register + - group: "" + kind: ServiceAccount + name: etcd - group: policy kind: PodDisruptionBudget name: etcd diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml new file mode 100644 index 00000000000..3e97f78628b --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_self_register_role.yaml @@ -0,0 +1,28 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +rules: +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml new file mode 100644 index 00000000000..f8cada01fe4 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_self_register_rolebinding.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: etcd-self-register + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register +subjects: +- kind: ServiceAccount + name: etcd + namespace: hcp-namespace diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml new file mode 100644 index 00000000000..00f053fdf28 --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_serviceaccount.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +imagePullSecrets: +- name: pull-secret +kind: ServiceAccount +metadata: + name: etcd + namespace: hcp-namespace + ownerReferences: + - apiVersion: hypershift.openshift.io/v1beta1 + blockOwnerDeletion: true + controller: true + kind: HostedControlPlane + name: hcp + uid: "" + resourceVersion: "1" diff --git a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml index ba0322c7603..46353bf2b68 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/testdata/etcd/zz_fixture_TestControlPlaneComponents_etcd_statefulset.yaml @@ -270,7 +270,7 @@ spec: initContainers: - args: - -c - - exec control-plane-operator resolve-dns ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc + - exec control-plane-operator resolve-dns --self-register ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc command: - /bin/bash env: @@ -279,6 +279,11 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP image: controlplane-operator imagePullPolicy: IfNotPresent name: ensure-dns @@ -351,6 +356,7 @@ spec: priorityClassName: hypershift-etcd restartPolicy: Always schedulerName: default-scheduler + serviceAccountName: etcd tolerations: - effect: NoSchedule key: hypershift.openshift.io/control-plane diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-role.yaml b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-role.yaml new file mode 100644 index 00000000000..ccedae0b47d --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-role.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: etcd-self-register +rules: +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - pods + verbs: + - get diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-rolebinding-defrag.yaml b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-rolebinding-defrag.yaml new file mode 100644 index 00000000000..34b2781493a --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-rolebinding-defrag.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: etcd-self-register-defrag +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register +subjects: +- kind: ServiceAccount + name: etcd-defrag-controller diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-rolebinding.yaml b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-rolebinding.yaml new file mode 100644 index 00000000000..f54159b5edc --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-self-register-rolebinding.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: etcd-self-register +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: etcd-self-register +subjects: +- kind: ServiceAccount + name: etcd diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-serviceaccount.yaml b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-serviceaccount.yaml new file mode 100644 index 00000000000..4dfa197c42e --- /dev/null +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/etcd-serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: etcd diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/statefulset.yaml b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/statefulset.yaml index cac5289f08b..ffd0e2124ea 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/statefulset.yaml +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/assets/etcd/statefulset.yaml @@ -187,9 +187,12 @@ spec: - mountPath: /etc/etcd/tls/etcd-ca name: etcd-ca initContainers: + # Self-register this pod's IP into an EndpointSlice before waiting for DNS resolution. + # This bypasses stale kube-controller-manager EndpointSlice cache issues under high HCP density + # (2,500+ namespaces), where the standard controller can delay DNS updates by minutes to hours. - args: - -c - - exec control-plane-operator resolve-dns ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc + - exec control-plane-operator resolve-dns --self-register ${HOSTNAME}.etcd-discovery.${NAMESPACE}.svc command: - /bin/bash env: @@ -198,6 +201,11 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP image: controlplane-operator imagePullPolicy: IfNotPresent name: ensure-dns diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/component.go b/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/component.go index 3a09412175d..6ae9bd12072 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/component.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/component.go @@ -54,6 +54,19 @@ func NewComponent() component.ControlPlaneComponent { "defrag-serviceaccount.yaml", component.WithPredicate(defragControllerPredicate), ). + WithManifestAdapter( + "etcd-serviceaccount.yaml", + ). + WithManifestAdapter( + "etcd-self-register-role.yaml", + ). + WithManifestAdapter( + "etcd-self-register-rolebinding.yaml", + ). + WithManifestAdapter( + "etcd-self-register-rolebinding-defrag.yaml", + component.WithPredicate(defragControllerPredicate), + ). Build() } diff --git a/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go b/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go index 49de8fc17d3..20ba6305f4e 100644 --- a/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go +++ b/control-plane-operator/controllers/hostedcontrolplane/v2/etcd/statefulset.go @@ -72,9 +72,14 @@ func adaptStatefulSet(cpContext component.WorkloadContext, sts *appsv1.StatefulS ) }) + // Use etcd SA for self-registration (all topologies) or etcd-defrag-controller SA for defrag (HA-only). + // The etcd SA is always created and has RBAC for EndpointSlice self-registration. + // The etcd-defrag-controller SA is only created in HA mode and has RBAC for defragmentation. if defragControllerPredicate(cpContext) { - sts.Spec.Template.Spec.Containers = append(sts.Spec.Template.Spec.Containers, buildEtcdDefragControllerContainer(hcp.Namespace)) sts.Spec.Template.Spec.ServiceAccountName = manifests.EtcdDefragControllerServiceAccount("").Name + sts.Spec.Template.Spec.Containers = append(sts.Spec.Template.Spec.Containers, buildEtcdDefragControllerContainer(hcp.Namespace)) + } else { + sts.Spec.Template.Spec.ServiceAccountName = manifests.EtcdServiceAccount("").Name } snapshotRestored := meta.IsStatusConditionTrue(hcp.Status.Conditions, string(hyperv1.EtcdSnapshotRestored)) diff --git a/dnsresolver/cmd.go b/dnsresolver/cmd.go index 1bf060af4cc..814d4efa759 100644 --- a/dnsresolver/cmd.go +++ b/dnsresolver/cmd.go @@ -5,20 +5,38 @@ import ( "fmt" "net" "os" + "strings" "time" + "github.com/openshift/hypershift/support/netutil" + + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" "github.com/spf13/cobra" ) func NewCommand() *cobra.Command { + var selfRegister bool + cmd := &cobra.Command{ Use: "resolve-dns NAME", Short: "Utility that ensures a DNS name can be resolved.", Run: func(cmd *cobra.Command, args []string) { if len(args) == 0 { fmt.Printf("Specify a DNS name to lookup\n") + os.Exit(1) + } + if selfRegister { + if err := selfRegisterEndpointSlice(args[0]); err != nil { + fmt.Printf("Warning: self-registration failed, falling back to DNS-only: %v\n", err) + } } if err := resolveDNS(context.Background(), args[0]); err != nil { fmt.Printf("Error: %v", err) @@ -26,6 +44,11 @@ func NewCommand() *cobra.Command { } }, } + + // --self-register creates an EndpointSlice for this pod before waiting for DNS resolution. + // This bypasses stale kube-controller-manager EndpointSlice cache issues under high HCP density, + // where the standard controller can delay DNS updates by minutes to hours. + cmd.Flags().BoolVar(&selfRegister, "self-register", false, "Register this pod's IP into an EndpointSlice before resolving DNS") return cmd } @@ -45,3 +68,128 @@ func resolveDNS(ctx context.Context, hostName string) error { } return nil } + +func selfRegisterEndpointSlice(dnsName string) error { + podIP := os.Getenv("POD_IP") + if podIP == "" { + return fmt.Errorf("POD_IP environment variable not set") + } + namespace := os.Getenv("NAMESPACE") + if namespace == "" { + return fmt.Errorf("NAMESPACE environment variable not set") + } + hostname, err := os.Hostname() + if err != nil { + return fmt.Errorf("failed to get hostname: %w", err) + } + + config, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("failed to get in-cluster config: %w", err) + } + client, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("failed to create kubernetes client: %w", err) + } + + return ensureEndpointSlice(context.Background(), client, dnsName, hostname, namespace, podIP) +} + +// ensureEndpointSlice creates or updates a self-registration EndpointSlice for +// this pod's IP so that CoreDNS can resolve the pod's DNS name without waiting +// for the standard EndpointSlice controller, which may have a stale informer +// cache under high cluster density. +func ensureEndpointSlice(ctx context.Context, client kubernetes.Interface, dnsName, hostname, namespace, podIP string) error { + serviceName, err := parseServiceName(dnsName) + if err != nil { + return fmt.Errorf("failed to parse service name from DNS name %q: %w", dnsName, err) + } + + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + pod, err := client.CoreV1().Pods(namespace).Get(ctx, hostname, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get pod %s/%s: %w", namespace, hostname, err) + } + + // Determine address type based on pod IP + isIPv4, err := netutil.IsIPv4Address(podIP) + if err != nil { + return fmt.Errorf("failed to parse pod IP %s: %w", podIP, err) + } + addressType := discoveryv1.AddressTypeIPv6 + if isIPv4 { + addressType = discoveryv1.AddressTypeIPv4 + } + + sliceName := fmt.Sprintf("%s-self-%s", serviceName, hostname) + endpointSlice := &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: sliceName, + Namespace: namespace, + Labels: map[string]string{ + discoveryv1.LabelServiceName: serviceName, + discoveryv1.LabelManagedBy: "etcd-self-register.hypershift.openshift.io", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Pod", + Name: pod.Name, + UID: pod.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + }, + }, + AddressType: addressType, + Endpoints: []discoveryv1.Endpoint{ + { + Addresses: []string{podIP}, + Hostname: ptr.To(hostname), + NodeName: ptr.To(pod.Spec.NodeName), + Conditions: discoveryv1.EndpointConditions{Ready: ptr.To(true)}, + TargetRef: &corev1.ObjectReference{ + Kind: "Pod", + Name: pod.Name, + Namespace: namespace, + UID: pod.UID, + }, + }, + }, + Ports: []discoveryv1.EndpointPort{ + {Name: ptr.To("peer"), Port: ptr.To(int32(2380)), Protocol: ptr.To(corev1.ProtocolTCP)}, + {Name: ptr.To("etcd-client"), Port: ptr.To(int32(2379)), Protocol: ptr.To(corev1.ProtocolTCP)}, + }, + } + + existing, err := client.DiscoveryV1().EndpointSlices(namespace).Get(ctx, sliceName, metav1.GetOptions{}) + if err == nil { + endpointSlice.ResourceVersion = existing.ResourceVersion + _, err = client.DiscoveryV1().EndpointSlices(namespace).Update(ctx, endpointSlice, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update EndpointSlice %s: %w", sliceName, err) + } + fmt.Printf("Updated self-registration EndpointSlice %s with address %s\n", sliceName, podIP) + } else if apierrors.IsNotFound(err) { + _, err = client.DiscoveryV1().EndpointSlices(namespace).Create(ctx, endpointSlice, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create EndpointSlice %s: %w", sliceName, err) + } + fmt.Printf("Created self-registration EndpointSlice %s with address %s\n", sliceName, podIP) + } else { + return fmt.Errorf("failed to get EndpointSlice %s: %w", sliceName, err) + } + return nil +} + +// parseServiceName extracts the service name from a headless service DNS name. +// Format: ...svc[.cluster.local] +func parseServiceName(dnsName string) (string, error) { + parts := strings.Split(dnsName, ".") + if len(parts) < 3 { + return "", fmt.Errorf("expected at least 3 dot-separated components, got %d", len(parts)) + } + return parts[1], nil +} diff --git a/dnsresolver/cmd_test.go b/dnsresolver/cmd_test.go new file mode 100644 index 00000000000..fa14e80f8fc --- /dev/null +++ b/dnsresolver/cmd_test.go @@ -0,0 +1,227 @@ +package dnsresolver + +import ( + "fmt" + "testing" + + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" +) + +func TestParseServiceName(t *testing.T) { + tests := []struct { + name string + dnsName string + expected string + expectError bool + }{ + { + name: "When given a standard headless service DNS name it should extract the service name", + dnsName: "etcd-0.etcd-discovery.my-namespace.svc", + expected: "etcd-discovery", + }, + { + name: "When given a fully qualified DNS name it should extract the service name", + dnsName: "etcd-0.etcd-discovery.my-namespace.svc.cluster.local", + expected: "etcd-discovery", + }, + { + name: "When given a DNS name with a long namespace it should extract the service name", + dnsName: "etcd-2.etcd-discovery.ocm-arohcpci01-2q7h5rjtm2oud3pn6i3890qa6p37sts3-i2y6k1a2u2a0z1h.svc", + expected: "etcd-discovery", + }, + { + name: "When given a DNS name with too few components it should return an error", + dnsName: "etcd-0.etcd-discovery", + expectError: true, + }, + { + name: "When given a single component it should return an error", + dnsName: "etcd-0", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewGomegaWithT(t) + result, err := parseServiceName(tt.dnsName) + if tt.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result).To(Equal(tt.expected)) + } + }) + } +} + +func TestEnsureEndpointSlice(t *testing.T) { + const ( + namespace = "ocm-test-namespace" + hostname = "etcd-0" + podIP = "10.128.64.186" + dnsName = "etcd-0.etcd-discovery.ocm-test-namespace.svc" + podUID = "test-pod-uid-1234" + nodeName = "aks-userswft1-12345-vmss000000" + ) + + newPod := func() *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: hostname, + Namespace: namespace, + UID: types.UID(podUID), + }, + Spec: corev1.PodSpec{ + NodeName: nodeName, + }, + } + } + + t.Run("When no EndpointSlice exists it should create one with correct fields", func(t *testing.T) { + g := NewGomegaWithT(t) + client := fake.NewClientset(newPod()) + + err := ensureEndpointSlice(t.Context(), client, dnsName, hostname, namespace, podIP) + g.Expect(err).NotTo(HaveOccurred()) + + slice, err := client.DiscoveryV1().EndpointSlices(namespace).Get(t.Context(), "etcd-discovery-self-etcd-0", metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + g.Expect(slice.Labels[discoveryv1.LabelServiceName]).To(Equal("etcd-discovery")) + g.Expect(slice.Labels[discoveryv1.LabelManagedBy]).To(Equal("etcd-self-register.hypershift.openshift.io")) + g.Expect(slice.AddressType).To(Equal(discoveryv1.AddressTypeIPv4)) + g.Expect(slice.Endpoints).To(HaveLen(1)) + g.Expect(slice.Endpoints[0].Addresses).To(Equal([]string{podIP})) + g.Expect(*slice.Endpoints[0].Hostname).To(Equal(hostname)) + g.Expect(*slice.Endpoints[0].NodeName).To(Equal(nodeName)) + g.Expect(*slice.Endpoints[0].Conditions.Ready).To(BeTrue()) + g.Expect(slice.Endpoints[0].TargetRef.Kind).To(Equal("Pod")) + g.Expect(slice.Endpoints[0].TargetRef.Name).To(Equal(hostname)) + g.Expect(slice.Endpoints[0].TargetRef.UID).To(Equal(types.UID(podUID))) + g.Expect(slice.Ports).To(HaveLen(2)) + g.Expect(*slice.Ports[0].Name).To(Equal("peer")) + g.Expect(*slice.Ports[0].Port).To(Equal(int32(2380))) + g.Expect(*slice.Ports[1].Name).To(Equal("etcd-client")) + g.Expect(*slice.Ports[1].Port).To(Equal(int32(2379))) + g.Expect(slice.OwnerReferences).To(HaveLen(1)) + g.Expect(slice.OwnerReferences[0].Kind).To(Equal("Pod")) + g.Expect(slice.OwnerReferences[0].Name).To(Equal(hostname)) + g.Expect(slice.OwnerReferences[0].UID).To(Equal(types.UID(podUID))) + }) + + t.Run("When an EndpointSlice already exists it should update it", func(t *testing.T) { + g := NewGomegaWithT(t) + existingSlice := &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd-discovery-self-etcd-0", + Namespace: namespace, + Labels: map[string]string{ + discoveryv1.LabelServiceName: "etcd-discovery", + discoveryv1.LabelManagedBy: "etcd-self-register.hypershift.openshift.io", + }, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Endpoints: []discoveryv1.Endpoint{ + {Addresses: []string{"10.0.0.99"}}, + }, + } + client := fake.NewClientset(newPod(), existingSlice) + + err := ensureEndpointSlice(t.Context(), client, dnsName, hostname, namespace, podIP) + g.Expect(err).NotTo(HaveOccurred()) + + slice, err := client.DiscoveryV1().EndpointSlices(namespace).Get(t.Context(), "etcd-discovery-self-etcd-0", metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(slice.Endpoints[0].Addresses).To(Equal([]string{podIP})) + }) + + t.Run("When given an IPv6 address it should set AddressTypeIPv6", func(t *testing.T) { + g := NewGomegaWithT(t) + client := fake.NewClientset(newPod()) + ipv6 := "fd00::1" + + err := ensureEndpointSlice(t.Context(), client, dnsName, hostname, namespace, ipv6) + g.Expect(err).NotTo(HaveOccurred()) + + slice, err := client.DiscoveryV1().EndpointSlices(namespace).Get(t.Context(), "etcd-discovery-self-etcd-0", metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(slice.AddressType).To(Equal(discoveryv1.AddressTypeIPv6)) + g.Expect(slice.Endpoints[0].Addresses).To(Equal([]string{ipv6})) + }) + + t.Run("When the pod does not exist it should return an error", func(t *testing.T) { + g := NewGomegaWithT(t) + client := fake.NewClientset() + + err := ensureEndpointSlice(t.Context(), client, dnsName, hostname, namespace, podIP) + g.Expect(err).To(MatchError(ContainSubstring("failed to get pod"))) + }) + + t.Run("When given an invalid DNS name it should return an error", func(t *testing.T) { + g := NewGomegaWithT(t) + client := fake.NewClientset(newPod()) + + err := ensureEndpointSlice(t.Context(), client, "invalid", hostname, namespace, podIP) + g.Expect(err).To(MatchError(ContainSubstring("failed to parse service name"))) + }) + + t.Run("When given a malformed IP address it should return an error", func(t *testing.T) { + g := NewGomegaWithT(t) + client := fake.NewClientset(newPod()) + invalidIP := "not-an-ip-address" + + err := ensureEndpointSlice(t.Context(), client, dnsName, hostname, namespace, invalidIP) + g.Expect(err).To(MatchError(ContainSubstring("failed to parse pod IP"))) + }) + + t.Run("When called for etcd-1 it should use the correct slice name and hostname", func(t *testing.T) { + g := NewGomegaWithT(t) + pod1 := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "etcd-1", + Namespace: namespace, + UID: "uid-etcd-1", + }, + Spec: corev1.PodSpec{NodeName: nodeName}, + } + client := fake.NewClientset(pod1) + + err := ensureEndpointSlice(t.Context(), client, "etcd-1.etcd-discovery.ocm-test-namespace.svc", "etcd-1", namespace, "10.128.64.187") + g.Expect(err).NotTo(HaveOccurred()) + + slice, err := client.DiscoveryV1().EndpointSlices(namespace).Get(t.Context(), "etcd-discovery-self-etcd-1", metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(*slice.Endpoints[0].Hostname).To(Equal("etcd-1")) + g.Expect(slice.Endpoints[0].Addresses).To(Equal([]string{"10.128.64.187"})) + }) + + t.Run("When multiple etcd pods self-register it should create separate EndpointSlices", func(t *testing.T) { + g := NewGomegaWithT(t) + pods := []runtime.Object{ + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "etcd-0", Namespace: namespace, UID: "uid-0"}, Spec: corev1.PodSpec{NodeName: nodeName}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "etcd-1", Namespace: namespace, UID: "uid-1"}, Spec: corev1.PodSpec{NodeName: nodeName}}, + &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "etcd-2", Namespace: namespace, UID: "uid-2"}, Spec: corev1.PodSpec{NodeName: nodeName}}, + } + client := fake.NewClientset(pods...) + + for i := range 3 { + h := metav1.ObjectMeta{Name: pods[i].(*corev1.Pod).Name}.Name + dns := h + ".etcd-discovery." + namespace + ".svc" + ip := fmt.Sprintf("10.128.64.%d", 186+i) + err := ensureEndpointSlice(t.Context(), client, dns, h, namespace, ip) + g.Expect(err).NotTo(HaveOccurred()) + } + + slices, err := client.DiscoveryV1().EndpointSlices(namespace).List(t.Context(), metav1.ListOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(slices.Items).To(HaveLen(3)) + }) +}