-
Notifications
You must be signed in to change notification settings - Fork 76
Tolerate additional labels and annotations under deployment's spec.template.metadata field #1711
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
base: main
Are you sure you want to change the base?
Changes from all commits
60ab95e
88421cd
c5c5f77
4ece3f9
ffaed31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -41,7 +41,7 @@ var diffFuncs = map[reflect.Type]diffFunc{ | |||||||||||
| reflect.TypeOf(rbacv1.Role{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(roleDiffOpts)), | ||||||||||||
| reflect.TypeOf(rbacv1.RoleBinding{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(rolebindingDiffOpts)), | ||||||||||||
| reflect.TypeOf(corev1.ServiceAccount{}): metadataDiffFunc, | ||||||||||||
| reflect.TypeOf(appsv1.Deployment{}): allDiffFuncs(deploymentDiffFunc, metadataDiffFunc, basicDiffFunc(deploymentDiffOpts)), | ||||||||||||
| reflect.TypeOf(appsv1.Deployment{}): allDiffFuncs(deploymentDiffFunc, metadataDiffFunc, podTemplateMetadataDiffFunc, basicDiffFunc(deploymentDiffOpts)), | ||||||||||||
| reflect.TypeOf(corev1.Pod{}): allDiffFuncs(podDiffFunc, metadataDiffFunc), | ||||||||||||
| reflect.TypeOf(corev1.ConfigMap{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(configmapDiffOpts)), | ||||||||||||
| reflect.TypeOf(corev1.Secret{}): allDiffFuncs(metadataDiffFunc, basicDiffFunc(secretDiffOpts)), | ||||||||||||
|
|
@@ -83,6 +83,33 @@ func metadataDiffFunc(spec, cluster crclient.Object) (delete, update bool) { | |||||||||||
| return false, false | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // podTemplateMetadataDiffFunc requires a Deployment to be updated if any label or annotation present in the spec | ||||||||||||
|
dkwon17 marked this conversation as resolved.
|
||||||||||||
| // deployment's pod template is missing from the cluster deployment's pod template or present with a different value. | ||||||||||||
| // Like metadataDiffFunc, it only checks the spec-to-cluster direction so that externally-added labels on the pod | ||||||||||||
| // template do not trigger an update. This is only safe because deploymentDiffOpts ignores PodTemplateSpec.ObjectMeta | ||||||||||||
| // — see diffopts.go. | ||||||||||||
| func podTemplateMetadataDiffFunc(spec, cluster crclient.Object) (delete, update bool) { | ||||||||||||
| specDeploy, ok := spec.(*appsv1.Deployment) | ||||||||||||
| if !ok { | ||||||||||||
| return false, false | ||||||||||||
| } | ||||||||||||
| clusterDeploy := cluster.(*appsv1.Deployment) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
It is unreachable today - Related: |
||||||||||||
|
|
||||||||||||
| clusterLabels := clusterDeploy.Spec.Template.Labels | ||||||||||||
| for k, v := range specDeploy.Spec.Template.Labels { | ||||||||||||
| if cv, ok := clusterLabels[k]; !ok || cv != v { | ||||||||||||
| return false, true | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| clusterAnnotations := clusterDeploy.Spec.Template.Annotations | ||||||||||||
| for k, v := range specDeploy.Spec.Template.Annotations { | ||||||||||||
|
rohanKanojia marked this conversation as resolved.
|
||||||||||||
| if cv, ok := clusterAnnotations[k]; !ok || cv != v { | ||||||||||||
| return false, true | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| return false, false | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // allDiffFuncs represents an 'and' condition across specified diffFuncs. Functions are checked in provided order, | ||||||||||||
| // returning the result of the first function to require an update/deletion. | ||||||||||||
| func allDiffFuncs(funcs ...diffFunc) diffFunc { | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,231 @@ | ||
| // Copyright (c) 2019-2026 Red Hat, Inc. | ||
|
dkwon17 marked this conversation as resolved.
|
||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package sync | ||
|
|
||
| import ( | ||
| "reflect" | ||
| "testing" | ||
|
|
||
| appsv1 "k8s.io/api/apps/v1" | ||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| func TestPodTemplateMetadataDiffFunc(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| specLabels map[string]string | ||
| specAnns map[string]string | ||
| clusterLabels map[string]string | ||
| clusterAnns map[string]string | ||
| expectUpdate bool | ||
| }{ | ||
| { | ||
| name: "no diff when labels match", | ||
| specLabels: map[string]string{"app": "test"}, | ||
| clusterLabels: map[string]string{"app": "test"}, | ||
| expectUpdate: false, | ||
| }, | ||
| { | ||
| name: "no diff when cluster has extra labels", | ||
| specLabels: map[string]string{"app": "test"}, | ||
| clusterLabels: map[string]string{"app": "test", "paas.redhat.com/appcode": "ITOS-123"}, | ||
| expectUpdate: false, | ||
| }, | ||
| { | ||
| name: "diff when spec label missing from cluster", | ||
| specLabels: map[string]string{"app": "test", "new-label": "value"}, | ||
| clusterLabels: map[string]string{"app": "test"}, | ||
| expectUpdate: true, | ||
| }, | ||
| { | ||
| name: "diff when spec label value differs", | ||
| specLabels: map[string]string{"app": "test-v2"}, | ||
| clusterLabels: map[string]string{"app": "test-v1"}, | ||
| expectUpdate: true, | ||
| }, | ||
| { | ||
| name: "no diff when cluster has extra annotations", | ||
| specAnns: map[string]string{"note": "hello"}, | ||
| clusterAnns: map[string]string{"note": "hello", "external.io/injected": "true"}, | ||
| expectUpdate: false, | ||
| }, | ||
| { | ||
| name: "diff when spec annotation missing from cluster", | ||
| specAnns: map[string]string{"note": "hello"}, | ||
| clusterAnns: map[string]string{}, | ||
| expectUpdate: true, | ||
| }, | ||
| { | ||
| name: "no diff with nil maps", | ||
| specLabels: nil, | ||
| clusterLabels: nil, | ||
| expectUpdate: false, | ||
| }, | ||
| { | ||
| name: "no diff when cluster has labels but spec has none", | ||
| specLabels: nil, | ||
| clusterLabels: map[string]string{"external": "label"}, | ||
| expectUpdate: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| spec := &appsv1.Deployment{ | ||
| Spec: appsv1.DeploymentSpec{ | ||
| Template: corev1.PodTemplateSpec{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Labels: tt.specLabels, | ||
| Annotations: tt.specAnns, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| cluster := &appsv1.Deployment{ | ||
| Spec: appsv1.DeploymentSpec{ | ||
| Template: corev1.PodTemplateSpec{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Labels: tt.clusterLabels, | ||
| Annotations: tt.clusterAnns, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| del, update := podTemplateMetadataDiffFunc(spec, cluster) | ||
| if del { | ||
| t.Errorf("podTemplateMetadataDiffFunc() delete = true, want false") | ||
| } | ||
| if update != tt.expectUpdate { | ||
| t.Errorf("podTemplateMetadataDiffFunc() update = %v, want %v", update, tt.expectUpdate) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestPodTemplateMetadataDiffFunc_NonDeployment(t *testing.T) { | ||
| spec := &corev1.ConfigMap{} | ||
| cluster := &corev1.ConfigMap{} | ||
| shouldDelete, shouldUpdate := podTemplateMetadataDiffFunc(spec, cluster) | ||
| if shouldDelete || shouldUpdate { | ||
| t.Errorf("podTemplateMetadataDiffFunc() should return (false, false) for non-Deployment types, got (%v, %v)", shouldDelete, shouldUpdate) | ||
| } | ||
| } | ||
|
|
||
| func TestDeploymentDiffOpts_IgnoresPodTemplateMetadata(t *testing.T) { | ||
|
dkwon17 marked this conversation as resolved.
|
||
| spec := &appsv1.Deployment{ | ||
| Spec: appsv1.DeploymentSpec{ | ||
| Selector: &metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": "test"}, | ||
| }, | ||
| Template: corev1.PodTemplateSpec{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Labels: map[string]string{"app": "test"}, | ||
| }, | ||
| Spec: corev1.PodSpec{ | ||
| Containers: []corev1.Container{{ | ||
| Name: "main", | ||
| Image: "test:latest", | ||
| }}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| cluster := spec.DeepCopy() | ||
| cluster.Spec.Template.Labels["external.io/injected"] = "true" | ||
|
|
||
| diffFn := basicDiffFunc(deploymentDiffOpts) | ||
| del, update := diffFn(spec, cluster) | ||
| if del { | ||
| t.Error("basicDiffFunc(deploymentDiffOpts) delete = true, want false") | ||
| } | ||
| if update { | ||
| t.Error("basicDiffFunc(deploymentDiffOpts) should not detect extra pod template labels as a diff") | ||
| } | ||
| } | ||
|
|
||
| func TestDeploymentDiffOpts_DetectsSpecChanges(t *testing.T) { | ||
| spec := &appsv1.Deployment{ | ||
| Spec: appsv1.DeploymentSpec{ | ||
| Selector: &metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": "test"}, | ||
| }, | ||
| Template: corev1.PodTemplateSpec{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Labels: map[string]string{"app": "test"}, | ||
| }, | ||
| Spec: corev1.PodSpec{ | ||
| Containers: []corev1.Container{{ | ||
| Name: "main", | ||
| Image: "test:v2", | ||
| }}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| cluster := spec.DeepCopy() | ||
| cluster.Spec.Template.Spec.Containers[0].Image = "test:v1" | ||
|
|
||
| diffFn := basicDiffFunc(deploymentDiffOpts) | ||
| del, update := diffFn(spec, cluster) | ||
| if del { | ||
| t.Error("basicDiffFunc(deploymentDiffOpts) delete = true, want false") | ||
| } | ||
| if !update { | ||
| t.Error("basicDiffFunc(deploymentDiffOpts) should detect container image changes as a diff") | ||
| } | ||
| } | ||
|
|
||
| func TestDeploymentFullDiff_ExternalLabelsNoUpdate(t *testing.T) { | ||
| specLabels := map[string]string{ | ||
| "controller.devfile.io/devworkspace_id": "workspace123", | ||
| "controller.devfile.io/devworkspace_name": "my-workspace", | ||
| } | ||
| spec := &appsv1.Deployment{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Labels: specLabels, | ||
| }, | ||
| Spec: appsv1.DeploymentSpec{ | ||
| Selector: &metav1.LabelSelector{ | ||
| MatchLabels: map[string]string{"app": "test"}, | ||
| }, | ||
| Template: corev1.PodTemplateSpec{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Labels: map[string]string{ | ||
| "controller.devfile.io/devworkspace_id": "workspace123", | ||
| }, | ||
| }, | ||
| Spec: corev1.PodSpec{ | ||
| Containers: []corev1.Container{{ | ||
| Name: "main", | ||
| Image: "test:latest", | ||
| }}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| cluster := spec.DeepCopy() | ||
| cluster.Labels["paas.redhat.com/appcode"] = "ITOS-123" | ||
| cluster.Spec.Template.Labels["paas.redhat.com/appcode"] = "ITOS-123" | ||
|
|
||
| deploymentDiff := diffFuncs[reflect.TypeOf(appsv1.Deployment{})] | ||
| del, update := deploymentDiff(spec, cluster) | ||
| if del { | ||
| t.Error("deployment diff should not trigger delete when only external labels are added to deployment and pod template metadata") | ||
| } | ||
| if update { | ||
| t.Error("deployment diff should not trigger update when only external labels are added to deployment and pod template metadata") | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ import ( | |
| "errors" | ||
| "reflect" | ||
|
|
||
| appsv1 "k8s.io/api/apps/v1" | ||
| corev1 "k8s.io/api/core/v1" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| crclient "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
|
@@ -67,9 +68,35 @@ func serviceAccountUpdateFunc(spec, cluster crclient.Object) (crclient.Object, e | |
| return spec, nil | ||
| } | ||
|
|
||
| func deploymentUpdateFunc(spec, cluster crclient.Object) (crclient.Object, error) { | ||
| if cluster == nil { | ||
| return defaultUpdateFunc(spec, cluster) | ||
| } | ||
| specDeploy := spec.DeepCopyObject().(*appsv1.Deployment) | ||
| clusterDeploy := cluster.(*appsv1.Deployment) | ||
| specDeploy.ResourceVersion = clusterDeploy.ResourceVersion | ||
|
|
||
| specDeploy.Spec.Template.Labels = mergeMaps(clusterDeploy.Spec.Template.Labels, specDeploy.Spec.Template.Labels) | ||
| specDeploy.Spec.Template.Annotations = mergeMaps(clusterDeploy.Spec.Template.Annotations, specDeploy.Spec.Template.Annotations) | ||
|
Comment on lines
+79
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Allow removal of previously configured pod-template metadata. When a user removes a label or annotation from the spec, 🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is an issue. If we want the label removed, can't it be just manually removed from the deployment spec? This would then cause the desired deployment spec and on-cluster deployment spec to both lack the now-removed label? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline pkg/provision/sync/diff.go --items all
ast-grep outline pkg/provision/sync/diffopts.go --items all
rg -n -C 5 'podTemplateMetadataDiffFunc|deploymentDiffOpts|deploymentUpdateFunc|getUpdateFunc\(' pkg/provision/sync
sed -n '1,205p' pkg/provision/sync/diff.go
sed -n '1,165p' pkg/provision/sync/diffopts.goLength of output: 28728
If you also manually remove the label from the on-cluster Deployment, then both copies will lack it. My concern is that changing the desired Deployment alone cannot remove a label that it previously managed. The same behavior applies to pod-template annotations. You are interacting with an AI system.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user wouldn't be able to remove label from the desired deployment, only on-cluster deployment. An externally added label would never be on the desired deployment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline pkg/provision/sync/diff.go --items all
ast-grep outline pkg/provision/sync/diffopts.go --items all
ast-grep outline pkg/provision/sync/update.go --items all
rg -n -C 4 'podTemplateMetadataDiffFunc|deploymentDiffOpts|deploymentUpdateFunc|Spec\.Template\.(Labels|Annotations)|Template:\s*corev1.PodTemplateSpec' pkg/provision/sync
sed -n '1,165p' pkg/provision/sync/diff.go
sed -n '1,130p' pkg/provision/sync/diffopts.go
rg -n -C 3 --type go 'Spec\.Template\.(Labels|Annotations)\s*=|Template:\s*(corev1\.)?PodTemplateSpec' -g '!**/*_test.go' | head -240Length of output: 38875 🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline pkg/provision/workspace/deployment.go --items all --match 'Deployment|deployment|Annotations|Labels'
sed -n '110,275p' pkg/provision/workspace/deployment.go
rg -n -C 3 --type go 'DevWorkspaceRestrictedAccessAnnotation' pkg/provision apis controllers | head -150
sed -n '37,85p' pkg/provision/sync/update_test.goLength of output: 16105
The code confirms a separate case for operator-managed metadata. I will keep the finding open for that operator-managed case. My earlier description of removing an external label from the desired Deployment was incorrect. ✏️ Learnings added
You are interacting with an AI system. |
||
| return specDeploy, nil | ||
| } | ||
|
|
||
| func mergeMaps(base, overlay map[string]string) map[string]string { | ||
| merged := make(map[string]string, len(base)+len(overlay)) | ||
| for k, v := range base { | ||
| merged[k] = v | ||
| } | ||
| for k, v := range overlay { | ||
| merged[k] = v | ||
| } | ||
| return merged | ||
| } | ||
|
|
||
| func getUpdateFunc(obj crclient.Object) updateFunc { | ||
| objType := reflect.TypeOf(obj).Elem() | ||
| switch objType { | ||
| case reflect.TypeOf(appsv1.Deployment{}): | ||
| return deploymentUpdateFunc | ||
| case reflect.TypeOf(corev1.Service{}): | ||
| return serviceUpdateFunc | ||
| case reflect.TypeOf(corev1.ServiceAccount{}): | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.