diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go
index d701b98a7c4..5b31f00f460 100644
--- a/pkg/features/kube_features.go
+++ b/pkg/features/kube_features.go
@@ -1242,6 +1242,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
},
EnvFiles: {
{Version: version.MustParse("1.34"), Default: false, PreRelease: featuregate.Alpha},
+ {Version: version.MustParse("1.35"), Default: true, PreRelease: featuregate.Beta},
},
EventedPLEG: {
{Version: version.MustParse("1.26"), Default: false, PreRelease: featuregate.Alpha},
diff --git a/pkg/kubelet/kubelet_pods.go b/pkg/kubelet/kubelet_pods.go
index 67791e17109..e95202d1506 100644
--- a/pkg/kubelet/kubelet_pods.go
+++ b/pkg/kubelet/kubelet_pods.go
@@ -932,12 +932,6 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container
return result, fmt.Errorf("failed to get host path for volume %q: %w", volume, err)
}
- // Validate key length, must not exceed 128 characters.
- // TODO: @HirazawaUi This limit will be relaxed after the EnvFiles feature gate beta stage.
- if len(key) > 128 {
- return result, fmt.Errorf("environment variable key %q exceeds maximum length of 128 characters", key)
- }
-
// Construct the full path to the environment variable file
// by combining hostPath with the specified path in FileKeyRef
envFilePath, err := securejoin.SecureJoin(hostPath, f.Path)
@@ -951,12 +945,6 @@ func (kl *Kubelet) makeEnvironmentVariables(pod *v1.Pod, container *v1.Container
return result, fmt.Errorf("couldn't parse env file")
}
- // Validate value size, must not exceed 32KB.
- // TODO: @HirazawaUi This limit will be relaxed after the EnvFiles feature gate beta stage.
- if len(runtimeVal) > 32*1024 {
- return result, fmt.Errorf("environment variable value for key %q exceeds maximum size of 32KB", key)
- }
-
// If the key was not found, and it's not optional, return an error
if runtimeVal == "" {
if optional {
diff --git a/pkg/kubelet/kubelet_pods_test.go b/pkg/kubelet/kubelet_pods_test.go
index 08bf95fc002..1b6fb9aa2f6 100644
--- a/pkg/kubelet/kubelet_pods_test.go
+++ b/pkg/kubelet/kubelet_pods_test.go
@@ -7023,7 +7023,7 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
{Name: "DATABASE", Value: "mydb"},
},
setupFiles: func() []string {
- content := "DATABASE=mydb\nAPI_KEY=secret123\n"
+ content := "DATABASE='mydb'\nAPI_KEY='secret123'\n"
return []string{createEnvFile("database.env", content)}
},
},
@@ -7052,7 +7052,7 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
{Name: "API_KEY", Value: "secret123"},
},
setupFiles: func() []string {
- content := "# This is a comment\n\nDATABASE=mydb\nAPI_KEY=secret123\n\n# Another comment\n"
+ content := "# This is a comment\n\nDATABASE='mydb'\nAPI_KEY='secret123'\n\n# Another comment\n"
return []string{createEnvFile("config.env", content)}
},
},
@@ -7080,8 +7080,10 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
expectedEnvs: []kubecontainer.EnvVar{
{Name: "DEBUG_MODE", Value: "true"},
},
+ expectedError: true,
+ errorContains: "couldn't parse env file",
setupFiles: func() []string {
- content := "DEBUG_MODE = true\nLOG_LEVEL = info\n"
+ content := "DEBUG_MODE = 'true'\nLOG_LEVEL = 'info'\n"
return []string{createEnvFile("debug.env", content)}
},
},
@@ -7109,7 +7111,7 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
expectedError: true,
errorContains: "environment variable key \"MISSING_KEY\" not found in file",
setupFiles: func() []string {
- content := "EXISTING_KEY=value\n"
+ content := "EXISTING_KEY='value'\n"
return []string{createEnvFile("config.env", content)}
},
},
@@ -7137,7 +7139,7 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
},
expectedEnvs: nil,
setupFiles: func() []string {
- content := "EXISTING_KEY=value\n"
+ content := "EXISTING_KEY='value'\n"
return []string{createEnvFile("config.env", content)}
},
},
@@ -7168,63 +7170,6 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
return []string{} // No files created
},
},
- {
- name: "key length exceeds 128 characters",
- container: &v1.Container{
- Env: []v1.EnvVar{
- {
- Name: "LONG_KEY",
- ValueFrom: &v1.EnvVarSource{
- FileKeyRef: &v1.FileKeySelector{
- VolumeName: "config-volume",
- Path: "config.env",
- Key: strings.Repeat("A", 129),
- },
- },
- },
- },
- },
- podVolumes: map[string]kubecontainer.VolumeInfo{
- "config-volume": {
- Mounter: &testVolumeMounter{path: tmpDir},
- },
- },
- expectedError: true,
- errorContains: "exceeds maximum length of 128 characters",
- setupFiles: func() []string {
- content := "EXISTING_KEY=value\n"
- return []string{createEnvFile("config.env", content)}
- },
- },
- {
- name: "value size exceeds 32KB",
- container: &v1.Container{
- Env: []v1.EnvVar{
- {
- Name: "LARGE_VALUE",
- ValueFrom: &v1.EnvVarSource{
- FileKeyRef: &v1.FileKeySelector{
- VolumeName: "config-volume",
- Path: "large.env",
- Key: "LARGE_VALUE",
- },
- },
- },
- },
- },
- podVolumes: map[string]kubecontainer.VolumeInfo{
- "config-volume": {
- Mounter: &testVolumeMounter{path: tmpDir},
- },
- },
- expectedError: true,
- errorContains: "environment variable value for key \"LARGE_VALUE\" exceeds maximum size of 32KB",
- setupFiles: func() []string {
- largeValue := strings.Repeat("A", 33*1024) // 33KB
- content := fmt.Sprintf("LARGE_VALUE=%s\n", largeValue)
- return []string{createEnvFile("large.env", content)}
- },
- },
{
name: "volume not found",
container: &v1.Container{
@@ -7315,8 +7260,8 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
{Name: "API_KEY", Value: "secret123"},
},
setupFiles: func() []string {
- dbContent := "DATABASE=mydb\n"
- apiContent := "API_KEY=secret123\n"
+ dbContent := "DATABASE='mydb'\n"
+ apiContent := "API_KEY='secret123'\n"
return []string{
createEnvFile("database.env", dbContent),
createEnvFile("api.env", apiContent),
@@ -7353,7 +7298,7 @@ func TestMakeEnvironmentVariablesWithFileKeyRef(t *testing.T) {
{Name: "FILE_VAR", Value: "file_value"},
},
setupFiles: func() []string {
- content := "FILE_VAR=file_value\n"
+ content := "FILE_VAR='file_value'\n"
return []string{createEnvFile("config.env", content)}
},
},
diff --git a/pkg/kubelet/util/env/env_util.go b/pkg/kubelet/util/env/env_util.go
index c01d90687e3..1de9317377b 100644
--- a/pkg/kubelet/util/env/env_util.go
+++ b/pkg/kubelet/util/env/env_util.go
@@ -23,16 +23,19 @@ import (
"strings"
)
-// ParseEnv implements a strict parser for .env environment files,
-// adhering to the format defined in the RFC documentation at https://smartmob-rfc.readthedocs.io/en/latest/2-dotenv.html.
+// ParseEnv implements a strict parser for environment files using a subset of POSIX shell syntax.
+// The parser enforces that all variable values must be enclosed in single quotes.
//
-// This function implements a strict parser for environment files similar to the requirements in the OCI and Docker env file RFCs:
-// - Leading whitespace is ignored for all lines.
-// - Blank lines (including those with only whitespace) are ignored.
-// - Lines starting with '#' are treated as comments and ignored.
-// - Each variable must be declared as VAR=VAL. Whitespace around '=' and at the end of the line is ignored.
-// - A backslash ('\') at the end of a variable declaration line indicates the value continues on the next line. The lines are joined with a single space, and the backslash is not included.
-// - If a continuation line is interrupted by a blank line or comment, it is considered invalid and an error is returned.
+// Supported format:
+// - VAR='value' - Values must be enclosed in single quotes
+// - Content within single quotes is preserved literally (no escape sequences or expansions)
+// - Multi-line values are supported (newlines within single quotes are preserved)
+// - Inline comments after the closing quote are supported (e.g., VAR='value' # comment)
+// - Leading whitespace before the variable name is ignored
+// - Blank lines (including those with only whitespace) are ignored when not within quotes
+// - Lines starting with '#' are treated as comments and ignored
+// - Whitespace before '=' is invalid (e.g., VAR = 'value' is rejected)
+// - Whitespace after '=' but before the quote results in empty assignment (e.g., VAR= 'value' assigns empty string)
func ParseEnv(envFilePath, key string) (string, error) {
file, err := os.Open(envFilePath)
if err != nil {
@@ -41,67 +44,85 @@ func ParseEnv(envFilePath, key string) (string, error) {
defer func() { _ = file.Close() }()
scanner := bufio.NewScanner(file)
- var (
- currentLine string
- inContinuation bool
- lineNum int
- )
+ lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
line = strings.TrimLeft(line, " \t")
+ // Skip blank lines
if line == "" {
- if inContinuation {
- return "", fmt.Errorf("invalid environment variable format at line %d: blank line in continuation", lineNum)
- }
- continue
- }
- if strings.HasPrefix(line, "#") {
- if inContinuation {
- return "", fmt.Errorf("invalid environment variable format at line %d: comment in continuation", lineNum)
- }
continue
}
- if inContinuation {
- trimmed := strings.TrimRight(line, " \t")
- if strings.HasSuffix(trimmed, "\\") {
- currentLine += " " + strings.TrimRight(trimmed[:len(trimmed)-1], " \t")
- continue
- } else {
- currentLine += " " + trimmed
- line = currentLine
- inContinuation = false
- currentLine = ""
- }
- } else {
- trimmed := strings.TrimRight(line, " \t")
- if strings.HasSuffix(trimmed, "\\") {
- currentLine = strings.TrimRight(trimmed[:len(trimmed)-1], " \t")
- inContinuation = true
- continue
- }
+ // Skip comments
+ if strings.HasPrefix(line, "#") {
+ continue
}
eqIdx := strings.Index(line, "=")
if eqIdx == -1 {
- return "", fmt.Errorf("invalid environment variable format at line %d", lineNum)
+ return "", fmt.Errorf("invalid environment variable format at line %d: missing '='", lineNum)
}
- varName := strings.TrimSpace(line[:eqIdx])
- varValue := strings.TrimRight(strings.TrimSpace(line[eqIdx+1:]), " \t")
+ // Variable name must not contain whitespace or trailing whitespace before '='
+ varNamePart := line[:eqIdx]
+ varName := strings.TrimRight(varNamePart, " \t")
if varName == "" {
- return "", fmt.Errorf("invalid environment variable format at line %d", lineNum)
+ return "", fmt.Errorf("invalid environment variable format at line %d: empty variable name", lineNum)
}
- if varName == key {
- return varValue, nil
- }
- }
- if inContinuation {
- return "", fmt.Errorf("unexpected end of file: unfinished line continuation")
+ // If trimming removed whitespace, it means there was whitespace before '='
+ if varNamePart != varName {
+ return "", fmt.Errorf("invalid environment variable format at line %d: whitespace before '=' is not allowed", lineNum)
+ }
+ valuePart := line[eqIdx+1:]
+
+ // Check if there's whitespace before any non-whitespace character
+ trimmedValue := strings.TrimLeft(valuePart, " \t")
+ if valuePart != trimmedValue {
+ // There is whitespace between '=' and the value
+ // This matches bash behavior: KEY= 'val1' results in KEY being empty
+ if varName == key {
+ return "", nil
+ }
+ continue
+ }
+
+ // Value must start with single quote
+ if !strings.HasPrefix(trimmedValue, "'") {
+ return "", fmt.Errorf("invalid environment variable format at line %d: value must be enclosed in single quotes", lineNum)
+ }
+
+ // Find the closing single quote (may span multiple lines)
+ var valueBuilder strings.Builder
+ rest := trimmedValue[1:]
+ startLineNum := lineNum
+
+ for {
+ closingIdx := strings.Index(rest, "'")
+ if closingIdx != -1 {
+ valueBuilder.WriteString(rest[:closingIdx])
+ afterQuote := strings.TrimLeft(rest[closingIdx+1:], " \t")
+ if afterQuote != "" && !strings.HasPrefix(afterQuote, "#") {
+ return "", fmt.Errorf("invalid environment variable format at line %d: unexpected content after closing quote", lineNum)
+ }
+
+ if varName == key {
+ return valueBuilder.String(), nil
+ }
+ break
+ }
+
+ valueBuilder.WriteString(rest)
+ valueBuilder.WriteString("\n")
+ if !scanner.Scan() {
+ return "", fmt.Errorf("invalid environment variable format starting at line %d: unclosed single quote", startLineNum)
+ }
+ lineNum++
+ rest = scanner.Text()
+ }
}
if err := scanner.Err(); err != nil {
diff --git a/pkg/kubelet/util/env/env_util_test.go b/pkg/kubelet/util/env/env_util_test.go
index 0b570e5e9d4..5a542d5d328 100644
--- a/pkg/kubelet/util/env/env_util_test.go
+++ b/pkg/kubelet/util/env/env_util_test.go
@@ -18,10 +18,42 @@ package env
import (
"os"
+ "os/exec"
+ "runtime"
"strings"
"testing"
)
+// testShellBehavior tests how a POSIX shell would interpret the given environment file content
+// and returns the value that would be assigned to the given key
+// This test will not run on Windows.
+func testShellBehavior(t *testing.T, envContent, key string) (string, error) {
+ tmpFile, err := os.CreateTemp("", "shell_test_*")
+ if err != nil {
+ t.Fatalf("failed to create temp file: %v", err)
+ }
+ defer func() { _ = os.Remove(tmpFile.Name()) }()
+
+ if _, err := tmpFile.Write([]byte(envContent)); err != nil {
+ t.Fatalf("failed to write to temp file: %v", err)
+ }
+ if err := tmpFile.Close(); err != nil {
+ t.Fatalf("failed to close temp file: %v", err)
+ }
+
+ // Use bash with POSIX mode to source the file and print the variable
+ // We use . instead of source for POSIX compliance
+ cmd := exec.Command("bash", "--posix", "-c",
+ "set -a && . \""+tmpFile.Name()+"\" && printf '%s' \"${"+key+"}\"")
+
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", err
+ }
+
+ return string(output), nil
+}
+
func TestParseEnv(t *testing.T) {
tempDir := t.TempDir()
@@ -36,245 +68,498 @@ func TestParseEnv(t *testing.T) {
tests := []testCase{
{
name: "ignore leading whitespace",
- envContent: ` KEY1=val1
- KEY2=val2
-KEY3=val3
+ envContent: ` KEY1='val1'
+ KEY2='val2'
+KEY3='val3'
`,
key: "KEY2",
- wantValue: "val2",
+ wantValue: `val2`,
},
{
name: "ignore blank and comment lines",
envContent: `# comment
-KEY1=foo
+KEY1='foo'
# another comment
-
-KEY2=bar
+
+KEY2='bar'
`,
key: "KEY2",
- wantValue: "bar",
+ wantValue: `bar`,
},
{
- name: "whitespace around = and trailing",
- envContent: `KEY1 = val1
-KEY2= val2
-KEY3=val3
-`,
- key: "KEY2",
- wantValue: "val2",
- },
- {
- name: "continuation line with \\",
- envContent: `KEY1=foo \
-bar \
-baz
-KEY2=val2
-`,
- key: "KEY1",
- wantValue: "foo bar baz",
- },
- {
- name: "continuation with whitespace and comment",
- envContent: `KEY1=foo \
- bar
-# comment
-KEY2=val2
-`,
- key: "KEY1",
- wantValue: "foo bar",
- },
- {
- name: "invalid line triggers error with line number",
- envContent: `KEY1=foo
-INVALID_LINE
-KEY2=bar
+ name: "whitespace around = and not allowed",
+ envContent: `KEY1 = 'val1'
+KEY2= 'val2'
+KEY3='val3'
`,
key: "KEY2",
wantErr: true,
- errContains: "at line 2",
- },
- {
- name: "unfinished continuation triggers error",
- envContent: `KEY1=foo \
-bar \
-`,
- key: "KEY1",
- wantErr: true,
- errContains: "unfinished line continuation",
+ errContains: `whitespace before '=' is not allowed`,
},
{
name: "key not found returns empty",
- envContent: `KEY1=foo
-KEY2=bar
+ envContent: `KEY1='foo'
+KEY2='bar'
`,
key: "KEY3",
- wantValue: "",
+ wantValue: ``,
},
{
name: "value with embedded #",
- envContent: `KEY1=foo#notcomment
-KEY2=bar
+ envContent: `KEY1='foo#notcomment'
+KEY2='bar'
`,
key: "KEY1",
- wantValue: "foo#notcomment",
+ wantValue: `foo#notcomment`,
},
{
- name: "key with trailing whitespace",
- envContent: `KEY1 =foo
-KEY2=bar
+ name: "key with whitespace (should error)",
+ envContent: `KEY1 ='foo'
+KEY2='bar'
`,
- key: "KEY1",
- wantValue: "foo",
+ key: "KEY1",
+ wantErr: true,
+ errContains: "whitespace before '=' is not allowed",
},
{
name: "value with leading and trailing whitespace",
- envContent: `KEY1= foo bar
-KEY2=bar
+ envContent: `KEY1=' foo bar '
+KEY2='bar'
`,
key: "KEY1",
- wantValue: "foo bar",
+ wantValue: ` foo bar `,
},
{
name: "multiple comments and blank lines",
envContent: `# first comment
# second comment
-KEY1=foo
+KEY1='foo'
# third comment
-KEY2=bar
+KEY2='bar'
`,
key: "KEY2",
- wantValue: "bar",
- },
- {
- name: "continuation with blank line in between (should error)",
- envContent: `KEY1=foo \
-
-bar
-`,
- key: "KEY1",
- wantErr: true,
- errContains: "invalid environment variable format",
- },
- {
- name: "continuation with comment in between (should error)",
- envContent: `KEY1=foo \
-# comment
-bar
-`,
- key: "KEY1",
- wantErr: true,
- errContains: "invalid environment variable format",
+ wantValue: `bar`,
},
{
name: "empty value",
- envContent: `KEY1=foo
-KEY2=
-KEY3=bar
+ envContent: `KEY1='foo'
+KEY2=''
+KEY3='bar'
`,
key: "KEY2",
- wantValue: "",
- },
- {
- name: "value with only spaces",
- envContent: `KEY1=foo
-KEY2=
-KEY3=bar
-`,
- key: "KEY2",
- wantValue: "",
+ wantValue: ``,
},
{
name: "key is empty (should error)",
- envContent: `=foo
-KEY2=bar
+ envContent: `='foo'
+KEY2='bar'
`,
key: "KEY2",
- wantValue: "bar",
wantErr: true,
errContains: "invalid environment variable format",
},
{
name: "multiple = in value",
- envContent: `KEY1=foo=bar=baz
-KEY2=bar
+ envContent: `KEY1='foo=bar=baz'
+KEY2='bar'
`,
key: "KEY1",
- wantValue: "foo=bar=baz",
- },
- {
- name: "continuation with trailing spaces",
- envContent: `KEY1=foo \
- bar \
- baz
-KEY2=bar
-`,
- key: "KEY1",
- wantValue: "foo bar baz",
+ wantValue: `foo=bar=baz`,
},
{
name: "comment after key value pair",
- envContent: `KEY1=foo
-KEY2=bar # comment
+ envContent: `KEY1='foo'
+KEY2='bar' # comment
`,
key: "KEY2",
- wantValue: "bar # comment",
- },
- {
- name: "blank line in continuation triggers error with line number",
- envContent: `KEY1=foo \
-
-bar=val`,
- key: "bar",
- wantErr: true,
- errContains: "at line 2",
- },
- {
- name: "comment in continuation triggers error with line number",
- envContent: `KEY1=foo \
-# comment
-bar=val`,
- key: "bar",
- wantErr: true,
- errContains: "at line 2",
+ wantValue: `bar`,
},
{
name: "missing key triggers error with line number",
- envContent: `=foo
-KEY2=bar`,
+ envContent: `='foo'
+KEY2='bar'`,
key: "KEY2",
wantErr: true,
errContains: "at line 1",
},
{
name: "value contains $VAR, should not expand",
- envContent: `KEY1=$VAR
-KEY2=bar`,
+ envContent: `KEY1='$VAR'
+KEY2='bar'`,
key: "KEY1",
- wantValue: "$VAR",
+ wantValue: `$VAR`,
},
{
name: "value contains ${VAR}, should not expand",
- envContent: `KEY1=${VAR}
-KEY2=bar`,
+ envContent: `KEY1='${VAR}'
+KEY2='bar'`,
key: "KEY1",
- wantValue: "${VAR}",
+ wantValue: `${VAR}`,
},
{
name: "value contains $HOME, should not expand",
- envContent: `KEY1=$HOME
-KEY2=bar`,
+ envContent: `KEY1='$HOME'
+KEY2='bar'`,
key: "KEY1",
- wantValue: "$HOME",
+ wantValue: `$HOME`,
},
{
name: "value contains mixed shell variable syntax, should not expand",
- envContent: `KEY1=foo$BAR-${HOME}_$
-KEY2=bar`,
+ envContent: `KEY1='foo$BAR-${HOME}_$'
+KEY2='bar'`,
key: "KEY1",
- wantValue: "foo$BAR-${HOME}_$",
+ wantValue: `foo$BAR-${HOME}_$`,
+ },
+ {
+ name: "invalid line triggers error with line number",
+ envContent: `KEY1='foo'
+INVALID_LINE
+KEY2='bar'
+`,
+ key: "KEY2",
+ wantErr: true,
+ errContains: "at line 2",
+ },
+ {
+ name: "unquoted value triggers error",
+ envContent: `KEY1='foo'
+KEY2=bar
+KEY3='baz'
+`,
+ key: "KEY2",
+ wantErr: true,
+ errContains: "value must be enclosed in single quotes",
+ },
+ {
+ name: "double quoted value triggers error",
+ envContent: `KEY1='foo'
+KEY2="bar"
+KEY3='baz'
+`,
+ key: "KEY2",
+ wantErr: true,
+ errContains: "value must be enclosed in single quotes",
+ },
+ {
+ name: "value with multiple adjacent quoted strings",
+ envContent: `KEY1='foo''bar'`,
+ key: "KEY1",
+ wantErr: true,
+ errContains: `unexpected content after closing quote`,
+ },
+ {
+ name: "unclosed single quote triggers error",
+ envContent: `KEY1='foo'
+KEY2='bar
+KEY3='baz'
+`,
+ key: "KEY2",
+ wantErr: true,
+ errContains: "unexpected content after closing quote",
+ },
+ {
+ name: "content after closing quote triggers error",
+ envContent: `KEY1='foo'
+KEY2='bar'baz
+KEY3='baz'
+`,
+ key: "KEY2",
+ wantErr: true,
+ errContains: "unexpected content after closing quote",
+ },
+ {
+ name: "empty quotes preserve literal content",
+ envContent: `KEY1=''
+KEY2=' '
+KEY3='bar'
+`,
+ key: "KEY1",
+ wantValue: ``,
+ },
+ {
+ name: "quotes preserve all literal content including spaces",
+ envContent: `KEY1=' foo bar '
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: ` foo bar `,
+ },
+ {
+ name: "special characters in value",
+ envContent: `KEY1='!@#$%^&*()_+-=[]{}|;:,.<>?/'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `!@#$%^&*()_+-=[]{}|;:,.<>?/`,
+ },
+ {
+ name: "newlines and tabs in value",
+ envContent: `KEY1='line1\nline2\tline3'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `line1\nline2\tline3`,
+ },
+ {
+ name: "unicode characters in value",
+ envContent: `KEY1='中文 Español Français 🌍'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `中文 Español Français 🌍`,
+ },
+ {
+ name: "backslashes in value",
+ envContent: `KEY1='path\\to\\file'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `path\\to\\file`,
+ },
+ {
+ name: "single quotes within value quotes",
+ envContent: `KEY1='value with \'nested\' quotes'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantErr: true,
+ errContains: "unexpected content after closing quote",
+ },
+ {
+ name: "double quotes within value quotes",
+ envContent: `KEY1='value with "nested" quotes'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantErr: false,
+ wantValue: `value with "nested" quotes`,
+ },
+ {
+ name: "empty single quotes",
+ envContent: `KEY1=''
+KEY2=''
+KEY3='bar'
+`,
+ key: "KEY2",
+ wantValue: ``,
+ },
+ {
+ name: "only whitespace in quotes",
+ envContent: `KEY1=' '
+KEY2='\t\n'
+KEY3='bar'
+`,
+ key: "KEY1",
+ wantValue: " ",
+ },
+ {
+ name: "complex JSON-like value",
+ envContent: `KEY1='{"name": "test", "value": 123, "nested": {"key": "val"}}'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `{"name": "test", "value": 123, "nested": {"key": "val"}}`,
+ },
+ {
+ name: "URL with special characters",
+ envContent: `KEY1='https://example.com/path?query=value&another=param'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `https://example.com/path?query=value&another=param`,
+ },
+ {
+ name: "XML-like content",
+ envContent: `KEY1='content'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `content`,
+ },
+ {
+ name: "base64 encoded data",
+ envContent: `KEY1='SGVsbG8gV29ybGQh'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `SGVsbG8gV29ybGQh`,
+ },
+ {
+ name: "regex pattern",
+ envContent: `KEY1='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$`,
+ },
+ {
+ name: "multi-line value with spaces",
+ envContent: `KEY1='line one
+ line two with spaces
+ line three with more spaces'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `line one
+ line two with spaces
+ line three with more spaces`,
+ },
+ {
+ name: "multi-line value with special characters",
+ envContent: `KEY1='line1: !@#$%^&*()
+line2: []{}|;:,.<>?/
+line3: \\\\tabs\\nnewlines'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `line1: !@#$%^&*()
+line2: []{}|;:,.<>?/
+line3: \\\\tabs\\nnewlines`,
+ },
+ {
+ name: "multi-line value with mixed content",
+ envContent: `KEY1='First line
+Second line with $VAR and ${HOME}
+Third line with spaces and tabs\\t
+Fourth line with special chars: !@#$%^&*()_+'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `First line
+Second line with $VAR and ${HOME}
+Third line with spaces and tabs\\t
+Fourth line with special chars: !@#$%^&*()_+`,
+ },
+ {
+ name: "multi-line value with empty lines",
+ envContent: `KEY1='line1
+
+line3 after empty line
+
+line5 after another empty'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `line1
+
+line3 after empty line
+
+line5 after another empty`,
+ },
+ {
+ name: "multi-line value with trailing spaces",
+ envContent: `KEY1='line1
+line2 with trailing spaces
+line3 '
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `line1
+line2 with trailing spaces
+line3 `,
+ },
+ {
+ name: "multi-line value with unicode characters",
+ envContent: `KEY1='中文 第一行
+English second line
+Français troisième ligne 🌍
+Русский четвертая строка'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `中文 第一行
+English second line
+Français troisième ligne 🌍
+Русский четвертая строка`,
+ },
+ {
+ name: "multi-line value with code-like content",
+ envContent: `KEY1='func main() {
+ fmt.Println(\"Hello, World!\")
+ for i := 0; i < 10; i++ {
+ fmt.Printf(\"i=%d\\n\", i)
+ }
+}'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `func main() {
+ fmt.Println(\"Hello, World!\")
+ for i := 0; i < 10; i++ {
+ fmt.Printf(\"i=%d\\n\", i)
+ }
+}`,
+ },
+ {
+ name: "multi-line value with JSON content",
+ envContent: `KEY1='{
+ "name": "test",
+ "value": 123,
+ "nested": {
+ "key": "val",
+ "array": [1, 2, 3]
+ }
+}'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `{
+ "name": "test",
+ "value": 123,
+ "nested": {
+ "key": "val",
+ "array": [1, 2, 3]
+ }
+}`,
+ },
+ {
+ name: "multi-line value with YAML content",
+ envContent: `KEY1='name: test
+value: 123
+nested:
+ key: val
+ array:
+ - 1
+ - 2
+ - 3'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `name: test
+value: 123
+nested:
+ key: val
+ array:
+ - 1
+ - 2
+ - 3`,
+ },
+ {
+ name: "multi-line value with Cert",
+ envContent: `KEY1='-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQSSSSSSSSSSSSSBlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAYEAyUgx65kBQf/Nl+EcOFkP8q7n7FDetNrH7WYAnpYnaWG8w4rA9ht9
+upttTNyRbCVpb32f5m8DFvVug0vAa/ksX8iSKzD53bWTuTQiN/i9Q/iG0eCNPR0KoW/gLV
+SS9pIWWVYNav6dBjknR85202+YryJkn8THAf8Kg5Lwl0dom41dZvN1DirbcqWOU1BKG2sl
+pFIq8BdXedRjwoNYngMvLBOb4CAfvQyKr1+ARQecqzPn4pTovYtIZRasIgrBOpSpHGZa70
+pTAVP5+KGXN5DigjQUWaje1AiEx5zk8J3T5AA0abSaNn0uE53tjgalTzcY9kxHdo2rgHJC
+huHhWsWslkcntkKp0V1Jc8oGv86Dp5mPhpfpMOK+vCe2TrS/saes9fNVxjorSpLl4xTU/V
+-----END OPENSSH PRIVATE KEY-----'
+KEY2='bar'
+`,
+ key: "KEY1",
+ wantValue: `-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQSSSSSSSSSSSSSBlwAAAAdzc2gtcn
+NhAAAAAwEAAQAAAYEAyUgx65kBQf/Nl+EcOFkP8q7n7FDetNrH7WYAnpYnaWG8w4rA9ht9
+upttTNyRbCVpb32f5m8DFvVug0vAa/ksX8iSKzD53bWTuTQiN/i9Q/iG0eCNPR0KoW/gLV
+SS9pIWWVYNav6dBjknR85202+YryJkn8THAf8Kg5Lwl0dom41dZvN1DirbcqWOU1BKG2sl
+pFIq8BdXedRjwoNYngMvLBOb4CAfvQyKr1+ARQecqzPn4pTovYtIZRasIgrBOpSpHGZa70
+pTAVP5+KGXN5DigjQUWaje1AiEx5zk8J3T5AA0abSaNn0uE53tjgalTzcY9kxHdo2rgHJC
+huHhWsWslkcntkKp0V1Jc8oGv86Dp5mPhpfpMOK+vCe2TrS/saes9fNVxjorSpLl4xTU/V
+-----END OPENSSH PRIVATE KEY-----`,
},
}
@@ -310,6 +595,18 @@ KEY2=bar`,
if gotValue != tt.wantValue {
t.Errorf("got %q, want %q", gotValue, tt.wantValue)
}
+
+ // Verify shell behavior matches our parser
+ if runtime.GOOS != "windows" {
+ shellValue, shellErr := testShellBehavior(t, tt.envContent, tt.key)
+ if shellErr != nil {
+ t.Errorf("shell failed to parse valid syntax: %v", shellErr)
+ return
+ }
+ if gotValue != shellValue {
+ t.Errorf("shell behavior mismatch: ParseEnv=%q, shell=%q", gotValue, shellValue)
+ }
+ }
})
}
}
diff --git a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
index 9cda31d3f82..a532cec975f 100644
--- a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
+++ b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml
@@ -569,6 +569,10 @@
lockToDefault: false
preRelease: Alpha
version: "1.34"
+ - default: true
+ lockToDefault: false
+ preRelease: Beta
+ version: "1.35"
- name: EventedPLEG
versionedSpecs:
- default: false
diff --git a/test/e2e/common/node/file_key.go b/test/e2e/common/node/file_key.go
index d575d8bc7e4..5aed89c53a2 100644
--- a/test/e2e/common/node/file_key.go
+++ b/test/e2e/common/node/file_key.go
@@ -61,7 +61,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'CONFIG_1=value1' > /data/config.env && echo 'CONFIG_2=value2' >> /data/config.env"},
+ Command: []string{"sh", "-c", `echo CONFIG_1=\'value1\' > /data/config.env && echo CONFIG_2=\'value2\' >> /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -136,7 +136,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'CONFIG_1=value1' > /data/config.env && echo 'CONFIG_2=value2' >> /data/config.env && echo 'CONFIG_3=value3' >> /data/config.env"},
+ Command: []string{"sh", "-c", `echo CONFIG_1=\'value1\' > /data/config.env && echo CONFIG_2=\'value2\' >> /data/config.env && echo CONFIG_3=\'value3\' >> /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -239,7 +239,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'EXISTING_KEY=existing_value' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo EXISTING_KEY=\'existing_value\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -313,7 +313,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'HOOK_CONFIG=hook_value' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo HOOK_CONFIG=\'hook_value\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -394,7 +394,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'HOOK_CONFIG=hook_value' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo HOOK_CONFIG=\'hook_value\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -475,7 +475,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'CONFIG_1=value1' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo CONFIG_1=\'value1\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -545,7 +545,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'CONFIG_1=value1' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo CONFIG_1=\'value1\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -607,7 +607,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'EXISTING_KEY=value' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo EXISTING_KEY=\'value\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -679,7 +679,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'EXISTING_KEY=value' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo EXISTING_KEY=\'value\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{
{
Name: "config",
@@ -749,7 +749,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'CONFIG_1=value1' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo CONFIG_1=\'value1\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{{Name: "config", MountPath: "/data"}},
},
{
@@ -807,7 +807,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'CONFIG_INIT=fail' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo CONFIG_INIT=\'fail\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{{Name: "config", MountPath: "/data"}},
},
{
@@ -856,7 +856,7 @@ var _ = SIGDescribe("FileKeyRef", framework.WithFeatureGate(features.EnvFiles),
InitContainers: []v1.Container{{
Name: "setup-envfile",
Image: imageutils.GetE2EImage(imageutils.BusyBox),
- Command: []string{"sh", "-c", "echo 'CONFIG_EPH=ephemeral' > /data/config.env"},
+ Command: []string{"sh", "-c", `echo CONFIG_EPH=\'ephemeral\' > /data/config.env`},
VolumeMounts: []v1.VolumeMount{{Name: "config", MountPath: "/data"}},
}},
Containers: []v1.Container{{
diff --git a/test/e2e_node/mirror_pod_test.go b/test/e2e_node/mirror_pod_test.go
index b5cec71d47b..01f1a2fedaf 100644
--- a/test/e2e_node/mirror_pod_test.go
+++ b/test/e2e_node/mirror_pod_test.go
@@ -32,8 +32,10 @@ import (
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apimachinery/pkg/util/wait"
clientset "k8s.io/client-go/kubernetes"
+ "k8s.io/kubernetes/pkg/features"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
"k8s.io/kubernetes/test/e2e/framework"
+ e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
imageutils "k8s.io/kubernetes/test/utils/image"
admissionapi "k8s.io/pod-security-admission/api"
"sigs.k8s.io/yaml"
@@ -43,7 +45,6 @@ import (
"github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/cli-runtime/pkg/printers"
- e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
e2evolume "k8s.io/kubernetes/test/e2e/framework/volume"
)
@@ -384,6 +385,103 @@ var _ = SIGDescribe("MirrorPod (Pod Generation)", func() {
})
})
+var _ = SIGDescribe("MirrorPod with EnvFiles", framework.WithFeatureGate(features.EnvFiles), func() {
+ f := framework.NewDefaultFramework("mirror-pod-envfiles")
+ f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
+ var ns, podPath, staticPodName, mirrorPodName string
+
+ ginkgo.Context("when creating a static pod with EnvFiles", func() {
+ ginkgo.BeforeEach(func() {
+ ns = f.Namespace.Name
+ staticPodName = "static-pod-envfiles-" + string(uuid.NewUUID())
+ mirrorPodName = staticPodName + "-" + framework.TestContext.NodeName
+ podPath = kubeletCfg.StaticPodPath
+ })
+
+ ginkgo.AfterEach(func(ctx context.Context) {
+ ginkgo.By("delete the static pod")
+ err := deleteStaticPod(podPath, staticPodName, ns)
+ framework.ExpectNoError(err)
+
+ ginkgo.By("wait for the mirror pod to disappear")
+ gomega.Eventually(ctx, func(ctx context.Context) error {
+ return checkMirrorPodDisappear(ctx, f.ClientSet, mirrorPodName, ns)
+ }, 2*time.Minute, time.Second*4).Should(gomega.Succeed())
+ })
+
+ ginkgo.It("should be able to consume variables from a file", func(ctx context.Context) {
+ podSpec := v1.PodSpec{
+ InitContainers: []v1.Container{
+ {
+ Name: "setup-envfile",
+ Image: imageutils.GetE2EImage(imageutils.BusyBox),
+ Command: []string{"sh", "-c", `echo CONFIG_1='value1' > /data/config.env && echo CONFIG_2=\'value2\' >> /data/config.env`},
+ VolumeMounts: []v1.VolumeMount{
+ {
+ Name: "config",
+ MountPath: "/data",
+ },
+ },
+ },
+ },
+ Containers: []v1.Container{
+ {
+ Name: "use-envfile",
+ Image: imageutils.GetE2EImage(imageutils.BusyBox),
+ Command: []string{"sh", "-c", "env | grep -E '(CONFIG_1|CONFIG_2)' | sort"},
+ Env: []v1.EnvVar{
+ {
+ Name: "CONFIG_1",
+ ValueFrom: &v1.EnvVarSource{
+ FileKeyRef: &v1.FileKeySelector{
+ VolumeName: "config",
+ Path: "config.env",
+ Key: "CONFIG_1",
+ },
+ },
+ },
+ {
+ Name: "CONFIG_2",
+ ValueFrom: &v1.EnvVarSource{
+ FileKeyRef: &v1.FileKeySelector{
+ VolumeName: "config",
+ Path: "config.env",
+ Key: "CONFIG_2",
+ },
+ },
+ },
+ },
+ },
+ },
+ RestartPolicy: v1.RestartPolicyNever,
+ Volumes: []v1.Volume{
+ {
+ Name: "config",
+ VolumeSource: v1.VolumeSource{
+ EmptyDir: &v1.EmptyDirVolumeSource{},
+ },
+ },
+ },
+ }
+
+ ginkgo.By("create the static pod with envfiles")
+ err := createStaticPodWithSpec(podPath, staticPodName, ns, podSpec)
+ framework.ExpectNoError(err)
+
+ ginkgo.By("wait for the mirror pod to succeed")
+ err = e2epod.WaitForPodSuccessInNamespace(ctx, f.ClientSet, mirrorPodName, ns)
+ framework.ExpectNoError(err)
+
+ ginkgo.By("checking the logs of the mirror pod")
+ logs, err := e2epod.GetPodLogs(ctx, f.ClientSet, ns, mirrorPodName, "use-envfile")
+ framework.ExpectNoError(err)
+
+ gomega.Expect(logs).To(gomega.ContainSubstring("CONFIG_1=value1"))
+ gomega.Expect(logs).To(gomega.ContainSubstring("CONFIG_2=value2"))
+ })
+ })
+})
+
func podVolumeDirectoryExists(uid types.UID) bool {
podVolumePath := fmt.Sprintf("/var/lib/kubelet/pods/%s/volumes/", uid)
var podVolumeDirectoryExists bool
diff --git a/test/e2e_node/standalone_test.go b/test/e2e_node/standalone_test.go
index c8420aaa001..63ba76eed21 100644
--- a/test/e2e_node/standalone_test.go
+++ b/test/e2e_node/standalone_test.go
@@ -24,27 +24,151 @@ import (
"io"
"net/http"
"os"
+ "path/filepath"
"strings"
"time"
v1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/uuid"
+ "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/kubernetes/pkg/cluster/ports"
+ "k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/test/e2e/feature"
"k8s.io/kubernetes/test/e2e/framework"
+ testutils "k8s.io/kubernetes/test/utils"
imageutils "k8s.io/kubernetes/test/utils/image"
admissionapi "k8s.io/pod-security-admission/api"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- testutils "k8s.io/kubernetes/test/utils"
)
+var _ = SIGDescribe(feature.StandaloneMode, framework.WithFeatureGate(features.EnvFiles), func() {
+ f := framework.NewDefaultFramework("static-pod-envfiles")
+ f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
+ ginkgo.Context("when creating a static pod with EnvFiles", func() {
+ var ns, podPath, staticPodName string
+
+ ginkgo.It("the pod should be running and consume variables", func(ctx context.Context) {
+ ns = f.Namespace.Name
+ staticPodName = "static-pod-envfiles-" + string(uuid.NewUUID())
+ podPath = kubeletCfg.StaticPodPath
+
+ podSpec := &v1.Pod{
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Pod",
+ APIVersion: "v1",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: staticPodName,
+ Namespace: ns,
+ },
+ Spec: v1.PodSpec{
+ InitContainers: []v1.Container{
+ {
+ Name: "setup-envfile",
+ Image: imageutils.GetE2EImage(imageutils.BusyBox),
+ Command: []string{"sh", "-c", `echo CONFIG_1=\'value1\' > /data/config.env && echo CONFIG_2=\'value2\' >> /data/config.env`},
+ VolumeMounts: []v1.VolumeMount{
+ {
+ Name: "config",
+ MountPath: "/data",
+ },
+ },
+ },
+ },
+ Containers: []v1.Container{
+ {
+ Name: "use-envfile",
+ Image: imageutils.GetE2EImage(imageutils.BusyBox),
+ Command: []string{"sh", "-c", "env | grep -E '(CONFIG_1|CONFIG_2)' | sort"},
+ Env: []v1.EnvVar{
+ {
+ Name: "CONFIG_1",
+ ValueFrom: &v1.EnvVarSource{
+ FileKeyRef: &v1.FileKeySelector{
+ VolumeName: "config",
+ Path: "config.env",
+ Key: "CONFIG_1",
+ },
+ },
+ },
+ {
+ Name: "CONFIG_2",
+ ValueFrom: &v1.EnvVarSource{
+ FileKeyRef: &v1.FileKeySelector{
+ VolumeName: "config",
+ Path: "config.env",
+ Key: "CONFIG_2",
+ },
+ },
+ },
+ },
+ },
+ },
+ RestartPolicy: v1.RestartPolicyNever,
+ Volumes: []v1.Volume{
+ {
+ Name: "config",
+ VolumeSource: v1.VolumeSource{
+ EmptyDir: &v1.EmptyDirVolumeSource{},
+ },
+ },
+ },
+ },
+ }
+
+ err := scheduleStaticPod(podPath, staticPodName, ns, podSpec)
+ framework.ExpectNoError(err)
+
+ gomega.Eventually(ctx, func(ctx context.Context) error {
+ pod, err := getPodFromStandaloneKubelet(ctx, ns, staticPodName)
+ if err != nil {
+ return fmt.Errorf("error getting pod(%v/%v) from standalone kubelet: %w", ns, staticPodName, err)
+ }
+ if pod.Status.Phase == v1.PodSucceeded {
+ return nil
+ }
+ if pod.Status.Phase == v1.PodFailed {
+ logs, err := getPodLogsFromStandaloneKubelet(ctx, ns, staticPodName, "use-envfile")
+ if err != nil {
+ framework.Logf("failed to get logs on pod failure: %v", err)
+ }
+ return fmt.Errorf("pod (%v/%v) failed, logs: %s", ns, staticPodName, logs)
+ }
+ return fmt.Errorf("pod (%v/%v) is not succeeded, phase: %s", ns, staticPodName, pod.Status.Phase)
+ }, f.Timeouts.PodStart, time.Second*5).Should(gomega.Succeed())
+
+ logs, err := getPodLogsFromStandaloneKubelet(ctx, ns, staticPodName, "use-envfile")
+ framework.ExpectNoError(err)
+
+ gomega.Expect(logs).To(gomega.ContainSubstring("CONFIG_1=value1"))
+ gomega.Expect(logs).To(gomega.ContainSubstring("CONFIG_2=value2"))
+ })
+
+ ginkgo.AfterEach(func(ctx context.Context) {
+ ginkgo.By(fmt.Sprintf("delete the static pod (%v/%v)", ns, staticPodName))
+ err := deleteStaticPod(podPath, staticPodName, ns)
+ framework.ExpectNoError(err)
+
+ ginkgo.By(fmt.Sprintf("wait for pod to disappear (%v/%v)", ns, staticPodName))
+ gomega.Eventually(ctx, func(ctx context.Context) error {
+ _, err := getPodFromStandaloneKubelet(ctx, ns, staticPodName)
+
+ if apierrors.IsNotFound(err) {
+ return nil
+ }
+ return fmt.Errorf("pod (%v/%v) still exists", ns, staticPodName)
+ }).Should(gomega.Succeed())
+ })
+ })
+})
+
var _ = SIGDescribe(feature.StandaloneMode, func() {
f := framework.NewDefaultFramework("static-pod")
f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
@@ -315,6 +439,36 @@ func getPodFromStandaloneKubelet(ctx context.Context, podNamespace string, podNa
return nil, apierrors.NewNotFound(schema.GroupResource{Resource: "pods"}, podName)
}
+func getPodLogsFromStandaloneKubelet(ctx context.Context, podNamespace string, podName string, containerName string) (string, error) {
+ pod, err := getPodFromStandaloneKubelet(ctx, podNamespace, podName)
+ if err != nil {
+ return "", fmt.Errorf("failed to get pod %s/%s: %w", podNamespace, podName, err)
+ }
+
+ logCRIDir := "/var/log/pods"
+ podLogDir := filepath.Join(logCRIDir, fmt.Sprintf("%s_%s_%s", pod.Namespace, pod.Name, pod.UID))
+ logFile := filepath.Join(podLogDir, containerName, "0.log")
+
+ var content []byte
+ err = wait.PollUntilContextTimeout(ctx, time.Second, time.Minute, true, func(ctx context.Context) (bool, error) {
+ var errRead error
+ content, errRead = os.ReadFile(logFile)
+ if errRead != nil {
+ if os.IsNotExist(errRead) {
+ return false, nil
+ }
+ return false, errRead
+ }
+ return true, nil
+ })
+
+ if err != nil {
+ return "", fmt.Errorf("could not read log file %s: %w", logFile, err)
+ }
+
+ return string(content), nil
+}
+
// Decodes the http response from /configz and returns a kubeletconfig.KubeletConfiguration (internal type).
func decodePods(respBody []byte) (*v1.PodList, error) {
// This hack because /pods reports the following structure: