Skip to content

Commit 257d0a0

Browse files
Fix KUBERNETES_MIN_VERSION propagation to operand workloads (#2245)
1 parent 6056fbe commit 257d0a0

File tree

3 files changed

+354
-0
lines changed

3 files changed

+354
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
Copyright 2026 The Knative Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common
18+
19+
import (
20+
"os"
21+
22+
mf "github.com/manifestival/manifestival"
23+
appsv1 "k8s.io/api/apps/v1"
24+
batchv1 "k8s.io/api/batch/v1"
25+
corev1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/client-go/kubernetes/scheme"
29+
"knative.dev/pkg/version"
30+
)
31+
32+
// KubernetesMinVersionTransform injects KUBERNETES_MIN_VERSION into all workloads
33+
// managed by this operator instance so operand components can honor the override.
34+
func KubernetesMinVersionTransform() mf.Transformer {
35+
minVersion := os.Getenv(version.KubernetesMinVersionKey)
36+
if minVersion == "" {
37+
return func(_ *unstructured.Unstructured) error {
38+
return nil
39+
}
40+
}
41+
42+
minVersionEnv := []corev1.EnvVar{{
43+
Name: version.KubernetesMinVersionKey,
44+
Value: minVersion,
45+
}}
46+
47+
return func(u *unstructured.Unstructured) error {
48+
var podSpec *corev1.PodSpec
49+
50+
switch u.GetKind() {
51+
case "Deployment":
52+
deployment := &appsv1.Deployment{}
53+
if err := scheme.Scheme.Convert(u, deployment, nil); err != nil {
54+
return err
55+
}
56+
podSpec = &deployment.Spec.Template.Spec
57+
applyMinVersionEnvVar(podSpec, minVersionEnv)
58+
if err := scheme.Scheme.Convert(deployment, u, nil); err != nil {
59+
return err
60+
}
61+
case "StatefulSet":
62+
ss := &appsv1.StatefulSet{}
63+
if err := scheme.Scheme.Convert(u, ss, nil); err != nil {
64+
return err
65+
}
66+
podSpec = &ss.Spec.Template.Spec
67+
applyMinVersionEnvVar(podSpec, minVersionEnv)
68+
if err := scheme.Scheme.Convert(ss, u, nil); err != nil {
69+
return err
70+
}
71+
case "DaemonSet":
72+
ds := &appsv1.DaemonSet{}
73+
if err := scheme.Scheme.Convert(u, ds, nil); err != nil {
74+
return err
75+
}
76+
podSpec = &ds.Spec.Template.Spec
77+
applyMinVersionEnvVar(podSpec, minVersionEnv)
78+
if err := scheme.Scheme.Convert(ds, u, nil); err != nil {
79+
return err
80+
}
81+
case "Job":
82+
job := &batchv1.Job{}
83+
if err := scheme.Scheme.Convert(u, job, nil); err != nil {
84+
return err
85+
}
86+
podSpec = &job.Spec.Template.Spec
87+
applyMinVersionEnvVar(podSpec, minVersionEnv)
88+
if err := scheme.Scheme.Convert(job, u, nil); err != nil {
89+
return err
90+
}
91+
default:
92+
return nil
93+
}
94+
95+
// Avoid superfluous updates from converted zero defaults.
96+
u.SetCreationTimestamp(metav1.Time{})
97+
return nil
98+
}
99+
}
100+
101+
func applyMinVersionEnvVar(podSpec *corev1.PodSpec, minVersionEnv []corev1.EnvVar) {
102+
for i := range podSpec.Containers {
103+
mergeEnv(&minVersionEnv, &podSpec.Containers[i].Env)
104+
}
105+
for i := range podSpec.InitContainers {
106+
mergeEnv(&minVersionEnv, &podSpec.InitContainers[i].Env)
107+
}
108+
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/*
2+
Copyright 2026 The Knative Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common
18+
19+
import (
20+
"fmt"
21+
"testing"
22+
23+
mf "github.com/manifestival/manifestival"
24+
appsv1 "k8s.io/api/apps/v1"
25+
batchv1 "k8s.io/api/batch/v1"
26+
corev1 "k8s.io/api/core/v1"
27+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/client-go/kubernetes/scheme"
29+
util "knative.dev/operator/pkg/reconciler/common/testing"
30+
"knative.dev/pkg/version"
31+
)
32+
33+
func TestKubernetesMinVersionTransformInjectsEnvVar(t *testing.T) {
34+
t.Setenv(version.KubernetesMinVersionKey, "v1.25.0")
35+
36+
makePodSpec := func() corev1.PodSpec {
37+
return corev1.PodSpec{
38+
Containers: []corev1.Container{{
39+
Name: "controller",
40+
Env: []corev1.EnvVar{
41+
{Name: "EXISTING", Value: "1"},
42+
{Name: version.KubernetesMinVersionKey, Value: "v1.20.0"},
43+
},
44+
}, {
45+
Name: "webhook",
46+
}},
47+
InitContainers: []corev1.Container{{
48+
Name: "init-a",
49+
}, {
50+
Name: "init-b",
51+
Env: []corev1.EnvVar{{Name: version.KubernetesMinVersionKey, Value: "v1.19.0"}},
52+
}},
53+
}
54+
}
55+
56+
testCases := []struct {
57+
name string
58+
obj interface{}
59+
}{
60+
{
61+
name: "Deployment",
62+
obj: util.MakeDeployment("controller", makePodSpec()),
63+
},
64+
{
65+
name: "StatefulSet",
66+
obj: util.MakeStatefulSet("controller", makePodSpec()),
67+
},
68+
{
69+
name: "DaemonSet",
70+
obj: util.MakeDaemonSet("controller", makePodSpec()),
71+
},
72+
{
73+
name: "Job",
74+
obj: util.MakeJob("controller", makePodSpec()),
75+
},
76+
}
77+
78+
for _, tc := range testCases {
79+
t.Run(tc.name, func(t *testing.T) {
80+
u := util.MakeUnstructured(t, tc.obj)
81+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
82+
if err != nil {
83+
t.Fatalf("Failed to create manifest: %v", err)
84+
}
85+
86+
manifest, err = manifest.Transform(KubernetesMinVersionTransform())
87+
if err != nil {
88+
t.Fatalf("Failed to transform manifest: %v", err)
89+
}
90+
91+
podSpec, err := podSpecFromResource(manifest.Resources()[0])
92+
if err != nil {
93+
t.Fatalf("Failed to extract pod spec: %v", err)
94+
}
95+
96+
for _, c := range podSpec.Containers {
97+
if !hasEnv(c.Env, version.KubernetesMinVersionKey, "v1.25.0") {
98+
t.Fatalf("container %q misses %s=v1.25.0", c.Name, version.KubernetesMinVersionKey)
99+
}
100+
}
101+
for _, c := range podSpec.InitContainers {
102+
if !hasEnv(c.Env, version.KubernetesMinVersionKey, "v1.25.0") {
103+
t.Fatalf("init container %q misses %s=v1.25.0", c.Name, version.KubernetesMinVersionKey)
104+
}
105+
}
106+
if !hasEnv(podSpec.Containers[0].Env, "EXISTING", "1") {
107+
t.Fatalf("existing env var was removed")
108+
}
109+
})
110+
}
111+
}
112+
113+
func TestKubernetesMinVersionTransformNoopWhenEnvUnset(t *testing.T) {
114+
t.Setenv(version.KubernetesMinVersionKey, "")
115+
116+
deployment := util.MakeDeployment("controller", corev1.PodSpec{
117+
Containers: []corev1.Container{{
118+
Name: "controller",
119+
Env: []corev1.EnvVar{{Name: "EXISTING", Value: "1"}},
120+
}},
121+
InitContainers: []corev1.Container{{
122+
Name: "init-a",
123+
Env: []corev1.EnvVar{{Name: "EXISTING_INIT", Value: "1"}},
124+
}},
125+
})
126+
u := util.MakeUnstructured(t, deployment)
127+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
128+
if err != nil {
129+
t.Fatalf("Failed to create manifest: %v", err)
130+
}
131+
132+
manifest, err = manifest.Transform(KubernetesMinVersionTransform())
133+
if err != nil {
134+
t.Fatalf("Failed to transform manifest: %v", err)
135+
}
136+
137+
var got appsv1.Deployment
138+
if err := scheme.Scheme.Convert(&manifest.Resources()[0], &got, nil); err != nil {
139+
t.Fatalf("Failed to convert deployment: %v", err)
140+
}
141+
142+
if hasEnv(got.Spec.Template.Spec.Containers[0].Env, version.KubernetesMinVersionKey, "") {
143+
t.Fatalf("unexpected %s env var was injected", version.KubernetesMinVersionKey)
144+
}
145+
if !hasEnv(got.Spec.Template.Spec.Containers[0].Env, "EXISTING", "1") {
146+
t.Fatalf("existing env var was changed")
147+
}
148+
if hasEnv(got.Spec.Template.Spec.InitContainers[0].Env, version.KubernetesMinVersionKey, "") {
149+
t.Fatalf("unexpected %s env var was injected into init container", version.KubernetesMinVersionKey)
150+
}
151+
if !hasEnv(got.Spec.Template.Spec.InitContainers[0].Env, "EXISTING_INIT", "1") {
152+
t.Fatalf("existing init container env var was changed")
153+
}
154+
}
155+
156+
func TestKubernetesMinVersionTransformUnsupportedKind(t *testing.T) {
157+
t.Setenv(version.KubernetesMinVersionKey, "v1.25.0")
158+
159+
u := unstructured.Unstructured{
160+
Object: map[string]interface{}{
161+
"apiVersion": "v1",
162+
"kind": "ConfigMap",
163+
"metadata": map[string]interface{}{
164+
"name": "test",
165+
},
166+
},
167+
}
168+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
169+
if err != nil {
170+
t.Fatalf("Failed to create manifest: %v", err)
171+
}
172+
173+
if _, err := manifest.Transform(KubernetesMinVersionTransform()); err != nil {
174+
t.Fatalf("Transform returned error for unsupported kind: %v", err)
175+
}
176+
}
177+
178+
func TestKubernetesMinVersionTransformConversionError(t *testing.T) {
179+
t.Setenv(version.KubernetesMinVersionKey, "v1.25.0")
180+
181+
u := unstructured.Unstructured{
182+
Object: map[string]interface{}{
183+
"apiVersion": "apps/v1",
184+
"kind": "Deployment",
185+
"metadata": map[string]interface{}{
186+
"name": "bad-deploy",
187+
},
188+
"spec": map[string]interface{}{
189+
"template": map[string]interface{}{
190+
"spec": map[string]interface{}{
191+
"containers": "not-a-list",
192+
},
193+
},
194+
},
195+
},
196+
}
197+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
198+
if err != nil {
199+
t.Fatalf("Failed to create manifest: %v", err)
200+
}
201+
202+
if _, err := manifest.Transform(KubernetesMinVersionTransform()); err == nil {
203+
t.Fatalf("expected conversion error, got nil")
204+
}
205+
}
206+
207+
func hasEnv(envs []corev1.EnvVar, name, value string) bool {
208+
for _, env := range envs {
209+
if env.Name == name {
210+
return env.Value == value
211+
}
212+
}
213+
return false
214+
}
215+
216+
func podSpecFromResource(u unstructured.Unstructured) (corev1.PodSpec, error) {
217+
switch u.GetKind() {
218+
case "Deployment":
219+
var d appsv1.Deployment
220+
if err := scheme.Scheme.Convert(&u, &d, nil); err != nil {
221+
return corev1.PodSpec{}, err
222+
}
223+
return d.Spec.Template.Spec, nil
224+
case "StatefulSet":
225+
var s appsv1.StatefulSet
226+
if err := scheme.Scheme.Convert(&u, &s, nil); err != nil {
227+
return corev1.PodSpec{}, err
228+
}
229+
return s.Spec.Template.Spec, nil
230+
case "DaemonSet":
231+
var d appsv1.DaemonSet
232+
if err := scheme.Scheme.Convert(&u, &d, nil); err != nil {
233+
return corev1.PodSpec{}, err
234+
}
235+
return d.Spec.Template.Spec, nil
236+
case "Job":
237+
var j batchv1.Job
238+
if err := scheme.Scheme.Convert(&u, &j, nil); err != nil {
239+
return corev1.PodSpec{}, err
240+
}
241+
return j.Spec.Template.Spec, nil
242+
default:
243+
return corev1.PodSpec{}, fmt.Errorf("unsupported kind: %s", u.GetKind())
244+
}
245+
}

pkg/reconciler/common/transformers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func transformers(ctx context.Context, obj base.KComponent) []mf.Transformer {
3535
ImageTransform(obj.GetSpec().GetRegistry(), logger),
3636
JobTransform(obj),
3737
ConfigMapTransform(obj.GetSpec().GetConfig(), logger),
38+
KubernetesMinVersionTransform(),
3839
ResourceRequirementsTransform(obj, logger),
3940
OverridesTransform(obj.GetSpec().GetWorkloadOverrides(), logger),
4041
ServicesTransform(obj, logger),

0 commit comments

Comments
 (0)