Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a small regression in Service updates #104601

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/api/service/testing/make.go
Expand Up @@ -168,3 +168,10 @@ func SetAllocateLoadBalancerNodePorts(val bool) Tweak {
svc.Spec.AllocateLoadBalancerNodePorts = utilpointer.BoolPtr(val)
}
}

// SetHealthCheckNodePort sets the healthCheckNodePort field for a Service.
func SetHealthCheckNodePort(value int32) Tweak {
return func(svc *api.Service) {
svc.Spec.HealthCheckNodePort = value
}
}
233 changes: 197 additions & 36 deletions pkg/registry/core/service/storage/rest_test.go
Expand Up @@ -622,48 +622,210 @@ func TestServiceRegistryUpdate(t *testing.T) {
}

func TestServiceRegistryUpdateUnspecifiedAllocations(t *testing.T) {
type proof func(t *testing.T, s *api.Service)
prove := func(proofs ...proof) []proof {
return proofs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low pri

on all proves ( don't think we should fix but we should ack it). idx is never tested against len(<the thing we want to test>). This could yield into undesirable error, specially when i think these should be part of make.go

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The good news is that they will panic if wrong.

I agree finding a way to re-use these will be nice - as a followup to the mega PR :)

}
proveClusterIP := func(idx int, ip string) proof {
return func(t *testing.T, s *api.Service) {
if want, got := ip, s.Spec.ClusterIPs[idx]; want != got {
t.Errorf("wrong ClusterIPs[%d]: want %q, got %q", idx, want, got)
}
}
}
proveNodePort := func(idx int, port int32) proof {
return func(t *testing.T, s *api.Service) {
got := s.Spec.Ports[idx].NodePort
if port > 0 && got != port {
t.Errorf("wrong Ports[%d].NodePort: want %d, got %d", idx, port, got)
} else if port < 0 && got == -port {
t.Errorf("wrong Ports[%d].NodePort: wanted anything but %d", idx, got)
}
}
}
proveHCNP := func(port int32) proof {
return func(t *testing.T, s *api.Service) {
got := s.Spec.HealthCheckNodePort
if port > 0 && got != port {
t.Errorf("wrong HealthCheckNodePort: want %d, got %d", port, got)
} else if port < 0 && got == -port {
t.Errorf("wrong HealthCheckNodePort: wanted anything but %d", got)
}
}
}

testCases := []struct {
name string
svc *api.Service // Need a clusterIP, NodePort, and HealthCheckNodePort allocated
tweak func(*api.Service)
name string
create *api.Service // Needs clusterIP, NodePort, and HealthCheckNodePort allocated
update *api.Service // Needs clusterIP, NodePort, and/or HealthCheckNodePort blank
expectError bool
prove []proof
}{{
name: "single-port",
svc: svctest.MakeService("foo",
name: "single-ip_single-port",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetClusterIPs("1.2.3.4"),
svctest.SetNodePorts(30093),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal)),
tweak: nil,
prove: prove(
proveClusterIP(0, "1.2.3.4"),
proveNodePort(0, 30093),
proveHCNP(30118)),
}, {
name: "multi-port",
svc: svctest.MakeService("foo",
name: "multi-ip_multi-port",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetClusterIPs("1.2.3.4", "2000::1"),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 30076),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP))),
prove: prove(
proveClusterIP(0, "1.2.3.4"),
proveClusterIP(1, "2000::1"),
proveNodePort(0, 30093),
proveNodePort(1, 30076),
proveHCNP(30118)),
}, {
name: "multi-ip_partial",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetClusterIPs("1.2.3.4", "2000::1"),
svctest.SetPorts(
svctest.MakeServicePort("p", 80, intstr.FromInt(80), api.ProtocolTCP),
svctest.MakeServicePort("q", 443, intstr.FromInt(443), api.ProtocolTCP))),
tweak: nil,
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 30076),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetClusterIPs("1.2.3.4")),
expectError: true,
}, {
name: "shuffle-ports",
svc: svctest.MakeService("foo",
name: "multi-port_partial",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 80, intstr.FromInt(80), api.ProtocolTCP),
svctest.MakeServicePort("q", 443, intstr.FromInt(443), api.ProtocolTCP))),
tweak: func(s *api.Service) {
s.Spec.Ports[0], s.Spec.Ports[1] = s.Spec.Ports[1], s.Spec.Ports[0]
},
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 30076),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 0)), // provide just 1 value
prove: prove(
proveNodePort(0, 30093),
proveNodePort(1, 30076),
proveHCNP(30118)),
}, {
name: "swap-ports",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 30076),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
// swapped from above
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP),
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP))),
prove: prove(
proveNodePort(0, 30076),
proveNodePort(1, 30093),
proveHCNP(30118)),
}, {
name: "partial-swap-ports",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 30076),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30076, 0), // set [0] to [1], omit [1]
svctest.SetHealthCheckNodePort(30118)),
prove: prove(
proveNodePort(0, 30076),
thockin marked this conversation as resolved.
Show resolved Hide resolved
proveNodePort(1, -30076),
proveHCNP(30118)),
}, {
name: "swap-port-with-hcnp",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 30076),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30076, 30118)), // set [0] to [1], set [1] to HCNP
expectError: true,
}, {
name: "partial-swap-port-with-hcnp",
create: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30093, 30076),
svctest.SetHealthCheckNodePort(30118)),
update: svctest.MakeService("foo",
svctest.SetTypeLoadBalancer,
svctest.SetExternalTrafficPolicy(api.ServiceExternalTrafficPolicyTypeLocal),
svctest.SetPorts(
svctest.MakeServicePort("p", 867, intstr.FromInt(867), api.ProtocolTCP),
svctest.MakeServicePort("q", 5309, intstr.FromInt(5309), api.ProtocolTCP)),
svctest.SetNodePorts(30118, 0)), // set [0] to HCNP, omit [1]
expectError: true,
}}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := genericapirequest.NewDefaultContext()
storage, server := NewTestREST(t, []api.IPFamily{api.IPv4Protocol})
storage, server := NewTestREST(t, []api.IPFamily{api.IPv4Protocol, api.IPv6Protocol})
defer server.Terminate(t)

svc := tc.svc.DeepCopy()
svc := tc.create.DeepCopy()
obj, err := storage.Create(ctx, svc.DeepCopy(), rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if err != nil {
t.Fatalf("Expected no error: %v", err)
t.Fatalf("unexpected error on create: %v", err)
}
createdSvc := obj.(*api.Service)
if createdSvc.Spec.ClusterIP == "" {
Expand All @@ -681,13 +843,21 @@ func TestServiceRegistryUpdateUnspecifiedAllocations(t *testing.T) {
t.Fatalf("expected HealthCheckNodePort to be set")
}

// Update from the original object - just change the selector.
// Update - change the selector to be sure.
svc = tc.update.DeepCopy()
svc.Spec.Selector = map[string]string{"bar": "baz2"}
svc.ResourceVersion = createdSvc.ResourceVersion

obj, _, err = storage.Update(ctx, svc.Name, rest.DefaultUpdatedObjectInfo(svc.DeepCopy()), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
obj, _, err = storage.Update(ctx, svc.Name, rest.DefaultUpdatedObjectInfo(svc.DeepCopy()),
rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
if tc.expectError {
if err == nil {
t.Fatalf("unexpected success on update")
}
return
}
if err != nil {
t.Fatalf("Expected no error: %v", err)
t.Fatalf("unexpected error on update: %v", err)
}
updatedSvc := obj.(*api.Service)

Expand All @@ -697,18 +867,9 @@ func TestServiceRegistryUpdateUnspecifiedAllocations(t *testing.T) {
if want, got := createdSvc.Spec.ClusterIPs, updatedSvc.Spec.ClusterIPs; !reflect.DeepEqual(want, got) {
t.Errorf("expected ClusterIPs to not change: wanted %v, got %v", want, got)
}
portmap := func(s *api.Service) map[string]int32 {
ret := map[string]int32{}
for _, p := range s.Spec.Ports {
ret[p.Name] = p.NodePort
}
return ret
}
if want, got := portmap(createdSvc), portmap(updatedSvc); !reflect.DeepEqual(want, got) {
t.Errorf("expected NodePort to not change: wanted %v, got %v", want, got)
}
if want, got := createdSvc.Spec.HealthCheckNodePort, updatedSvc.Spec.HealthCheckNodePort; want != got {
t.Errorf("expected HealthCheckNodePort to not change: wanted %v, got %v", want, got)

for _, proof := range tc.prove {
proof(t, updatedSvc)
}
})
}
Expand Down
22 changes: 21 additions & 1 deletion pkg/registry/core/service/strategy.go
Expand Up @@ -318,17 +318,37 @@ func patchAllocatedValues(newSvc, oldSvc *api.Service) {
}

if needsNodePort(oldSvc) && needsNodePort(newSvc) {
nodePortsUsed := func(svc *api.Service) sets.Int32 {
used := sets.NewInt32()
for _, p := range svc.Spec.Ports {
if p.NodePort != 0 {
used.Insert(p.NodePort)
}
}
return used
}

// Build a set of all the ports in oldSvc that are also in newSvc. We know
// we can't patch these values.
used := nodePortsUsed(oldSvc).Intersection(nodePortsUsed(newSvc))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jordan's example considers healthCheckNodePort too https://github.com/kubernetes/kubernetes/pull/103532/files#r694831725

If the user sets a new Nodeport value that matches the current allocated HealthCheckNodePort, should we change the HealthCheckNodePort?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the tests *swap-port-with-hcnp consider that should fail?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that has always failed, since the node port allocate step happens before the HCNP deallocate. I am fine with that continuing to fail, and honestly, letting nodeports fail isn't SO bad, but it is a breakage in 22 vs 21.


// Map NodePorts by name. The user may have changed other properties
// of the port, but we won't see that here.
np := map[string]int32{}
for i := range oldSvc.Spec.Ports {
p := &oldSvc.Spec.Ports[i]
np[p.Name] = p.NodePort
}

// If newSvc is missing values, try to patch them in when we know them and
// they haven't been used for another port.
for i := range newSvc.Spec.Ports {
p := &newSvc.Spec.Ports[i]
if p.NodePort == 0 {
p.NodePort = np[p.Name]
oldVal := np[p.Name]
if !used.Has(oldVal) {
p.NodePort = oldVal
}
}
}
}
Expand Down