diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go index cfa5663ce0d..06e8605704e 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go @@ -463,6 +463,7 @@ func isAllowedFlag(flagName string) bool { kubeadmcmdoptions.KubeconfigDir, kubeadmcmdoptions.UploadCerts, kubeadmcmdoptions.Kustomize, + kubeadmcmdoptions.Patches, "print-join-command", "rootfs", "v") if knownFlags.Has(flagName) { return true diff --git a/cmd/kubeadm/app/cmd/init.go b/cmd/kubeadm/app/cmd/init.go index 6994c127489..76f2299e0d4 100644 --- a/cmd/kubeadm/app/cmd/init.go +++ b/cmd/kubeadm/app/cmd/init.go @@ -99,6 +99,7 @@ type initOptions struct { uploadCerts bool skipCertificateKeyPrint bool kustomizeDir string + patchesDir string } // compile-time assert that the local data object satisfies the phases data interface. @@ -121,6 +122,7 @@ type initData struct { uploadCerts bool skipCertificateKeyPrint bool kustomizeDir string + patchesDir string } // NewCmdInit returns "kubeadm init" command. @@ -277,6 +279,7 @@ func AddInitOtherFlags(flagSet *flag.FlagSet, initOptions *initOptions) { "Don't print the key used to encrypt the control-plane certificates.", ) options.AddKustomizePodsFlag(flagSet, &initOptions.kustomizeDir) + options.AddPatchesFlag(flagSet, &initOptions.patchesDir) } // newInitOptions returns a struct ready for being used for creating cmd init flags. @@ -413,6 +416,7 @@ func newInitData(cmd *cobra.Command, args []string, options *initOptions, out io uploadCerts: options.uploadCerts, skipCertificateKeyPrint: options.skipCertificateKeyPrint, kustomizeDir: options.kustomizeDir, + patchesDir: options.patchesDir, }, nil } @@ -550,6 +554,11 @@ func (d *initData) KustomizeDir() string { return d.kustomizeDir } +// PatchesDir returns the folder where patches for components are stored +func (d *initData) PatchesDir() string { + return d.patchesDir +} + func printJoinCommand(out io.Writer, adminKubeConfigPath, token string, i *initData) error { joinControlPlaneCommand, err := cmdutil.GetJoinControlPlaneCommand(adminKubeConfigPath, token, i.CertificateKey(), i.skipTokenPrint, i.skipCertificateKeyPrint) if err != nil { diff --git a/cmd/kubeadm/app/cmd/join.go b/cmd/kubeadm/app/cmd/join.go index bf728eecc80..8487bd5aba4 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -130,6 +130,7 @@ type joinOptions struct { externalcfg *kubeadmapiv1beta2.JoinConfiguration joinControlPlane *kubeadmapiv1beta2.JoinControlPlane kustomizeDir string + patchesDir string } // compile-time assert that the local data object satisfies the phases data interface. @@ -145,6 +146,7 @@ type joinData struct { ignorePreflightErrors sets.String outputWriter io.Writer kustomizeDir string + patchesDir string } // NewCmdJoin returns "kubeadm join" command. @@ -286,6 +288,7 @@ func addJoinOtherFlags(flagSet *flag.FlagSet, joinOptions *joinOptions) { "Create a new control plane instance on this node", ) options.AddKustomizePodsFlag(flagSet, &joinOptions.kustomizeDir) + options.AddPatchesFlag(flagSet, &joinOptions.patchesDir) } // newJoinOptions returns a struct ready for being used for creating cmd join flags. @@ -441,6 +444,7 @@ func newJoinData(cmd *cobra.Command, args []string, opt *joinOptions, out io.Wri ignorePreflightErrors: ignorePreflightErrorsSet, outputWriter: out, kustomizeDir: opt.kustomizeDir, + patchesDir: opt.patchesDir, }, nil } @@ -511,6 +515,11 @@ func (j *joinData) KustomizeDir() string { return j.kustomizeDir } +// PatchesDir returns the folder where patches for components are stored +func (j *joinData) PatchesDir() string { + return j.patchesDir +} + // fetchInitConfigurationFromJoinConfiguration retrieves the init configuration from a join configuration, performing the discovery func fetchInitConfigurationFromJoinConfiguration(cfg *kubeadmapi.JoinConfiguration, tlsBootstrapCfg *clientcmdapi.Config) (*kubeadmapi.InitConfiguration, error) { // Retrieves the kubeadm configuration diff --git a/cmd/kubeadm/app/cmd/options/constant.go b/cmd/kubeadm/app/cmd/options/constant.go index b9154826d56..09a521af8da 100644 --- a/cmd/kubeadm/app/cmd/options/constant.go +++ b/cmd/kubeadm/app/cmd/options/constant.go @@ -145,4 +145,7 @@ const ( // Kustomize flag sets the folder where kustomize patches for static pod manifest are stored Kustomize = "experimental-kustomize" + + // Patches flag sets the folder where kubeadm component patches are stored + Patches = "experimental-patches" ) diff --git a/cmd/kubeadm/app/cmd/options/generic.go b/cmd/kubeadm/app/cmd/options/generic.go index 717403a1935..039d398551a 100644 --- a/cmd/kubeadm/app/cmd/options/generic.go +++ b/cmd/kubeadm/app/cmd/options/generic.go @@ -17,6 +17,7 @@ limitations under the License. package options import ( + "fmt" "strings" "github.com/spf13/pflag" @@ -92,4 +93,17 @@ func AddKubeadmOtherFlags(flagSet *pflag.FlagSet, rootfsPath *string) { // AddKustomizePodsFlag adds the --kustomize flag to the given flagset func AddKustomizePodsFlag(fs *pflag.FlagSet, kustomizeDir *string) { fs.StringVarP(kustomizeDir, Kustomize, "k", *kustomizeDir, "The path where kustomize patches for static pod manifests are stored.") + fs.MarkDeprecated(Kustomize, fmt.Sprintf("This flag is deprecated and will be removed in a future version. Please use %s instead.", Patches)) +} + +// AddPatchesFlag adds the --patches flag to the given flagset +func AddPatchesFlag(fs *pflag.FlagSet, patchesDir *string) { + fs.StringVar(patchesDir, Patches, *patchesDir, `Path to a directory that contains files named `+ + `"target[suffix][+patchtype].extension". For example, `+ + `"kube-apiserver0+merge.yaml" or just "etcd.json". `+ + `"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/phases/init/controlplane.go b/cmd/kubeadm/app/cmd/phases/init/controlplane.go index 69c8642906d..aa35d8b8bca 100644 --- a/cmd/kubeadm/app/cmd/phases/init/controlplane.go +++ b/cmd/kubeadm/app/cmd/phases/init/controlplane.go @@ -145,6 +145,6 @@ func runControlPlaneSubphase(component string) func(c workflow.RunData) error { cfg := data.Cfg() fmt.Printf("[control-plane] Creating static Pod manifest for %q\n", component) - return controlplane.CreateStaticPodFiles(data.ManifestDir(), data.KustomizeDir(), &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, component) + return controlplane.CreateStaticPodFiles(data.ManifestDir(), data.KustomizeDir(), data.PatchesDir(), &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, component) } } diff --git a/cmd/kubeadm/app/cmd/phases/init/data.go b/cmd/kubeadm/app/cmd/phases/init/data.go index e251f9379d9..90e7d1befd1 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data.go +++ b/cmd/kubeadm/app/cmd/phases/init/data.go @@ -46,4 +46,5 @@ type InitData interface { Client() (clientset.Interface, error) Tokens() []string KustomizeDir() string + PatchesDir() string } diff --git a/cmd/kubeadm/app/cmd/phases/init/data_test.go b/cmd/kubeadm/app/cmd/phases/init/data_test.go index 8b2730c79be..838867839e2 100644 --- a/cmd/kubeadm/app/cmd/phases/init/data_test.go +++ b/cmd/kubeadm/app/cmd/phases/init/data_test.go @@ -49,3 +49,4 @@ func (t *testInitData) OutputWriter() io.Writer { return nil } func (t *testInitData) Client() (clientset.Interface, error) { return nil, nil } func (t *testInitData) Tokens() []string { return nil } func (t *testInitData) KustomizeDir() string { return "" } +func (t *testInitData) PatchesDir() string { return "" } diff --git a/cmd/kubeadm/app/cmd/phases/init/etcd.go b/cmd/kubeadm/app/cmd/phases/init/etcd.go index ae090b7b9bf..b67a6023da9 100644 --- a/cmd/kubeadm/app/cmd/phases/init/etcd.go +++ b/cmd/kubeadm/app/cmd/phases/init/etcd.go @@ -70,6 +70,7 @@ func getEtcdPhaseFlags() []string { options.CfgPath, options.ImageRepository, options.Kustomize, + options.Patches, } return flags } @@ -93,7 +94,7 @@ func runEtcdPhaseLocal() func(c workflow.RunData) error { fmt.Printf("[dryrun] Would ensure that %q directory is present\n", cfg.Etcd.Local.DataDir) } fmt.Printf("[etcd] Creating static Pod manifest for local etcd in %q\n", data.ManifestDir()) - if err := etcdphase.CreateLocalEtcdStaticPodManifestFile(data.ManifestDir(), data.KustomizeDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint); err != nil { + if err := etcdphase.CreateLocalEtcdStaticPodManifestFile(data.ManifestDir(), data.KustomizeDir(), data.PatchesDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint); err != nil { return errors.Wrap(err, "error creating local etcd static pod manifest file") } } else { diff --git a/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go b/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go index fbdec4041a5..4b947ebe183 100644 --- a/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go +++ b/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go @@ -43,7 +43,7 @@ func getControlPlaneJoinPhaseFlags(name string) []string { options.NodeName, } if name == "etcd" { - flags = append(flags, options.Kustomize) + flags = append(flags, options.Kustomize, options.Patches) } if name != "mark-control-plane" { flags = append(flags, options.APIServerAdvertiseAddress) @@ -139,8 +139,9 @@ func runEtcdPhase(c workflow.RunData) error { // From https://coreos.com/etcd/docs/latest/v2/runtime-configuration.html // "If you add a new member to a 1-node cluster, the cluster cannot make progress before the new member starts // because it needs two members as majority to agree on the consensus. You will only see this behavior between the time - // etcdctl member add informs the cluster about the new member and the new member successfully establishing a connection to the // existing one." - if err := etcdphase.CreateStackedEtcdStaticPodManifestFile(client, kubeadmconstants.GetStaticPodDirectory(), data.KustomizeDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint); err != nil { + // etcdctl member add informs the cluster about the new member and the new member successfully establishing a connection to the + // existing one." + if err := etcdphase.CreateStackedEtcdStaticPodManifestFile(client, kubeadmconstants.GetStaticPodDirectory(), data.KustomizeDir(), data.PatchesDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint); err != nil { return errors.Wrap(err, "error creating local etcd static pod manifest file") } diff --git a/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go b/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go index 07f38959790..0c757623d4a 100644 --- a/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go +++ b/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go @@ -79,6 +79,7 @@ func getControlPlanePreparePhaseFlags(name string) []string { options.TokenStr, options.CertificateKey, options.Kustomize, + options.Patches, } case "download-certs": flags = []string{ @@ -124,6 +125,7 @@ func getControlPlanePreparePhaseFlags(name string) []string { options.CfgPath, options.ControlPlane, options.Kustomize, + options.Patches, } default: flags = []string{} @@ -190,6 +192,7 @@ func runControlPlanePrepareControlPlaneSubphase(c workflow.RunData) error { err := controlplane.CreateStaticPodFiles( kubeadmconstants.GetStaticPodDirectory(), data.KustomizeDir(), + data.PatchesDir(), &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, component, diff --git a/cmd/kubeadm/app/cmd/phases/join/data.go b/cmd/kubeadm/app/cmd/phases/join/data.go index 9b833f6ea87..879ce3ce3fd 100644 --- a/cmd/kubeadm/app/cmd/phases/join/data.go +++ b/cmd/kubeadm/app/cmd/phases/join/data.go @@ -36,4 +36,5 @@ type JoinData interface { IgnorePreflightErrors() sets.String OutputWriter() io.Writer KustomizeDir() string + PatchesDir() string } diff --git a/cmd/kubeadm/app/cmd/phases/join/data_test.go b/cmd/kubeadm/app/cmd/phases/join/data_test.go index a5f38b12251..d0f980432e8 100644 --- a/cmd/kubeadm/app/cmd/phases/join/data_test.go +++ b/cmd/kubeadm/app/cmd/phases/join/data_test.go @@ -39,3 +39,4 @@ func (j *testJoinData) ClientSet() (*clientset.Clientset, error) { return func (j *testJoinData) IgnorePreflightErrors() sets.String { return nil } func (j *testJoinData) OutputWriter() io.Writer { return nil } func (j *testJoinData) KustomizeDir() string { return "" } +func (j *testJoinData) PatchesDir() string { return "" } diff --git a/cmd/kubeadm/app/cmd/phases/upgrade/node/controlplane.go b/cmd/kubeadm/app/cmd/phases/upgrade/node/controlplane.go index d7749fb648d..d2628aa3cda 100644 --- a/cmd/kubeadm/app/cmd/phases/upgrade/node/controlplane.go +++ b/cmd/kubeadm/app/cmd/phases/upgrade/node/controlplane.go @@ -40,6 +40,7 @@ func NewControlPlane() workflow.Phase { options.CertificateRenewal, options.EtcdUpgrade, options.Kustomize, + options.Patches, }, } return phase @@ -65,16 +66,17 @@ func runControlPlane() func(c workflow.RunData) error { etcdUpgrade := data.EtcdUpgrade() renewCerts := data.RenewCerts() kustomizeDir := data.KustomizeDir() + patchesDir := data.PatchesDir() // Upgrade the control plane and etcd if installed on this node fmt.Printf("[upgrade] Upgrading your Static Pod-hosted control plane instance to version %q...\n", cfg.KubernetesVersion) if dryRun { - return upgrade.DryRunStaticPodUpgrade(kustomizeDir, cfg) + return upgrade.DryRunStaticPodUpgrade(kustomizeDir, patchesDir, cfg) } waiter := apiclient.NewKubeWaiter(data.Client(), upgrade.UpgradeManifestTimeout, os.Stdout) - if err := upgrade.PerformStaticPodUpgrade(client, waiter, cfg, etcdUpgrade, renewCerts, kustomizeDir); err != nil { + if err := upgrade.PerformStaticPodUpgrade(client, waiter, cfg, etcdUpgrade, renewCerts, kustomizeDir, patchesDir); err != nil { return errors.Wrap(err, "couldn't complete the static pod upgrade") } diff --git a/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go b/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go index 27878ad4308..91339d9aec2 100644 --- a/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go +++ b/cmd/kubeadm/app/cmd/phases/upgrade/node/data.go @@ -34,4 +34,5 @@ type Data interface { Client() clientset.Interface IgnorePreflightErrors() sets.String KustomizeDir() string + PatchesDir() string } diff --git a/cmd/kubeadm/app/cmd/upgrade/apply.go b/cmd/kubeadm/app/cmd/upgrade/apply.go index 536e12e392b..6af5afc71a3 100644 --- a/cmd/kubeadm/app/cmd/upgrade/apply.go +++ b/cmd/kubeadm/app/cmd/upgrade/apply.go @@ -52,6 +52,7 @@ type applyFlags struct { renewCerts bool imagePullTimeout time.Duration kustomizeDir string + patchesDir string } // sessionIsInteractive returns true if the session is of an interactive type (the default, can be opted out of with -y, -f or --dry-run) @@ -89,6 +90,7 @@ func NewCmdApply(apf *applyPlanFlags) *cobra.Command { // TODO: The flag was deprecated in 1.19; remove the flag following a GA deprecation policy of 12 months or 2 releases (whichever is longer) cmd.Flags().MarkDeprecated("image-pull-timeout", "This flag is deprecated and will be removed in a future version.") options.AddKustomizePodsFlag(cmd.Flags(), &flags.kustomizeDir) + options.AddPatchesFlag(cmd.Flags(), &flags.patchesDir) return cmd } @@ -215,8 +217,8 @@ func PerformControlPlaneUpgrade(flags *applyFlags, client clientset.Interface, w fmt.Printf("[upgrade/apply] Upgrading your Static Pod-hosted control plane to version %q...\n", internalcfg.KubernetesVersion) if flags.dryRun { - return upgrade.DryRunStaticPodUpgrade(flags.kustomizeDir, internalcfg) + return upgrade.DryRunStaticPodUpgrade(flags.kustomizeDir, flags.patchesDir, internalcfg) } - return upgrade.PerformStaticPodUpgrade(client, waiter, internalcfg, flags.etcdUpgrade, flags.renewCerts, flags.kustomizeDir) + return upgrade.PerformStaticPodUpgrade(client, waiter, internalcfg, flags.etcdUpgrade, flags.renewCerts, flags.kustomizeDir, flags.patchesDir) } diff --git a/cmd/kubeadm/app/cmd/upgrade/node.go b/cmd/kubeadm/app/cmd/upgrade/node.go index 4593291b0aa..6103d780938 100644 --- a/cmd/kubeadm/app/cmd/upgrade/node.go +++ b/cmd/kubeadm/app/cmd/upgrade/node.go @@ -44,6 +44,7 @@ type nodeOptions struct { renewCerts bool dryRun bool kustomizeDir string + patchesDir string ignorePreflightErrors []string } @@ -61,6 +62,7 @@ type nodeData struct { isControlPlaneNode bool client clientset.Interface kustomizeDir string + patchesDir string ignorePreflightErrors sets.String } @@ -82,6 +84,7 @@ func NewCmdNode() *cobra.Command { // flags could be eventually inherited by the sub-commands automatically generated for phases addUpgradeNodeFlags(cmd.Flags(), nodeOptions) options.AddKustomizePodsFlag(cmd.Flags(), &nodeOptions.kustomizeDir) + options.AddPatchesFlag(cmd.Flags(), &nodeOptions.patchesDir) // initialize the workflow runner with the list of phases nodeRunner.AppendPhase(phases.NewPreflightPhase()) @@ -162,6 +165,7 @@ func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions) (*node client: client, isControlPlaneNode: isControlPlaneNode, kustomizeDir: options.kustomizeDir, + patchesDir: options.patchesDir, ignorePreflightErrors: ignorePreflightErrorsSet, }, nil } @@ -206,6 +210,11 @@ func (d *nodeData) KustomizeDir() string { return d.kustomizeDir } +// PatchesDir returns the folder where patches for components are stored +func (d *nodeData) PatchesDir() string { + return d.patchesDir +} + // IgnorePreflightErrors returns the list of preflight errors to ignore. func (d *nodeData) IgnorePreflightErrors() sets.String { return d.ignorePreflightErrors diff --git a/cmd/kubeadm/app/phases/controlplane/manifests.go b/cmd/kubeadm/app/phases/controlplane/manifests.go index 55dd08ae450..bcd6db80b2c 100644 --- a/cmd/kubeadm/app/phases/controlplane/manifests.go +++ b/cmd/kubeadm/app/phases/controlplane/manifests.go @@ -19,6 +19,7 @@ package controlplane import ( "fmt" "net" + "os" "path/filepath" "strconv" "strings" @@ -37,9 +38,9 @@ import ( ) // CreateInitStaticPodManifestFiles will write all static pod manifest files needed to bring up the control plane. -func CreateInitStaticPodManifestFiles(manifestDir, kustomizeDir string, cfg *kubeadmapi.InitConfiguration) error { +func CreateInitStaticPodManifestFiles(manifestDir, kustomizeDir, patchesDir string, cfg *kubeadmapi.InitConfiguration) error { klog.V(1).Infoln("[control-plane] creating static Pod files") - return CreateStaticPodFiles(manifestDir, kustomizeDir, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, kubeadmconstants.KubeAPIServer, kubeadmconstants.KubeControllerManager, kubeadmconstants.KubeScheduler) + return CreateStaticPodFiles(manifestDir, kustomizeDir, patchesDir, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, kubeadmconstants.KubeAPIServer, kubeadmconstants.KubeControllerManager, kubeadmconstants.KubeScheduler) } // GetStaticPodSpecs returns all staticPodSpecs actualized to the context of the current configuration @@ -90,7 +91,7 @@ func GetStaticPodSpecs(cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmap } // CreateStaticPodFiles creates all the requested static pod files. -func CreateStaticPodFiles(manifestDir, kustomizeDir string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint, componentNames ...string) error { +func CreateStaticPodFiles(manifestDir, kustomizeDir, patchesDir string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint, componentNames ...string) error { // gets the StaticPodSpecs, actualized for the current ClusterConfiguration klog.V(1).Infoln("[control-plane] getting StaticPodSpecs") specs := GetStaticPodSpecs(cfg, endpoint) @@ -117,6 +118,15 @@ func CreateStaticPodFiles(manifestDir, kustomizeDir string, cfg *kubeadmapi.Clus spec = *kustomizedSpec } + // if patchesDir is defined, patch the static Pod manifest + if patchesDir != "" { + patchedSpec, err := staticpodutil.PatchStaticPod(&spec, patchesDir, os.Stdout) + if err != nil { + return errors.Wrapf(err, "failed to patch static Pod manifest file for %q", componentName) + } + spec = *patchedSpec + } + // writes the StaticPodSpec to disk if err := staticpodutil.WriteStaticPodToDisk(componentName, manifestDir, spec); err != nil { return errors.Wrapf(err, "failed to create static pod manifest file for %q", componentName) diff --git a/cmd/kubeadm/app/phases/controlplane/manifests_test.go b/cmd/kubeadm/app/phases/controlplane/manifests_test.go index d7592bec07c..b12dc383b2b 100644 --- a/cmd/kubeadm/app/phases/controlplane/manifests_test.go +++ b/cmd/kubeadm/app/phases/controlplane/manifests_test.go @@ -125,7 +125,7 @@ func TestCreateStaticPodFilesAndWrappers(t *testing.T) { // Execute createStaticPodFunction manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName) - err := CreateStaticPodFiles(manifestPath, "", cfg, &kubeadmapi.APIEndpoint{}, test.components...) + err := CreateStaticPodFiles(manifestPath, "", "", cfg, &kubeadmapi.APIEndpoint{}, test.components...) if err != nil { t.Errorf("Error executing createStaticPodFunction: %v", err) return @@ -174,7 +174,7 @@ func TestCreateStaticPodFilesKustomize(t *testing.T) { // Execute createStaticPodFunction with kustomizations manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName) - err = CreateStaticPodFiles(manifestPath, kustomizePath, cfg, &kubeadmapi.APIEndpoint{}, kubeadmconstants.KubeAPIServer) + err = CreateStaticPodFiles(manifestPath, kustomizePath, "", cfg, &kubeadmapi.APIEndpoint{}, kubeadmconstants.KubeAPIServer) if err != nil { t.Errorf("Error executing createStaticPodFunction: %v", err) return @@ -191,6 +191,52 @@ func TestCreateStaticPodFilesKustomize(t *testing.T) { } } +func TestCreateStaticPodFilesWithPatches(t *testing.T) { + // Create temp folder for the test case + tmpdir := testutil.SetupTempDir(t) + defer os.RemoveAll(tmpdir) + + // Creates a Cluster Configuration + cfg := &kubeadmapi.ClusterConfiguration{ + KubernetesVersion: "v1.9.0", + } + + patchesPath := filepath.Join(tmpdir, "patch-files") + err := os.MkdirAll(patchesPath, 0777) + if err != nil { + t.Fatalf("Couldn't create %s", patchesPath) + } + + patchString := dedent.Dedent(` + metadata: + annotations: + patched: "true" + `) + + err = ioutil.WriteFile(filepath.Join(patchesPath, kubeadmconstants.KubeAPIServer+".yaml"), []byte(patchString), 0644) + if err != nil { + t.Fatalf("WriteFile returned unexpected error: %v", err) + } + + // Execute createStaticPodFunction with patches + manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName) + err = CreateStaticPodFiles(manifestPath, "", patchesPath, cfg, &kubeadmapi.APIEndpoint{}, kubeadmconstants.KubeAPIServer) + if err != nil { + t.Errorf("Error executing createStaticPodFunction: %v", err) + return + } + + pod, err := staticpodutil.ReadStaticPodFromDisk(filepath.Join(manifestPath, fmt.Sprintf("%s.yaml", kubeadmconstants.KubeAPIServer))) + if err != nil { + t.Errorf("Error executing ReadStaticPodFromDisk: %v", err) + return + } + + if _, ok := pod.ObjectMeta.Annotations["patched"]; !ok { + t.Errorf("Patches were not applied to %s", kubeadmconstants.KubeAPIServer) + } +} + func TestGetAPIServerCommand(t *testing.T) { var tests = []struct { name string diff --git a/cmd/kubeadm/app/phases/etcd/local.go b/cmd/kubeadm/app/phases/etcd/local.go index fd9df8f7309..7ce5012755e 100644 --- a/cmd/kubeadm/app/phases/etcd/local.go +++ b/cmd/kubeadm/app/phases/etcd/local.go @@ -19,6 +19,7 @@ package etcd import ( "fmt" "net" + "os" "path/filepath" "strconv" "strings" @@ -48,7 +49,7 @@ const ( // CreateLocalEtcdStaticPodManifestFile will write local etcd static pod manifest file. // This function is used by init - when the etcd cluster is empty - or by kubeadm // upgrade - when the etcd cluster is already up and running (and the --initial-cluster flag have no impact) -func CreateLocalEtcdStaticPodManifestFile(manifestDir, kustomizeDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint) error { +func CreateLocalEtcdStaticPodManifestFile(manifestDir, kustomizeDir, patchesDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint) error { if cfg.Etcd.External != nil { return errors.New("etcd static pod manifest cannot be generated for cluster using external etcd") } @@ -64,6 +65,15 @@ func CreateLocalEtcdStaticPodManifestFile(manifestDir, kustomizeDir string, node spec = *kustomizedSpec } + // if patchesDir is defined, patch the static Pod manifest + if patchesDir != "" { + patchedSpec, err := staticpodutil.PatchStaticPod(&spec, patchesDir, os.Stdout) + if err != nil { + return errors.Wrapf(err, "failed to patch static Pod manifest file for %q", kubeadmconstants.Etcd) + } + spec = *patchedSpec + } + // writes etcd StaticPod to disk if err := staticpodutil.WriteStaticPodToDisk(kubeadmconstants.Etcd, manifestDir, spec); err != nil { return err @@ -141,7 +151,7 @@ func RemoveStackedEtcdMemberFromCluster(client clientset.Interface, cfg *kubeadm // CreateStackedEtcdStaticPodManifestFile will write local etcd static pod manifest file // for an additional etcd member that is joining an existing local/stacked etcd cluster. // Other members of the etcd cluster will be notified of the joining node in beforehand as well. -func CreateStackedEtcdStaticPodManifestFile(client clientset.Interface, manifestDir, kustomizeDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint) error { +func CreateStackedEtcdStaticPodManifestFile(client clientset.Interface, manifestDir, kustomizeDir, patchesDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint) error { // creates an etcd client that connects to all the local/stacked etcd members klog.V(1).Info("creating etcd client that connects to etcd pods") etcdClient, err := etcdutil.NewFromCluster(client, cfg.CertificatesDir) @@ -194,6 +204,15 @@ func CreateStackedEtcdStaticPodManifestFile(client clientset.Interface, manifest spec = *kustomizedSpec } + // if patchesDir is defined, patch the static Pod manifest + if patchesDir != "" { + patchedSpec, err := staticpodutil.PatchStaticPod(&spec, patchesDir, os.Stdout) + if err != nil { + return errors.Wrapf(err, "failed to patch static Pod manifest file for %q", kubeadmconstants.Etcd) + } + spec = *patchedSpec + } + // writes etcd StaticPod to disk if err := staticpodutil.WriteStaticPodToDisk(kubeadmconstants.Etcd, manifestDir, spec); err != nil { return err diff --git a/cmd/kubeadm/app/phases/etcd/local_test.go b/cmd/kubeadm/app/phases/etcd/local_test.go index b2c0dd0ccdc..42136dfa06a 100644 --- a/cmd/kubeadm/app/phases/etcd/local_test.go +++ b/cmd/kubeadm/app/phases/etcd/local_test.go @@ -96,7 +96,7 @@ func TestCreateLocalEtcdStaticPodManifestFile(t *testing.T) { for _, test := range tests { // Execute createStaticPodFunction manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName) - err := CreateLocalEtcdStaticPodManifestFile(manifestPath, "", "", test.cfg, &kubeadmapi.APIEndpoint{}) + err := CreateLocalEtcdStaticPodManifestFile(manifestPath, "", "", "", test.cfg, &kubeadmapi.APIEndpoint{}) if !test.expectedError { if err != nil { @@ -149,7 +149,7 @@ func TestCreateLocalEtcdStaticPodManifestFileKustomize(t *testing.T) { // Execute createStaticPodFunction with kustomizations manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName) - err = CreateLocalEtcdStaticPodManifestFile(manifestPath, kustomizePath, "", cfg, &kubeadmapi.APIEndpoint{}) + err = CreateLocalEtcdStaticPodManifestFile(manifestPath, kustomizePath, "", "", cfg, &kubeadmapi.APIEndpoint{}) if err != nil { t.Errorf("Error executing createStaticPodFunction: %v", err) return @@ -166,6 +166,56 @@ func TestCreateLocalEtcdStaticPodManifestFileKustomize(t *testing.T) { } } +func TestCreateLocalEtcdStaticPodManifestFileWithPatches(t *testing.T) { + // Create temp folder for the test case + tmpdir := testutil.SetupTempDir(t) + defer os.RemoveAll(tmpdir) + + // Creates a Cluster Configuration + cfg := &kubeadmapi.ClusterConfiguration{ + KubernetesVersion: "v1.7.0", + Etcd: kubeadmapi.Etcd{ + Local: &kubeadmapi.LocalEtcd{ + DataDir: tmpdir + "/etcd", + }, + }, + } + + patchesPath := filepath.Join(tmpdir, "patch-files") + err := os.MkdirAll(patchesPath, 0777) + if err != nil { + t.Fatalf("Couldn't create %s", patchesPath) + } + + patchString := dedent.Dedent(` + metadata: + annotations: + patched: "true" + `) + + err = ioutil.WriteFile(filepath.Join(patchesPath, kubeadmconstants.Etcd+".yaml"), []byte(patchString), 0644) + if err != nil { + t.Fatalf("WriteFile returned unexpected error: %v", err) + } + + manifestPath := filepath.Join(tmpdir, kubeadmconstants.ManifestsSubDirName) + err = CreateLocalEtcdStaticPodManifestFile(manifestPath, "", patchesPath, "", cfg, &kubeadmapi.APIEndpoint{}) + if err != nil { + t.Errorf("Error executing createStaticPodFunction: %v", err) + return + } + + pod, err := staticpodutil.ReadStaticPodFromDisk(filepath.Join(manifestPath, kubeadmconstants.Etcd+".yaml")) + if err != nil { + t.Errorf("Error executing ReadStaticPodFromDisk: %v", err) + return + } + + if _, ok := pod.ObjectMeta.Annotations["patched"]; !ok { + t.Errorf("Patches were not applied to %s", kubeadmconstants.Etcd) + } +} + func TestGetEtcdCommand(t *testing.T) { var tests = []struct { name string diff --git a/cmd/kubeadm/app/phases/upgrade/staticpods.go b/cmd/kubeadm/app/phases/upgrade/staticpods.go index 309272f711d..64d13abb310 100644 --- a/cmd/kubeadm/app/phases/upgrade/staticpods.go +++ b/cmd/kubeadm/app/phases/upgrade/staticpods.go @@ -55,6 +55,8 @@ type StaticPodPathManager interface { KubernetesDir() string // KustomizeDir should point to the folder where kustomize patches for static pod manifest are stored KustomizeDir() string + // PatchesDir should point to the folder where patches for components are stored + PatchesDir() string // RealManifestPath gets the file path for the component in the "real" static pod manifest directory used by the kubelet RealManifestPath(component string) string // RealManifestDir should point to the static pod manifest directory used by the kubelet @@ -77,6 +79,7 @@ type StaticPodPathManager interface { type KubeStaticPodPathManager struct { kubernetesDir string kustomizeDir string + patchesDir string realManifestDir string tempManifestDir string backupManifestDir string @@ -87,10 +90,11 @@ type KubeStaticPodPathManager struct { } // NewKubeStaticPodPathManager creates a new instance of KubeStaticPodPathManager -func NewKubeStaticPodPathManager(kubernetesDir, kustomizeDir, tempDir, backupDir, backupEtcdDir string, keepManifestDir, keepEtcdDir bool) StaticPodPathManager { +func NewKubeStaticPodPathManager(kubernetesDir, kustomizeDir, patchesDir, tempDir, backupDir, backupEtcdDir string, keepManifestDir, keepEtcdDir bool) StaticPodPathManager { return &KubeStaticPodPathManager{ kubernetesDir: kubernetesDir, kustomizeDir: kustomizeDir, + patchesDir: patchesDir, realManifestDir: filepath.Join(kubernetesDir, constants.ManifestsSubDirName), tempManifestDir: tempDir, backupManifestDir: backupDir, @@ -101,7 +105,7 @@ func NewKubeStaticPodPathManager(kubernetesDir, kustomizeDir, tempDir, backupDir } // NewKubeStaticPodPathManagerUsingTempDirs creates a new instance of KubeStaticPodPathManager with temporary directories backing it -func NewKubeStaticPodPathManagerUsingTempDirs(kubernetesDir, kustomizeDir string, saveManifestsDir, saveEtcdDir bool) (StaticPodPathManager, error) { +func NewKubeStaticPodPathManagerUsingTempDirs(kubernetesDir, kustomizeDir, patchesDir string, saveManifestsDir, saveEtcdDir bool) (StaticPodPathManager, error) { upgradedManifestsDir, err := constants.CreateTempDirForKubeadm(kubernetesDir, "kubeadm-upgraded-manifests") if err != nil { @@ -116,7 +120,7 @@ func NewKubeStaticPodPathManagerUsingTempDirs(kubernetesDir, kustomizeDir string return nil, err } - return NewKubeStaticPodPathManager(kubernetesDir, kustomizeDir, upgradedManifestsDir, backupManifestsDir, backupEtcdDir, saveManifestsDir, saveEtcdDir), nil + return NewKubeStaticPodPathManager(kubernetesDir, kustomizeDir, patchesDir, upgradedManifestsDir, backupManifestsDir, backupEtcdDir, saveManifestsDir, saveEtcdDir), nil } // MoveFile should move a file from oldPath to newPath @@ -134,6 +138,11 @@ func (spm *KubeStaticPodPathManager) KustomizeDir() string { return spm.kustomizeDir } +// PatchesDir should point to the folder where patches for components are stored +func (spm *KubeStaticPodPathManager) PatchesDir() string { + return spm.patchesDir +} + // RealManifestPath gets the file path for the component in the "real" static pod manifest directory used by the kubelet func (spm *KubeStaticPodPathManager) RealManifestPath(component string) string { return constants.GetStaticPodFilepath(component, spm.realManifestDir) @@ -323,7 +332,7 @@ func performEtcdStaticPodUpgrade(certsRenewMgr *renewal.Manager, client clientse // Write the updated etcd static Pod manifest into the temporary directory, at this point no etcd change // has occurred in any aspects. - if err := etcdphase.CreateLocalEtcdStaticPodManifestFile(pathMgr.TempManifestDir(), pathMgr.KustomizeDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint); err != nil { + if err := etcdphase.CreateLocalEtcdStaticPodManifestFile(pathMgr.TempManifestDir(), pathMgr.KustomizeDir(), pathMgr.PatchesDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint); err != nil { return true, errors.Wrap(err, "error creating local etcd static pod manifest file") } @@ -469,7 +478,7 @@ func StaticPodControlPlane(client clientset.Interface, waiter apiclient.Waiter, // Write the updated static Pod manifests into the temporary directory fmt.Printf("[upgrade/staticpods] Writing new Static Pod manifests to %q\n", pathMgr.TempManifestDir()) - err = controlplane.CreateInitStaticPodManifestFiles(pathMgr.TempManifestDir(), pathMgr.KustomizeDir(), cfg) + err = controlplane.CreateInitStaticPodManifestFiles(pathMgr.TempManifestDir(), pathMgr.KustomizeDir(), pathMgr.PatchesDir(), cfg) if err != nil { return errors.Wrap(err, "error creating init static pod manifest files") } @@ -596,14 +605,14 @@ func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, component string, } // GetPathManagerForUpgrade returns a path manager properly configured for the given InitConfiguration. -func GetPathManagerForUpgrade(kubernetesDir, kustomizeDir string, internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade bool) (StaticPodPathManager, error) { +func GetPathManagerForUpgrade(kubernetesDir, kustomizeDir, patchesDir string, internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade bool) (StaticPodPathManager, error) { isExternalEtcd := internalcfg.Etcd.External != nil - return NewKubeStaticPodPathManagerUsingTempDirs(kubernetesDir, kustomizeDir, true, etcdUpgrade && !isExternalEtcd) + return NewKubeStaticPodPathManagerUsingTempDirs(kubernetesDir, kustomizeDir, patchesDir, true, etcdUpgrade && !isExternalEtcd) } // PerformStaticPodUpgrade performs the upgrade of the control plane components for a static pod hosted cluster -func PerformStaticPodUpgrade(client clientset.Interface, waiter apiclient.Waiter, internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade, renewCerts bool, kustomizeDir string) error { - pathManager, err := GetPathManagerForUpgrade(constants.KubernetesDir, kustomizeDir, internalcfg, etcdUpgrade) +func PerformStaticPodUpgrade(client clientset.Interface, waiter apiclient.Waiter, internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade, renewCerts bool, kustomizeDir, patchesDir string) error { + pathManager, err := GetPathManagerForUpgrade(constants.KubernetesDir, kustomizeDir, patchesDir, internalcfg, etcdUpgrade) if err != nil { return err } @@ -613,14 +622,14 @@ func PerformStaticPodUpgrade(client clientset.Interface, waiter apiclient.Waiter } // DryRunStaticPodUpgrade fakes an upgrade of the control plane -func DryRunStaticPodUpgrade(kustomizeDir string, internalcfg *kubeadmapi.InitConfiguration) error { +func DryRunStaticPodUpgrade(kustomizeDir, patchesDir string, internalcfg *kubeadmapi.InitConfiguration) error { dryRunManifestDir, err := constants.CreateTempDirForKubeadm("", "kubeadm-upgrade-dryrun") if err != nil { return err } defer os.RemoveAll(dryRunManifestDir) - if err := controlplane.CreateInitStaticPodManifestFiles(dryRunManifestDir, kustomizeDir, internalcfg); err != nil { + if err := controlplane.CreateInitStaticPodManifestFiles(dryRunManifestDir, kustomizeDir, patchesDir, internalcfg); err != nil { return err } diff --git a/cmd/kubeadm/app/phases/upgrade/staticpods_test.go b/cmd/kubeadm/app/phases/upgrade/staticpods_test.go index e97c80e9a83..d206672dd71 100644 --- a/cmd/kubeadm/app/phases/upgrade/staticpods_test.go +++ b/cmd/kubeadm/app/phases/upgrade/staticpods_test.go @@ -144,6 +144,7 @@ func (w *fakeWaiter) WaitForKubeletAndFunc(f func() error) error { type fakeStaticPodPathManager struct { kubernetesDir string kustomizeDir string + patchesDir string realManifestDir string tempManifestDir string backupManifestDir string @@ -199,6 +200,10 @@ func (spm *fakeStaticPodPathManager) KustomizeDir() string { return spm.kustomizeDir } +func (spm *fakeStaticPodPathManager) PatchesDir() string { + return spm.patchesDir +} + func (spm *fakeStaticPodPathManager) RealManifestPath(component string) string { return constants.GetStaticPodFilepath(component, spm.realManifestDir) } @@ -488,11 +493,11 @@ func TestStaticPodControlPlane(t *testing.T) { } // Initialize the directory with v1.7 manifests; should then be upgraded to v1.8 using the method - err = controlplanephase.CreateInitStaticPodManifestFiles(pathMgr.RealManifestDir(), pathMgr.KustomizeDir(), oldcfg) + err = controlplanephase.CreateInitStaticPodManifestFiles(pathMgr.RealManifestDir(), pathMgr.KustomizeDir(), pathMgr.PatchesDir(), oldcfg) if err != nil { t.Fatalf("couldn't run CreateInitStaticPodManifestFiles: %v", err) } - err = etcdphase.CreateLocalEtcdStaticPodManifestFile(pathMgr.RealManifestDir(), pathMgr.KustomizeDir(), oldcfg.NodeRegistration.Name, &oldcfg.ClusterConfiguration, &oldcfg.LocalAPIEndpoint) + err = etcdphase.CreateLocalEtcdStaticPodManifestFile(pathMgr.RealManifestDir(), pathMgr.KustomizeDir(), pathMgr.PatchesDir(), oldcfg.NodeRegistration.Name, &oldcfg.ClusterConfiguration, &oldcfg.LocalAPIEndpoint) if err != nil { t.Fatalf("couldn't run CreateLocalEtcdStaticPodManifestFile: %v", err) } @@ -628,7 +633,7 @@ func TestCleanupDirs(t *testing.T) { backupEtcdDir, cleanup := getTempDir(t, "backupEtcdDir") defer cleanup() - mgr := NewKubeStaticPodPathManager(realKubernetesDir, "", tempManifestDir, backupManifestDir, backupEtcdDir, test.keepManifest, test.keepEtcd) + mgr := NewKubeStaticPodPathManager(realKubernetesDir, "", "", tempManifestDir, backupManifestDir, backupEtcdDir, test.keepManifest, test.keepEtcd) err := mgr.CleanupDirs() if err != nil { t.Errorf("unexpected error cleaning up: %v", err) @@ -943,7 +948,7 @@ func TestGetPathManagerForUpgrade(t *testing.T) { os.RemoveAll(tmpdir) }() - pathmgr, err := GetPathManagerForUpgrade(tmpdir, "", test.cfg, test.etcdUpgrade) + pathmgr, err := GetPathManagerForUpgrade(tmpdir, "", "", test.cfg, test.etcdUpgrade) if err != nil { t.Fatalf("unexpected error creating path manager: %v", err) } diff --git a/cmd/kubeadm/app/util/BUILD b/cmd/kubeadm/app/util/BUILD index 7eed2645d16..a919803f21a 100644 --- a/cmd/kubeadm/app/util/BUILD +++ b/cmd/kubeadm/app/util/BUILD @@ -89,6 +89,7 @@ filegroup( "//cmd/kubeadm/app/util/kubeconfig:all-srcs", "//cmd/kubeadm/app/util/kustomize:all-srcs", "//cmd/kubeadm/app/util/output:all-srcs", + "//cmd/kubeadm/app/util/patches:all-srcs", "//cmd/kubeadm/app/util/pkiutil:all-srcs", "//cmd/kubeadm/app/util/pubkeypin:all-srcs", "//cmd/kubeadm/app/util/runtime:all-srcs", diff --git a/cmd/kubeadm/app/util/patches/BUILD b/cmd/kubeadm/app/util/patches/BUILD new file mode 100644 index 00000000000..298bc3aa121 --- /dev/null +++ b/cmd/kubeadm/app/util/patches/BUILD @@ -0,0 +1,40 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["patches.go"], + importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/patches", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", + "//vendor/github.com/evanphx/json-patch:go_default_library", + "//vendor/github.com/pkg/errors:go_default_library", + "//vendor/sigs.k8s.io/yaml:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["patches_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/api/core/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/kubeadm/app/util/patches/patches.go b/cmd/kubeadm/app/util/patches/patches.go new file mode 100644 index 00000000000..3b1ad23d0e3 --- /dev/null +++ b/cmd/kubeadm/app/util/patches/patches.go @@ -0,0 +1,342 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package patches + +import ( + "bufio" + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/strategicpatch" + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/yaml" +) + +// PatchTarget defines a target to be patched, such as a control-plane static Pod. +type PatchTarget struct { + // Name must be the name of a known target. In the case of Kubernetes objects + // this is likely to match the ObjectMeta.Name of a target. + Name string + + // StrategicMergePatchObject is only used for strategic merge patches. + // It represents the underlying object type that is patched - e.g. "v1.Pod" + StrategicMergePatchObject interface{} + + // Data must contain the bytes that will be patched. + Data []byte +} + +// PatchManager defines an object that can apply patches. +type PatchManager struct { + patchSets []*patchSet + knownTargets []string + output io.Writer +} + +// patchSet defines a set of patches of a certain type that can patch a PatchTarget. +type patchSet struct { + targetName string + patchType types.PatchType + patches []string +} + +// String() is used for unit-testing. +func (ps *patchSet) String() string { + return fmt.Sprintf( + "{%q, %q, %#v}", + ps.targetName, + ps.patchType, + ps.patches, + ) +} + +var ( + pathLock = &sync.RWMutex{} + pathCache = map[string]*PatchManager{} + + patchTypes = map[string]types.PatchType{ + "json": types.JSONPatchType, + "merge": types.MergePatchType, + "strategic": types.StrategicMergePatchType, + "": types.StrategicMergePatchType, // Default + } + patchTypeList = []string{"json", "merge", "strategic"} + patchTypesJoined = strings.Join(patchTypeList, "|") + knownExtensions = []string{"json", "yaml"} + + regExtension = regexp.MustCompile(`.+\.(` + strings.Join(knownExtensions, "|") + `)$`) +) + +// GetPatchManagerForPath creates a patch manager that can be used to apply patches to "knownTargets". +// "path" should contain patches that can be used to patch the "knownTargets". +// If "output" is non-nil, messages about actions performed by the manager would go on this io.Writer. +func GetPatchManagerForPath(path string, knownTargets []string, output io.Writer) (*PatchManager, error) { + pathLock.RLock() + if pm, known := pathCache[path]; known { + pathLock.RUnlock() + return pm, nil + } + pathLock.RUnlock() + + if output == nil { + output = ioutil.Discard + } + + fmt.Fprintf(output, "[patches] Reading patches from path %q\n", path) + + // Get the files in the path. + patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(path, knownTargets, output) + if err != nil { + return nil, err + } + + if len(patchFiles) > 0 { + fmt.Fprintf(output, "[patches] Found the following patch files: %v\n", patchFiles) + } + if len(ignoredFiles) > 0 { + fmt.Fprintf(output, "[patches] Ignored the following files: %v\n", ignoredFiles) + } + + pm := &PatchManager{ + patchSets: patchSets, + knownTargets: knownTargets, + output: output, + } + pathLock.Lock() + pathCache[path] = pm + pathLock.Unlock() + + return pm, nil +} + +// ApplyPatchesToTarget takes a patch target and patches its "Data" using the patches +// stored in the patch manager. The resulted "Data" is always converted to JSON. +func (pm *PatchManager) ApplyPatchesToTarget(patchTarget *PatchTarget) error { + var err error + var patchedData []byte + + var found bool + for _, pt := range pm.knownTargets { + if pt == patchTarget.Name { + found = true + break + } + } + if !found { + return errors.Errorf("unknown patch target name %q, must be one of %v", patchTarget.Name, pm.knownTargets) + } + + // Always convert the target data to JSON. + patchedData, err = yaml.YAMLToJSON(patchTarget.Data) + if err != nil { + return err + } + + // Iterate over the patchSets. + for _, patchSet := range pm.patchSets { + if patchSet.targetName != patchTarget.Name { + continue + } + + // Iterate over the patches in the patchSets. + for _, patch := range patchSet.patches { + patchBytes := []byte(patch) + + // Patch based on the patch type. + switch patchSet.patchType { + + // JSON patch. + case types.JSONPatchType: + var patchObj jsonpatch.Patch + patchObj, err = jsonpatch.DecodePatch(patchBytes) + if err == nil { + patchedData, err = patchObj.Apply(patchedData) + } + + // Merge patch. + case types.MergePatchType: + patchedData, err = jsonpatch.MergePatch(patchedData, patchBytes) + + // Strategic merge patch. + case types.StrategicMergePatchType: + patchedData, err = strategicpatch.StrategicMergePatch( + patchedData, + patchBytes, + patchTarget.StrategicMergePatchObject, + ) + } + + if err != nil { + return errors.Wrapf(err, "could not apply the following patch of type %q to target %q:\n%s\n", + patchSet.patchType, + patchTarget.Name, + patch) + } + fmt.Fprintf(pm.output, "[patches] Applied patch of type %q to target %q\n", patchSet.patchType, patchTarget.Name) + } + + // Update the data for this patch target. + patchTarget.Data = patchedData + } + + return nil +} + +// parseFilename validates a file name and retrieves the encoded target name and patch type. +// - On unknown extension or target name it returns a warning +// - On unknown patch type it returns an error +// - On success it returns a target name and patch type +func parseFilename(fileName string, knownTargets []string) (string, types.PatchType, error, error) { + // Return a warning if the extension cannot be matched. + if !regExtension.MatchString(fileName) { + return "", "", errors.Errorf("the file extension must be one of %v", knownExtensions), nil + } + + regFileNameSplit := regexp.MustCompile( + fmt.Sprintf(`^(%s)([^.+\n]*)?(\+)?(%s)?`, strings.Join(knownTargets, "|"), patchTypesJoined), + ) + // Extract the target name and patch type. The resulting sub-string slice would look like this: + // [full-match, targetName, suffix, +, patchType] + sub := regFileNameSplit.FindStringSubmatch(fileName) + if sub == nil { + return "", "", errors.Errorf("unknown target, must be one of %v", knownTargets), nil + } + targetName := sub[1] + + if len(sub[3]) > 0 && len(sub[4]) == 0 { + return "", "", nil, errors.Errorf("unknown or missing patch type after '+', must be one of %v", patchTypeList) + } + patchType := patchTypes[sub[4]] + + return targetName, patchType, nil, nil +} + +// createPatchSet creates a patchSet object, by splitting the given "data" by "\n---". +func createPatchSet(targetName string, patchType types.PatchType, data string) (*patchSet, error) { + var patches []string + + // Split the patches and convert them to JSON. + // Data that is already JSON will not cause an error. + buf := bytes.NewBuffer([]byte(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(buf)) + for { + patch, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + return nil, errors.Wrapf(err, "could not split patches for data:\n%s\n", data) + } + + patch = bytes.TrimSpace(patch) + if len(patch) == 0 { + continue + } + + patchJSON, err := yaml.YAMLToJSON(patch) + if err != nil { + return nil, errors.Wrapf(err, "could not convert patch to JSON:\n%s\n", patch) + } + patches = append(patches, string(patchJSON)) + } + + return &patchSet{ + targetName: targetName, + patchType: patchType, + patches: patches, + }, nil +} + +// getPatchSetsFromPath walks a path, ignores sub-directories and non-patch files, and +// returns a list of patchFile objects. +func getPatchSetsFromPath(targetPath string, knownTargets []string, output io.Writer) ([]*patchSet, []string, []string, error) { + patchFiles := []string{} + ignoredFiles := []string{} + patchSets := []*patchSet{} + + // Check if targetPath is a directory. + info, err := os.Lstat(targetPath) + if err != nil { + goto return_path_error + } + if !info.IsDir() { + err = errors.New("not a directory") + goto return_path_error + } + + err = filepath.Walk(targetPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Sub-directories and "." are ignored. + if info.IsDir() { + return nil + } + + baseName := info.Name() + + // Parse the filename and retrieve the target and patch type + targetName, patchType, warn, err := parseFilename(baseName, knownTargets) + if err != nil { + return err + } + if warn != nil { + fmt.Fprintf(output, "[patches] Ignoring file %q: %v\n", baseName, warn) + ignoredFiles = append(ignoredFiles, baseName) + return nil + } + + // Read the patch file. + data, err := ioutil.ReadFile(path) + if err != nil { + return errors.Wrapf(err, "could not read the file %q", path) + } + + if len(data) == 0 { + fmt.Fprintf(output, "[patches] Ignoring empty file: %q\n", baseName) + ignoredFiles = append(ignoredFiles, baseName) + return nil + } + + // Create a patchSet object. + patchSet, err := createPatchSet(targetName, patchType, string(data)) + if err != nil { + return err + } + + patchFiles = append(patchFiles, baseName) + patchSets = append(patchSets, patchSet) + return nil + }) + +return_path_error: + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "could not list patch files for path %q", targetPath) + } + + return patchSets, patchFiles, ignoredFiles, nil +} diff --git a/cmd/kubeadm/app/util/patches/patches_test.go b/cmd/kubeadm/app/util/patches/patches_test.go new file mode 100644 index 00000000000..7d2e8f18a67 --- /dev/null +++ b/cmd/kubeadm/app/util/patches/patches_test.go @@ -0,0 +1,413 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package patches + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +var testKnownTargets = []string{ + "etcd", + "kube-apiserver", + "kube-controller-manager", + "kube-scheduler", +} + +const testDirPattern = "patch-files" + +func TestParseFilename(t *testing.T) { + tests := []struct { + name string + fileName string + expectedTargetName string + expectedPatchType types.PatchType + expectedWarning bool + expectedError bool + }{ + { + name: "valid: known target and patch type", + fileName: "etcd+merge.json", + expectedTargetName: "etcd", + expectedPatchType: types.MergePatchType, + }, + { + name: "valid: known target and default patch type", + fileName: "etcd0.yaml", + expectedTargetName: "etcd", + expectedPatchType: types.StrategicMergePatchType, + }, + { + name: "valid: known target and custom patch type", + fileName: "etcd0+merge.yaml", + expectedTargetName: "etcd", + expectedPatchType: types.MergePatchType, + }, + { + name: "invalid: unknown target", + fileName: "foo.yaml", + expectedWarning: true, + }, + { + name: "invalid: unknown extension", + fileName: "etcd.foo", + expectedWarning: true, + }, + { + name: "invalid: missing extension", + fileName: "etcd", + expectedWarning: true, + }, + { + name: "invalid: unknown patch type", + fileName: "etcd+foo.json", + expectedError: true, + }, + { + name: "invalid: missing patch type", + fileName: "etcd+.json", + expectedError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + targetName, patchType, warn, err := parseFilename(tc.fileName, testKnownTargets) + if (err != nil) != tc.expectedError { + t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) + } + if (warn != nil) != tc.expectedWarning { + t.Errorf("expected warning: %v, got: %v, warning: %v", tc.expectedWarning, warn != nil, warn) + } + if targetName != tc.expectedTargetName { + t.Errorf("expected target name: %v, got: %v", tc.expectedTargetName, targetName) + } + if patchType != tc.expectedPatchType { + t.Errorf("expected patch type: %v, got: %v", tc.expectedPatchType, patchType) + } + }) + } +} + +func TestCreatePatchSet(t *testing.T) { + tests := []struct { + name string + targetName string + patchType types.PatchType + expectedPatchSet *patchSet + data string + }{ + { + + name: "valid: YAML patches are separated and converted to JSON", + targetName: "etcd", + patchType: types.StrategicMergePatchType, + data: "foo: bar\n---\nfoo: baz\n", + expectedPatchSet: &patchSet{ + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, + }, + }, + { + name: "valid: JSON patches are separated", + targetName: "etcd", + patchType: types.StrategicMergePatchType, + data: `{"foo":"bar"}` + "\n---\n" + `{"foo":"baz"}`, + expectedPatchSet: &patchSet{ + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, + }, + }, + { + name: "valid: empty patches are ignored", + targetName: "etcd", + patchType: types.StrategicMergePatchType, + data: `{"foo":"bar"}` + "\n---\n ---\n" + `{"foo":"baz"}`, + expectedPatchSet: &patchSet{ + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ps, _ := createPatchSet(tc.targetName, tc.patchType, tc.data) + if !reflect.DeepEqual(ps, tc.expectedPatchSet) { + t.Fatalf("expected patch set:\n%+v\ngot:\n%+v\n", tc.expectedPatchSet, ps) + } + }) + } +} + +func TestGetPatchSetsForPathMustBeDirectory(t *testing.T) { + tempFile, err := ioutil.TempFile("", "test-file") + if err != nil { + t.Errorf("error creating temporary file: %v", err) + } + defer os.Remove(tempFile.Name()) + + _, _, _, err = getPatchSetsFromPath(tempFile.Name(), testKnownTargets, ioutil.Discard) + if err == nil { + t.Fatalf("expected error for non-directory path %q", tempFile.Name()) + } +} + +func TestGetPatchSetsForPath(t *testing.T) { + const patchData = `{"foo":"bar"}` + + tests := []struct { + name string + filesToWrite []string + expectedPatchSets []*patchSet + expectedPatchFiles []string + expectedIgnoredFiles []string + expectedError bool + patchData string + }{ + { + name: "valid: patch files are sorted and non-patch files are ignored", + filesToWrite: []string{"kube-scheduler+merge.json", "kube-apiserver+json.yaml", "etcd.yaml", "foo", "bar.json"}, + patchData: patchData, + expectedPatchSets: []*patchSet{ + { + targetName: "etcd", + patchType: types.StrategicMergePatchType, + patches: []string{patchData}, + }, + { + targetName: "kube-apiserver", + patchType: types.JSONPatchType, + patches: []string{patchData}, + }, + { + targetName: "kube-scheduler", + patchType: types.MergePatchType, + patches: []string{patchData}, + }, + }, + expectedPatchFiles: []string{"etcd.yaml", "kube-apiserver+json.yaml", "kube-scheduler+merge.json"}, + expectedIgnoredFiles: []string{"bar.json", "foo"}, + }, + { + name: "valid: empty files are ignored", + patchData: "", + filesToWrite: []string{"kube-scheduler.json"}, + expectedPatchFiles: []string{}, + expectedIgnoredFiles: []string{"kube-scheduler.json"}, + expectedPatchSets: []*patchSet{}, + }, + { + name: "invalid: bad patch type in filename returns and error", + filesToWrite: []string{"kube-scheduler+foo.json"}, + expectedError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", testDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + for _, file := range tc.filesToWrite { + filePath := filepath.Join(tempDir, file) + err := ioutil.WriteFile(filePath, []byte(tc.patchData), 0644) + if err != nil { + t.Fatalf("could not write temporary file %q", filePath) + } + } + + patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(tempDir, testKnownTargets, ioutil.Discard) + if (err != nil) != tc.expectedError { + t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) + } + + if !reflect.DeepEqual(tc.expectedPatchFiles, patchFiles) { + t.Fatalf("expected patch files:\n%+v\ngot:\n%+v", tc.expectedPatchFiles, patchFiles) + } + if !reflect.DeepEqual(tc.expectedIgnoredFiles, ignoredFiles) { + t.Fatalf("expected ignored files:\n%+v\ngot:\n%+v", tc.expectedIgnoredFiles, ignoredFiles) + } + if !reflect.DeepEqual(tc.expectedPatchSets, patchSets) { + t.Fatalf("expected patch sets:\n%+v\ngot:\n%+v", tc.expectedPatchSets, patchSets) + } + }) + } +} + +func TestGetPatchManagerForPath(t *testing.T) { + type file struct { + name string + data string + } + + tests := []struct { + name string + files []*file + patchTarget *PatchTarget + expectedData []byte + expectedError bool + }{ + { + name: "valid: patch a kube-apiserver target using merge patch; json patch is applied first", + patchTarget: &PatchTarget{ + Name: "kube-apiserver", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\nbaz: qux\n"), + }, + expectedData: []byte(`{"baz":"qux","foo":"patched"}`), + files: []*file{ + { + name: "kube-apiserver+merge.yaml", + data: "foo: patched", + }, + { + name: "kube-apiserver+json.json", + data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, + }, + }, + }, + { + name: "valid: kube-apiserver target is patched with json patch", + patchTarget: &PatchTarget{ + Name: "kube-apiserver", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + expectedData: []byte(`{"foo":"zzz"}`), + files: []*file{ + { + name: "kube-apiserver+json.json", + data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, + }, + }, + }, + { + name: "valid: kube-apiserver target is patched with strategic merge patch", + patchTarget: &PatchTarget{ + Name: "kube-apiserver", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + expectedData: []byte(`{"foo":"zzz"}`), + files: []*file{ + { + name: "kube-apiserver+strategic.json", + data: `{"foo":"zzz"}`, + }, + }, + }, + { + name: "valid: etcd target is not changed because there are no patches for it", + patchTarget: &PatchTarget{ + Name: "etcd", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + expectedData: []byte("foo: bar\n"), + files: []*file{ + { + name: "kube-apiserver+merge.yaml", + data: "foo: patched", + }, + }, + }, + { + name: "invalid: cannot patch etcd target due to malformed json patch", + patchTarget: &PatchTarget{ + Name: "etcd", + StrategicMergePatchObject: v1.Pod{}, + Data: []byte("foo: bar\n"), + }, + files: []*file{ + { + name: "etcd+json.json", + data: `{"foo":"zzz"}`, + }, + }, + expectedError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", testDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + for _, file := range tc.files { + filePath := filepath.Join(tempDir, file.name) + err := ioutil.WriteFile(filePath, []byte(file.data), 0644) + if err != nil { + t.Fatalf("could not write temporary file %q", filePath) + } + } + + pm, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) + if err != nil { + t.Fatal(err) + } + + err = pm.ApplyPatchesToTarget(tc.patchTarget) + if (err != nil) != tc.expectedError { + t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) + } + if err != nil { + return + } + + if !bytes.Equal(tc.patchTarget.Data, tc.expectedData) { + t.Fatalf("expected result:\n%s\ngot:\n%s", tc.expectedData, tc.patchTarget.Data) + } + }) + } +} + +func TestGetPatchManagerForPathCache(t *testing.T) { + tempDir, err := ioutil.TempDir("", testDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + pmOld, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) + if err != nil { + t.Fatal(err) + } + pmNew, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) + if err != nil { + t.Fatal(err) + } + if pmOld != pmNew { + t.Logf("path %q was not cached, expected pointer: %p, got: %p", tempDir, pmOld, pmNew) + } +} diff --git a/cmd/kubeadm/app/util/staticpod/BUILD b/cmd/kubeadm/app/util/staticpod/BUILD index eae28fe765b..e3c3cdbc4ed 100644 --- a/cmd/kubeadm/app/util/staticpod/BUILD +++ b/cmd/kubeadm/app/util/staticpod/BUILD @@ -29,6 +29,7 @@ go_library( "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/util:go_default_library", "//cmd/kubeadm/app/util/kustomize:go_default_library", + "//cmd/kubeadm/app/util/patches:go_default_library", "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", diff --git a/cmd/kubeadm/app/util/staticpod/utils.go b/cmd/kubeadm/app/util/staticpod/utils.go index aa8d0f77a1b..fd243165113 100644 --- a/cmd/kubeadm/app/util/staticpod/utils.go +++ b/cmd/kubeadm/app/util/staticpod/utils.go @@ -19,6 +19,7 @@ package staticpod import ( "bytes" "fmt" + "io" "io/ioutil" "math" "net/url" @@ -37,6 +38,7 @@ import ( kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util/kustomize" + "k8s.io/kubernetes/cmd/kubeadm/app/util/patches" ) const ( @@ -180,6 +182,48 @@ func KustomizeStaticPod(pod *v1.Pod, kustomizeDir string) (*v1.Pod, error) { return pod2, nil } +// PatchStaticPod applies patches stored in patchesDir to a static Pod. +func PatchStaticPod(pod *v1.Pod, patchesDir string, output io.Writer) (*v1.Pod, error) { + // Marshal the Pod manifest into YAML. + podYAML, err := kubeadmutil.MarshalToYaml(pod, v1.SchemeGroupVersion) + if err != nil { + return pod, errors.Wrapf(err, "failed to marshal Pod manifest to YAML") + } + + var knownTargets = []string{ + kubeadmconstants.Etcd, + kubeadmconstants.KubeAPIServer, + kubeadmconstants.KubeControllerManager, + kubeadmconstants.KubeScheduler, + } + + patchManager, err := patches.GetPatchManagerForPath(patchesDir, knownTargets, output) + if err != nil { + return pod, err + } + + patchTarget := &patches.PatchTarget{ + Name: pod.Name, + StrategicMergePatchObject: v1.Pod{}, + Data: podYAML, + } + if err := patchManager.ApplyPatchesToTarget(patchTarget); err != nil { + return pod, err + } + + obj, err := kubeadmutil.UnmarshalFromYaml(patchTarget.Data, v1.SchemeGroupVersion) + if err != nil { + return pod, errors.Wrap(err, "failed to unmarshal patched manifest from YAML") + } + + pod2, ok := obj.(*v1.Pod) + if !ok { + return pod, errors.Wrap(err, "patched manifest is not a valid Pod object") + } + + return pod2, nil +} + // WriteStaticPodToDisk writes a static pod file to disk func WriteStaticPodToDisk(componentName, manifestDir string, pod v1.Pod) error { diff --git a/cmd/kubeadm/app/util/staticpod/utils_test.go b/cmd/kubeadm/app/util/staticpod/utils_test.go index 4071df9a126..23b88271d6d 100644 --- a/cmd/kubeadm/app/util/staticpod/utils_test.go +++ b/cmd/kubeadm/app/util/staticpod/utils_test.go @@ -796,3 +796,84 @@ func TestKustomizeStaticPod(t *testing.T) { t.Error("Kustomize did not apply patches corresponding to the resource") } } + +func TestPatchStaticPod(t *testing.T) { + type file struct { + name string + data string + } + + tests := []struct { + name string + files []*file + pod *v1.Pod + expectedPod *v1.Pod + expectedError bool + }{ + { + name: "valid: patch a kube-apiserver target using a couple of ordered patches", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver", + Namespace: "foo", + }, + }, + expectedPod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-apiserver", + Namespace: "bar2", + }, + }, + files: []*file{ + { + name: "kube-apiserver1+merge.json", + data: `{"metadata":{"namespace":"bar2"}}`, + }, + { + name: "kube-apiserver0+json.json", + data: `[{"op": "replace", "path": "/metadata/namespace", "value": "bar1"}]`, + }, + }, + }, + { + name: "invalid: unknown patch target name", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + }, + }, + expectedError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tempDir, err := ioutil.TempDir("", "patch-files") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + for _, file := range tc.files { + filePath := filepath.Join(tempDir, file.name) + err := ioutil.WriteFile(filePath, []byte(file.data), 0644) + if err != nil { + t.Fatalf("could not write temporary file %q", filePath) + } + } + + pod, err := PatchStaticPod(tc.pod, tempDir, ioutil.Discard) + if (err != nil) != tc.expectedError { + t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, (err != nil), err) + } + if err != nil { + return + } + + if tc.expectedPod.String() != pod.String() { + t.Fatalf("expected object:\n%s\ngot:\n%s", tc.expectedPod.String(), pod.String()) + } + }) + } +}