mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-16 15:20:17 +00:00
Merge pull request #92017 from neolit123/1.19-patches
kubeadm: introduce --experimental-patches and deprecate --experimental-kustomize
This commit is contained in:
commit
d71a09271c
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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.`,
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -46,4 +46,5 @@ type InitData interface {
|
||||
Client() (clientset.Interface, error)
|
||||
Tokens() []string
|
||||
KustomizeDir() string
|
||||
PatchesDir() string
|
||||
}
|
||||
|
@ -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 "" }
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -36,4 +36,5 @@ type JoinData interface {
|
||||
IgnorePreflightErrors() sets.String
|
||||
OutputWriter() io.Writer
|
||||
KustomizeDir() string
|
||||
PatchesDir() string
|
||||
}
|
||||
|
@ -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 "" }
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -34,4 +34,5 @@ type Data interface {
|
||||
Client() clientset.Interface
|
||||
IgnorePreflightErrors() sets.String
|
||||
KustomizeDir() string
|
||||
PatchesDir() string
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
|
40
cmd/kubeadm/app/util/patches/BUILD
Normal file
40
cmd/kubeadm/app/util/patches/BUILD
Normal file
@ -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"],
|
||||
)
|
342
cmd/kubeadm/app/util/patches/patches.go
Normal file
342
cmd/kubeadm/app/util/patches/patches.go
Normal file
@ -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
|
||||
}
|
413
cmd/kubeadm/app/util/patches/patches_test.go
Normal file
413
cmd/kubeadm/app/util/patches/patches_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user