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: