diff --git a/cmd/kubelet/app/options/options.go b/cmd/kubelet/app/options/options.go index 313c3624157..1cba5c8736b 100644 --- a/cmd/kubelet/app/options/options.go +++ b/cmd/kubelet/app/options/options.go @@ -89,6 +89,7 @@ type KubeletFlags struct { // KubeletDropinConfigDirectory is the path to a directory containing drop-in configuration files that override settings from defaults and the --config file. // The path may be absolute or relative; relative paths are under the Kubelet's current working directory. // Drop-in files must have a '.conf' suffix and are processed in lexical order. Only '.conf' files are loaded; all other files are ignored. + // All .conf files in the directory and its subdirectories are processed in lexical order. KubeletDropinConfigDirectory string // WindowsService should be set to true if kubelet is running as a service on Windows. @@ -278,7 +279,7 @@ func (f *KubeletFlags) AddFlags(mainfs *pflag.FlagSet) { f.addOSFlags(fs) fs.StringVar(&f.KubeletConfigFile, "config", f.KubeletConfigFile, "The Kubelet will load its initial configuration from this file. The path may be absolute or relative; relative paths start at the Kubelet's current working directory. Omit this flag to use the built-in default configuration values. Command-line flags override configuration from this file.") - fs.StringVar(&f.KubeletDropinConfigDirectory, "config-dir", "", "Path to a directory containing drop-in configuration files that override settings from defaults and the --config file. The path may be absolute or relative; relative paths start at the Kubelet's current working directory. Drop-in files must have a '.conf' suffix (e.g., '99-kubelet-address.conf') and are processed in lexical order. Only '.conf' files are loaded; all other files are ignored. [default='']") + fs.StringVar(&f.KubeletDropinConfigDirectory, "config-dir", "", "Path to a directory containing drop-in configuration files that override settings from defaults and the --config file. The path may be absolute or relative; relative paths start at the Kubelet's current working directory. Drop-in files must have a '.conf' suffix (e.g., '99-kubelet-address.conf') and are processed in lexical order. Only '.conf' files are loaded; all other files are ignored. All .conf files in the directory and its subdirectories are processed in lexical order. [default='']") fs.StringVar(&f.KubeConfig, "kubeconfig", f.KubeConfig, "Path to a kubeconfig file, specifying how to connect to the API server. Providing --kubeconfig enables API server mode, omitting --kubeconfig enables standalone mode.") fs.StringVar(&f.BootstrapKubeconfig, "bootstrap-kubeconfig", f.BootstrapKubeconfig, "Path to a kubeconfig file that will be used to get client certificate for kubelet. "+ diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index c164ff3c852..638cacc7699 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -330,7 +330,7 @@ is checked every 20 seconds (also configurable with a flag).`, // configurations in files with lower numeric prefixes are applied first, followed by higher numeric prefixes. // For example, if the drop-in directory contains files named "10-config.conf" and "20-config.conf", // the configurations in "10-config.conf" will be applied first, and then the configurations in "20-config.conf" will be applied, -// potentially overriding the previous values. +// potentially overriding the previous values. Files in subdirectories are also processed in lexical order. // Returns a list of skipped files and an error. func mergeKubeletConfigurations(kubeletConfig *kubeletconfiginternal.KubeletConfiguration, kubeletDropInConfigDir string) ([]string, error) { const dropinFileExtension = ".conf" diff --git a/cmd/kubelet/app/server_test.go b/cmd/kubelet/app/server_test.go index 4605a634e11..2e1eaf7f4a4 100644 --- a/cmd/kubelet/app/server_test.go +++ b/cmd/kubelet/app/server_test.go @@ -416,3 +416,108 @@ port: [this is not valid }) } } + +func TestMergeKubeletConfigsWithSubdirs(t *testing.T) { + testCases := []struct { + name string + kubeletConfig *kubeletconfiginternal.KubeletConfiguration + dropins map[string]string // map[relativePath]content + overwrittenConfigFields map[string]interface{} + }{ + { + name: "subdirectories are processed in lexical order", + kubeletConfig: &kubeletconfiginternal.KubeletConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: "KubeletConfiguration", + APIVersion: "kubelet.config.k8s.io/v1beta1", + }, + Port: int32(9090), + ReadOnlyPort: int32(10257), + }, + dropins: map[string]string{ + "10-kubelet.conf": ` +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: KubeletConfiguration +port: 8080 +`, + "subdir/20-kubelet.conf": ` +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: KubeletConfiguration +port: 7777 +readOnlyPort: 9999 +`, + }, + overwrittenConfigFields: map[string]interface{}{ + "Port": int32(7777), + "ReadOnlyPort": int32(9999), + }, + }, + { + name: "same filename in multiple directories - lexical order applies", + kubeletConfig: &kubeletconfiginternal.KubeletConfiguration{ + TypeMeta: metav1.TypeMeta{ + Kind: "KubeletConfiguration", + APIVersion: "kubelet.config.k8s.io/v1beta1", + }, + Port: int32(9090), + ReadOnlyPort: int32(10255), + }, + dropins: map[string]string{ + "10-kubelet.conf": ` +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: KubeletConfiguration +port: 8080 +`, + "sub-dir1/10-kubelet.conf": ` +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: KubeletConfiguration +port: 7070 +readOnlyPort: 8888 +`, + "sub-dir2/10-kubelet.conf": ` +apiVersion: kubelet.config.k8s.io/v1beta1 +kind: KubeletConfiguration +port: 6060 +readOnlyPort: 9999 +`, + }, + overwrittenConfigFields: map[string]interface{}{ + "Port": int32(6060), + "ReadOnlyPort": int32(9999), + }, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + tempDir := t.TempDir() + kubeletConfDir := filepath.Join(tempDir, "kubelet.conf.d") + err := os.Mkdir(kubeletConfDir, 0755) + require.NoError(t, err, "Failed to create kubelet.conf.d directory") + + // Create drop-in files including those in subdirectories + for relPath, content := range test.dropins { + fullPath := filepath.Join(kubeletConfDir, relPath) + // Create subdirectory if needed + dir := filepath.Dir(fullPath) + if dir != kubeletConfDir { + err = os.MkdirAll(dir, 0755) + require.NoError(t, err, "Failed to create subdirectory: "+dir) + } + err = os.WriteFile(fullPath, []byte(content), 0644) + require.NoError(t, err, "Failed to create drop-in file: "+relPath) + } + + // Merge the kubelet configurations + _, err = mergeKubeletConfigurations(test.kubeletConfig, kubeletConfDir) + require.NoError(t, err, "failed to merge kubelet drop-in configs") + + // Verify the merged configuration fields + for fieldName, expectedValue := range test.overwrittenConfigFields { + value := reflect.ValueOf(test.kubeletConfig).Elem() + field := value.FieldByName(fieldName) + require.Equal(t, expectedValue, field.Interface(), "Field mismatch: "+fieldName) + } + }) + } +}