diff --git a/cmd/kubeadm/app/cmd/join.go b/cmd/kubeadm/app/cmd/join.go index 0eb3558f3aa..a965407e8e7 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "text/template" @@ -132,6 +133,7 @@ type joinOptions struct { externalcfg *kubeadmapiv1.JoinConfiguration joinControlPlane *kubeadmapiv1.JoinControlPlane patchesDir string + dryRun bool } // compile-time assert that the local data object satisfies the phases data interface. @@ -147,6 +149,8 @@ type joinData struct { ignorePreflightErrors sets.String outputWriter io.Writer patchesDir string + dryRun bool + dryRunDir string } // newCmdJoin returns "kubeadm join" command. @@ -295,6 +299,10 @@ func addJoinOtherFlags(flagSet *flag.FlagSet, joinOptions *joinOptions) { &joinOptions.controlPlane, options.ControlPlane, joinOptions.controlPlane, "Create a new control plane instance on this node", ) + flagSet.BoolVar( + &joinOptions.dryRun, options.DryRun, joinOptions.dryRun, + "Don't apply any changes; just output what would be done.", + ) options.AddPatchesFlag(flagSet, &joinOptions.patchesDir) } @@ -445,12 +453,22 @@ func newJoinData(cmd *cobra.Command, args []string, opt *joinOptions, out io.Wri } } + // if dry running, creates a temporary folder to save kubeadm generated files + dryRunDir := "" + if opt.dryRun { + if dryRunDir, err = kubeadmconstants.CreateTempDirForKubeadm("", "kubeadm-join-dryrun"); err != nil { + return nil, errors.Wrap(err, "couldn't create a temporary directory on dryrun") + } + } + return &joinData{ cfg: cfg, tlsBootstrapCfg: tlsBootstrapCfg, ignorePreflightErrors: ignorePreflightErrorsSet, outputWriter: out, patchesDir: opt.patchesDir, + dryRun: opt.dryRun, + dryRunDir: dryRunDir, }, nil } @@ -467,6 +485,43 @@ func (j *joinData) Cfg() *kubeadmapi.JoinConfiguration { return j.cfg } +// DryRun returns the DryRun flag. +func (j *joinData) DryRun() bool { + return j.dryRun +} + +// KubeConfigDir returns the path of the Kubernetes configuration folder or the temporary folder path in case of DryRun. +func (j *joinData) KubeConfigDir() string { + if j.dryRun { + return j.dryRunDir + } + return kubeadmconstants.KubernetesDir +} + +// KubeletDir returns the path of the kubelet configuration folder or the temporary folder in case of DryRun. +func (j *joinData) KubeletDir() string { + if j.dryRun { + return j.dryRunDir + } + return kubeadmconstants.KubeletRunDirectory +} + +// ManifestDir returns the path where manifest should be stored or the temporary folder path in case of DryRun. +func (j *joinData) ManifestDir() string { + if j.dryRun { + return j.dryRunDir + } + return kubeadmconstants.GetStaticPodDirectory() +} + +// CertificateWriteDir returns the path where certs should be stored or the temporary folder path in case of DryRun. +func (j *joinData) CertificateWriteDir() string { + if j.dryRun { + return j.dryRunDir + } + return j.initCfg.CertificatesDir +} + // TLSBootstrapCfg returns the cluster-info (kubeconfig). func (j *joinData) TLSBootstrapCfg() (*clientcmdapi.Config, error) { if j.tlsBootstrapCfg != nil { @@ -497,7 +552,8 @@ func (j *joinData) ClientSet() (*clientset.Clientset, error) { if j.clientSet != nil { return j.clientSet, nil } - path := kubeadmconstants.GetAdminKubeConfigPath() + path := filepath.Join(j.KubeConfigDir(), kubeadmconstants.AdminKubeConfigFileName) + client, err := kubeconfigutil.ClientSetFromFile(path) if err != nil { return nil, errors.Wrap(err, "[preflight] couldn't create Kubernetes client") diff --git a/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go b/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go index 6e5b202a271..55d380683d7 100644 --- a/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go +++ b/cmd/kubeadm/app/cmd/phases/init/waitcontrolplane.go @@ -19,7 +19,6 @@ package phases import ( "fmt" "io" - "path/filepath" "text/template" "time" @@ -80,9 +79,12 @@ func runWaitControlPlanePhase(c workflow.RunData) error { return errors.New("wait-control-plane phase invoked with an invalid data struct") } - // If we're dry-running, print the generated manifests - if err := printFilesIfDryRunning(data); err != nil { - return errors.Wrap(err, "error printing files on dryrun") + // If we're dry-running, print the generated manifests. + // TODO: think of a better place to move this call - e.g. a hidden phase. + if data.DryRun() { + if err := dryrunutil.PrintFilesIfDryRunning(true /* needPrintManifest */, data.ManifestDir(), data.OutputWriter()); err != nil { + return errors.Wrap(err, "error printing files on dryrun") + } } // waiter holds the apiclient.Waiter implementation of choice, responsible for querying the API server in various ways and waiting for conditions to be fulfilled @@ -119,36 +121,6 @@ func runWaitControlPlanePhase(c workflow.RunData) error { return nil } -// printFilesIfDryRunning prints the Static Pod manifests to stdout and informs about the temporary directory to go and lookup -func printFilesIfDryRunning(data InitData) error { - if !data.DryRun() { - return nil - } - manifestDir := data.ManifestDir() - - fmt.Printf("[dryrun] Wrote certificates, kubeconfig files and control plane manifests to the %q directory\n", manifestDir) - fmt.Println("[dryrun] The certificates or kubeconfig files would not be printed due to their sensitive nature") - fmt.Printf("[dryrun] Please examine the %q directory for details about what would be written\n", manifestDir) - - // Print the contents of the upgraded manifests and pretend like they were in /etc/kubernetes/manifests - files := []dryrunutil.FileToPrint{} - // Print static pod manifests - for _, component := range kubeadmconstants.ControlPlaneComponents { - realPath := kubeadmconstants.GetStaticPodFilepath(component, manifestDir) - outputPath := kubeadmconstants.GetStaticPodFilepath(component, kubeadmconstants.GetStaticPodDirectory()) - files = append(files, dryrunutil.NewFileToPrint(realPath, outputPath)) - } - // Print kubelet config manifests - kubeletConfigFiles := []string{kubeadmconstants.KubeletConfigurationFileName, kubeadmconstants.KubeletEnvFileName} - for _, filename := range kubeletConfigFiles { - realPath := filepath.Join(manifestDir, filename) - outputPath := filepath.Join(kubeadmconstants.KubeletRunDirectory, filename) - files = append(files, dryrunutil.NewFileToPrint(realPath, outputPath)) - } - - return dryrunutil.PrintDryRunFiles(files, data.OutputWriter()) -} - // newControlPlaneWaiter returns a new waiter that is used to wait on the control plane to boot up. func newControlPlaneWaiter(dryRun bool, timeout time.Duration, client clientset.Interface, out io.Writer) (apiclient.Waiter, error) { if dryRun { diff --git a/cmd/kubeadm/app/cmd/phases/join/checketcd.go b/cmd/kubeadm/app/cmd/phases/join/checketcd.go index 6cab02b254e..7cd45959c77 100644 --- a/cmd/kubeadm/app/cmd/phases/join/checketcd.go +++ b/cmd/kubeadm/app/cmd/phases/join/checketcd.go @@ -66,5 +66,5 @@ func runCheckEtcdPhase(c workflow.RunData) error { return err } - return etcdphase.CheckLocalEtcdClusterStatus(client, &cfg.ClusterConfiguration) + return etcdphase.CheckLocalEtcdClusterStatus(client, data.CertificateWriteDir()) } diff --git a/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go b/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go index e0b718db354..c89a2a7ae1a 100644 --- a/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go +++ b/cmd/kubeadm/app/cmd/phases/join/controlplanejoin.go @@ -132,9 +132,13 @@ func runEtcdPhase(c workflow.RunData) error { return nil } - // Create the etcd data directory - if err := etcdutil.CreateDataDirectory(cfg.Etcd.Local.DataDir); err != nil { - return err + if !data.DryRun() { + // Create the etcd data directory + if err := etcdutil.CreateDataDirectory(cfg.Etcd.Local.DataDir); err != nil { + return err + } + } else { + fmt.Printf("[dryrun] Would ensure that %q directory is present\n", cfg.Etcd.Local.DataDir) } // Adds a new etcd instance; in order to do this the new etcd instance should be "announced" to @@ -147,8 +151,7 @@ func runEtcdPhase(c workflow.RunData) error { // 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." - // TODO: add support for join dry-run: https://github.com/kubernetes/kubeadm/issues/2505 - if err := etcdphase.CreateStackedEtcdStaticPodManifestFile(client, kubeadmconstants.GetStaticPodDirectory(), data.PatchesDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, false /* isDryRun */); err != nil { + if err := etcdphase.CreateStackedEtcdStaticPodManifestFile(client, data.ManifestDir(), data.PatchesDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, data.DryRun(), data.CertificateWriteDir()); err != nil { return errors.Wrap(err, "error creating local etcd static pod manifest file") } @@ -188,8 +191,12 @@ func runMarkControlPlanePhase(c workflow.RunData) error { return err } - if err := markcontrolplanephase.MarkControlPlane(client, cfg.NodeRegistration.Name, cfg.NodeRegistration.Taints); err != nil { - return errors.Wrap(err, "error applying control-plane label and taints") + if !data.DryRun() { + if err := markcontrolplanephase.MarkControlPlane(client, cfg.NodeRegistration.Name, cfg.NodeRegistration.Taints); err != nil { + return errors.Wrap(err, "error applying control-plane label and taints") + } + } else { + fmt.Printf("[dryrun] Would mark node %s as a control-plane\n", cfg.NodeRegistration.Name) } return nil diff --git a/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go b/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go index 86007d75557..967e33849ae 100644 --- a/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go +++ b/cmd/kubeadm/app/cmd/phases/join/controlplaneprepare.go @@ -18,6 +18,7 @@ package phases import ( "fmt" + "path/filepath" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow" @@ -189,17 +190,21 @@ func runControlPlanePrepareControlPlaneSubphase(c workflow.RunData) error { return err } - fmt.Printf("[control-plane] Using manifest folder %q\n", kubeadmconstants.GetStaticPodDirectory()) + fmt.Printf("[control-plane] Using manifest folder %q\n", data.ManifestDir()) + + // If we're dry-running, set CertificatesDir to default value to get the right cert path in static pod yaml + if data.DryRun() { + cfg.CertificatesDir = filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.DefaultCertificateDir) + } + for _, component := range kubeadmconstants.ControlPlaneComponents { fmt.Printf("[control-plane] Creating static Pod manifest for %q\n", component) err := controlplane.CreateStaticPodFiles( - kubeadmconstants.GetStaticPodDirectory(), + data.ManifestDir(), data.PatchesDir(), &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, - // TODO: add support for join dry-run: - // https://github.com/kubernetes/kubeadm/issues/2505 - false, + data.DryRun(), component, ) if err != nil { @@ -225,6 +230,11 @@ func runControlPlanePrepareDownloadCertsPhaseLocal(c workflow.RunData) error { return err } + // If we're dry-running, download certs to tmp dir + if data.DryRun() { + cfg.CertificatesDir = data.CertificateWriteDir() + } + client, err := bootstrapClient(data) if err != nil { return err @@ -275,12 +285,12 @@ func runControlPlanePrepareKubeconfigPhaseLocal(c workflow.RunData) error { } fmt.Println("[kubeconfig] Generating kubeconfig files") - fmt.Printf("[kubeconfig] Using kubeconfig folder %q\n", kubeadmconstants.KubernetesDir) + fmt.Printf("[kubeconfig] Using kubeconfig folder %q\n", data.KubeConfigDir()) // Generate kubeconfig files for controller manager, scheduler and for the admin/kubeadm itself // NB. The kubeconfig file for kubelet will be generated by the TLS bootstrap process in // following steps of the join --control-plane workflow - if err := kubeconfigphase.CreateJoinControlPlaneKubeConfigFiles(kubeadmconstants.KubernetesDir, cfg); err != nil { + if err := kubeconfigphase.CreateJoinControlPlaneKubeConfigFiles(data.KubeConfigDir(), cfg); err != nil { return errors.Wrap(err, "error generating kubeconfig files") } diff --git a/cmd/kubeadm/app/cmd/phases/join/data.go b/cmd/kubeadm/app/cmd/phases/join/data.go index d33221bf0b2..e45b622ac35 100644 --- a/cmd/kubeadm/app/cmd/phases/join/data.go +++ b/cmd/kubeadm/app/cmd/phases/join/data.go @@ -37,4 +37,9 @@ type JoinData interface { IgnorePreflightErrors() sets.String OutputWriter() io.Writer PatchesDir() string + DryRun() bool + KubeConfigDir() string + KubeletDir() string + ManifestDir() string + CertificateWriteDir() string } diff --git a/cmd/kubeadm/app/cmd/phases/join/data_test.go b/cmd/kubeadm/app/cmd/phases/join/data_test.go index ae0b8936661..3d297530d1e 100644 --- a/cmd/kubeadm/app/cmd/phases/join/data_test.go +++ b/cmd/kubeadm/app/cmd/phases/join/data_test.go @@ -40,3 +40,8 @@ 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) PatchesDir() string { return "" } +func (t *testJoinData) DryRun() bool { return false } +func (t *testJoinData) KubeConfigDir() string { return "" } +func (t *testJoinData) KubeletDir() string { return "" } +func (t *testJoinData) ManifestDir() string { return "" } +func (t *testJoinData) CertificateWriteDir() string { return "" } diff --git a/cmd/kubeadm/app/cmd/phases/join/kubelet.go b/cmd/kubeadm/app/cmd/phases/join/kubelet.go index eb96dd64bbe..6e9f6e6b422 100644 --- a/cmd/kubeadm/app/cmd/phases/join/kubelet.go +++ b/cmd/kubeadm/app/cmd/phases/join/kubelet.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "os" + "path/filepath" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" @@ -28,6 +29,7 @@ import ( kubeletphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubelet" patchnodephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/patchnode" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" + dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" v1 "k8s.io/api/core/v1" @@ -103,7 +105,12 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) { if err != nil { return err } - bootstrapKubeConfigFile := kubeadmconstants.GetBootstrapKubeletKubeConfigPath() + + data, ok := c.(JoinData) + if !ok { + return errors.New("kubelet-start phase invoked with an invalid data struct") + } + bootstrapKubeConfigFile := filepath.Join(data.KubeConfigDir(), kubeadmconstants.KubeletBootstrapKubeConfigFileName) // Deletes the bootstrapKubeConfigFile, so the credential used for TLS bootstrap is removed from disk defer os.Remove(bootstrapKubeConfigFile) @@ -116,9 +123,16 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) { // Write the ca certificate to disk so kubelet can use it for authentication cluster := tlsBootstrapCfg.Contexts[tlsBootstrapCfg.CurrentContext].Cluster - if _, err := os.Stat(cfg.CACertPath); os.IsNotExist(err) { - klog.V(1).Infof("[kubelet-start] writing CA certificate at %s", cfg.CACertPath) - if err := certutil.WriteCert(cfg.CACertPath, tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil { + + // If we're dry-running, write ca cert in tmp + caPath := cfg.CACertPath + if data.DryRun() { + caPath = filepath.Join(data.CertificateWriteDir(), kubeadmconstants.CACertName) + } + + if _, err := os.Stat(caPath); os.IsNotExist(err) { + klog.V(1).Infof("[kubelet-start] writing CA certificate at %s", caPath) + if err := certutil.WriteCert(caPath, tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil { return errors.Wrap(err, "couldn't save the CA certificate to disk") } } @@ -152,11 +166,15 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) { // Configure the kubelet. In this short timeframe, kubeadm is trying to stop/restart the kubelet // Try to stop the kubelet service so no race conditions occur when configuring it - klog.V(1).Infoln("[kubelet-start] Stopping the kubelet") - kubeletphase.TryStopKubelet() + if !data.DryRun() { + klog.V(1).Infoln("[kubelet-start] Stopping the kubelet") + kubeletphase.TryStopKubelet() + } else { + fmt.Println("[dryrun] Would stop the kubelet") + } // Write the configuration for the kubelet (using the bootstrap token credentials) to disk so the kubelet can start - if err := kubeletphase.WriteConfigToDisk(&initCfg.ClusterConfiguration, kubeadmconstants.KubeletRunDirectory); err != nil { + if err := kubeletphase.WriteConfigToDisk(&initCfg.ClusterConfiguration, data.KubeletDir()); err != nil { return err } @@ -164,10 +182,20 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) { // register the joining node with the specified taints if the node // is not a control-plane. The mark-control-plane phase will register the taints otherwise. registerTaintsUsingFlags := cfg.ControlPlane == nil - if err := kubeletphase.WriteKubeletDynamicEnvFile(&initCfg.ClusterConfiguration, &initCfg.NodeRegistration, registerTaintsUsingFlags, kubeadmconstants.KubeletRunDirectory); err != nil { + if err := kubeletphase.WriteKubeletDynamicEnvFile(&initCfg.ClusterConfiguration, &initCfg.NodeRegistration, registerTaintsUsingFlags, data.KubeletDir()); err != nil { return err } + if data.DryRun() { + fmt.Println("[dryrun] Would start the kubelet") + // If we're dry-running, print the kubelet config manifests and print static pod manifests if joining a control plane. + // TODO: think of a better place to move this call - e.g. a hidden phase. + if err := dryrunutil.PrintFilesIfDryRunning(cfg.ControlPlane != nil, data.ManifestDir(), data.OutputWriter()); err != nil { + return errors.Wrap(err, "error printing files on dryrun") + } + return nil + } + // Try to start the kubelet service in case it's inactive fmt.Println("[kubelet-start] Starting the kubelet") kubeletphase.TryStartKubelet() diff --git a/cmd/kubeadm/app/cmd/phases/join/preflight.go b/cmd/kubeadm/app/cmd/phases/join/preflight.go index 4fb31943b3d..a8e178fe1ee 100644 --- a/cmd/kubeadm/app/cmd/phases/join/preflight.go +++ b/cmd/kubeadm/app/cmd/phases/join/preflight.go @@ -123,6 +123,11 @@ func runPreflight(c workflow.RunData) error { return err } + if j.DryRun() { + fmt.Println("[preflight] Would pull the required images (like 'kubeadm config images pull')") + return nil + } + fmt.Println("[preflight] Pulling images required for setting up a Kubernetes cluster") fmt.Println("[preflight] This might take a minute or two, depending on the speed of your internet connection") fmt.Println("[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'") diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index 7a0b055c4f0..ab74769c047 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -47,6 +47,9 @@ const ( // CertificateValidity defines the validity for all the signed certificates generated by kubeadm CertificateValidity = time.Hour * 24 * 365 + // DefaultCertificateDir defines default certificate directory + DefaultCertificateDir = "pki" + // CACertAndKeyBaseName defines certificate authority base name CACertAndKeyBaseName = "ca" // CACertName defines certificate name diff --git a/cmd/kubeadm/app/phases/etcd/local.go b/cmd/kubeadm/app/phases/etcd/local.go index dd62f78e9a3..ec954ac717c 100644 --- a/cmd/kubeadm/app/phases/etcd/local.go +++ b/cmd/kubeadm/app/phases/etcd/local.go @@ -67,12 +67,12 @@ func CreateLocalEtcdStaticPodManifestFile(manifestDir, patchesDir string, nodeNa } // CheckLocalEtcdClusterStatus verifies health state of local/stacked etcd cluster before installing a new etcd member -func CheckLocalEtcdClusterStatus(client clientset.Interface, cfg *kubeadmapi.ClusterConfiguration) error { +func CheckLocalEtcdClusterStatus(client clientset.Interface, certificatesDir string) error { klog.V(1).Info("[etcd] Checking etcd cluster health") // 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) + etcdClient, err := etcdutil.NewFromCluster(client, certificatesDir) if err != nil { return err } @@ -134,32 +134,40 @@ 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, patchesDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint, isDryRun bool) error { +func CreateStackedEtcdStaticPodManifestFile(client clientset.Interface, manifestDir, patchesDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint, isDryRun bool, certificatesDir string) 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) + etcdClient, err := etcdutil.NewFromCluster(client, certificatesDir) if err != nil { return err } etcdPeerAddress := etcdutil.GetPeerURL(endpoint) - klog.V(1).Infof("[etcd] Adding etcd member: %s", etcdPeerAddress) var cluster []etcdutil.Member - cluster, err = etcdClient.AddMember(nodeName, etcdPeerAddress) - if err != nil { - return err + if isDryRun { + fmt.Printf("[dryrun] Would add etcd member: %s\n", etcdPeerAddress) + } else { + klog.V(1).Infof("[etcd] Adding etcd member: %s", etcdPeerAddress) + cluster, err = etcdClient.AddMember(nodeName, etcdPeerAddress) + if err != nil { + return err + } + fmt.Println("[etcd] Announced new etcd member joining to the existing etcd cluster") + klog.V(1).Infof("Updated etcd member list: %v", cluster) } - fmt.Println("[etcd] Announced new etcd member joining to the existing etcd cluster") - klog.V(1).Infof("Updated etcd member list: %v", cluster) - fmt.Printf("[etcd] Creating static Pod manifest for %q\n", kubeadmconstants.Etcd) if err := prepareAndWriteEtcdStaticPod(manifestDir, patchesDir, cfg, endpoint, nodeName, cluster, isDryRun); err != nil { return err } + if isDryRun { + fmt.Println("[dryrun] Would wait for the new etcd member to join the cluster") + return nil + } + fmt.Printf("[etcd] Waiting for the new etcd member to join the cluster. This can take up to %v\n", etcdHealthyCheckInterval*etcdHealthyCheckRetries) if _, err := etcdClient.WaitForClusterAvailable(etcdHealthyCheckRetries, etcdHealthyCheckInterval); err != nil { return err diff --git a/cmd/kubeadm/app/util/dryrun/dryrun.go b/cmd/kubeadm/app/util/dryrun/dryrun.go index 46bba3ae8d4..43ff69a5010 100644 --- a/cmd/kubeadm/app/util/dryrun/dryrun.go +++ b/cmd/kubeadm/app/util/dryrun/dryrun.go @@ -24,6 +24,7 @@ import ( "time" "k8s.io/kubernetes/cmd/kubeadm/app/constants" + kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -139,3 +140,32 @@ func (w *Waiter) WaitForStaticPodSingleHash(_ string, _ string) (string, error) func (w *Waiter) WaitForStaticPodHashChange(_, _, _ string) error { return nil } + +// PrintFilesIfDryRunning prints the static pod manifests to stdout and informs about the temporary directory to go and lookup when dry running +func PrintFilesIfDryRunning(needPrintManifest bool, manifestDir string, outputWriter io.Writer) error { + var files []FileToPrint + // Print static pod manifests if it is a control plane + if needPrintManifest { + fmt.Printf("[dryrun] Wrote certificates, kubeconfig files and control plane manifests to the %q directory\n", manifestDir) + for _, component := range kubeadmconstants.ControlPlaneComponents { + realPath := kubeadmconstants.GetStaticPodFilepath(component, manifestDir) + outputPath := kubeadmconstants.GetStaticPodFilepath(component, kubeadmconstants.GetStaticPodDirectory()) + files = append(files, NewFileToPrint(realPath, outputPath)) + } + } else { + fmt.Printf("[dryrun] Wrote certificates and kubeconfig files to the %q directory\n", manifestDir) + } + + fmt.Println("[dryrun] The certificates or kubeconfig files would not be printed due to their sensitive nature") + fmt.Printf("[dryrun] Please examine the %q directory for details about what would be written\n", manifestDir) + + // Print kubelet config manifests + kubeletConfigFiles := []string{kubeadmconstants.KubeletConfigurationFileName, kubeadmconstants.KubeletEnvFileName} + for _, filename := range kubeletConfigFiles { + realPath := filepath.Join(manifestDir, filename) + outputPath := filepath.Join(kubeadmconstants.KubeletRunDirectory, filename) + files = append(files, NewFileToPrint(realPath, outputPath)) + } + + return PrintDryRunFiles(files, outputWriter) +}