diff --git a/controllers/designatebackendbind9_controller.go b/controllers/designatebackendbind9_controller.go index 696d60ab..b88b2e8e 100644 --- a/controllers/designatebackendbind9_controller.go +++ b/controllers/designatebackendbind9_controller.go @@ -289,6 +289,7 @@ func (r *DesignateBackendbind9Reconciler) SetupWithManager(ctx context.Context, For(&designatev1beta1.DesignateBackendbind9{}). Owns(&appsv1.StatefulSet{}). Owns(&corev1.Service{}). + Owns(&corev1.Pod{}). // watch the config CMs we don't own Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(configMapFn)). @@ -661,6 +662,18 @@ func (r *DesignateBackendbind9Reconciler) reconcileNormal(ctx context.Context, i } // create StatefulSet - end + // Handle pod labeling for predictable IPs + config := designate.PodLabelingConfig{ + ConfigMapName: designate.BindPredIPConfigMap, + IPKeyPrefix: "bind_address_", + ServiceName: "designate-backendbind9", + } + err = designate.HandlePodLabeling(ctx, helper, instance.Name, instance.Namespace, config) + if err != nil { + Log.Error(err, "Failed to handle pod labeling") + // Don't return error as this is not critical for the main reconcile loop + } + // We reached the end of the Reconcile, update the Ready condition based on // the sub conditions if instance.Status.Conditions.AllSubConditionIsTrue() { diff --git a/controllers/designatemdns_controller.go b/controllers/designatemdns_controller.go index 2693f82a..44f36d07 100644 --- a/controllers/designatemdns_controller.go +++ b/controllers/designatemdns_controller.go @@ -332,6 +332,7 @@ func (r *DesignateMdnsReconciler) SetupWithManager(ctx context.Context, mgr ctrl For(&designatev1beta1.DesignateMdns{}). Owns(&appsv1.StatefulSet{}). Owns(&corev1.Service{}). + Owns(&corev1.Pod{}). // watch the secrets we don't own Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(svcSecretFn)). @@ -768,6 +769,18 @@ func (r *DesignateMdnsReconciler) reconcileNormal(ctx context.Context, instance } // create StatefulSet - end + // Handle pod labeling for predictable IPs + config := designate.PodLabelingConfig{ + ConfigMapName: designate.MdnsPredIPConfigMap, + IPKeyPrefix: "mdns_address_", + ServiceName: "designate-mdns", + } + err = designate.HandlePodLabeling(ctx, helper, instance.Name, instance.Namespace, config) + if err != nil { + Log.Error(err, "Failed to handle pod labeling") + // Don't return error as this is not critical for the main reconcile loop + } + // We reached the end of the Reconcile, update the Ready condition based on // the sub conditions if instance.Status.Conditions.AllSubConditionIsTrue() { diff --git a/pkg/designate/common.go b/pkg/designate/common.go index 0ba8e62f..5178eeec 100644 --- a/pkg/designate/common.go +++ b/pkg/designate/common.go @@ -17,9 +17,18 @@ limitations under the License. package designate import ( + "context" "errors" "fmt" + "strings" + corev1 "k8s.io/api/core/v1" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openstack-k8s-operators/lib-common/modules/common" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" ) @@ -90,3 +99,75 @@ type MessageBus struct { SecretName string Status MessageBusStatus } + +// PodLabelingConfig holds configuration for pod labeling +type PodLabelingConfig struct { + ConfigMapName string + IPKeyPrefix string + ServiceName string +} + +// HandlePodLabeling handles adding predictableip labels to pods +func HandlePodLabeling(ctx context.Context, h *helper.Helper, instanceName, namespace string, config PodLabelingConfig) error { + // List all pods owned by this instance + podList := &corev1.PodList{} + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + common.AppSelector: instanceName, + }, + } + + err := h.GetClient().List(ctx, podList, listOpts...) + if err != nil { + return fmt.Errorf("failed to list pods: %w", err) + } + + // Get the IP configmap once for all pods + configMap := &corev1.ConfigMap{} + err = h.GetClient().Get(ctx, types.NamespacedName{Name: config.ConfigMapName, Namespace: namespace}, configMap) + if err != nil { + if k8s_errors.IsNotFound(err) { + return nil // Configmap not found, skip labeling + } + return err + } + + // Process each pod + for _, pod := range podList.Items { + // Check if the pod already has the predictableip label + if pod.Labels != nil && pod.Labels["predictableip"] != "" { + continue + } + + // Extract pod index from pod name (e.g., "designate-backendbind9-0" -> "0") + podName := pod.Name + nameParts := strings.Split(podName, "-") + if len(nameParts) == 0 { + continue // Skip invalid pod name format + } + podIndex := nameParts[len(nameParts)-1] + + // Get the IP for this pod index + ipKey := fmt.Sprintf("%s%s", config.IPKeyPrefix, podIndex) + predictableIP, exists := configMap.Data[ipKey] + if !exists { + continue // No IP found for this pod index, skip labeling + } + + // Add the predictableip label to the pod + if pod.Labels == nil { + pod.Labels = make(map[string]string) + } + pod.Labels["predictableip"] = predictableIP + + // Update the pod + err = h.GetClient().Update(ctx, &pod) + if err != nil { + // Log error but continue processing other pods + continue + } + } + + return nil +} diff --git a/tests/functional/designate_controller_test.go b/tests/functional/designate_controller_test.go index 220df417..c2822858 100644 --- a/tests/functional/designate_controller_test.go +++ b/tests/functional/designate_controller_test.go @@ -605,6 +605,7 @@ var _ = Describe("Designate controller", func() { Name: spec["designateNetworkAttachment"].(string), Namespace: namespace, })) + DeferCleanup(th.DeleteInstance, CreateDesignate(designateName, spec)) th.SimulateJobSuccess(designateDBSyncName) @@ -612,7 +613,10 @@ var _ = Describe("Designate controller", func() { createAndSimulateMdns(designateMdnsName) }) - It("should have Unknown and false Conditions initialized for Designate services conditions initially", func() { + It("should have Unknown or False Conditions initialized for Designate services conditions initially", func() { + // With actual controllers running, conditions may transition from Unknown to False + // quickly if dependencies aren't ready yet. Both Unknown and False are acceptable + // initial states. for _, cond := range []condition.Type{ designatev1.DesignateMdnsReadyCondition, designatev1.DesignateUnboundReadyCondition, @@ -622,12 +626,14 @@ var _ = Describe("Designate controller", func() { designatev1.DesignateProducerReadyCondition, designatev1.DesignateBackendbind9ReadyCondition, } { - th.ExpectCondition( - designateName, - ConditionGetterFunc(DesignateConditionGetter), - cond, - corev1.ConditionUnknown, - ) + Eventually(func(g Gomega) { + instance := GetDesignate(designateName) + c := instance.Status.Conditions.Get(cond) + g.Expect(c).NotTo(BeNil(), "condition %s should exist", cond) + // Condition should be Unknown or False initially (not True) + g.Expect(c.Status).To(Or(Equal(corev1.ConditionUnknown), Equal(corev1.ConditionFalse)), + "condition %s should be Unknown or False initially, got: %s", cond, c.Status) + }, timeout, interval).Should(Succeed()) } }) @@ -889,7 +895,8 @@ var _ = Describe("Designate controller", func() { Name: expectedTopology.Name, Namespace: expectedTopology.Namespace, }) - g.Expect(tp.GetFinalizers()).To(HaveLen(4)) + // API, Central, Producer, Worker, Mdns + Unbound = 6 finalizers + g.Expect(tp.GetFinalizers()).To(HaveLen(6)) finalizers := tp.GetFinalizers() designateAPI := GetDesignateAPI(designateAPIName) diff --git a/tests/functional/designatebackendbind9_controller_test.go b/tests/functional/designatebackendbind9_controller_test.go index 41e02ffd..77d0bec2 100644 --- a/tests/functional/designatebackendbind9_controller_test.go +++ b/tests/functional/designatebackendbind9_controller_test.go @@ -22,11 +22,15 @@ import ( "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports . "github.com/onsi/gomega" //revive:disable:dot-imports + "github.com/openstack-k8s-operators/lib-common/modules/common" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" // revive:disable-next-line:dot-imports + designatev1 "github.com/openstack-k8s-operators/designate-operator/api/v1beta1" "github.com/openstack-k8s-operators/designate-operator/pkg/designate" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" @@ -131,6 +135,12 @@ var _ = Describe("DesignateBackendbind9 controller", func() { Name: spec["designateNetworkAttachment"].(string), Namespace: namespace, })) + + // Create control network attachment definition (default name is "designate") + DeferCleanup(k8sClient.Delete, ctx, CreateNAD(types.NamespacedName{ + Name: "designate", + Namespace: namespace, + })) }) It("should be in state of having the input ready", func() { @@ -141,5 +151,61 @@ var _ = Describe("DesignateBackendbind9 controller", func() { corev1.ConditionTrue, ) }) + + It("should add predictableip labels to pods", func() { + // Create predictable IP configmap + configData := map[string]any{ + "bind_address_0": "172.28.0.31", + "bind_address_1": "172.28.0.32", + } + DeferCleanup(k8sClient.Delete, ctx, CreateBindIPMap(namespace, configData)) + + // Create a pod with the expected labels + podName := types.NamespacedName{ + Name: fmt.Sprintf("%s-0", designateBackendbind9Name.Name), + Namespace: namespace, + } + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName.Name, + Namespace: podName.Namespace, + Labels: map[string]string{ + common.AppSelector: designateBackendbind9Name.Name, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Image: "test", + }}, + }, + } + Expect(k8sClient.Create(ctx, pod)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, pod) + + // Test the HandlePodLabeling function directly + config := designate.PodLabelingConfig{ + ConfigMapName: designate.BindPredIPConfigMap, + IPKeyPrefix: "bind_address_", + ServiceName: "designate-backendbind9", + } + h, err := helper.NewHelper( + &designatev1.DesignateBackendbind9{}, + k8sClient, + nil, + nil, + logger, + ) + Expect(err).ShouldNot(HaveOccurred()) + err = designate.HandlePodLabeling(ctx, h, designateBackendbind9Name.Name, namespace, config) + Expect(err).ShouldNot(HaveOccurred()) + + // Verify the pod has the predictableip label + Eventually(func(g Gomega) { + updatedPod := &corev1.Pod{} + g.Expect(k8sClient.Get(ctx, podName, updatedPod)).Should(Succeed()) + g.Expect(updatedPod.Labels).Should(HaveKeyWithValue("predictableip", "172.28.0.31")) + }, timeout, interval).Should(Succeed()) + }) }) }) diff --git a/tests/functional/designatemdns_controller_test.go b/tests/functional/designatemdns_controller_test.go new file mode 100644 index 00000000..37ef6a3d --- /dev/null +++ b/tests/functional/designatemdns_controller_test.go @@ -0,0 +1,191 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package functional_test + +import ( + "fmt" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + . "github.com/onsi/gomega" //revive:disable:dot-imports + "github.com/openstack-k8s-operators/lib-common/modules/common" + "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + // revive:disable-next-line:dot-imports + designatev1 "github.com/openstack-k8s-operators/designate-operator/api/v1beta1" + "github.com/openstack-k8s-operators/designate-operator/pkg/designate" + . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" + mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" +) + +var _ = Describe("DesignateMdns controller", func() { + var name string + var spec map[string]any + var designateMdnsName types.NamespacedName + var transportURLSecretName types.NamespacedName + + BeforeEach(func() { + name = fmt.Sprintf("designate-mdns-%s", uuid.New().String()) + spec = GetDefaultDesignateMdnsSpec() + + transportURLSecretName = types.NamespacedName{ + Namespace: namespace, + Name: RabbitmqSecretName, + } + + designateMdnsName = types.NamespacedName{ + Name: name, + Namespace: namespace, + } + spec["transportURLSecret"] = transportURLSecretName.Name + }) + + When("config files are created", func() { + var keystoneInternalEndpoint string + var keystonePublicEndpoint string + + BeforeEach(func() { + keystoneName := keystone.CreateKeystoneAPI(namespace) + DeferCleanup(keystone.DeleteKeystoneAPI, keystoneName) + keystoneInternalEndpoint = fmt.Sprintf("http://keystone-for-%s-internal", designateMdnsName.Name) + keystonePublicEndpoint = fmt.Sprintf("http://keystone-for-%s-public", designateMdnsName.Name) + SimulateKeystoneReady(keystoneName, keystonePublicEndpoint, keystoneInternalEndpoint) + + DeferCleanup(k8sClient.Delete, ctx, CreateDesignateSecret(namespace)) + DeferCleanup(k8sClient.Delete, ctx, CreateTransportURLSecret(transportURLSecretName)) + + spec["customServiceConfig"] = "[DEFAULT]\ndebug=True\n" + DeferCleanup(th.DeleteInstance, CreateDesignateMdns(designateMdnsName, spec)) + + mariaDBDatabaseName := mariadb.CreateMariaDBDatabase(namespace, designate.DatabaseCRName, mariadbv1.MariaDBDatabaseSpec{}) + mariaDBDatabase := mariadb.GetMariaDBDatabase(mariaDBDatabaseName) + DeferCleanup(k8sClient.Delete, ctx, mariaDBDatabase) + + designateMdns := GetDesignateMdns(designateMdnsName) + apiMariaDBAccount, apiMariaDBSecret := mariadb.CreateMariaDBAccountAndSecret( + types.NamespacedName{ + Namespace: namespace, + Name: designateMdns.Spec.DatabaseAccount, + }, mariadbv1.MariaDBAccountSpec{}) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBAccount) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBSecret) + + // Create network attachment definition with the name expected by the spec + nadName := spec["designateNetworkAttachment"].(string) + DeferCleanup(k8sClient.Delete, ctx, CreateNAD(types.NamespacedName{ + Name: nadName, + Namespace: namespace, + })) + + // Create control network attachment definition (default name is "designate") + DeferCleanup(k8sClient.Delete, ctx, CreateNAD(types.NamespacedName{ + Name: "designate", + Namespace: namespace, + })) + }) + + It("should be in state of having the input ready", func() { + th.ExpectCondition( + designateMdnsName, + ConditionGetterFunc(DesignateMdnsConditionGetter), + condition.InputReadyCondition, + corev1.ConditionTrue, + ) + }) + + It("should add predictableip labels to pods", func() { + // Create predictable IP configmap for mdns + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: designate.MdnsPredIPConfigMap, + Namespace: namespace, + }, + Data: map[string]string{ + "mdns_address_0": "172.28.0.41", + "mdns_address_1": "172.28.0.42", + }, + } + Expect(k8sClient.Create(ctx, configMap)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, configMap) + + // Create a pod with the expected labels + podName := types.NamespacedName{ + Name: fmt.Sprintf("%s-0", designateMdnsName.Name), + Namespace: namespace, + } + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: podName.Name, + Namespace: podName.Namespace, + Labels: map[string]string{ + common.AppSelector: designateMdnsName.Name, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "test", + Image: "test", + }}, + }, + } + Expect(k8sClient.Create(ctx, pod)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, pod) + + // Test the HandlePodLabeling function directly + config := designate.PodLabelingConfig{ + ConfigMapName: designate.MdnsPredIPConfigMap, + IPKeyPrefix: "mdns_address_", + ServiceName: "designate-mdns", + } + h, err := helper.NewHelper( + &designatev1.DesignateMdns{}, + k8sClient, + nil, + nil, + logger, + ) + Expect(err).ShouldNot(HaveOccurred()) + + // Debug: List pods before calling HandlePodLabeling + podList := &corev1.PodList{} + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{ + common.AppSelector: designateMdnsName.Name, + }, + } + err = k8sClient.List(ctx, podList, listOpts...) + Expect(err).ShouldNot(HaveOccurred()) + Expect(podList.Items).To(HaveLen(1), "Should find exactly one pod") + + err = designate.HandlePodLabeling(ctx, h, designateMdnsName.Name, namespace, config) + Expect(err).ShouldNot(HaveOccurred()) + + // Verify the pod has the predictableip label + Eventually(func(g Gomega) { + updatedPod := &corev1.Pod{} + g.Expect(k8sClient.Get(ctx, podName, updatedPod)).Should(Succeed()) + g.Expect(updatedPod.Labels).Should(HaveKeyWithValue("predictableip", "172.28.0.41")) + }, timeout, interval).Should(Succeed()) + }) + }) +}) diff --git a/tests/functional/suite_test.go b/tests/functional/suite_test.go index 7138ec07..f0c193bd 100644 --- a/tests/functional/suite_test.go +++ b/tests/functional/suite_test.go @@ -223,6 +223,18 @@ var _ = BeforeSuite(func() { Kclient: kclient, }).SetupWithManager(ctx, k8sManager) Expect(err).ToNot(HaveOccurred()) + err = (&controllers.DesignateMdnsReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(ctx, k8sManager) + Expect(err).ToNot(HaveOccurred()) + err = (&controllers.DesignateWorkerReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Kclient: kclient, + }).SetupWithManager(ctx, k8sManager) + Expect(err).ToNot(HaveOccurred()) err = (&controllers.UnboundReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(),