diff --git a/cmd/kubeadm/app/apis/kubeadm/types.go b/cmd/kubeadm/app/apis/kubeadm/types.go index 0e30c483106..94ed5399be5 100644 --- a/cmd/kubeadm/app/apis/kubeadm/types.go +++ b/cmd/kubeadm/app/apis/kubeadm/types.go @@ -421,8 +421,8 @@ type HostPathMount struct { type Patches struct { // Directory is a path to a directory that contains files named "target[suffix][+patchtype].extension". // For example, "kube-apiserver0+merge.yaml" or just "etcd.json". "target" can be one of - // "kube-apiserver", "kube-controller-manager", "kube-scheduler", "etcd". "patchtype" can be one - // of "strategic" "merge" or "json" and they match the patch formats supported by kubectl. + // "kube-apiserver", "kube-controller-manager", "kube-scheduler", "etcd", "kubeletconfiguration". + // "patchtype" can be one of "strategic" "merge" or "json" and they match the patch formats supported by kubectl. // The default "patchtype" is "strategic". "extension" must be either "json" or "yaml". // "suffix" is an optional string that can be used to determine which patches are applied // first alpha-numerically. diff --git a/cmd/kubeadm/app/apis/kubeadm/v1beta3/types.go b/cmd/kubeadm/app/apis/kubeadm/v1beta3/types.go index 525e2046925..eaa2984f40f 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1beta3/types.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1beta3/types.go @@ -435,8 +435,8 @@ type HostPathMount struct { type Patches struct { // Directory is a path to a directory that contains files named "target[suffix][+patchtype].extension". // For example, "kube-apiserver0+merge.yaml" or just "etcd.json". "target" can be one of - // "kube-apiserver", "kube-controller-manager", "kube-scheduler", "etcd". "patchtype" can be one - // of "strategic" "merge" or "json" and they match the patch formats supported by kubectl. + // "kube-apiserver", "kube-controller-manager", "kube-scheduler", "etcd", "kubeletconfiguration". + // "patchtype" can be one of "strategic" "merge" or "json" and they match the patch formats supported by kubectl. // The default "patchtype" is "strategic". "extension" must be either "json" or "yaml". // "suffix" is an optional string that can be used to determine which patches are applied // first alpha-numerically. diff --git a/cmd/kubeadm/app/cmd/options/generic.go b/cmd/kubeadm/app/cmd/options/generic.go index debe6f95ddc..19436dcba9a 100644 --- a/cmd/kubeadm/app/cmd/options/generic.go +++ b/cmd/kubeadm/app/cmd/options/generic.go @@ -96,7 +96,7 @@ func AddPatchesFlag(fs *pflag.FlagSet, patchesDir *string) { const usage = `Path to a directory that contains files named ` + `"target[suffix][+patchtype].extension". For example, ` + `"kube-apiserver0+merge.yaml" or just "etcd.json". ` + - `"target" can be one of "kube-apiserver", "kube-controller-manager", "kube-scheduler", "etcd". ` + + `"target" can be one of "kube-apiserver", "kube-controller-manager", "kube-scheduler", "etcd", "kubeletconfiguration". ` + `"patchtype" can be one of "strategic", "merge" or "json" and they match the patch formats ` + `supported by kubectl. The default "patchtype" is "strategic". "extension" must be either ` + `"json" or "yaml". "suffix" is an optional string that can be used to determine ` + diff --git a/cmd/kubeadm/app/cmd/phases/init/kubelet.go b/cmd/kubeadm/app/cmd/phases/init/kubelet.go index bf43d23821a..204004b28e6 100644 --- a/cmd/kubeadm/app/cmd/phases/init/kubelet.go +++ b/cmd/kubeadm/app/cmd/phases/init/kubelet.go @@ -48,6 +48,7 @@ func NewKubeletStartPhase() workflow.Phase { options.CfgPath, options.NodeCRISocket, options.NodeName, + options.Patches, }, } } @@ -74,7 +75,7 @@ func runKubeletStart(c workflow.RunData) error { } // Write the kubelet configuration file to disk. - if err := kubeletphase.WriteConfigToDisk(&data.Cfg().ClusterConfiguration, data.KubeletDir()); err != nil { + if err := kubeletphase.WriteConfigToDisk(&data.Cfg().ClusterConfiguration, data.KubeletDir(), data.PatchesDir(), data.OutputWriter()); err != nil { return errors.Wrap(err, "error writing kubelet configuration to disk") } diff --git a/cmd/kubeadm/app/cmd/phases/join/kubelet.go b/cmd/kubeadm/app/cmd/phases/join/kubelet.go index 377db62dab0..113a591c536 100644 --- a/cmd/kubeadm/app/cmd/phases/join/kubelet.go +++ b/cmd/kubeadm/app/cmd/phases/join/kubelet.go @@ -76,6 +76,7 @@ func NewKubeletStartPhase() workflow.Phase { options.TokenDiscoverySkipCAHash, options.TLSBootstrapToken, options.TokenStr, + options.Patches, }, } } @@ -174,7 +175,7 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) { } // Write the configuration for the kubelet (using the bootstrap token credentials) to disk so the kubelet can start - if err := kubeletphase.WriteConfigToDisk(&initCfg.ClusterConfiguration, data.KubeletDir()); err != nil { + if err := kubeletphase.WriteConfigToDisk(&initCfg.ClusterConfiguration, data.KubeletDir(), data.PatchesDir(), data.OutputWriter()); err != nil { return err } diff --git a/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go b/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go index c6ab05a970f..429ce185937 100644 --- a/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go +++ b/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go @@ -17,6 +17,8 @@ limitations under the License. package node import ( + "io" + "k8s.io/apimachinery/pkg/util/sets" clientset "k8s.io/client-go/kubernetes" @@ -35,4 +37,5 @@ type Data interface { IgnorePreflightErrors() sets.String PatchesDir() string KubeConfigPath() string + OutputWriter() io.Writer } diff --git a/cmd/kubeadm/app/cmd/phases/upgrade/node/kubeletconfig.go b/cmd/kubeadm/app/cmd/phases/upgrade/node/kubeletconfig.go index 773b5a8d7ad..5ec99c4e260 100644 --- a/cmd/kubeadm/app/cmd/phases/upgrade/node/kubeletconfig.go +++ b/cmd/kubeadm/app/cmd/phases/upgrade/node/kubeletconfig.go @@ -48,6 +48,7 @@ func NewKubeletConfigPhase() workflow.Phase { InheritFlags: []string{ options.DryRun, options.KubeconfigPath, + options.Patches, }, } return phase @@ -73,7 +74,7 @@ func runKubeletConfigPhase() func(c workflow.RunData) error { // TODO: Checkpoint the current configuration first so that if something goes wrong it can be recovered // Store the kubelet component configuration. - if err = kubeletphase.WriteConfigToDisk(&cfg.ClusterConfiguration, kubeletDir); err != nil { + if err = kubeletphase.WriteConfigToDisk(&cfg.ClusterConfiguration, kubeletDir, data.PatchesDir(), data.OutputWriter()); err != nil { return err } diff --git a/cmd/kubeadm/app/cmd/upgrade/apply.go b/cmd/kubeadm/app/cmd/upgrade/apply.go index 9900964db3e..ee99707aa27 100644 --- a/cmd/kubeadm/app/cmd/upgrade/apply.go +++ b/cmd/kubeadm/app/cmd/upgrade/apply.go @@ -168,7 +168,7 @@ func runApply(flags *applyFlags, args []string) error { // Upgrade RBAC rules and addons. klog.V(1).Infoln("[upgrade/postupgrade] upgrading RBAC rules and addons") - if err := upgrade.PerformPostUpgradeTasks(client, cfg, flags.dryRun); err != nil { + if err := upgrade.PerformPostUpgradeTasks(client, cfg, flags.patchesDir, flags.dryRun, flags.applyPlanFlags.out); err != nil { return errors.Wrap(err, "[upgrade/postupgrade] FATAL post-upgrade error") } diff --git a/cmd/kubeadm/app/cmd/upgrade/node.go b/cmd/kubeadm/app/cmd/upgrade/node.go index 8548fc79364..b76f78ab55c 100644 --- a/cmd/kubeadm/app/cmd/upgrade/node.go +++ b/cmd/kubeadm/app/cmd/upgrade/node.go @@ -17,6 +17,7 @@ limitations under the License. package upgrade import ( + "io" "os" "github.com/pkg/errors" @@ -63,10 +64,11 @@ type nodeData struct { patchesDir string ignorePreflightErrors sets.String kubeConfigPath string + outputWriter io.Writer } // newCmdNode returns the cobra command for `kubeadm upgrade node` -func newCmdNode() *cobra.Command { +func newCmdNode(out io.Writer) *cobra.Command { nodeOptions := newNodeOptions() nodeRunner := workflow.NewRunner() @@ -92,7 +94,7 @@ func newCmdNode() *cobra.Command { // sets the data builder function, that will be used by the runner // both when running the entire workflow or single phases nodeRunner.SetDataInitializer(func(cmd *cobra.Command, args []string) (workflow.RunData, error) { - return newNodeData(cmd, args, nodeOptions) + return newNodeData(cmd, args, nodeOptions, out) }) // binds the Runner to kubeadm upgrade node command by altering @@ -123,7 +125,7 @@ func addUpgradeNodeFlags(flagSet *flag.FlagSet, nodeOptions *nodeOptions) { // newNodeData returns a new nodeData struct to be used for the execution of the kubeadm upgrade node workflow. // This func takes care of validating nodeOptions passed to the command, and then it converts // options into the internal InitConfiguration type that is used as input all the phases in the kubeadm upgrade node workflow -func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions) (*nodeData, error) { +func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions, out io.Writer) (*nodeData, error) { client, err := getClient(options.kubeConfigPath, options.dryRun) if err != nil { return nil, errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", options.kubeConfigPath) @@ -168,6 +170,7 @@ func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions) (*node patchesDir: options.patchesDir, ignorePreflightErrors: ignorePreflightErrorsSet, kubeConfigPath: options.kubeConfigPath, + outputWriter: out, }, nil } @@ -215,3 +218,7 @@ func (d *nodeData) IgnorePreflightErrors() sets.String { func (d *nodeData) KubeConfigPath() string { return d.kubeConfigPath } + +func (d *nodeData) OutputWriter() io.Writer { + return d.outputWriter +} diff --git a/cmd/kubeadm/app/cmd/upgrade/upgrade.go b/cmd/kubeadm/app/cmd/upgrade/upgrade.go index f65376b6f0b..32499f1d4ac 100644 --- a/cmd/kubeadm/app/cmd/upgrade/upgrade.go +++ b/cmd/kubeadm/app/cmd/upgrade/upgrade.go @@ -60,7 +60,7 @@ func NewCmdUpgrade(out io.Writer) *cobra.Command { cmd.AddCommand(newCmdApply(flags)) cmd.AddCommand(newCmdPlan(flags)) cmd.AddCommand(newCmdDiff(out)) - cmd.AddCommand(newCmdNode()) + cmd.AddCommand(newCmdNode(out)) return cmd } diff --git a/cmd/kubeadm/app/phases/kubelet/config.go b/cmd/kubeadm/app/phases/kubelet/config.go index f64e93364e9..9f43896a173 100644 --- a/cmd/kubeadm/app/phases/kubelet/config.go +++ b/cmd/kubeadm/app/phases/kubelet/config.go @@ -18,6 +18,7 @@ package kubelet import ( "fmt" + "io" "os" "path/filepath" @@ -27,16 +28,18 @@ import ( rbac "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientset "k8s.io/client-go/kubernetes" + kubeletconfig "k8s.io/kubelet/config/v1beta1" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" "k8s.io/kubernetes/cmd/kubeadm/app/componentconfigs" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" + "k8s.io/kubernetes/cmd/kubeadm/app/util/patches" ) // WriteConfigToDisk writes the kubelet config object down to a file // Used at "kubeadm init" and "kubeadm upgrade" time -func WriteConfigToDisk(cfg *kubeadmapi.ClusterConfiguration, kubeletDir string) error { +func WriteConfigToDisk(cfg *kubeadmapi.ClusterConfiguration, kubeletDir, patchesDir string, output io.Writer) error { kubeletCfg, ok := cfg.ComponentConfigs[componentconfigs.KubeletGroup] if !ok { return errors.New("no kubelet component config found") @@ -51,6 +54,25 @@ func WriteConfigToDisk(cfg *kubeadmapi.ClusterConfiguration, kubeletDir string) return err } + // Apply patches to the KubeletConfiguration + if len(patchesDir) != 0 { + target := "kubeletconfiguration" + knownTargets := []string{target} + patchManager, err := patches.GetPatchManagerForPath(patchesDir, knownTargets, output) + if err != nil { + return err + } + patchTarget := &patches.PatchTarget{ + Name: target, + StrategicMergePatchObject: kubeletconfig.KubeletConfiguration{}, + Data: kubeletBytes, + } + if err := patchManager.ApplyPatchesToTarget(patchTarget); err != nil { + return err + } + kubeletBytes = patchTarget.Data + } + return writeConfigBytesToDisk(kubeletBytes, kubeletDir) } diff --git a/cmd/kubeadm/app/phases/upgrade/postupgrade.go b/cmd/kubeadm/app/phases/upgrade/postupgrade.go index aad71841736..190c3afefa9 100644 --- a/cmd/kubeadm/app/phases/upgrade/postupgrade.go +++ b/cmd/kubeadm/app/phases/upgrade/postupgrade.go @@ -19,6 +19,7 @@ package upgrade import ( "context" "fmt" + "io" "os" "strings" @@ -48,7 +49,7 @@ import ( // PerformPostUpgradeTasks runs nearly the same functions as 'kubeadm init' would do // Note that the mark-control-plane phase is left out, not needed, and no token is created as that doesn't belong to the upgrade -func PerformPostUpgradeTasks(client clientset.Interface, cfg *kubeadmapi.InitConfiguration, dryRun bool) error { +func PerformPostUpgradeTasks(client clientset.Interface, cfg *kubeadmapi.InitConfiguration, patchesDir string, dryRun bool, out io.Writer) error { errs := []error{} // Upload currently used configuration to the cluster @@ -64,7 +65,7 @@ func PerformPostUpgradeTasks(client clientset.Interface, cfg *kubeadmapi.InitCon } // Write the new kubelet config down to disk and the env file if needed - if err := writeKubeletConfigFiles(client, cfg, dryRun); err != nil { + if err := writeKubeletConfigFiles(client, cfg, patchesDir, dryRun, out); err != nil { errs = append(errs, err) } @@ -158,7 +159,7 @@ func PerformPostUpgradeTasks(client clientset.Interface, cfg *kubeadmapi.InitCon return errorsutil.NewAggregate(errs) } -func writeKubeletConfigFiles(client clientset.Interface, cfg *kubeadmapi.InitConfiguration, dryRun bool) error { +func writeKubeletConfigFiles(client clientset.Interface, cfg *kubeadmapi.InitConfiguration, patchesDir string, dryRun bool, out io.Writer) error { kubeletDir, err := GetKubeletDir(dryRun) if err != nil { // The error here should never occur in reality, would only be thrown if /tmp doesn't exist on the machine. @@ -166,7 +167,7 @@ func writeKubeletConfigFiles(client clientset.Interface, cfg *kubeadmapi.InitCon } errs := []error{} // Write the configuration for the kubelet down to disk so the upgraded kubelet can start with fresh config - if err := kubeletphase.WriteConfigToDisk(&cfg.ClusterConfiguration, kubeletDir); err != nil { + if err := kubeletphase.WriteConfigToDisk(&cfg.ClusterConfiguration, kubeletDir, patchesDir, out); err != nil { errs = append(errs, errors.Wrap(err, "error writing kubelet configuration to file")) } diff --git a/cmd/kubeadm/app/util/patches/patches_test.go b/cmd/kubeadm/app/util/patches/patches_test.go index 48aa1691463..b704c820928 100644 --- a/cmd/kubeadm/app/util/patches/patches_test.go +++ b/cmd/kubeadm/app/util/patches/patches_test.go @@ -26,7 +26,7 @@ import ( "github.com/pkg/errors" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -35,6 +35,7 @@ var testKnownTargets = []string{ "kube-apiserver", "kube-controller-manager", "kube-scheduler", + "kubeletconfiguration", } const testDirPattern = "patch-files" @@ -312,6 +313,21 @@ func TestGetPatchManagerForPath(t *testing.T) { }, }, }, + { + name: "valid: kubeletconfiguration target is patched with json patch", + patchTarget: &PatchTarget{ + Name: "kubeletconfiguration", + StrategicMergePatchObject: nil, + Data: []byte("foo: bar\n"), + }, + expectedData: []byte(`{"foo":"zzz"}`), + files: []*file{ + { + name: "kubeletconfiguration+json.json", + data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, + }, + }, + }, { name: "valid: kube-apiserver target is patched with strategic merge patch", patchTarget: &PatchTarget{