Skip to content

Commit b0d34ba

Browse files
committed
Add predictableIPs label to pods
This commit adds an helper label to each pod with the value of its predictableip. This is useful if we want to then use this value to afterwards advertise the IP address using frr, instead of having to introspect the octavia configmap. Jira: https://issues.redhat.com/browse/OSPRH-20083
1 parent c8f8333 commit b0d34ba

File tree

4 files changed

+333
-0
lines changed

4 files changed

+333
-0
lines changed

controllers/amphoracontroller_controller.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,20 @@ func (r *OctaviaAmphoraControllerReconciler) reconcileNormal(ctx context.Context
498498
}
499499
// create DaemonSet - end
500500

501+
// Handle pod labeling for predictable IPs
502+
ipKeyPrefix := "rsyslog_"
503+
if instance.Spec.Role == "healthmanager" {
504+
ipKeyPrefix = "hm_"
505+
}
506+
config := PodLabelingConfig{
507+
ConfigMapName: octavia.HmConfigMap,
508+
IPKeyPrefix: ipKeyPrefix,
509+
ServiceName: instance.Name,
510+
}
511+
if err := HandlePodLabeling(ctx, helper, instance.Name, instance.Namespace, config); err != nil {
512+
Log.Error(err, "Failed to handle pod labeling")
513+
}
514+
501515
// We reached the end of the Reconcile, update the Ready condition based on
502516
// the sub conditions
503517
if instance.Status.Conditions.AllSubConditionIsTrue() {
@@ -863,6 +877,7 @@ func (r *OctaviaAmphoraControllerReconciler) SetupWithManager(mgr ctrl.Manager)
863877
Owns(&corev1.Secret{}).
864878
Owns(&corev1.ConfigMap{}).
865879
Owns(&appsv1.DaemonSet{}).
880+
Owns(&corev1.Pod{}).
866881
// watch the secrets we don't own
867882
Watches(&corev1.Secret{},
868883
handler.EnqueueRequestsFromMapFunc(svcSecretFn)).

controllers/octavia_common.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ import (
1818
"fmt"
1919

2020
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
21+
"github.com/openstack-k8s-operators/lib-common/modules/common"
2122
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2223
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
24+
corev1 "k8s.io/api/core/v1"
2325
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/types"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/controller-runtime/pkg/log"
2429
)
2530

2631
type conditionUpdater interface {
@@ -73,3 +78,61 @@ func ensureTopology(
7378
}
7479
return topology, nil
7580
}
81+
82+
// PodLabelingConfig contains configuration for pod labeling
83+
type PodLabelingConfig struct {
84+
ConfigMapName string
85+
IPKeyPrefix string
86+
ServiceName string
87+
}
88+
89+
// HandlePodLabeling adds predictableip labels to all pods owned by the specified instance
90+
func HandlePodLabeling(ctx context.Context, helper *helper.Helper, instanceName, namespace string, config PodLabelingConfig) error {
91+
// Get the ConfigMap once
92+
configMap := &corev1.ConfigMap{}
93+
if err := helper.GetClient().Get(ctx, types.NamespacedName{Name: config.ConfigMapName, Namespace: namespace}, configMap); err != nil {
94+
return fmt.Errorf("failed to get configmap %s: %w", config.ConfigMapName, err)
95+
}
96+
97+
// List all pods owned by this instance
98+
podList := &corev1.PodList{}
99+
listOpts := []client.ListOption{
100+
client.InNamespace(namespace),
101+
client.MatchingLabels(map[string]string{
102+
common.AppSelector: instanceName,
103+
}),
104+
}
105+
106+
if err := helper.GetClient().List(ctx, podList, listOpts...); err != nil {
107+
return fmt.Errorf("failed to list pods: %w", err)
108+
}
109+
110+
// Process each pod
111+
for i := range podList.Items {
112+
pod := &podList.Items[i]
113+
114+
// Skip if already labeled or no node assigned
115+
if (pod.Labels != nil && pod.Labels["predictableip"] != "") || pod.Spec.NodeName == "" {
116+
continue
117+
}
118+
119+
// Get predictable IP from configmap
120+
ipKey := fmt.Sprintf("%s%s", config.IPKeyPrefix, pod.Spec.NodeName)
121+
predictableIP, exists := configMap.Data[ipKey]
122+
if !exists {
123+
continue // Skip pods without predictable IPs
124+
}
125+
126+
// Add the label and update
127+
if pod.Labels == nil {
128+
pod.Labels = make(map[string]string)
129+
}
130+
pod.Labels["predictableip"] = predictableIP
131+
132+
if err := helper.GetClient().Update(ctx, pod); err != nil {
133+
log.FromContext(ctx).Error(err, "Failed to update pod", "pod", pod.Name, "predictableIP", predictableIP)
134+
}
135+
}
136+
137+
return nil
138+
}

controllers/octaviarsyslog_controller.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,16 @@ func (r *OctaviaRsyslogReconciler) reconcileNormal(ctx context.Context, instance
409409
}
410410
// create DaemonSet - end
411411

412+
// Handle pod labeling for predictable IPs
413+
config := PodLabelingConfig{
414+
ConfigMapName: octavia.HmConfigMap,
415+
IPKeyPrefix: "rsyslog_",
416+
ServiceName: instance.Name,
417+
}
418+
if err := HandlePodLabeling(ctx, helper, instance.Name, instance.Namespace, config); err != nil {
419+
Log.Error(err, "Failed to handle pod labeling")
420+
}
421+
412422
// We reached the end of the Reconcile, update the Ready condition based on
413423
// the sub conditions
414424
if instance.Status.Conditions.AllSubConditionIsTrue() {
@@ -512,6 +522,7 @@ func (r *OctaviaRsyslogReconciler) SetupWithManager(mgr ctrl.Manager) error {
512522
Owns(&corev1.Service{}).
513523
Owns(&corev1.ConfigMap{}).
514524
Owns(&appsv1.DaemonSet{}).
525+
Owns(&corev1.Pod{}).
515526
Watches(&topologyv1.Topology{},
516527
handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc),
517528
builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
Copyright 2023.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package functional_test
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/google/uuid"
23+
. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
24+
. "github.com/onsi/gomega" //revive:disable:dot-imports
25+
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/types"
29+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
30+
31+
"github.com/openstack-k8s-operators/lib-common/modules/common"
32+
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
33+
octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1"
34+
"github.com/openstack-k8s-operators/octavia-operator/controllers"
35+
"github.com/openstack-k8s-operators/octavia-operator/pkg/octavia"
36+
)
37+
38+
var _ = Describe("Pod Labeling", func() {
39+
var (
40+
configMapName types.NamespacedName
41+
healthManagerPod *corev1.Pod
42+
rsyslogPod *corev1.Pod
43+
existingLabeledPod *corev1.Pod
44+
)
45+
46+
BeforeEach(func() {
47+
configMapName = types.NamespacedName{
48+
Name: "octavia-hmport-map",
49+
Namespace: namespace,
50+
}
51+
52+
// Create configmap with test data
53+
configMap := &corev1.ConfigMap{
54+
ObjectMeta: metav1.ObjectMeta{
55+
Name: configMapName.Name,
56+
Namespace: configMapName.Namespace,
57+
},
58+
Data: map[string]string{
59+
"hm_worker-1": "172.23.0.100",
60+
"hm_worker-2": "172.23.0.101",
61+
"rsyslog_worker-1": "172.23.0.200",
62+
"rsyslog_worker-2": "172.23.0.201",
63+
},
64+
}
65+
Expect(k8sClient.Create(ctx, configMap)).To(Succeed())
66+
DeferCleanup(k8sClient.Delete, ctx, configMap)
67+
68+
// Create test pods
69+
healthManagerPod = &corev1.Pod{
70+
ObjectMeta: metav1.ObjectMeta{
71+
Name: fmt.Sprintf("healthmanager-pod-%s", uuid.New().String()[:8]),
72+
Namespace: namespace,
73+
Labels: map[string]string{
74+
common.AppSelector: "octavia-healthmanager",
75+
},
76+
},
77+
Spec: corev1.PodSpec{
78+
NodeName: "worker-1",
79+
Containers: []corev1.Container{{
80+
Name: "test-container",
81+
Image: "test-image",
82+
}},
83+
},
84+
}
85+
Expect(k8sClient.Create(ctx, healthManagerPod)).To(Succeed())
86+
DeferCleanup(k8sClient.Delete, ctx, healthManagerPod)
87+
88+
rsyslogPod = &corev1.Pod{
89+
ObjectMeta: metav1.ObjectMeta{
90+
Name: fmt.Sprintf("rsyslog-pod-%s", uuid.New().String()[:8]),
91+
Namespace: namespace,
92+
Labels: map[string]string{
93+
common.AppSelector: "octavia-rsyslog",
94+
},
95+
},
96+
Spec: corev1.PodSpec{
97+
NodeName: "worker-2",
98+
Containers: []corev1.Container{{
99+
Name: "test-container",
100+
Image: "test-image",
101+
}},
102+
},
103+
}
104+
Expect(k8sClient.Create(ctx, rsyslogPod)).To(Succeed())
105+
DeferCleanup(k8sClient.Delete, ctx, rsyslogPod)
106+
107+
// Create pod with existing predictableip label
108+
existingLabeledPod = &corev1.Pod{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Name: fmt.Sprintf("existing-labeled-pod-%s", uuid.New().String()[:8]),
111+
Namespace: namespace,
112+
Labels: map[string]string{
113+
common.AppSelector: "octavia-rsyslog",
114+
"predictableip": "existing-ip",
115+
},
116+
},
117+
Spec: corev1.PodSpec{
118+
NodeName: "worker-1",
119+
Containers: []corev1.Container{{
120+
Name: "test-container",
121+
Image: "test-image",
122+
}},
123+
},
124+
}
125+
Expect(k8sClient.Create(ctx, existingLabeledPod)).To(Succeed())
126+
DeferCleanup(k8sClient.Delete, ctx, existingLabeledPod)
127+
})
128+
129+
Context("HandlePodLabeling function", func() {
130+
It("should label healthmanager pods with hm_ IP addresses", func() {
131+
// Create helper
132+
dummyInstance := &octaviav1.OctaviaAmphoraController{
133+
ObjectMeta: metav1.ObjectMeta{
134+
Name: "test-healthmanager",
135+
Namespace: namespace,
136+
},
137+
}
138+
h, err := helper.NewHelper(
139+
dummyInstance,
140+
k8sClient,
141+
nil, // No kclient needed for this test
142+
k8sClient.Scheme(),
143+
zap.New(zap.UseDevMode(true)), // Test logger
144+
)
145+
Expect(err).NotTo(HaveOccurred())
146+
147+
config := controllers.PodLabelingConfig{
148+
ConfigMapName: octavia.HmConfigMap,
149+
IPKeyPrefix: "hm_",
150+
ServiceName: "octavia-healthmanager",
151+
}
152+
153+
err = controllers.HandlePodLabeling(ctx, h, "octavia-healthmanager", namespace, config)
154+
Expect(err).NotTo(HaveOccurred())
155+
156+
// Verify the pod got labeled correctly
157+
Eventually(func(g Gomega) {
158+
pod := &corev1.Pod{}
159+
err := k8sClient.Get(ctx, types.NamespacedName{
160+
Name: healthManagerPod.Name,
161+
Namespace: namespace,
162+
}, pod)
163+
g.Expect(err).NotTo(HaveOccurred())
164+
g.Expect(pod.Labels).To(HaveKeyWithValue("predictableip", "172.23.0.100"))
165+
}, timeout, interval).Should(Succeed())
166+
})
167+
168+
It("should label rsyslog pods with rsyslog_ IP addresses", func() {
169+
// Create helper
170+
dummyInstance := &octaviav1.OctaviaAmphoraController{
171+
ObjectMeta: metav1.ObjectMeta{
172+
Name: "test-rsyslog",
173+
Namespace: namespace,
174+
},
175+
}
176+
h, err := helper.NewHelper(
177+
dummyInstance,
178+
k8sClient,
179+
nil, // No kclient needed for this test
180+
k8sClient.Scheme(),
181+
zap.New(zap.UseDevMode(true)), // Test logger
182+
)
183+
Expect(err).NotTo(HaveOccurred())
184+
185+
config := controllers.PodLabelingConfig{
186+
ConfigMapName: octavia.HmConfigMap,
187+
IPKeyPrefix: "rsyslog_",
188+
ServiceName: "octavia-rsyslog",
189+
}
190+
191+
err = controllers.HandlePodLabeling(ctx, h, "octavia-rsyslog", namespace, config)
192+
Expect(err).NotTo(HaveOccurred())
193+
194+
// Verify the pod got labeled correctly
195+
Eventually(func(g Gomega) {
196+
pod := &corev1.Pod{}
197+
err := k8sClient.Get(ctx, types.NamespacedName{
198+
Name: rsyslogPod.Name,
199+
Namespace: namespace,
200+
}, pod)
201+
g.Expect(err).NotTo(HaveOccurred())
202+
g.Expect(pod.Labels).To(HaveKeyWithValue("predictableip", "172.23.0.201"))
203+
}, timeout, interval).Should(Succeed())
204+
})
205+
206+
It("should skip pods that already have predictableip labels", func() {
207+
// Create helper
208+
dummyInstance := &octaviav1.OctaviaAmphoraController{
209+
ObjectMeta: metav1.ObjectMeta{
210+
Name: "test-existing",
211+
Namespace: namespace,
212+
},
213+
}
214+
h, err := helper.NewHelper(
215+
dummyInstance,
216+
k8sClient,
217+
nil, // No kclient needed for this test
218+
k8sClient.Scheme(),
219+
zap.New(zap.UseDevMode(true)), // Test logger
220+
)
221+
Expect(err).NotTo(HaveOccurred())
222+
223+
config := controllers.PodLabelingConfig{
224+
ConfigMapName: octavia.HmConfigMap,
225+
IPKeyPrefix: "rsyslog_",
226+
ServiceName: "octavia-rsyslog",
227+
}
228+
229+
err = controllers.HandlePodLabeling(ctx, h, "octavia-rsyslog", namespace, config)
230+
Expect(err).NotTo(HaveOccurred())
231+
232+
// Verify the existing label wasn't changed
233+
Eventually(func(g Gomega) {
234+
pod := &corev1.Pod{}
235+
err := k8sClient.Get(ctx, types.NamespacedName{
236+
Name: existingLabeledPod.Name,
237+
Namespace: namespace,
238+
}, pod)
239+
g.Expect(err).NotTo(HaveOccurred())
240+
g.Expect(pod.Labels).To(HaveKeyWithValue("predictableip", "existing-ip"))
241+
}, timeout, interval).Should(Succeed())
242+
})
243+
})
244+
})

0 commit comments

Comments
 (0)