Skip to content

Commit 1fc1485

Browse files
authored
update createHeadlessService() to update labels (#7865)
1 parent f8d5420 commit 1fc1485

File tree

2 files changed

+221
-21
lines changed

2 files changed

+221
-21
lines changed

cmd/nginx-ingress/main.go

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"os/signal"
1313
"path/filepath"
14+
"reflect"
1415
"regexp"
1516
"runtime"
1617
"strings"
@@ -1089,7 +1090,7 @@ func updateSelfWithVersionInfo(ctx context.Context, eventLog record.EventRecorde
10891090
}
10901091
}
10911092

1092-
func createAndValidateHeadlessService(ctx context.Context, kubeClient *kubernetes.Clientset, cfgParams *configs.ConfigParams, controllerNamespace string, pod *api_v1.Pod) error {
1093+
func createAndValidateHeadlessService(ctx context.Context, kubeClient kubernetes.Interface, cfgParams *configs.ConfigParams, controllerNamespace string, pod *api_v1.Pod) error {
10931094
l := nl.LoggerFromContext(ctx)
10941095
owner := pod.ObjectMeta.OwnerReferences[0]
10951096
name := owner.Name
@@ -1107,13 +1108,7 @@ func createAndValidateHeadlessService(ctx context.Context, kubeClient *kubernete
11071108
return nil
11081109
}
11091110

1110-
func createHeadlessService(l *slog.Logger, kubeClient *kubernetes.Clientset, controllerNamespace string, svcName string, configMapNamespacedName string, pod *api_v1.Pod) error {
1111-
existing, err := kubeClient.CoreV1().Services(controllerNamespace).Get(context.Background(), svcName, meta_v1.GetOptions{})
1112-
if err == nil && existing != nil {
1113-
nl.Infof(l, "headless service %s/%s already exists, skipping creating.", controllerNamespace, svcName)
1114-
return nil
1115-
}
1116-
1111+
func createHeadlessService(l *slog.Logger, kubeClient kubernetes.Interface, controllerNamespace string, svcName string, configMapNamespacedName string, pod *api_v1.Pod) error {
11171112
configMapName := strings.SplitN(configMapNamespacedName, "/", 2)
11181113
if len(configMapName) != 2 {
11191114
return fmt.Errorf("wrong syntax for ConfigMap: %q", configMapNamespacedName)
@@ -1125,24 +1120,49 @@ func createHeadlessService(l *slog.Logger, kubeClient *kubernetes.Clientset, con
11251120
return err
11261121
}
11271122

1123+
requiredSelectors := pod.Labels
1124+
requiredOwnerReferences := []meta_v1.OwnerReference{
1125+
{
1126+
APIVersion: "v1",
1127+
Kind: "ConfigMap",
1128+
Name: configMapObj.Name,
1129+
UID: configMapObj.UID,
1130+
Controller: commonhelpers.BoolToPointerBool(true),
1131+
BlockOwnerDeletion: commonhelpers.BoolToPointerBool(true),
1132+
},
1133+
}
1134+
existing, err := kubeClient.CoreV1().Services(controllerNamespace).Get(context.Background(), svcName, meta_v1.GetOptions{})
1135+
if err == nil && existing != nil {
1136+
needsUpdate := false
1137+
if !reflect.DeepEqual(existing.Spec.Selector, requiredSelectors) {
1138+
existing.Spec.Selector = requiredSelectors
1139+
needsUpdate = true
1140+
}
1141+
if !reflect.DeepEqual(existing.OwnerReferences, requiredOwnerReferences) {
1142+
existing.OwnerReferences = requiredOwnerReferences
1143+
needsUpdate = true
1144+
}
1145+
if needsUpdate {
1146+
nl.Infof(l, "Headless service %s/%s exists and needs update. Updating...", controllerNamespace, svcName)
1147+
_, updateErr := kubeClient.CoreV1().Services(controllerNamespace).Update(context.Background(), existing, meta_v1.UpdateOptions{})
1148+
if updateErr != nil {
1149+
return fmt.Errorf("failed to update headless service %s/%s: %w", controllerNamespace, svcName, updateErr)
1150+
}
1151+
nl.Infof(l, "Successfully updated headless service %s/%s.", controllerNamespace, svcName)
1152+
}
1153+
return nil
1154+
}
1155+
1156+
nl.Infof(l, "Headless service %s/%s not found. Creating...", controllerNamespace, svcName)
11281157
svc := &api_v1.Service{
11291158
ObjectMeta: meta_v1.ObjectMeta{
1130-
Name: svcName,
1131-
Namespace: controllerNamespace,
1132-
OwnerReferences: []meta_v1.OwnerReference{
1133-
{
1134-
APIVersion: "v1",
1135-
Kind: "ConfigMap",
1136-
Name: configMapObj.Name,
1137-
UID: configMapObj.UID,
1138-
Controller: commonhelpers.BoolToPointerBool(true),
1139-
BlockOwnerDeletion: commonhelpers.BoolToPointerBool(true),
1140-
},
1141-
},
1159+
Name: svcName,
1160+
Namespace: controllerNamespace,
1161+
OwnerReferences: requiredOwnerReferences,
11421162
},
11431163
Spec: api_v1.ServiceSpec{
11441164
ClusterIP: api_v1.ClusterIPNone,
1145-
Selector: pod.Labels,
1165+
Selector: requiredSelectors,
11461166
},
11471167
}
11481168

cmd/nginx-ingress/main_test.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@ package main
33
import (
44
"bytes"
55
"context"
6+
"fmt"
67
"io"
78
"log/slog"
89
"regexp"
910
"testing"
1011

12+
"github.com/nginx/kubernetes-ingress/internal/configs/commonhelpers"
1113
nl "github.com/nginx/kubernetes-ingress/internal/logger"
1214
nic_glog "github.com/nginx/kubernetes-ingress/internal/logger/glog"
1315
"github.com/nginx/kubernetes-ingress/internal/logger/levels"
16+
"github.com/stretchr/testify/assert"
17+
api_v1 "k8s.io/api/core/v1"
18+
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/runtime"
20+
"k8s.io/apimachinery/pkg/types"
1421
pkgversion "k8s.io/apimachinery/pkg/version"
1522
fakediscovery "k8s.io/client-go/discovery/fake"
1623
"k8s.io/client-go/kubernetes/fake"
@@ -131,3 +138,176 @@ func TestK8sVersionValidationBad(t *testing.T) {
131138
})
132139
}
133140
}
141+
142+
func TestCreateHeadlessService(t *testing.T) {
143+
logger := nl.LoggerFromContext(context.Background())
144+
controllerNamespace := "default"
145+
configMapName := "test-configmap"
146+
configMapNamespace := "default"
147+
configMapNamespacedName := fmt.Sprintf("%s/%s", configMapNamespace, configMapName)
148+
podName := "test-pod"
149+
podLabels := map[string]string{"app": "my-app", "pod-hash": "12345"}
150+
svcName := "test-hl-service"
151+
152+
pod := &api_v1.Pod{
153+
ObjectMeta: meta_v1.ObjectMeta{
154+
Name: podName,
155+
Namespace: controllerNamespace,
156+
Labels: podLabels,
157+
},
158+
}
159+
160+
configMap := &api_v1.ConfigMap{
161+
ObjectMeta: meta_v1.ObjectMeta{
162+
Name: configMapName,
163+
Namespace: configMapNamespace,
164+
UID: types.UID("uid-cm"),
165+
},
166+
}
167+
168+
expectedOwnerReferences := []meta_v1.OwnerReference{
169+
{
170+
APIVersion: "v1",
171+
Kind: "ConfigMap",
172+
Name: configMap.Name,
173+
UID: configMap.UID,
174+
Controller: commonhelpers.BoolToPointerBool(true),
175+
BlockOwnerDeletion: commonhelpers.BoolToPointerBool(true),
176+
},
177+
}
178+
179+
testCases := []struct {
180+
name string
181+
existingService *api_v1.Service
182+
expectedAction string
183+
expectedSelector map[string]string
184+
expectedOwnerRefs []meta_v1.OwnerReference
185+
initialClientObjects []runtime.Object
186+
}{
187+
{
188+
name: "Create service if none found",
189+
expectedAction: "create",
190+
expectedSelector: podLabels,
191+
expectedOwnerRefs: expectedOwnerReferences,
192+
initialClientObjects: []runtime.Object{pod, configMap},
193+
},
194+
{
195+
name: "Skip update if labels and ownerReferences are the same",
196+
existingService: &api_v1.Service{
197+
ObjectMeta: meta_v1.ObjectMeta{
198+
Name: svcName,
199+
Namespace: controllerNamespace,
200+
OwnerReferences: expectedOwnerReferences,
201+
},
202+
Spec: api_v1.ServiceSpec{
203+
Selector: podLabels,
204+
},
205+
},
206+
expectedAction: "none",
207+
expectedSelector: podLabels,
208+
expectedOwnerRefs: expectedOwnerReferences,
209+
initialClientObjects: []runtime.Object{pod, configMap},
210+
},
211+
{
212+
name: "Update service if labels differ",
213+
existingService: &api_v1.Service{
214+
ObjectMeta: meta_v1.ObjectMeta{
215+
Name: svcName,
216+
Namespace: controllerNamespace,
217+
OwnerReferences: expectedOwnerReferences,
218+
},
219+
Spec: api_v1.ServiceSpec{
220+
Selector: map[string]string{"pod-hash": "67890"},
221+
},
222+
},
223+
expectedAction: "update",
224+
expectedSelector: podLabels,
225+
expectedOwnerRefs: expectedOwnerReferences,
226+
initialClientObjects: []runtime.Object{pod, configMap},
227+
},
228+
{
229+
name: "Update service if ownerReferences differ",
230+
existingService: &api_v1.Service{
231+
ObjectMeta: meta_v1.ObjectMeta{
232+
Name: svcName,
233+
Namespace: controllerNamespace,
234+
OwnerReferences: []meta_v1.OwnerReference{
235+
{Name: "old-owner"},
236+
},
237+
},
238+
Spec: api_v1.ServiceSpec{
239+
Selector: podLabels,
240+
},
241+
},
242+
expectedAction: "update",
243+
expectedSelector: podLabels,
244+
expectedOwnerRefs: expectedOwnerReferences,
245+
initialClientObjects: []runtime.Object{pod, configMap},
246+
},
247+
{
248+
name: "Update service if both labels and ownerReferences differ",
249+
existingService: &api_v1.Service{
250+
ObjectMeta: meta_v1.ObjectMeta{
251+
Name: svcName,
252+
Namespace: controllerNamespace,
253+
OwnerReferences: []meta_v1.OwnerReference{
254+
{Name: "old-owner"},
255+
},
256+
},
257+
Spec: api_v1.ServiceSpec{
258+
Selector: map[string]string{"old-label": "true"},
259+
},
260+
},
261+
expectedAction: "update",
262+
expectedSelector: podLabels,
263+
expectedOwnerRefs: expectedOwnerReferences,
264+
initialClientObjects: []runtime.Object{pod, configMap},
265+
},
266+
}
267+
268+
for _, tc := range testCases {
269+
t.Run(tc.name, func(t *testing.T) {
270+
clientObjects := tc.initialClientObjects
271+
if tc.existingService != nil {
272+
clientObjects = append(clientObjects, tc.existingService)
273+
}
274+
clientset := fake.NewSimpleClientset(clientObjects...)
275+
276+
err := createHeadlessService(logger, clientset, controllerNamespace, svcName, configMapNamespacedName, pod)
277+
assert.NoError(t, err)
278+
279+
service, err := clientset.CoreV1().Services(controllerNamespace).Get(context.Background(), svcName, meta_v1.GetOptions{})
280+
assert.NoError(t, err, "Failed to get service after create/update")
281+
282+
if err == nil {
283+
assert.Equal(t, tc.expectedSelector, service.Spec.Selector, "Service selector mismatch")
284+
assert.Equal(t, tc.expectedOwnerRefs, service.OwnerReferences, "Service OwnerReferences mismatch")
285+
}
286+
287+
actions := clientset.Actions()
288+
var serviceCreated, serviceUpdated bool
289+
for _, action := range actions {
290+
if action.Matches("create", "services") {
291+
serviceCreated = true
292+
}
293+
if action.Matches("update", "services") {
294+
serviceUpdated = true
295+
}
296+
}
297+
298+
switch tc.expectedAction {
299+
case "create":
300+
assert.True(t, serviceCreated, "service to be created")
301+
assert.False(t, serviceUpdated, "no service update when creation is expected")
302+
case "update":
303+
assert.True(t, serviceUpdated, "service to be updated")
304+
assert.False(t, serviceCreated, "no service creation when update is expected")
305+
case "none":
306+
assert.False(t, serviceCreated, "no service creation when no action is expected")
307+
assert.False(t, serviceUpdated, "no service update when no action is expected")
308+
default:
309+
t.Fatalf("Invalid expectedAction: %s", tc.expectedAction)
310+
}
311+
})
312+
}
313+
}

0 commit comments

Comments
 (0)