Skip to content

Commit 60dd999

Browse files
authored
Merge pull request kubernetes#94443 from aojea/slicesLabels
endpointslice controller should mirror parent service labels
2 parents bf9354d + b7d8045 commit 60dd999

File tree

5 files changed

+684
-24
lines changed

5 files changed

+684
-24
lines changed

pkg/controller/endpointslice/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ go_library(
1414
deps = [
1515
"//pkg/api/v1/pod:go_default_library",
1616
"//pkg/apis/core:go_default_library",
17+
"//pkg/apis/core/v1/helper:go_default_library",
1718
"//pkg/apis/discovery/validation:go_default_library",
1819
"//pkg/controller:go_default_library",
1920
"//pkg/controller/endpointslice/metrics:go_default_library",

pkg/controller/endpointslice/reconciler.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,11 @@ func (r *reconciler) finalize(
248248

249249
// reconcileByPortMapping compares the endpoints found in existing slices with
250250
// the list of desired endpoints and returns lists of slices to create, update,
251-
// and delete. The logic is split up into several main steps:
251+
// and delete. It also checks that the slices mirror the parent services labels.
252+
// The logic is split up into several main steps:
252253
// 1. Iterate through existing slices, delete endpoints that are no longer
253-
// desired and update matching endpoints that have changed.
254+
// desired and update matching endpoints that have changed. It also checks
255+
// if the slices have the labels of the parent services, and updates them if not.
254256
// 2. Iterate through slices that have been modified in 1 and fill them up with
255257
// any remaining desired endpoints.
256258
// 3. If there still desired endpoints left, try to fit them into a previously
@@ -289,6 +291,9 @@ func (r *reconciler) reconcileByPortMapping(
289291
}
290292
}
291293

294+
// generate the slice labels and check if parent labels have changed
295+
labels, labelsChanged := setEndpointSliceLabels(existingSlice, service)
296+
292297
// If an endpoint was updated or removed, mark for update or delete
293298
if endpointUpdated || len(existingSlice.Endpoints) != len(newEndpoints) {
294299
if len(existingSlice.Endpoints) > len(newEndpoints) {
@@ -301,9 +306,16 @@ func (r *reconciler) reconcileByPortMapping(
301306
// otherwise, copy and mark for update
302307
epSlice := existingSlice.DeepCopy()
303308
epSlice.Endpoints = newEndpoints
309+
epSlice.Labels = labels
304310
slicesByName[existingSlice.Name] = epSlice
305311
sliceNamesToUpdate.Insert(epSlice.Name)
306312
}
313+
} else if labelsChanged {
314+
// if labels have changed, copy and mark for update
315+
epSlice := existingSlice.DeepCopy()
316+
epSlice.Labels = labels
317+
slicesByName[existingSlice.Name] = epSlice
318+
sliceNamesToUpdate.Insert(epSlice.Name)
307319
} else {
308320
// slices with no changes will be useful if there are leftover endpoints
309321
sliceNamesUnchanged.Insert(existingSlice.Name)

pkg/controller/endpointslice/reconciler_test.go

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ func TestReconcile1Pod(t *testing.T) {
7272
namespace := "test"
7373
ipv6Family := corev1.IPv6Protocol
7474
svcv4, _ := newServiceAndEndpointMeta("foo", namespace)
75+
svcv4ClusterIP, _ := newServiceAndEndpointMeta("foo", namespace)
76+
svcv4ClusterIP.Spec.ClusterIP = "1.1.1.1"
77+
svcv4Labels, _ := newServiceAndEndpointMeta("foo", namespace)
78+
svcv4Labels.Labels = map[string]string{"foo": "bar"}
79+
svcv4BadLabels, _ := newServiceAndEndpointMeta("foo", namespace)
80+
svcv4BadLabels.Labels = map[string]string{discovery.LabelServiceName: "bad",
81+
discovery.LabelManagedBy: "actor", corev1.IsHeadlessService: "invalid"}
7582
svcv6, _ := newServiceAndEndpointMeta("foo", namespace)
7683
svcv6.Spec.IPFamily = &ipv6Family
7784
svcv6ClusterIP, _ := newServiceAndEndpointMeta("foo", namespace)
@@ -94,6 +101,7 @@ func TestReconcile1Pod(t *testing.T) {
94101
service corev1.Service
95102
expectedAddressType discovery.AddressType
96103
expectedEndpoint discovery.Endpoint
104+
expectedLabels map[string]string
97105
}{
98106
"ipv4": {
99107
service: svcv4,
@@ -112,6 +120,80 @@ func TestReconcile1Pod(t *testing.T) {
112120
Name: "pod1",
113121
},
114122
},
123+
expectedLabels: map[string]string{
124+
discovery.LabelManagedBy: controllerName,
125+
discovery.LabelServiceName: "foo",
126+
corev1.IsHeadlessService: "",
127+
},
128+
},
129+
"ipv4-clusterip": {
130+
service: svcv4ClusterIP,
131+
expectedAddressType: discovery.AddressTypeIPv4,
132+
expectedEndpoint: discovery.Endpoint{
133+
Addresses: []string{"1.2.3.4"},
134+
Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
135+
Topology: map[string]string{
136+
"kubernetes.io/hostname": "node-1",
137+
"topology.kubernetes.io/zone": "us-central1-a",
138+
"topology.kubernetes.io/region": "us-central1",
139+
},
140+
TargetRef: &corev1.ObjectReference{
141+
Kind: "Pod",
142+
Namespace: namespace,
143+
Name: "pod1",
144+
},
145+
},
146+
expectedLabels: map[string]string{
147+
discovery.LabelManagedBy: controllerName,
148+
discovery.LabelServiceName: "foo",
149+
},
150+
},
151+
"ipv4-labels": {
152+
service: svcv4Labels,
153+
expectedAddressType: discovery.AddressTypeIPv4,
154+
expectedEndpoint: discovery.Endpoint{
155+
Addresses: []string{"1.2.3.4"},
156+
Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
157+
Topology: map[string]string{
158+
"kubernetes.io/hostname": "node-1",
159+
"topology.kubernetes.io/zone": "us-central1-a",
160+
"topology.kubernetes.io/region": "us-central1",
161+
},
162+
TargetRef: &corev1.ObjectReference{
163+
Kind: "Pod",
164+
Namespace: namespace,
165+
Name: "pod1",
166+
},
167+
},
168+
expectedLabels: map[string]string{
169+
discovery.LabelManagedBy: controllerName,
170+
discovery.LabelServiceName: "foo",
171+
"foo": "bar",
172+
corev1.IsHeadlessService: "",
173+
},
174+
},
175+
"ipv4-bad-labels": {
176+
service: svcv4BadLabels,
177+
expectedAddressType: discovery.AddressTypeIPv4,
178+
expectedEndpoint: discovery.Endpoint{
179+
Addresses: []string{"1.2.3.4"},
180+
Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
181+
Topology: map[string]string{
182+
"kubernetes.io/hostname": "node-1",
183+
"topology.kubernetes.io/zone": "us-central1-a",
184+
"topology.kubernetes.io/region": "us-central1",
185+
},
186+
TargetRef: &corev1.ObjectReference{
187+
Kind: "Pod",
188+
Namespace: namespace,
189+
Name: "pod1",
190+
},
191+
},
192+
expectedLabels: map[string]string{
193+
discovery.LabelManagedBy: controllerName,
194+
discovery.LabelServiceName: "foo",
195+
corev1.IsHeadlessService: "",
196+
},
115197
},
116198
"ipv6": {
117199
service: svcv6,
@@ -130,6 +212,11 @@ func TestReconcile1Pod(t *testing.T) {
130212
Name: "pod1",
131213
},
132214
},
215+
expectedLabels: map[string]string{
216+
discovery.LabelManagedBy: controllerName,
217+
discovery.LabelServiceName: "foo",
218+
corev1.IsHeadlessService: "",
219+
},
133220
},
134221
"ipv6-clusterip": {
135222
service: svcv6ClusterIP,
@@ -148,6 +235,10 @@ func TestReconcile1Pod(t *testing.T) {
148235
Name: "pod1",
149236
},
150237
},
238+
expectedLabels: map[string]string{
239+
discovery.LabelManagedBy: controllerName,
240+
discovery.LabelServiceName: "foo",
241+
},
151242
},
152243
}
153244

@@ -174,8 +265,8 @@ func TestReconcile1Pod(t *testing.T) {
174265
t.Errorf("Expected EndpointSlice name to start with %s, got %s", testCase.service.Name, slice.Name)
175266
}
176267

177-
if slice.Labels[discovery.LabelServiceName] != testCase.service.Name {
178-
t.Errorf("Expected EndpointSlice to have label set with %s value, got %s", testCase.service.Name, slice.Labels[discovery.LabelServiceName])
268+
if !reflect.DeepEqual(testCase.expectedLabels, slice.Labels) {
269+
t.Errorf("Expected EndpointSlice to have labels: %v , got %v", testCase.expectedLabels, slice.Labels)
179270
}
180271

181272
if slice.Annotations[corev1.EndpointsLastChangeTriggerTime] != triggerTime.Format(time.RFC3339Nano) {
@@ -431,6 +522,81 @@ func TestReconcileEndpointSlicesUpdating(t *testing.T) {
431522
expectUnorderedSlicesWithLengths(t, fetchEndpointSlices(t, client, namespace), []int{100, 100, 50})
432523
}
433524

525+
// In some cases, such as service labels updates, all slices for that service will require a change
526+
// This test ensures that we are updating those slices and not calling create + delete for each
527+
func TestReconcileEndpointSlicesServicesLabelsUpdating(t *testing.T) {
528+
client := newClientset()
529+
namespace := "test"
530+
svc, _ := newServiceAndEndpointMeta("foo", namespace)
531+
532+
// start with 250 pods
533+
pods := []*corev1.Pod{}
534+
for i := 0; i < 250; i++ {
535+
ready := !(i%3 == 0)
536+
pods = append(pods, newPod(i, namespace, ready, 1))
537+
}
538+
539+
r := newReconciler(client, []*corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "node-1"}}}, defaultMaxEndpointsPerSlice)
540+
reconcileHelper(t, r, &svc, pods, []*discovery.EndpointSlice{}, time.Now())
541+
numActionsExpected := 3
542+
assert.Len(t, client.Actions(), numActionsExpected, "Expected 3 additional clientset actions")
543+
544+
slices := fetchEndpointSlices(t, client, namespace)
545+
numActionsExpected++
546+
expectUnorderedSlicesWithLengths(t, slices, []int{100, 100, 50})
547+
548+
// update service with new labels
549+
svc.Labels = map[string]string{"foo": "bar"}
550+
reconcileHelper(t, r, &svc, pods, []*discovery.EndpointSlice{&slices[0], &slices[1], &slices[2]}, time.Now())
551+
552+
numActionsExpected += 3
553+
assert.Len(t, client.Actions(), numActionsExpected, "Expected 3 additional clientset actions")
554+
expectActions(t, client.Actions(), 3, "update", "endpointslices")
555+
556+
newSlices := fetchEndpointSlices(t, client, namespace)
557+
expectUnorderedSlicesWithLengths(t, newSlices, []int{100, 100, 50})
558+
// check that the labels were updated
559+
for _, slice := range newSlices {
560+
w, ok := slice.Labels["foo"]
561+
if !ok {
562+
t.Errorf("Expected label \"foo\" from parent service not found")
563+
} else if "bar" != w {
564+
t.Errorf("Expected EndpointSlice to have parent service labels: have %s value, expected bar", w)
565+
}
566+
}
567+
}
568+
569+
// In some cases, such as service labels updates, all slices for that service will require a change
570+
// However, this should not happen for reserved labels
571+
func TestReconcileEndpointSlicesServicesReservedLabels(t *testing.T) {
572+
client := newClientset()
573+
namespace := "test"
574+
svc, _ := newServiceAndEndpointMeta("foo", namespace)
575+
576+
// start with 250 pods
577+
pods := []*corev1.Pod{}
578+
for i := 0; i < 250; i++ {
579+
ready := !(i%3 == 0)
580+
pods = append(pods, newPod(i, namespace, ready, 1))
581+
}
582+
583+
r := newReconciler(client, []*corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "node-1"}}}, defaultMaxEndpointsPerSlice)
584+
reconcileHelper(t, r, &svc, pods, []*discovery.EndpointSlice{}, time.Now())
585+
numActionsExpected := 3
586+
assert.Len(t, client.Actions(), numActionsExpected, "Expected 3 additional clientset actions")
587+
slices := fetchEndpointSlices(t, client, namespace)
588+
numActionsExpected++
589+
expectUnorderedSlicesWithLengths(t, slices, []int{100, 100, 50})
590+
591+
// update service with new labels
592+
svc.Labels = map[string]string{discovery.LabelServiceName: "bad", discovery.LabelManagedBy: "actor", corev1.IsHeadlessService: "invalid"}
593+
reconcileHelper(t, r, &svc, pods, []*discovery.EndpointSlice{&slices[0], &slices[1], &slices[2]}, time.Now())
594+
assert.Len(t, client.Actions(), numActionsExpected, "Expected no additional clientset actions")
595+
596+
newSlices := fetchEndpointSlices(t, client, namespace)
597+
expectUnorderedSlicesWithLengths(t, newSlices, []int{100, 100, 50})
598+
}
599+
434600
// In this test, we start with 10 slices that only have 30 endpoints each
435601
// An initial reconcile makes no changes (as desired to limit writes)
436602
// When we change a service port, all slices will need to be updated in some way

pkg/controller/endpointslice/utils.go

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"time"
2222

2323
corev1 "k8s.io/api/core/v1"
24+
v1 "k8s.io/api/core/v1"
2425
discovery "k8s.io/api/discovery/v1beta1"
2526
apiequality "k8s.io/apimachinery/pkg/api/equality"
2627
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -30,6 +31,7 @@ import (
3031
"k8s.io/klog/v2"
3132
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
3233
api "k8s.io/kubernetes/pkg/apis/core"
34+
helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
3335
"k8s.io/kubernetes/pkg/apis/discovery/validation"
3436
endpointutil "k8s.io/kubernetes/pkg/controller/util/endpoint"
3537
utilnet "k8s.io/utils/net"
@@ -152,12 +154,9 @@ func endpointsEqualBeyondHash(ep1, ep2 *discovery.Endpoint) bool {
152154
func newEndpointSlice(service *corev1.Service, endpointMeta *endpointMeta) *discovery.EndpointSlice {
153155
gvk := schema.GroupVersionKind{Version: "v1", Kind: "Service"}
154156
ownerRef := metav1.NewControllerRef(service, gvk)
155-
return &discovery.EndpointSlice{
157+
epSlice := &discovery.EndpointSlice{
156158
ObjectMeta: metav1.ObjectMeta{
157-
Labels: map[string]string{
158-
discovery.LabelServiceName: service.Name,
159-
discovery.LabelManagedBy: controllerName,
160-
},
159+
Labels: map[string]string{},
161160
GenerateName: getEndpointSlicePrefix(service.Name),
162161
OwnerReferences: []metav1.OwnerReference{*ownerRef},
163162
Namespace: service.Namespace,
@@ -166,6 +165,10 @@ func newEndpointSlice(service *corev1.Service, endpointMeta *endpointMeta) *disc
166165
AddressType: endpointMeta.AddressType,
167166
Endpoints: []discovery.Endpoint{},
168167
}
168+
// add parent service labels
169+
epSlice.Labels, _ = setEndpointSliceLabels(epSlice, service)
170+
171+
return epSlice
169172
}
170173

171174
// getEndpointSlicePrefix returns a suitable prefix for an EndpointSlice name.
@@ -278,6 +281,62 @@ func serviceControllerKey(endpointSlice *discovery.EndpointSlice) (string, error
278281
return fmt.Sprintf("%s/%s", endpointSlice.Namespace, serviceName), nil
279282
}
280283

284+
// setEndpointSliceLabels returns a map with the new endpoint slices labels and true if there was an update.
285+
// Slices labels must be equivalent to the Service labels except for the reserved IsHeadlessService, LabelServiceName and LabelManagedBy labels
286+
// Changes to IsHeadlessService, LabelServiceName and LabelManagedBy labels on the Service do not result in updates to EndpointSlice labels.
287+
func setEndpointSliceLabels(epSlice *discovery.EndpointSlice, service *corev1.Service) (map[string]string, bool) {
288+
updated := false
289+
epLabels := make(map[string]string)
290+
svcLabels := make(map[string]string)
291+
292+
// check if the endpoint slice and the service have the same labels
293+
// clone current slice labels except the reserved labels
294+
for key, value := range epSlice.Labels {
295+
if IsReservedLabelKey(key) {
296+
continue
297+
}
298+
// copy endpoint slice labels
299+
epLabels[key] = value
300+
}
301+
302+
for key, value := range service.Labels {
303+
if IsReservedLabelKey(key) {
304+
klog.Warningf("Service %s/%s using reserved endpoint slices label, skipping label %s: %s", service.Namespace, service.Name, key, value)
305+
continue
306+
}
307+
// copy service labels
308+
svcLabels[key] = value
309+
}
310+
311+
// if the labels are not identical update the slice with the corresponding service labels
312+
if !apiequality.Semantic.DeepEqual(epLabels, svcLabels) {
313+
updated = true
314+
}
315+
316+
// add or remove headless label depending on the service Type
317+
if !helper.IsServiceIPSet(service) {
318+
svcLabels[v1.IsHeadlessService] = ""
319+
} else {
320+
delete(svcLabels, v1.IsHeadlessService)
321+
}
322+
323+
// override endpoint slices reserved labels
324+
svcLabels[discovery.LabelServiceName] = service.Name
325+
svcLabels[discovery.LabelManagedBy] = controllerName
326+
327+
return svcLabels, updated
328+
}
329+
330+
// IsReservedLabelKey return true if the label is one of the reserved label for slices
331+
func IsReservedLabelKey(label string) bool {
332+
if label == discovery.LabelServiceName ||
333+
label == discovery.LabelManagedBy ||
334+
label == v1.IsHeadlessService {
335+
return true
336+
}
337+
return false
338+
}
339+
281340
// endpointSliceEndpointLen helps sort endpoint slices by the number of
282341
// endpoints they contain.
283342
type endpointSliceEndpointLen []*discovery.EndpointSlice

0 commit comments

Comments
 (0)