Skip to content

Commit 9e7b4e0

Browse files
committed
feat: add EnvVarKeyDefaultsToName feature gate (KEP-5883)
When the EnvVarKeyDefaultsToName feature gate is enabled, the `key` field in `configMapKeyRef` and `secretKeyRef` may be omitted. The kubelet then defaults the lookup key to the enclosing `env[*].name` at runtime. Changes: - Register EnvVarKeyDefaultsToName feature gate (alpha, v1.35, default off) - Update validateConfigMapKeySelector/validateSecretKeySelector to allow an empty key when the gate is on, validating that env var name is also a valid ConfigMap/Secret key - Update makeEnvironmentVariables() in kubelet to default an empty key to env var name when the gate is on - Add unit tests for both the validation and kubelet resolution paths Ref: kubernetes#132195 KEP: kubernetes/enhancements#5955
1 parent 8cd57a9 commit 9e7b4e0

File tree

5 files changed

+393
-6
lines changed

5 files changed

+393
-6
lines changed

pkg/apis/core/validation/validation.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2790,11 +2790,11 @@ func validateEnvVarValueFrom(ev core.EnvVar, fldPath *field.Path, opts PodValida
27902790
}
27912791
if ev.ValueFrom.ConfigMapKeyRef != nil {
27922792
numSources++
2793-
allErrs = append(allErrs, validateConfigMapKeySelector(ev.ValueFrom.ConfigMapKeyRef, fldPath.Child("configMapKeyRef"))...)
2793+
allErrs = append(allErrs, validateConfigMapKeySelector(ev.ValueFrom.ConfigMapKeyRef, ev.Name, fldPath.Child("configMapKeyRef"))...)
27942794
}
27952795
if ev.ValueFrom.SecretKeyRef != nil {
27962796
numSources++
2797-
allErrs = append(allErrs, validateSecretKeySelector(ev.ValueFrom.SecretKeyRef, fldPath.Child("secretKeyRef"))...)
2797+
allErrs = append(allErrs, validateSecretKeySelector(ev.ValueFrom.SecretKeyRef, ev.Name, fldPath.Child("secretKeyRef"))...)
27982798
}
27992799

28002800
if ev.ValueFrom.FileKeyRef != nil {
@@ -2980,15 +2980,24 @@ func validateContainerResourceDivisor(rName string, divisor resource.Quantity, f
29802980
return allErrs
29812981
}
29822982

2983-
func validateConfigMapKeySelector(s *core.ConfigMapKeySelector, fldPath *field.Path) field.ErrorList {
2983+
func validateConfigMapKeySelector(s *core.ConfigMapKeySelector, envVarName string, fldPath *field.Path) field.ErrorList {
29842984
allErrs := field.ErrorList{}
29852985

29862986
nameFn := ValidateNameFunc(ValidateSecretName)
29872987
for _, msg := range nameFn(s.Name, false) {
29882988
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), s.Name, msg))
29892989
}
29902990
if len(s.Key) == 0 {
2991-
allErrs = append(allErrs, field.Required(fldPath.Child("key"), ""))
2991+
if utilfeature.DefaultFeatureGate.Enabled(features.EnvVarKeyDefaultsToName) {
2992+
// key is omitted; it will default to envVarName at runtime.
2993+
// Validate that the env var name is also a valid ConfigMap key.
2994+
for _, msg := range validation.IsConfigMapKey(envVarName) {
2995+
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), envVarName,
2996+
"may not be omitted: env var name is not a valid ConfigMap key: "+msg))
2997+
}
2998+
} else {
2999+
allErrs = append(allErrs, field.Required(fldPath.Child("key"), ""))
3000+
}
29923001
} else {
29933002
for _, msg := range validation.IsConfigMapKey(s.Key) {
29943003
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), s.Key, msg))
@@ -2998,15 +3007,24 @@ func validateConfigMapKeySelector(s *core.ConfigMapKeySelector, fldPath *field.P
29983007
return allErrs
29993008
}
30003009

3001-
func validateSecretKeySelector(s *core.SecretKeySelector, fldPath *field.Path) field.ErrorList {
3010+
func validateSecretKeySelector(s *core.SecretKeySelector, envVarName string, fldPath *field.Path) field.ErrorList {
30023011
allErrs := field.ErrorList{}
30033012

30043013
nameFn := ValidateNameFunc(ValidateSecretName)
30053014
for _, msg := range nameFn(s.Name, false) {
30063015
allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), s.Name, msg))
30073016
}
30083017
if len(s.Key) == 0 {
3009-
allErrs = append(allErrs, field.Required(fldPath.Child("key"), ""))
3018+
if utilfeature.DefaultFeatureGate.Enabled(features.EnvVarKeyDefaultsToName) {
3019+
// key is omitted; it will default to envVarName at runtime.
3020+
// Validate that the env var name is also a valid ConfigMap key.
3021+
for _, msg := range validation.IsConfigMapKey(envVarName) {
3022+
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), envVarName,
3023+
"may not be omitted: env var name is not a valid Secret key: "+msg))
3024+
}
3025+
} else {
3026+
allErrs = append(allErrs, field.Required(fldPath.Child("key"), ""))
3027+
}
30103028
} else {
30113029
for _, msg := range validation.IsConfigMapKey(s.Key) {
30123030
allErrs = append(allErrs, field.Invalid(fldPath.Child("key"), s.Key, msg))

pkg/apis/core/validation/validation_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29433,3 +29433,143 @@ func TestValidateContainerStateTransition(t *testing.T) {
2943329433
})
2943429434
}
2943529435
}
29436+
29437+
func TestValidateEnvVarKeyDefaultsToName(t *testing.T) {
29438+
testCases := []struct {
29439+
name string
29440+
featureEnabled bool
29441+
envVar core.EnvVar
29442+
expectError bool
29443+
errorField string
29444+
}{
29445+
{
29446+
name: "configMapKeyRef: empty key accepted when gate is on",
29447+
featureEnabled: true,
29448+
envVar: core.EnvVar{
29449+
Name: "LOG_LEVEL",
29450+
ValueFrom: &core.EnvVarSource{
29451+
ConfigMapKeyRef: &core.ConfigMapKeySelector{
29452+
LocalObjectReference: core.LocalObjectReference{Name: "app-config"},
29453+
Key: "",
29454+
},
29455+
},
29456+
},
29457+
expectError: false,
29458+
},
29459+
{
29460+
name: "secretKeyRef: empty key accepted when gate is on",
29461+
featureEnabled: true,
29462+
envVar: core.EnvVar{
29463+
Name: "DB_PASSWORD",
29464+
ValueFrom: &core.EnvVarSource{
29465+
SecretKeyRef: &core.SecretKeySelector{
29466+
LocalObjectReference: core.LocalObjectReference{Name: "db-secret"},
29467+
Key: "",
29468+
},
29469+
},
29470+
},
29471+
expectError: false,
29472+
},
29473+
{
29474+
name: "configMapKeyRef: empty key rejected when gate is off",
29475+
featureEnabled: false,
29476+
envVar: core.EnvVar{
29477+
Name: "LOG_LEVEL",
29478+
ValueFrom: &core.EnvVarSource{
29479+
ConfigMapKeyRef: &core.ConfigMapKeySelector{
29480+
LocalObjectReference: core.LocalObjectReference{Name: "app-config"},
29481+
Key: "",
29482+
},
29483+
},
29484+
},
29485+
expectError: true,
29486+
errorField: "valueFrom.configMapKeyRef.key",
29487+
},
29488+
{
29489+
name: "secretKeyRef: empty key rejected when gate is off",
29490+
featureEnabled: false,
29491+
envVar: core.EnvVar{
29492+
Name: "DB_PASSWORD",
29493+
ValueFrom: &core.EnvVarSource{
29494+
SecretKeyRef: &core.SecretKeySelector{
29495+
LocalObjectReference: core.LocalObjectReference{Name: "db-secret"},
29496+
Key: "",
29497+
},
29498+
},
29499+
},
29500+
expectError: true,
29501+
errorField: "valueFrom.secretKeyRef.key",
29502+
},
29503+
{
29504+
name: "configMapKeyRef: env var name that is invalid as a ConfigMap key is rejected when gate is on",
29505+
featureEnabled: true,
29506+
envVar: core.EnvVar{
29507+
Name: "INVALID=NAME",
29508+
ValueFrom: &core.EnvVarSource{
29509+
ConfigMapKeyRef: &core.ConfigMapKeySelector{
29510+
LocalObjectReference: core.LocalObjectReference{Name: "app-config"},
29511+
Key: "",
29512+
},
29513+
},
29514+
},
29515+
expectError: true,
29516+
errorField: "valueFrom.configMapKeyRef.key",
29517+
},
29518+
{
29519+
name: "explicit key is always accepted when gate is on",
29520+
featureEnabled: true,
29521+
envVar: core.EnvVar{
29522+
Name: "MY_VAR",
29523+
ValueFrom: &core.EnvVarSource{
29524+
ConfigMapKeyRef: &core.ConfigMapKeySelector{
29525+
LocalObjectReference: core.LocalObjectReference{Name: "app-config"},
29526+
Key: "some-key",
29527+
},
29528+
},
29529+
},
29530+
expectError: false,
29531+
},
29532+
{
29533+
name: "explicit key is always accepted when gate is off",
29534+
featureEnabled: false,
29535+
envVar: core.EnvVar{
29536+
Name: "MY_VAR",
29537+
ValueFrom: &core.EnvVarSource{
29538+
SecretKeyRef: &core.SecretKeySelector{
29539+
LocalObjectReference: core.LocalObjectReference{Name: "db-secret"},
29540+
Key: "explicit-key",
29541+
},
29542+
},
29543+
},
29544+
expectError: false,
29545+
},
29546+
}
29547+
29548+
for _, tc := range testCases {
29549+
t.Run(tc.name, func(t *testing.T) {
29550+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EnvVarKeyDefaultsToName, tc.featureEnabled)
29551+
29552+
opts := PodValidationOptions{}
29553+
errs := validateEnvVarValueFrom(tc.envVar, field.NewPath("valueFrom"), opts)
29554+
29555+
if tc.expectError && len(errs) == 0 {
29556+
t.Errorf("expected validation error for field %q, got none", tc.errorField)
29557+
}
29558+
if !tc.expectError && len(errs) > 0 {
29559+
t.Errorf("expected no validation errors, got: %v", errs)
29560+
}
29561+
if tc.expectError && tc.errorField != "" && len(errs) > 0 {
29562+
found := false
29563+
for _, e := range errs {
29564+
if e.Field == tc.errorField {
29565+
found = true
29566+
break
29567+
}
29568+
}
29569+
if !found {
29570+
t.Errorf("expected error on field %q, got errors on: %v", tc.errorField, errs)
29571+
}
29572+
}
29573+
})
29574+
}
29575+
}

pkg/features/kube_features.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,13 @@ const (
295295
// from the specified file in the emptyDir volume, without mounting the file.
296296
EnvFiles featuregate.Feature = "EnvFiles"
297297

298+
// owner: @dap0am
299+
// kep: http://kep.k8s.io/5883
300+
//
301+
// Allow the `key` field in `configMapKeyRef` and `secretKeyRef` to be
302+
// omitted, defaulting to the enclosing env var's `name` at runtime.
303+
EnvVarKeyDefaultsToName featuregate.Feature = "EnvVarKeyDefaultsToName"
304+
298305
// owner: @harche
299306
// kep: http://kep.k8s.io/3386
300307
//
@@ -1218,6 +1225,9 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
12181225
EnvFiles: {
12191226
{Version: version.MustParse("1.34"), Default: false, PreRelease: featuregate.Alpha},
12201227
},
1228+
EnvVarKeyDefaultsToName: {
1229+
{Version: version.MustParse("1.35"), Default: false, PreRelease: featuregate.Alpha},
1230+
},
12211231
EventedPLEG: {
12221232
{Version: version.MustParse("1.26"), Default: false, PreRelease: featuregate.Alpha},
12231233
},
@@ -2089,6 +2099,8 @@ var defaultKubernetesFeatureGateDependencies = map[featuregate.Feature][]feature
20892099

20902100
EnvFiles: {},
20912101

2102+
EnvVarKeyDefaultsToName: {},
2103+
20922104
EventedPLEG: {},
20932105

20942106
ExecProbeTimeout: {},

pkg/kubelet/kubelet_pods.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,9 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container
866866
cm := envVar.ValueFrom.ConfigMapKeyRef
867867
name := cm.Name
868868
key := cm.Key
869+
if key == "" && utilfeature.DefaultFeatureGate.Enabled(features.EnvVarKeyDefaultsToName) {
870+
key = envVar.Name
871+
}
869872
optional := cm.Optional != nil && *cm.Optional
870873
configMap, ok := configMaps[name]
871874
if !ok {
@@ -893,6 +896,9 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container
893896
s := envVar.ValueFrom.SecretKeyRef
894897
name := s.Name
895898
key := s.Key
899+
if key == "" && utilfeature.DefaultFeatureGate.Enabled(features.EnvVarKeyDefaultsToName) {
900+
key = envVar.Name
901+
}
896902
optional := s.Optional != nil && *s.Optional
897903
secret, ok := secrets[name]
898904
if !ok {

0 commit comments

Comments
 (0)