Skip to content

Commit 653296d

Browse files
Fix labels and annotations propagation to k8s service on update (#15907)
Co-authored-by: David Simansky <[email protected]>
1 parent ed7562c commit 653296d

File tree

5 files changed

+188
-3
lines changed

5 files changed

+188
-3
lines changed

pkg/reconciler/route/reconcile_resources.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"knative.dev/networking/pkg/apis/networking"
3838
netv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1"
3939
"knative.dev/pkg/controller"
40+
"knative.dev/pkg/kmap"
4041
"knative.dev/pkg/logging"
4142
"knative.dev/serving/pkg/apis/serving"
4243
v1 "knative.dev/serving/pkg/apis/serving/v1"
@@ -256,10 +257,14 @@ func (c *Reconciler) updatePlaceholderServices(ctx context.Context, route *v1.Ro
256257

257258
if canUpdate {
258259
// Make sure that the service has the proper specification.
259-
if !equality.Semantic.DeepEqual(from.Service.Spec, to.Service.Spec) {
260+
if !equality.Semantic.DeepEqual(from.Spec, to.Spec) ||
261+
!equality.Semantic.DeepEqual(from.Service.Annotations, kmap.Union(from.Service.Annotations, to.Service.Annotations)) ||
262+
!equality.Semantic.DeepEqual(from.Service.Labels, kmap.Union(from.Service.Labels, to.Service.Labels)) {
260263
// Don't modify the informers copy.
261264
existing := from.Service.DeepCopy()
262265
existing.Spec = to.Service.Spec
266+
existing.Annotations = kmap.Union(from.Service.Annotations, to.Service.Annotations)
267+
existing.Labels = kmap.Union(from.Service.Labels, to.Service.Labels)
263268
if _, err := c.kubeclient.CoreV1().Services(ns).Update(ctx, existing, metav1.UpdateOptions{}); err != nil {
264269
return err
265270
}

pkg/reconciler/route/table_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ func TestReconcile(t *testing.T) {
786786
},
787787
withReadyIngress,
788788
),
789-
simpleK8sService(Route("default", "different-domain", WithConfigTarget("config"))),
789+
simpleK8sService(Route("default", "different-domain", WithConfigTarget("config"), WithRouteLabel(map[string]string{"app": "prod"}))),
790790
},
791791
WantUpdates: []clientgotesting.UpdateActionImpl{{
792792
Object: simpleIngress(

pkg/testing/v1/service.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func WithServiceAnnotation(k, v string) ServiceOption {
145145
}
146146
}
147147

148-
// WithServiceAnnotationRemoved adds the given annotation to the service.
148+
// WithServiceAnnotationRemoved removes the given annotation from the service.
149149
func WithServiceAnnotationRemoved(k string) ServiceOption {
150150
return func(svc *v1.Service) {
151151
svc.Annotations = kmeta.FilterMap(svc.Annotations, func(s string) bool {
@@ -154,6 +154,15 @@ func WithServiceAnnotationRemoved(k string) ServiceOption {
154154
}
155155
}
156156

157+
// WithServiceLabelRemoved removes the given label from the service.
158+
func WithServiceLabelRemoved(k string) ServiceOption {
159+
return func(svc *v1.Service) {
160+
svc.Labels = kmeta.FilterMap(svc.Labels, func(s string) bool {
161+
return k == s
162+
})
163+
}
164+
}
165+
157166
// WithServiceImage sets the container image to be the provided string.
158167
func WithServiceImage(img string) ServiceOption {
159168
return func(svc *v1.Service) {

test/conformance/api/v1/service_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package v1
2121

2222
import (
2323
"context"
24+
"strings"
2425
"testing"
2526

2627
"github.com/google/go-cmp/cmp"
@@ -618,6 +619,9 @@ func TestAnnotationPropagation(t *testing.T) {
618619
if err := validateAnnotations(objects); err != nil {
619620
t.Error("Annotations are incorrect:", err)
620621
}
622+
if err := validateK8sServiceAnnotations(t, clients, names); err != nil {
623+
t.Error(err)
624+
}
621625

622626
if objects.Service, err = v1test.UpdateService(t, clients, names,
623627
rtesting.WithServiceAnnotation("juicy", "jamba")); err != nil {
@@ -651,6 +655,9 @@ func TestAnnotationPropagation(t *testing.T) {
651655
if err := validateAnnotations(objects, "juicy"); err != nil {
652656
t.Error("Annotations are incorrect:", err)
653657
}
658+
if err := validateK8sServiceAnnotations(t, clients, names, "juicy"); err != nil {
659+
t.Error(err)
660+
}
654661

655662
if objects.Service, err = v1test.UpdateService(t, clients, names,
656663
rtesting.WithServiceAnnotationRemoved("juicy")); err != nil {
@@ -684,12 +691,20 @@ func TestAnnotationPropagation(t *testing.T) {
684691
if err := validateAnnotations(objects); err != nil {
685692
t.Error("Annotations are incorrect:", err)
686693
}
694+
if err := validateK8sServiceAnnotations(t, clients, names, "juicy"); err != nil {
695+
t.Error(err)
696+
}
687697
if _, ok := objects.Config.Annotations["juicy"]; ok {
688698
t.Error("Config still has `juicy` annotation")
689699
}
690700
if _, ok := objects.Route.Annotations["juicy"]; ok {
691701
t.Error("Route still has `juicy` annotation")
692702
}
703+
if err := validateK8sServiceAnnotations(t, clients, names, "juicy"); err != nil {
704+
if !strings.Contains(err.Error(), "was empty") {
705+
t.Error(err)
706+
}
707+
}
693708
}
694709

695710
// TestServiceCreateWithMultipleContainers tests both Creation paths for a service.
@@ -750,6 +765,126 @@ func TestServiceCreateWithMultipleContainers(t *testing.T) {
750765
}
751766
}
752767

768+
func TestLabelPropagation(t *testing.T) {
769+
t.Parallel()
770+
clients := test.Setup(t)
771+
772+
names := test.ResourceNames{
773+
Service: test.ObjectNameForTest(t),
774+
Image: test.PizzaPlanet1,
775+
}
776+
777+
// Clean up on test failure or interrupt
778+
test.EnsureTearDown(t, clients, &names)
779+
780+
// Setup initial Service
781+
objects, err := v1test.CreateServiceReady(t, clients, &names)
782+
if err != nil {
783+
t.Fatalf("Failed to create initial Service %v: %v", names.Service, err)
784+
}
785+
786+
// Validate State after Creation
787+
if err = validateControlPlane(t, clients, names, "1"); err != nil {
788+
t.Error(err)
789+
}
790+
791+
// Validate State after Creation
792+
793+
if err = validateControlPlane(t, clients, names, "1"); err != nil {
794+
t.Error(err)
795+
}
796+
797+
if err := validateLabelsPropagation(t, *objects, names); err != nil {
798+
t.Error(err)
799+
}
800+
if err := validateK8sServiceLabels(t, clients, names); err != nil {
801+
t.Error(err)
802+
}
803+
804+
if objects.Service, err = v1test.UpdateService(t, clients, names,
805+
rtesting.WithServiceLabel("juicy", "jamba")); err != nil {
806+
t.Fatalf("Service %s was not updated with new annotation: %v", names.Service, err)
807+
}
808+
809+
// Trigger new Revision generation
810+
t.Log("Updating the Service to use a different image.")
811+
image2 := pkgtest.ImagePath(test.PizzaPlanet2)
812+
if objects.Service, err = v1test.UpdateService(t, clients, names, rtesting.WithServiceImage(image2)); err != nil {
813+
t.Fatalf("Update for Service %s with new image %s failed: %v", names.Service, image2, err)
814+
}
815+
t.Log("Service should reflect new revision created and ready in status.")
816+
names.Revision, err = v1test.WaitForServiceLatestRevision(clients, names)
817+
if err != nil {
818+
t.Fatal("New image not reflected in Service:", err)
819+
}
820+
t.Log("Waiting for Service to transition to Ready.")
821+
if err := v1test.WaitForServiceState(clients.ServingClient, names.Service, v1test.IsServiceReady, "ServiceIsReady"); err != nil {
822+
t.Fatal("Error waiting for the service to become ready for the latest revision:", err)
823+
}
824+
objects, err = v1test.GetResourceObjects(clients, names)
825+
if err != nil {
826+
t.Error("Error getting objects:", err)
827+
}
828+
829+
// Validate updated labels
830+
if err := validateLabelsPropagation(t, *objects, names); err != nil {
831+
t.Error("Annotations are incorrect:", err)
832+
}
833+
if _, ok := objects.Config.Labels["juicy"]; !ok {
834+
t.Error("Config missing `juicy` label")
835+
}
836+
if _, ok := objects.Route.Labels["juicy"]; !ok {
837+
t.Error("Route missing `juicy` label")
838+
}
839+
if err := validateK8sServiceLabels(t, clients, names, "juicy"); err != nil {
840+
t.Error(err)
841+
}
842+
843+
if objects.Service, err = v1test.UpdateService(t, clients, names,
844+
rtesting.WithServiceLabelRemoved("juicy")); err != nil {
845+
t.Fatalf("Service %s was not updated with annotation deleted: %v", names.Service, err)
846+
}
847+
848+
// Trigger new Revision generation
849+
t.Log("Updating the Service to use a different image.")
850+
image3 := pkgtest.ImagePath(test.HelloWorld)
851+
if objects.Service, err = v1test.UpdateService(t, clients, names, rtesting.WithServiceImage(image3)); err != nil {
852+
t.Fatalf("Update for Service %s with new image %s failed: %v", names.Service, image3, err)
853+
}
854+
t.Log("Service should reflect new revision created and ready in status.")
855+
names.Revision, err = v1test.WaitForServiceLatestRevision(clients, names)
856+
if err != nil {
857+
t.Fatal("New image not reflected in Service:", err)
858+
}
859+
t.Log("Waiting for Service to transition to Ready.")
860+
if err := v1test.WaitForServiceState(clients.ServingClient, names.Service, v1test.IsServiceReady, "ServiceIsReady"); err != nil {
861+
t.Fatal("Error waiting for the service to become ready for the latest revision:", err)
862+
}
863+
objects, err = v1test.GetResourceObjects(clients, names)
864+
if err != nil {
865+
t.Error("Error getting objects:", err)
866+
}
867+
868+
// Now we can validate the annotations.
869+
if err := validateLabelsPropagation(t, *objects, names); err != nil {
870+
t.Error(err)
871+
}
872+
if err := validateK8sServiceLabels(t, clients, names, "juicy"); err != nil {
873+
t.Error(err)
874+
}
875+
if _, ok := objects.Config.Labels["juicy"]; ok {
876+
t.Error("Config still has `juicy` annotation")
877+
}
878+
if _, ok := objects.Route.Labels["juicy"]; ok {
879+
t.Error("Route still has `juicy` annotation")
880+
}
881+
if err := validateK8sServiceLabels(t, clients, names, "juicy"); err != nil {
882+
if !strings.Contains(err.Error(), "was empty") {
883+
t.Error(err)
884+
}
885+
}
886+
}
887+
753888
// TestServiceCreateWithEmptyDir tests both Creation paths for a service with emptydir enabled.
754889
// The test performs a series of Validate steps to ensure that the service transitions as expected during each step.
755890
func TestServiceCreateWithEmptyDir(t *testing.T) {

test/conformance/api/v1/util.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,27 @@ func validateLabelsPropagation(t testing.TB, objects v1test.ResourceObjects, nam
218218
return nil
219219
}
220220

221+
func validateK8sServiceLabels(t *testing.T, clients *test.Clients, names test.ResourceNames, extraKeys ...string) error {
222+
t.Log("Validate Labels on Kubernetes Service Object")
223+
k8sService, err := clients.KubeClient.CoreV1().Services(test.ServingFlags.TestNamespace).Get(context.Background(), names.Service, metav1.GetOptions{})
224+
if err != nil {
225+
return fmt.Errorf("failed to get k8s service %v: %w", names.Service, err)
226+
}
227+
labels := k8sService.GetLabels()
228+
if labels["serving.knative.dev/service"] != names.Service {
229+
return fmt.Errorf("expected Service name in K8s Service label %q but got %q ", names.Service, labels["serving.knative.dev/service"])
230+
}
231+
if labels["serving.knative.dev/route"] != names.Service {
232+
return fmt.Errorf("expected Route name in K8s Service label %q but got %q ", names.Service, labels["serving.knative.dev/route"])
233+
}
234+
for _, extraKey := range extraKeys {
235+
if got := labels[extraKey]; got == "" {
236+
return fmt.Errorf("expected %s label to be set, but was empty", extraKey)
237+
}
238+
}
239+
return nil
240+
}
241+
221242
func validateAnnotations(objs *v1test.ResourceObjects, extraKeys ...string) error {
222243
// This checks whether the annotations are set on the resources that
223244
// expect them to have.
@@ -244,6 +265,21 @@ func validateAnnotations(objs *v1test.ResourceObjects, extraKeys ...string) erro
244265
return nil
245266
}
246267

268+
func validateK8sServiceAnnotations(t *testing.T, clients *test.Clients, names test.ResourceNames, extraKeys ...string) error {
269+
t.Log("Validate Annotations on Kubernetes Service Object")
270+
k8sService, err := clients.KubeClient.CoreV1().Services(test.ServingFlags.TestNamespace).Get(context.Background(), names.Service, metav1.GetOptions{})
271+
if err != nil {
272+
return fmt.Errorf("failed to get k8s service %v: %w", names.Service, err)
273+
}
274+
annotations := k8sService.GetAnnotations()
275+
for _, extraKey := range append([]string{serving.CreatorAnnotation, serving.UpdaterAnnotation}, extraKeys...) {
276+
if got := annotations[extraKey]; got == "" {
277+
return fmt.Errorf("expected %s annotation to be set, but was empty", extraKey)
278+
}
279+
}
280+
return nil
281+
}
282+
247283
func validateReleaseServiceShape(objs *v1test.ResourceObjects) error {
248284
// Traffic should be routed to the lastest created revision.
249285
if got, want := objs.Service.Status.Traffic[0].RevisionName, objs.Config.Status.LatestReadyRevisionName; got != want {

0 commit comments

Comments
 (0)