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

Preserve loadBalancerClass on Service updates #3641

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion webhooks/core/service_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,41 @@ func (m *serviceMutator) MutateCreate(ctx context.Context, obj runtime.Object) (
}

func (m *serviceMutator) MutateUpdate(ctx context.Context, obj runtime.Object, oldObj runtime.Object) (runtime.Object, error) {
return obj, nil
// this mutator only cares about Service objects
newSvc, ok := obj.(*corev1.Service)
if !ok {
return obj, nil
}

oldSvc, ok := oldObj.(*corev1.Service)
if !ok {
return obj, nil
}

if newSvc.Spec.Type != corev1.ServiceTypeLoadBalancer {
return obj, nil
}

// does the old Service object have spec.loadBalancerClass?
if oldSvc.Spec.LoadBalancerClass != nil && *oldSvc.Spec.LoadBalancerClass != "" {
// if so, let's inspect the incoming object for the same field

// does the new Service object lack spec.loadBalancerClass?
// if so, set it to the old value
// if yes, then leave it be because someone wanted it that way, let the user deal with the error
if newSvc.Spec.LoadBalancerClass == nil || *newSvc.Spec.LoadBalancerClass == "" {
newSvc.Spec.LoadBalancerClass = oldSvc.Spec.LoadBalancerClass

Copy link
Collaborator

@oliviassss oliviassss Apr 11, 2024

Choose a reason for hiding this comment

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

I was just thinking some corner cases - would this change cause some issue when this field in the newSvc is intended to be nil or ""?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since this handler is for updates to an existing service only, there's a couple of reasons the field would be missing/empty:

  1. User is updating the service object via patch, submitting only modified fields. In this case, tacking the LBC field onto the update is a noop, and there's no change in behavior.
  2. User is updating the service object via put (this is the kubectl replace) and omits the field. The normal response would be an error, as the field is immutable.
    With this handler, the error does not occur as we putting the field back. This is a change in behavior.
    Whether this change is unexpected - I think it is not, because presumably user has enabled the webhook, is aware of the webhook effects, and expects LB Services in their cluster to be managed by AWS LBC. So this change, then, just prevents the user from running into an error (and this is the entire reason behind this PR because our deployment tooling - Helm - runs into this very error).
    Importantly, because the handler inspects the existing Service, it will only touch service that are known to be managed by AWS LBC, and not something else.

m.logger.Info("preserved loadBalancerClass", "service", newSvc.Name, "loadBalancerClass", *newSvc.Spec.LoadBalancerClass)
return newSvc, nil
}

m.logger.Info("service already has loadBalancerClass, skipping", "service", newSvc.Name, "loadBalancerClass", *newSvc.Spec.LoadBalancerClass)
return newSvc, nil
}

m.logger.Info("service did not originally have a loadBalancerClass, skipping", "service", newSvc.Name)
return newSvc, nil
}

// +kubebuilder:webhook:path=/mutate-v1-service,mutating=true,failurePolicy=fail,groups="",resources=services,verbs=create,versions=v1,name=mservice.elbv2.k8s.aws,sideEffects=None,webhookVersions=v1,admissionReviewVersions=v1beta1
Expand Down
50 changes: 50 additions & 0 deletions webhooks/core/service_mutator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package core

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
)

func TestMutateUpdate_WhenServiceIsNotLoadBalancer(t *testing.T) {
m := &serviceMutator{}
svc := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeClusterIP}}
_, err := m.MutateUpdate(context.Background(), svc, svc)
assert.NoError(t, err)

assert.Nil(t, svc.Spec.LoadBalancerClass)
}

func TestMutateUpdate_WhenOldServiceHasLoadBalancerClassAndNewServiceDoesNot(t *testing.T) {
m := &serviceMutator{}
oldSvc := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: stringPtr("old-class")}}
newSvc := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}}
_, err := m.MutateUpdate(context.Background(), newSvc, oldSvc)
assert.NoError(t, err)
assert.Equal(t, "old-class", *newSvc.Spec.LoadBalancerClass)
}

func TestMutateUpdate_WhenOldServiceHasLoadBalancerClassAndNewServiceHasDifferent(t *testing.T) {
m := &serviceMutator{}
oldSvc := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: stringPtr("old-class")}}
newSvc := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, LoadBalancerClass: stringPtr("new-class")}}
_, err := m.MutateUpdate(context.Background(), newSvc, oldSvc)
assert.NoError(t, err)
assert.Equal(t, "new-class", *newSvc.Spec.LoadBalancerClass)
}

func TestMutateUpdate_WhenOldServiceDoesNotHaveLoadBalancerClass(t *testing.T) {
m := &serviceMutator{}
oldSvc := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}}
newSvc := &corev1.Service{Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer}}
_, err := m.MutateUpdate(context.Background(), newSvc, oldSvc)
assert.NoError(t, err)

assert.Nil(t, newSvc.Spec.LoadBalancerClass)
}

func stringPtr(s string) *string {
return &s
}