diff --git a/api/go.mod b/api/go.mod index cb850ee0..7fb7a6e4 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,8 +3,8 @@ module github.com/openstack-k8s-operators/octavia-operator/api go 1.24.4 require ( - github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35 - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00 + github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251124130651-1ff40691b66d + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef k8s.io/api v0.31.13 k8s.io/apimachinery v0.31.13 sigs.k8s.io/controller-runtime v0.19.7 @@ -37,12 +37,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.27.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect diff --git a/api/go.sum b/api/go.sum index 044719a7..98ae1a71 100644 --- a/api/go.sum +++ b/api/go.sum @@ -78,10 +78,10 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35 h1:QFFGu93A+XCvDUxZIgfBE4gB5hEdVQAIw+E8dF1kP/E= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35/go.mod h1:qq8BCRxTEmLRriUsQ4HeDUzqltWg32MQPDTMhgbBGK4= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00 h1:Xih6tYYqiDVllo4fDGHqTPL+M2biO5YLOUmbiTqrW/I= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00/go.mod h1:PMoNILOdQ1Ij7DyrKgljN6RAiq8pFM2AGsUb6mcxe98= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251124130651-1ff40691b66d h1:5513vDczN+/Sc/vNIVus+M/Li61oP5/sQzSiPRCmUSA= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251124130651-1ff40691b66d/go.mod h1:U6fKKmnazlF/il/jP5DQdKzkh0QX3Z95Pau46KoeTMo= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef h1:1j7kk+D4ZdIXm6C/IwEjuTzIuvWUytxO39E/x94JY7k= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:kUT/SyuxZiOcX8ZuvpFN3PaQa2V8uQon8YwY+1RoQWM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/go.mod b/go.mod index 2c8ada60..1591b0a5 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,11 @@ require ( github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.7 github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 - github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35 + github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251124130651-1ff40691b66d github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1 - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251027074416-ab5c045dbe00 - github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251015110425-ad0381ce8cd4 + github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251002102126-84fdf59cb2fb github.com/openstack-k8s-operators/octavia-operator/api v0.0.0-00010101000000-000000000000 github.com/openstack-k8s-operators/ovn-operator/api v0.6.1-0.20251029144102-54e2a6b7520d go.uber.org/zap v1.27.0 // indirect @@ -24,7 +24,7 @@ require ( require ( github.com/google/uuid v1.6.0 - github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20250929092825-4c2402451077 + github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef golang.org/x/crypto v0.43.0 k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d ) diff --git a/go.sum b/go.sum index 7f1d0852..7b7cca96 100644 --- a/go.sum +++ b/go.sum @@ -118,20 +118,20 @@ github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35 h1:QFFGu93A+XCvDUxZIgfBE4gB5hEdVQAIw+E8dF1kP/E= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251030184102-82d2cbaafd35/go.mod h1:qq8BCRxTEmLRriUsQ4HeDUzqltWg32MQPDTMhgbBGK4= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251124130651-1ff40691b66d h1:5513vDczN+/Sc/vNIVus+M/Li61oP5/sQzSiPRCmUSA= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251124130651-1ff40691b66d/go.mod h1:U6fKKmnazlF/il/jP5DQdKzkh0QX3Z95Pau46KoeTMo= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1 h1:QohvX44nxoV2GwvvOURGXYyDuCn4SCrnwubTKJtzehY= github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1/go.mod h1:FMFoO4MjEQ85JpdLtDHxYSZxvJ9KzHua+HdKhpl0KRI= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00 h1:Xih6tYYqiDVllo4fDGHqTPL+M2biO5YLOUmbiTqrW/I= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251027074416-ab5c045dbe00/go.mod h1:PMoNILOdQ1Ij7DyrKgljN6RAiq8pFM2AGsUb6mcxe98= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef h1:1j7kk+D4ZdIXm6C/IwEjuTzIuvWUytxO39E/x94JY7k= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:kUT/SyuxZiOcX8ZuvpFN3PaQa2V8uQon8YwY+1RoQWM= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251027074416-ab5c045dbe00 h1:YwkGrTpeeAq9bk09u9Hp96BEZb8X3XgnMfoyxypelVM= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251027074416-ab5c045dbe00/go.mod h1:yf13jWb60XV26eA7A8o86ZCXNWBLNK9dPkTSWFaTPCw= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077 h1:9tpPDBV2RLXMDgt13ec8XR2OatFriItseqg+Oyvx9GA= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077/go.mod h1:JPQHkExlxeT6MU3DNJgXXJJG0NMQHlZwxxfbYRaP3eg= -github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20250929092825-4c2402451077 h1:h11tW/Ntg9OiKCnKVAMgR+ka12597ai31OeAD1FGa4s= -github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20250929092825-4c2402451077/go.mod h1:tWZFuXyOZZI+h4uAwaBqyRcvpN7f+PGTHYRDV9VltOk= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251015110425-ad0381ce8cd4 h1:4qDSDLX7HpCIdnlUExyPc3DkyCq+73PLPb99FVj1CZk= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251015110425-ad0381ce8cd4/go.mod h1:lOZNSKG7MMkhMjL7OQXKscy+dH2mxs3HPD+oj4wVytA= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef h1:U9cgXJs/GuO6/0bRn6oaS7ovDrabyGPZpmZyAWksUuQ= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:lgYyrXEYA2BPsq4Kg6dqa+QsHgOjMPyOsEYrvyYW3jk= +github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251002102126-84fdf59cb2fb h1:QOEsifnJzqSl+6wFy3Lx81g/qk2bOx/LtXahERd67KM= +github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251002102126-84fdf59cb2fb/go.mod h1:yQRH2BR1S59QxsrbV9jOZ5cDkM7hV+qGlKaxWpcGYGA= github.com/openstack-k8s-operators/ovn-operator/api v0.6.1-0.20251029144102-54e2a6b7520d h1:3hgkyXrMTTaGQjZ0OSK9qW58jCJ+FktafLvwU9JKrFU= github.com/openstack-k8s-operators/ovn-operator/api v0.6.1-0.20251029144102-54e2a6b7520d/go.mod h1:RTW7SRp+Fn8JmIjdOgLl3GB3tTAhnI0q2XgauR1eEUM= github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec h1:saovr368HPAKHN0aRPh8h8n9s9dn3d8Frmfua0UYRlc= diff --git a/internal/controller/amphoracontroller_controller.go b/internal/controller/amphoracontroller_controller.go index e3ef8a61..1f867e37 100644 --- a/internal/controller/amphoracontroller_controller.go +++ b/internal/controller/amphoracontroller_controller.go @@ -498,6 +498,20 @@ func (r *OctaviaAmphoraControllerReconciler) reconcileNormal(ctx context.Context } // create DaemonSet - end + // Handle pod labeling for predictable IPs + ipKeyPrefix := "rsyslog_" + if instance.Spec.Role == "healthmanager" { + ipKeyPrefix = "hm_" + } + config := PodLabelingConfig{ + ConfigMapName: octavia.HmConfigMap, + IPKeyPrefix: ipKeyPrefix, + ServiceName: instance.Name, + } + if err := HandlePodLabeling(ctx, helper, instance.Name, instance.Namespace, config); err != nil { + Log.Error(err, "Failed to handle pod labeling") + } + // We reached the end of the Reconcile, update the Ready condition based on // the sub conditions if instance.Status.Conditions.AllSubConditionIsTrue() { @@ -863,6 +877,7 @@ func (r *OctaviaAmphoraControllerReconciler) SetupWithManager(mgr ctrl.Manager) Owns(&corev1.Secret{}). Owns(&corev1.ConfigMap{}). Owns(&appsv1.DaemonSet{}). + Owns(&corev1.Pod{}). // watch the secrets we don't own Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(svcSecretFn)). diff --git a/internal/controller/octavia_common.go b/internal/controller/octavia_common.go index c709d401..d0fe93a3 100644 --- a/internal/controller/octavia_common.go +++ b/internal/controller/octavia_common.go @@ -17,10 +17,16 @@ import ( "context" "fmt" + networkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" + "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" + "sigs.k8s.io/controller-runtime/pkg/log" ) type conditionUpdater interface { @@ -73,3 +79,66 @@ func ensureTopology( } return topology, nil } + +// PodLabelingConfig contains configuration for pod labeling +type PodLabelingConfig struct { + ConfigMapName string + IPKeyPrefix string + ServiceName string +} + +// HandlePodLabeling adds predictableip labels to all pods owned by the specified instance +func HandlePodLabeling(ctx context.Context, helper *helper.Helper, instanceName, namespace string, config PodLabelingConfig) error { + // Get the ConfigMap once + configMap := &corev1.ConfigMap{} + if err := helper.GetClient().Get(ctx, types.NamespacedName{Name: config.ConfigMapName, Namespace: namespace}, configMap); err != nil { + return fmt.Errorf("failed to get configmap %s: %w", config.ConfigMapName, err) + } + + // List all pods owned by this instance + podList := &corev1.PodList{} + listOpts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels(map[string]string{ + common.AppSelector: instanceName, + }), + } + + if err := helper.GetClient().List(ctx, podList, listOpts...); err != nil { + return fmt.Errorf("failed to list pods: %w", err) + } + + // Process each pod + for i := range podList.Items { + pod := &podList.Items[i] + + // Skip if no node assigned + if pod.Spec.NodeName == "" { + continue + } + + // Get predictable IP from configmap + ipKey := fmt.Sprintf("%s%s", config.IPKeyPrefix, pod.Spec.NodeName) + predictableIP, exists := configMap.Data[ipKey] + if !exists { + continue // Skip pods without predictable IPs + } + + // Skip if label already matches + if pod.Labels != nil && pod.Labels[networkv1.PredictableIPLabel] == predictableIP { + continue + } + + // Add or update the label + if pod.Labels == nil { + pod.Labels = make(map[string]string) + } + pod.Labels[networkv1.PredictableIPLabel] = predictableIP + + if err := helper.GetClient().Update(ctx, pod); err != nil { + log.FromContext(ctx).Error(err, "Failed to update pod", "pod", pod.Name, "predictableIP", predictableIP) + } + } + + return nil +} diff --git a/internal/controller/octaviarsyslog_controller.go b/internal/controller/octaviarsyslog_controller.go index 5339bb35..321b4dbb 100644 --- a/internal/controller/octaviarsyslog_controller.go +++ b/internal/controller/octaviarsyslog_controller.go @@ -409,6 +409,16 @@ func (r *OctaviaRsyslogReconciler) reconcileNormal(ctx context.Context, instance } // create DaemonSet - end + // Handle pod labeling for predictable IPs + config := PodLabelingConfig{ + ConfigMapName: octavia.HmConfigMap, + IPKeyPrefix: "rsyslog_", + ServiceName: instance.Name, + } + if err := HandlePodLabeling(ctx, helper, instance.Name, instance.Namespace, config); err != nil { + Log.Error(err, "Failed to handle pod labeling") + } + // We reached the end of the Reconcile, update the Ready condition based on // the sub conditions if instance.Status.Conditions.AllSubConditionIsTrue() { @@ -512,6 +522,7 @@ func (r *OctaviaRsyslogReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.Service{}). Owns(&corev1.ConfigMap{}). Owns(&appsv1.DaemonSet{}). + Owns(&corev1.Pod{}). Watches(&topologyv1.Topology{}, handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), builder.WithPredicates(predicate.GenerationChangedPredicate{})). diff --git a/test/functional/pod_labeling_test.go b/test/functional/pod_labeling_test.go new file mode 100644 index 00000000..324e163b --- /dev/null +++ b/test/functional/pod_labeling_test.go @@ -0,0 +1,304 @@ +/* +Copyright 2023. + +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 + + 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/log/zap" + + "github.com/openstack-k8s-operators/lib-common/modules/common" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1" + "github.com/openstack-k8s-operators/octavia-operator/internal/controller" + "github.com/openstack-k8s-operators/octavia-operator/internal/octavia" +) + +var _ = Describe("Pod Labeling", func() { + var ( + configMapName types.NamespacedName + healthManagerPod *corev1.Pod + rsyslogPod *corev1.Pod + existingLabeledPod *corev1.Pod + ) + + BeforeEach(func() { + configMapName = types.NamespacedName{ + Name: "octavia-hmport-map", + Namespace: namespace, + } + + // Create configmap with test data + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName.Name, + Namespace: configMapName.Namespace, + }, + Data: map[string]string{ + "hm_worker-1": "172.23.0.100", + "hm_worker-2": "172.23.0.101", + "rsyslog_worker-1": "172.23.0.200", + "rsyslog_worker-2": "172.23.0.201", + }, + } + Expect(k8sClient.Create(ctx, configMap)).To(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, configMap) + + // Create test pods + healthManagerPod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("healthmanager-pod-%s", uuid.New().String()[:8]), + Namespace: namespace, + Labels: map[string]string{ + common.AppSelector: "octavia-healthmanager", + }, + }, + Spec: corev1.PodSpec{ + NodeName: "worker-1", + Containers: []corev1.Container{{ + Name: "test-container", + Image: "test-image", + }}, + }, + } + Expect(k8sClient.Create(ctx, healthManagerPod)).To(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, healthManagerPod) + + rsyslogPod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("rsyslog-pod-%s", uuid.New().String()[:8]), + Namespace: namespace, + Labels: map[string]string{ + common.AppSelector: "octavia-rsyslog", + }, + }, + Spec: corev1.PodSpec{ + NodeName: "worker-2", + Containers: []corev1.Container{{ + Name: "test-container", + Image: "test-image", + }}, + }, + } + Expect(k8sClient.Create(ctx, rsyslogPod)).To(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, rsyslogPod) + + // Create pod with existing predictableip label + existingLabeledPod = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("existing-labeled-pod-%s", uuid.New().String()[:8]), + Namespace: namespace, + Labels: map[string]string{ + common.AppSelector: "octavia-rsyslog", + "predictableip": "existing-ip", + }, + }, + Spec: corev1.PodSpec{ + NodeName: "worker-1", + Containers: []corev1.Container{{ + Name: "test-container", + Image: "test-image", + }}, + }, + } + Expect(k8sClient.Create(ctx, existingLabeledPod)).To(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, existingLabeledPod) + }) + + Context("HandlePodLabeling function", func() { + It("should label healthmanager pods with hm_ IP addresses", func() { + // Create helper + dummyInstance := &octaviav1.OctaviaAmphoraController{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-healthmanager", + Namespace: namespace, + }, + } + h, err := helper.NewHelper( + dummyInstance, + k8sClient, + nil, // No kclient needed for this test + k8sClient.Scheme(), + zap.New(zap.UseDevMode(true)), // Test logger + ) + Expect(err).NotTo(HaveOccurred()) + + config := controller.PodLabelingConfig{ + ConfigMapName: octavia.HmConfigMap, + IPKeyPrefix: "hm_", + ServiceName: "octavia-healthmanager", + } + + err = controller.HandlePodLabeling(ctx, h, "octavia-healthmanager", namespace, config) + Expect(err).NotTo(HaveOccurred()) + + // Verify the pod got labeled correctly + Eventually(func(g Gomega) { + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: healthManagerPod.Name, + Namespace: namespace, + }, pod) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(pod.Labels).To(HaveKeyWithValue("predictableip", "172.23.0.100")) + }, timeout, interval).Should(Succeed()) + }) + + It("should label rsyslog pods with rsyslog_ IP addresses", func() { + // Create helper + dummyInstance := &octaviav1.OctaviaAmphoraController{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-rsyslog", + Namespace: namespace, + }, + } + h, err := helper.NewHelper( + dummyInstance, + k8sClient, + nil, // No kclient needed for this test + k8sClient.Scheme(), + zap.New(zap.UseDevMode(true)), // Test logger + ) + Expect(err).NotTo(HaveOccurred()) + + config := controller.PodLabelingConfig{ + ConfigMapName: octavia.HmConfigMap, + IPKeyPrefix: "rsyslog_", + ServiceName: "octavia-rsyslog", + } + + err = controller.HandlePodLabeling(ctx, h, "octavia-rsyslog", namespace, config) + Expect(err).NotTo(HaveOccurred()) + + // Verify the pod got labeled correctly + Eventually(func(g Gomega) { + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: rsyslogPod.Name, + Namespace: namespace, + }, pod) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(pod.Labels).To(HaveKeyWithValue("predictableip", "172.23.0.201")) + }, timeout, interval).Should(Succeed()) + }) + + It("should skip pods that already have correct predictableip labels", func() { + // Create helper + dummyInstance := &octaviav1.OctaviaAmphoraController{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-existing", + Namespace: namespace, + }, + } + h, err := helper.NewHelper( + dummyInstance, + k8sClient, + nil, // No kclient needed for this test + k8sClient.Scheme(), + zap.New(zap.UseDevMode(true)), // Test logger + ) + Expect(err).NotTo(HaveOccurred()) + + config := controller.PodLabelingConfig{ + ConfigMapName: octavia.HmConfigMap, + IPKeyPrefix: "rsyslog_", + ServiceName: "octavia-rsyslog", + } + + err = controller.HandlePodLabeling(ctx, h, "octavia-rsyslog", namespace, config) + Expect(err).NotTo(HaveOccurred()) + + // Verify the label was updated to match configmap + Eventually(func(g Gomega) { + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: existingLabeledPod.Name, + Namespace: namespace, + }, pod) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(pod.Labels).To(HaveKeyWithValue("predictableip", "172.23.0.200")) + }, timeout, interval).Should(Succeed()) + }) + + It("should update IP label when pod has stale IP from different node", func() { + // Create a pod on worker-2 but with stale label from worker-1 + // This simulates a defensive scenario where label somehow got out of sync + stalePod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("stale-pod-%s", uuid.New().String()[:8]), + Namespace: namespace, + Labels: map[string]string{ + common.AppSelector: "octavia-rsyslog", + "predictableip": "172.23.0.200", // Stale IP for worker-1 + }, + }, + Spec: corev1.PodSpec{ + NodeName: "worker-2", // Actually on worker-2 + Containers: []corev1.Container{{ + Name: "test-container", + Image: "test-image", + }}, + }, + } + Expect(k8sClient.Create(ctx, stalePod)).To(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, stalePod) + + // Create helper and run labeling + dummyInstance := &octaviav1.OctaviaAmphoraController{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-stale", + Namespace: namespace, + }, + } + h, err := helper.NewHelper( + dummyInstance, + k8sClient, + nil, + k8sClient.Scheme(), + zap.New(zap.UseDevMode(true)), + ) + Expect(err).NotTo(HaveOccurred()) + + config := controller.PodLabelingConfig{ + ConfigMapName: octavia.HmConfigMap, + IPKeyPrefix: "rsyslog_", + ServiceName: "octavia-rsyslog", + } + + err = controller.HandlePodLabeling(ctx, h, "octavia-rsyslog", namespace, config) + Expect(err).NotTo(HaveOccurred()) + + // Verify the label was corrected to match the current node's IP + Eventually(func(g Gomega) { + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Name: stalePod.Name, + Namespace: namespace, + }, pod) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(pod.Labels).To(HaveKeyWithValue("predictableip", "172.23.0.201")) + }, timeout, interval).Should(Succeed()) + }) + }) +})