Commit c2faa3bf authored by fabriziopandini's avatar fabriziopandini

kubeadm upgrade node

parent 472b9011
...@@ -59,6 +59,9 @@ const ( ...@@ -59,6 +59,9 @@ const (
// KubernetesVersion flag sets the Kubernetes version for the control plane. // KubernetesVersion flag sets the Kubernetes version for the control plane.
KubernetesVersion = "kubernetes-version" KubernetesVersion = "kubernetes-version"
// KubeletVersion flag sets the version for the kubelet config.
KubeletVersion = "kubelet-version"
// NetworkingDNSDomain flag sets the domain for services, e.g. "myorg.internal". // NetworkingDNSDomain flag sets the domain for services, e.g. "myorg.internal".
NetworkingDNSDomain = "service-dns-domain" NetworkingDNSDomain = "service-dns-domain"
......
/*
Copyright 2019 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 node
import (
"fmt"
"os"
"github.com/pkg/errors"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
)
// NewControlPlane creates a kubeadm workflow phase that implements handling of control-plane upgrade.
func NewControlPlane() workflow.Phase {
phase := workflow.Phase{
Name: "control-plane",
Short: "Upgrade the control plane instance deployed on this node, if any",
Run: runControlPlane(),
InheritFlags: []string{
options.DryRun,
options.KubeconfigPath,
},
}
return phase
}
func runControlPlane() func(c workflow.RunData) error {
return func(c workflow.RunData) error {
data, ok := c.(Data)
if !ok {
return errors.New("control-plane phase invoked with an invalid data struct")
}
// if this is not a control-plande node, this phase should not be executed
if !data.IsControlPlaneNode() {
fmt.Printf("[upgrade] Skipping phase. Not a control plane node")
}
// otherwise, retrieve all the info required for control plane upgrade
cfg := data.Cfg()
client := data.Client()
dryRun := data.DryRun()
etcdUpgrade := data.EtcdUpgrade()
renewCerts := data.RenewCerts()
// 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(cfg)
}
waiter := apiclient.NewKubeWaiter(data.Client(), upgrade.UpgradeManifestTimeout, os.Stdout)
if err := upgrade.PerformStaticPodUpgrade(client, waiter, cfg, etcdUpgrade, renewCerts); err != nil {
return errors.Wrap(err, "couldn't complete the static pod upgrade")
}
fmt.Println("[upgrade] The control plane instance for this node was successfully updated!")
return nil
}
}
/*
Copyright 2019 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 node
import (
clientset "k8s.io/client-go/kubernetes"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
)
// Data is the interface to use for kubeadm upgrade node phases.
// The "nodeData" type from "cmd/upgrade/node.go" must satisfy this interface.
type Data interface {
EtcdUpgrade() bool
RenewCerts() bool
DryRun() bool
KubeletVersion() string
Cfg() *kubeadmapi.InitConfiguration
IsControlPlaneNode() bool
Client() clientset.Interface
}
/*
Copyright 2019 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 node
import (
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeletphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubelet"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
"k8s.io/kubernetes/pkg/util/normalizer"
)
var (
kubeletConfigLongDesc = normalizer.LongDesc(`
Download the kubelet configuration from a ConfigMap of the form "kubelet-config-1.X" in the cluster,
where X is the minor version of the kubelet. kubeadm uses the KuberneteVersion field in the kubeadm-config
ConfigMap to determine what the _desired_ kubelet version is, but the user can override this by using the
--kubelet-version parameter.
`)
)
// NewKubeletConfigPhase creates a kubeadm workflow phase that implements handling of kubelet-config upgrade.
func NewKubeletConfigPhase() workflow.Phase {
phase := workflow.Phase{
Name: "kubelet-config",
Short: "Upgrade the kubelet configuration for this node",
Long: kubeletConfigLongDesc,
Run: runKubeletConfigPhase(),
InheritFlags: []string{
options.DryRun,
options.KubeconfigPath,
options.KubeletVersion,
},
}
return phase
}
func runKubeletConfigPhase() func(c workflow.RunData) error {
return func(c workflow.RunData) error {
data, ok := c.(Data)
if !ok {
return errors.New("kubelet-config phase invoked with an invalid data struct")
}
// otherwise, retrieve all the info required for kubelet config upgrade
cfg := data.Cfg()
client := data.Client()
dryRun := data.DryRun()
// Set up the kubelet directory to use. If dry-running, this will return a fake directory
kubeletDir, err := upgrade.GetKubeletDir(dryRun)
if err != nil {
return err
}
// Gets the target kubelet version.
// by default kubelet version is expected to be equal to ClusterConfiguration.KubernetesVersion, but
// users can specify a different kubelet version (this is a legacy of the original implementation
// of `kubeam upgrade node config` which we are preserving in order to don't break GA contract)
kubeletVersionStr := cfg.ClusterConfiguration.KubernetesVersion
if data.KubeletVersion() != "" && data.KubeletVersion() != kubeletVersionStr {
kubeletVersionStr = data.KubeletVersion()
fmt.Printf("[upgrade] Using kubelet config version %s, while kubernetes-version is %s\n", kubeletVersionStr, cfg.ClusterConfiguration.KubernetesVersion)
}
// Parse the desired kubelet version
kubeletVersion, err := version.ParseSemantic(kubeletVersionStr)
if err != nil {
return err
}
// TODO: Checkpoint the current configuration first so that if something goes wrong it can be recovered
if err := kubeletphase.DownloadConfig(client, kubeletVersion, kubeletDir); err != nil {
return err
}
// If we're dry-running, print the generated manifests
if dryRun {
if err := printFilesIfDryRunning(dryRun, kubeletDir); err != nil {
return errors.Wrap(err, "error printing files on dryrun")
}
return nil
}
fmt.Println("[upgrade] The configuration for this node was successfully updated!")
fmt.Println("[upgrade] Now you should go ahead and upgrade the kubelet package using your package manager.")
return nil
}
}
// printFilesIfDryRunning prints the Static Pod manifests to stdout and informs about the temporary directory to go and lookup
func printFilesIfDryRunning(dryRun bool, kubeletDir string) error {
if !dryRun {
return nil
}
// Print the contents of the upgraded file and pretend like they were in kubeadmconstants.KubeletRunDirectory
fileToPrint := dryrunutil.FileToPrint{
RealPath: filepath.Join(kubeletDir, constants.KubeletConfigurationFileName),
PrintPath: filepath.Join(constants.KubeletRunDirectory, constants.KubeletConfigurationFileName),
}
return dryrunutil.PrintDryRunFiles([]dryrunutil.FileToPrint{fileToPrint}, os.Stdout)
}
...@@ -18,7 +18,6 @@ package upgrade ...@@ -18,7 +18,6 @@ package upgrade
import ( import (
"fmt" "fmt"
"os"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
...@@ -31,13 +30,10 @@ import ( ...@@ -31,13 +30,10 @@ import (
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
"k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/features" "k8s.io/kubernetes/cmd/kubeadm/app/features"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
etcdutil "k8s.io/kubernetes/cmd/kubeadm/app/util/etcd"
) )
const ( const (
...@@ -230,50 +226,9 @@ func PerformControlPlaneUpgrade(flags *applyFlags, client clientset.Interface, w ...@@ -230,50 +226,9 @@ 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) fmt.Printf("[upgrade/apply] Upgrading your Static Pod-hosted control plane to version %q...\n", internalcfg.KubernetesVersion)
if flags.dryRun { if flags.dryRun {
return DryRunStaticPodUpgrade(internalcfg) return upgrade.DryRunStaticPodUpgrade(internalcfg)
} }
// Don't save etcd backup directory if etcd is HA, as this could cause corruption // Don't save etcd backup directory if etcd is HA, as this could cause corruption
return PerformStaticPodUpgrade(client, waiter, internalcfg, flags.etcdUpgrade, flags.renewCerts) return upgrade.PerformStaticPodUpgrade(client, waiter, internalcfg, flags.etcdUpgrade, flags.renewCerts)
}
// GetPathManagerForUpgrade returns a path manager properly configured for the given InitConfiguration.
func GetPathManagerForUpgrade(kubernetesDir string, internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade bool) (upgrade.StaticPodPathManager, error) {
isHAEtcd := etcdutil.CheckConfigurationIsHA(&internalcfg.Etcd)
return upgrade.NewKubeStaticPodPathManagerUsingTempDirs(kubernetesDir, true, etcdUpgrade && !isHAEtcd)
}
// 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) error {
pathManager, err := GetPathManagerForUpgrade(constants.KubernetesDir, internalcfg, etcdUpgrade)
if err != nil {
return err
}
// The arguments oldEtcdClient and newEtdClient, are uninitialized because passing in the clients allow for mocking the client during testing
return upgrade.StaticPodControlPlane(client, waiter, pathManager, internalcfg, etcdUpgrade, renewCerts, nil, nil)
}
// DryRunStaticPodUpgrade fakes an upgrade of the control plane
func DryRunStaticPodUpgrade(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, internalcfg); err != nil {
return err
}
// Print the contents of the upgraded manifests and pretend like they were in /etc/kubernetes/manifests
files := []dryrunutil.FileToPrint{}
for _, component := range constants.ControlPlaneComponents {
realPath := constants.GetStaticPodFilepath(component, dryRunManifestDir)
outputPath := constants.GetStaticPodFilepath(component, constants.GetStaticPodDirectory())
files = append(files, dryrunutil.NewFileToPrint(realPath, outputPath))
}
return dryrunutil.PrintDryRunFiles(files, os.Stdout)
} }
...@@ -17,11 +17,7 @@ limitations under the License. ...@@ -17,11 +17,7 @@ limitations under the License.
package upgrade package upgrade
import ( import (
"io/ioutil"
"os"
"testing" "testing"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
) )
func TestSessionIsInteractive(t *testing.T) { func TestSessionIsInteractive(t *testing.T) {
...@@ -65,89 +61,3 @@ func TestSessionIsInteractive(t *testing.T) { ...@@ -65,89 +61,3 @@ func TestSessionIsInteractive(t *testing.T) {
}) })
} }
} }
func TestGetPathManagerForUpgrade(t *testing.T) {
haEtcd := &kubeadmapi.InitConfiguration{
ClusterConfiguration: kubeadmapi.ClusterConfiguration{
Etcd: kubeadmapi.Etcd{
External: &kubeadmapi.ExternalEtcd{
Endpoints: []string{"10.100.0.1:2379", "10.100.0.2:2379", "10.100.0.3:2379"},
},
},
},
}
noHAEtcd := &kubeadmapi.InitConfiguration{}
tests := []struct {
name string
cfg *kubeadmapi.InitConfiguration
etcdUpgrade bool
shouldDeleteEtcd bool
}{
{
name: "ha etcd but no etcd upgrade",
cfg: haEtcd,
etcdUpgrade: false,
shouldDeleteEtcd: true,
},
{
name: "non-ha etcd with etcd upgrade",
cfg: noHAEtcd,
etcdUpgrade: true,
shouldDeleteEtcd: false,
},
{
name: "ha etcd and etcd upgrade",
cfg: haEtcd,
etcdUpgrade: true,
shouldDeleteEtcd: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Use a temporary directory
tmpdir, err := ioutil.TempDir("", "TestGetPathManagerForUpgrade")
if err != nil {
t.Fatalf("unexpected error making temporary directory: %v", err)
}
defer func() {
os.RemoveAll(tmpdir)
}()
pathmgr, err := GetPathManagerForUpgrade(tmpdir, test.cfg, test.etcdUpgrade)
if err != nil {
t.Fatalf("unexpected error creating path manager: %v", err)
}
if _, err := os.Stat(pathmgr.BackupManifestDir()); os.IsNotExist(err) {
t.Errorf("expected manifest dir %s to exist, but it did not (%v)", pathmgr.BackupManifestDir(), err)
}
if _, err := os.Stat(pathmgr.BackupEtcdDir()); os.IsNotExist(err) {
t.Errorf("expected etcd dir %s to exist, but it did not (%v)", pathmgr.BackupEtcdDir(), err)
}
if err := pathmgr.CleanupDirs(); err != nil {
t.Fatalf("unexpected error cleaning up directories: %v", err)
}
if _, err := os.Stat(pathmgr.BackupManifestDir()); os.IsNotExist(err) {
t.Errorf("expected manifest dir %s to exist, but it did not (%v)", pathmgr.BackupManifestDir(), err)
}
if test.shouldDeleteEtcd {
if _, err := os.Stat(pathmgr.BackupEtcdDir()); !os.IsNotExist(err) {
t.Errorf("expected etcd dir %s not to exist, but it did (%v)", pathmgr.BackupEtcdDir(), err)
}
} else {
if _, err := os.Stat(pathmgr.BackupEtcdDir()); os.IsNotExist(err) {
t.Errorf("expected etcd dir %s to exist, but it did not", pathmgr.BackupEtcdDir())
}
}
})
}
}
...@@ -17,52 +17,29 @@ limitations under the License. ...@@ -17,52 +17,29 @@ limitations under the License.
package upgrade package upgrade
import ( import (
"fmt"
"os" "os"
"path/filepath"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/version" flag "github.com/spf13/pflag"
"k8s.io/klog"
clientset "k8s.io/client-go/kubernetes"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" phases "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/upgrade/node"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow"
"k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeletphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubelet" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
"k8s.io/kubernetes/pkg/util/node"
"k8s.io/kubernetes/pkg/util/normalizer"
)
var (
upgradeNodeConfigLongDesc = normalizer.LongDesc(`
Download the kubelet configuration from a ConfigMap of the form "kubelet-config-1.X" in the cluster,
where X is the minor version of the kubelet. kubeadm uses the --kubelet-version parameter to determine
what the _desired_ kubelet version is. Give
`)
upgradeNodeConfigExample = normalizer.Examples(fmt.Sprintf(`
# Download the kubelet configuration from the ConfigMap in the cluster. Use a specific desired kubelet version.
kubeadm upgrade node config --kubelet-version %s
# Simulate the downloading of the kubelet configuration from the ConfigMap in the cluster with a specific desired
# version. Do not change any state locally on the node.
kubeadm upgrade node config --kubelet-version %[1]s --dry-run
`, constants.CurrentKubernetesVersion))
) )
type nodeUpgradeFlags struct { // nodeOptions defines all the options exposed via flags by kubeadm upgrade node.
kubeConfigPath string // Please note that this structure includes the public kubeadm config API, but only a subset of the options
kubeletVersionStr string // supported by this api will be exposed as a flag.
dryRun bool type nodeOptions struct {
}
type controlplaneUpgradeFlags struct {
kubeConfigPath string kubeConfigPath string
kubeletVersion string
advertiseAddress string advertiseAddress string
nodeName string nodeName string
etcdUpgrade bool etcdUpgrade bool
...@@ -70,170 +47,217 @@ type controlplaneUpgradeFlags struct { ...@@ -70,170 +47,217 @@ type controlplaneUpgradeFlags struct {
dryRun bool dryRun bool
} }
// NewCmdNode returns the cobra command for `kubeadm upgrade node` // compile-time assert that the local data object satisfies the phases data interface.
func NewCmdNode() *cobra.Command { var _ phases.Data = &nodeData{}
cmd := &cobra.Command{
Use: "node", // nodeData defines all the runtime information used when running the kubeadm upgrade node worklow;
Short: "Upgrade commands for a node in the cluster. Currently only support upgrading the configuration, not the kubelet itself", // this data is shared across all the phases that are included in the workflow.
RunE: cmdutil.SubCmdRunE("node"), type nodeData struct {
} etcdUpgrade bool
cmd.AddCommand(NewCmdUpgradeNodeConfig()) renewCerts bool
cmd.AddCommand(NewCmdUpgradeControlPlane()) dryRun bool
return cmd kubeletVersion string
cfg *kubeadmapi.InitConfiguration
isControlPlaneNode bool
client clientset.Interface
} }
// NewCmdUpgradeNodeConfig returns the cobra.Command for downloading the new/upgrading the kubelet configuration from the kubelet-config-1.X // NewCmdNode returns the cobra command for `kubeadm upgrade node`
// ConfigMap in the cluster func NewCmdNode() *cobra.Command {
func NewCmdUpgradeNodeConfig() *cobra.Command { nodeOptions := newNodeOptions()
flags := &nodeUpgradeFlags{ nodeRunner := workflow.NewRunner()
kubeConfigPath: constants.GetKubeletKubeConfigPath(),
kubeletVersionStr: "",
dryRun: false,
}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "config", Use: "node",
Short: "Download the kubelet configuration from the cluster ConfigMap kubelet-config-1.X, where X is the minor version of the kubelet", Short: "Upgrade commands for a node in the cluster",
Long: upgradeNodeConfigLongDesc,
Example: upgradeNodeConfigExample,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := RunUpgradeNodeConfig(flags) err := nodeRunner.Run(args)
kubeadmutil.CheckErr(err) kubeadmutil.CheckErr(err)
}, },
Args: cobra.NoArgs,
} }
options.AddKubeConfigFlag(cmd.Flags(), &flags.kubeConfigPath) // adds flags to the node command
cmd.Flags().BoolVar(&flags.dryRun, options.DryRun, flags.dryRun, "Do not change any state, just output the actions that would be performed.") // flags could be eventually inherited by the sub-commands automatically generated for phases
cmd.Flags().StringVar(&flags.kubeletVersionStr, "kubelet-version", flags.kubeletVersionStr, "The *desired* version for the kubelet after the upgrade.") addUpgradeNodeFlags(cmd.Flags(), nodeOptions)
return cmd
}
// NewCmdUpgradeControlPlane returns the cobra.Command for upgrading the controlplane instance on this node // initialize the workflow runner with the list of phases
func NewCmdUpgradeControlPlane() *cobra.Command { nodeRunner.AppendPhase(phases.NewControlPlane())
nodeRunner.AppendPhase(phases.NewKubeletConfigPhase())
flags := &controlplaneUpgradeFlags{
kubeConfigPath: constants.GetKubeletKubeConfigPath(),
advertiseAddress: "",
etcdUpgrade: true,
renewCerts: true,
dryRun: false,
}
cmd := &cobra.Command{ // sets the data builder function, that will be used by the runner
Use: "experimental-control-plane", // both when running the entire workflow or single phases
Short: "Upgrade the control plane instance deployed on this node. IMPORTANT. This command should be executed after executing `kubeadm upgrade apply` on another control plane instance", nodeRunner.SetDataInitializer(func(cmd *cobra.Command, args []string) (workflow.RunData, error) {
Long: upgradeNodeConfigLongDesc, return newNodeData(cmd, args, nodeOptions)
Example: upgradeNodeConfigExample, })
Run: func(cmd *cobra.Command, args []string) {
if flags.nodeName == "" { // binds the Runner to kubeadm upgrade node command by altering
klog.V(1).Infoln("[upgrade] found NodeName empty; considered OS hostname as NodeName") // command help, adding --skip-phases flag and by adding phases subcommands
} nodeRunner.BindToCommand(cmd)
nodeName, err := node.GetHostname(flags.nodeName)
if err != nil {
kubeadmutil.CheckErr(err)
}
flags.nodeName = nodeName
if flags.advertiseAddress == "" {
ip, err := configutil.ChooseAPIServerBindAddress(nil)
if err != nil {
kubeadmutil.CheckErr(err)
return
}
flags.advertiseAddress = ip.String()
}
err = RunUpgradeControlPlane(flags) // upgrade node config command is subject to GA deprecation policy, so we should deprecate it
kubeadmutil.CheckErr(err) // and keep it here for one year or three releases - the longer of the two - starting from v1.15 included
}, cmd.AddCommand(NewCmdUpgradeNodeConfig())
} // upgrade node experimental control plane can be removed, but we are keeping it for one more cycle
cmd.AddCommand(NewCmdUpgradeControlPlane())
options.AddKubeConfigFlag(cmd.Flags(), &flags.kubeConfigPath)
cmd.Flags().BoolVar(&flags.dryRun, options.DryRun, flags.dryRun, "Do not change any state, just output the actions that would be performed.")
cmd.Flags().BoolVar(&flags.etcdUpgrade, "etcd-upgrade", flags.etcdUpgrade, "Perform the upgrade of etcd.")
cmd.Flags().BoolVar(&flags.renewCerts, "certificate-renewal", flags.renewCerts, "Perform the renewal of certificates used by component changed during upgrades.")
return cmd return cmd
} }
// RunUpgradeNodeConfig is executed when `kubeadm upgrade node config` runs. // newNodeOptions returns a struct ready for being used for creating cmd kubeadm upgrade node flags.
func RunUpgradeNodeConfig(flags *nodeUpgradeFlags) error { func newNodeOptions() *nodeOptions {
if len(flags.kubeletVersionStr) == 0 { return &nodeOptions{
return errors.New("the --kubelet-version argument is required") kubeConfigPath: constants.GetKubeletKubeConfigPath(),
dryRun: false,
} }
}
func addUpgradeNodeFlags(flagSet *flag.FlagSet, nodeOptions *nodeOptions) {
options.AddKubeConfigFlag(flagSet, &nodeOptions.kubeConfigPath)
flagSet.BoolVar(&nodeOptions.dryRun, options.DryRun, nodeOptions.dryRun, "Do not change any state, just output the actions that would be performed.")
flagSet.StringVar(&nodeOptions.kubeletVersion, options.KubeletVersion, nodeOptions.kubeletVersion, "The *desired* version for the kubelet config after the upgrade. If not specified, the KubernetesVersion from the kubeadm-config ConfigMap will be used")
}
// Set up the kubelet directory to use. If dry-running, use a fake directory // newNodeData returns a new nodeData struct to be used for the execution of the kubeadm upgrade node workflow.
kubeletDir, err := upgrade.GetKubeletDir(flags.dryRun) // This func takes care of validating nodeOptions passed to the command, and then it converts
// options into the internal InitConfiguration type that is used as input all the phases in the kubeadm upgrade node workflow
func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions) (*nodeData, error) {
client, err := getClient(options.kubeConfigPath, options.dryRun)
if err != nil { if err != nil {
return err return nil, errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", options.kubeConfigPath)
} }
client, err := getClient(flags.kubeConfigPath, flags.dryRun) // isControlPlane checks if a node is a control-plane node by looking up
if err != nil { // the kube-apiserver manifest file
return errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", flags.kubeConfigPath) isControlPlaneNode := true
filepath := kubeadmconstants.GetStaticPodFilepath(kubeadmconstants.KubeAPIServer, kubeadmconstants.GetStaticPodDirectory())
if _, err := os.Stat(filepath); os.IsNotExist(err) {
isControlPlaneNode = false
} }
// Parse the desired kubelet version // Fetches the cluster configuration
kubeletVersion, err := version.ParseSemantic(flags.kubeletVersionStr) // NB in case of control-plane node, we are reading all the info for the node; in case of NOT control-plane node
// (worker node), we are not reading local API address and the CRI socket from the node object
cfg, err := configutil.FetchInitConfigurationFromCluster(client, os.Stdout, "upgrade", !isControlPlaneNode)
if err != nil { if err != nil {
return err return nil, errors.Wrap(err, "unable to fetch the kubeadm-config ConfigMap")
}
// TODO: Checkpoint the current configuration first so that if something goes wrong it can be recovered
if err := kubeletphase.DownloadConfig(client, kubeletVersion, kubeletDir); err != nil {
return err
} }
// If we're dry-running, print the generated manifests, otherwise do nothing return &nodeData{
if err := printFilesIfDryRunning(flags.dryRun, kubeletDir); err != nil { etcdUpgrade: options.etcdUpgrade,
return errors.Wrap(err, "error printing files on dryrun") renewCerts: options.renewCerts,
} dryRun: options.dryRun,
kubeletVersion: options.kubeletVersion,
cfg: cfg,
client: client,
isControlPlaneNode: isControlPlaneNode,
}, nil
}
fmt.Println("[upgrade] The configuration for this node was successfully updated!") // DryRun returns the dryRun flag.
fmt.Println("[upgrade] Now you should go ahead and upgrade the kubelet package using your package manager.") func (d *nodeData) DryRun() bool {
return nil return d.dryRun
} }
// printFilesIfDryRunning prints the Static Pod manifests to stdout and informs about the temporary directory to go and lookup // EtcdUpgrade returns the etcdUpgrade flag.
func printFilesIfDryRunning(dryRun bool, kubeletDir string) error { func (d *nodeData) EtcdUpgrade() bool {
if !dryRun { return d.etcdUpgrade
return nil }
}
// Print the contents of the upgraded file and pretend like they were in kubeadmconstants.KubeletRunDirectory // RenewCerts returns the renewCerts flag.
fileToPrint := dryrunutil.FileToPrint{ func (d *nodeData) RenewCerts() bool {
RealPath: filepath.Join(kubeletDir, constants.KubeletConfigurationFileName), return d.renewCerts
PrintPath: filepath.Join(constants.KubeletRunDirectory, constants.KubeletConfigurationFileName),
}
return dryrunutil.PrintDryRunFiles([]dryrunutil.FileToPrint{fileToPrint}, os.Stdout)
} }
// RunUpgradeControlPlane is executed when `kubeadm upgrade node controlplane` runs. // KubeletVersion returns the kubeletVersion flag.
func RunUpgradeControlPlane(flags *controlplaneUpgradeFlags) error { func (d *nodeData) KubeletVersion() string {
return d.kubeletVersion
}
client, err := getClient(flags.kubeConfigPath, flags.dryRun) // Cfg returns initConfiguration.
if err != nil { func (d *nodeData) Cfg() *kubeadmapi.InitConfiguration {
return errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", flags.kubeConfigPath) return d.cfg
} }
waiter := apiclient.NewKubeWaiter(client, upgrade.UpgradeManifestTimeout, os.Stdout) // IsControlPlaneNode returns the isControlPlaneNode flag.
func (d *nodeData) IsControlPlaneNode() bool {
return d.isControlPlaneNode
}
// Fetches the cluster configuration // Client returns a Kubernetes client to be used by kubeadm.
cfg, err := configutil.FetchInitConfigurationFromCluster(client, os.Stdout, "upgrade", false) func (d *nodeData) Client() clientset.Interface {
if err != nil { return d.client
return errors.Wrap(err, "unable to fetch the kubeadm-config ConfigMap") }
}
// NewCmdUpgradeNodeConfig returns the cobra.Command for downloading the new/upgrading the kubelet configuration from the kubelet-config-1.X
// ConfigMap in the cluster
// TODO: to remove when 1.18 is released
func NewCmdUpgradeNodeConfig() *cobra.Command {
nodeOptions := newNodeOptions()
nodeRunner := workflow.NewRunner()
cmd := &cobra.Command{
Use: "config",
Short: "Download the kubelet configuration from the cluster ConfigMap kubelet-config-1.X, where X is the minor version of the kubelet",
Deprecated: "use \"kubeadm upgrade node\" instead",
Run: func(cmd *cobra.Command, args []string) {
// This is required for preserving the old behavior of `kubeadm upgrade node config`.
// The new implementation exposed as a phase under `kubeadm upgrade node` infers the target
// kubelet config version from the kubeadm-config ConfigMap
if len(nodeOptions.kubeletVersion) == 0 {
kubeadmutil.CheckErr(errors.New("the --kubelet-version argument is required"))
}
// Upgrade the control plane and etcd if installed on this node err := nodeRunner.Run(args)
fmt.Printf("[upgrade] Upgrading your Static Pod-hosted control plane instance to version %q...\n", cfg.KubernetesVersion) kubeadmutil.CheckErr(err)
if flags.dryRun { },
return DryRunStaticPodUpgrade(cfg)
} }
if err := PerformStaticPodUpgrade(client, waiter, cfg, flags.etcdUpgrade, flags.renewCerts); err != nil { // adds flags to the node command
return errors.Wrap(err, "couldn't complete the static pod upgrade") addUpgradeNodeFlags(cmd.Flags(), nodeOptions)
// initialize the workflow runner with the list of phases
nodeRunner.AppendPhase(phases.NewKubeletConfigPhase())
// sets the data builder function, that will be used by the runner
// both when running the entire workflow or single phases
nodeRunner.SetDataInitializer(func(cmd *cobra.Command, args []string) (workflow.RunData, error) {
return newNodeData(cmd, args, nodeOptions)
})
return cmd
}
// NewCmdUpgradeControlPlane returns the cobra.Command for upgrading the controlplane instance on this node
// TODO: to remove when 1.16 is released
func NewCmdUpgradeControlPlane() *cobra.Command {
nodeOptions := newNodeOptions()
nodeRunner := workflow.NewRunner()
cmd := &cobra.Command{
Use: "experimental-control-plane",
Short: "Upgrade the control plane instance deployed on this node. IMPORTANT. This command should be executed after executing `kubeadm upgrade apply` on another control plane instance",
Deprecated: "this command is deprecated. Use \"kubeadm upgrade node\" instead",
Run: func(cmd *cobra.Command, args []string) {
err := nodeRunner.Run(args)
kubeadmutil.CheckErr(err)
},
} }
fmt.Println("[upgrade] The control plane instance for this node was successfully updated!") // adds flags to the node command
return nil options.AddKubeConfigFlag(cmd.Flags(), &nodeOptions.kubeConfigPath)
cmd.Flags().BoolVar(&nodeOptions.dryRun, options.DryRun, nodeOptions.dryRun, "Do not change any state, just output the actions that would be performed.")
cmd.Flags().BoolVar(&nodeOptions.etcdUpgrade, "etcd-upgrade", nodeOptions.etcdUpgrade, "Perform the upgrade of etcd.")
cmd.Flags().BoolVar(&nodeOptions.renewCerts, "certificate-renewal", nodeOptions.renewCerts, "Perform the renewal of certificates used by component changed during upgrades.")
// initialize the workflow runner with the list of phases
nodeRunner.AppendPhase(phases.NewControlPlane())
// sets the data builder function, that will be used by the runner
// both when running the entire workflow or single phases
nodeRunner.SetDataInitializer(func(cmd *cobra.Command, args []string) (workflow.RunData, error) {
return newNodeData(cmd, args, nodeOptions)
})
return cmd
} }
...@@ -31,10 +31,12 @@ import ( ...@@ -31,10 +31,12 @@ import (
"k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/constants"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal" "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/renewal"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane" controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd" etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd"
"k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
etcdutil "k8s.io/kubernetes/cmd/kubeadm/app/util/etcd" etcdutil "k8s.io/kubernetes/cmd/kubeadm/app/util/etcd"
"k8s.io/kubernetes/cmd/kubeadm/app/util/staticpod" "k8s.io/kubernetes/cmd/kubeadm/app/util/staticpod"
) )
...@@ -583,3 +585,44 @@ func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, component string, ...@@ -583,3 +585,44 @@ func renewCertsByComponent(cfg *kubeadmapi.InitConfiguration, component string,
return nil return nil
} }
// GetPathManagerForUpgrade returns a path manager properly configured for the given InitConfiguration.
func GetPathManagerForUpgrade(kubernetesDir string, internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade bool) (StaticPodPathManager, error) {
isHAEtcd := etcdutil.CheckConfigurationIsHA(&internalcfg.Etcd)
return NewKubeStaticPodPathManagerUsingTempDirs(kubernetesDir, true, etcdUpgrade && !isHAEtcd)
}
// 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) error {
pathManager, err := GetPathManagerForUpgrade(constants.KubernetesDir, internalcfg, etcdUpgrade)
if err != nil {
return err
}
// The arguments oldEtcdClient and newEtdClient, are uninitialized because passing in the clients allow for mocking the client during testing
return StaticPodControlPlane(client, waiter, pathManager, internalcfg, etcdUpgrade, renewCerts, nil, nil)
}
// DryRunStaticPodUpgrade fakes an upgrade of the control plane
func DryRunStaticPodUpgrade(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, internalcfg); err != nil {
return err
}
// Print the contents of the upgraded manifests and pretend like they were in /etc/kubernetes/manifests
files := []dryrunutil.FileToPrint{}
for _, component := range constants.ControlPlaneComponents {
realPath := constants.GetStaticPodFilepath(component, dryRunManifestDir)
outputPath := constants.GetStaticPodFilepath(component, constants.GetStaticPodDirectory())
files = append(files, dryrunutil.NewFileToPrint(realPath, outputPath))
}
return dryrunutil.PrintDryRunFiles(files, os.Stdout)
}
...@@ -909,3 +909,89 @@ func getEmbeddedCerts(tmpDir, kubeConfig string) ([]*x509.Certificate, error) { ...@@ -909,3 +909,89 @@ func getEmbeddedCerts(tmpDir, kubeConfig string) ([]*x509.Certificate, error) {
return certutil.ParseCertsPEM(authInfo.ClientCertificateData) return certutil.ParseCertsPEM(authInfo.ClientCertificateData)
} }
func TestGetPathManagerForUpgrade(t *testing.T) {
haEtcd := &kubeadmapi.InitConfiguration{
ClusterConfiguration: kubeadmapi.ClusterConfiguration{
Etcd: kubeadmapi.Etcd{
External: &kubeadmapi.ExternalEtcd{
Endpoints: []string{"10.100.0.1:2379", "10.100.0.2:2379", "10.100.0.3:2379"},
},
},
},
}
noHAEtcd := &kubeadmapi.InitConfiguration{}
tests := []struct {
name string
cfg *kubeadmapi.InitConfiguration
etcdUpgrade bool
shouldDeleteEtcd bool
}{
{
name: "ha etcd but no etcd upgrade",
cfg: haEtcd,
etcdUpgrade: false,
shouldDeleteEtcd: true,
},
{
name: "non-ha etcd with etcd upgrade",
cfg: noHAEtcd,
etcdUpgrade: true,
shouldDeleteEtcd: false,
},
{
name: "ha etcd and etcd upgrade",
cfg: haEtcd,
etcdUpgrade: true,
shouldDeleteEtcd: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Use a temporary directory
tmpdir, err := ioutil.TempDir("", "TestGetPathManagerForUpgrade")
if err != nil {
t.Fatalf("unexpected error making temporary directory: %v", err)
}
defer func() {
os.RemoveAll(tmpdir)
}()
pathmgr, err := GetPathManagerForUpgrade(tmpdir, test.cfg, test.etcdUpgrade)
if err != nil {
t.Fatalf("unexpected error creating path manager: %v", err)
}
if _, err := os.Stat(pathmgr.BackupManifestDir()); os.IsNotExist(err) {
t.Errorf("expected manifest dir %s to exist, but it did not (%v)", pathmgr.BackupManifestDir(), err)
}
if _, err := os.Stat(pathmgr.BackupEtcdDir()); os.IsNotExist(err) {
t.Errorf("expected etcd dir %s to exist, but it did not (%v)", pathmgr.BackupEtcdDir(), err)
}
if err := pathmgr.CleanupDirs(); err != nil {
t.Fatalf("unexpected error cleaning up directories: %v", err)
}
if _, err := os.Stat(pathmgr.BackupManifestDir()); os.IsNotExist(err) {
t.Errorf("expected manifest dir %s to exist, but it did not (%v)", pathmgr.BackupManifestDir(), err)
}
if test.shouldDeleteEtcd {
if _, err := os.Stat(pathmgr.BackupEtcdDir()); !os.IsNotExist(err) {
t.Errorf("expected etcd dir %s not to exist, but it did (%v)", pathmgr.BackupEtcdDir(), err)
}
} else {
if _, err := os.Stat(pathmgr.BackupEtcdDir()); os.IsNotExist(err) {
t.Errorf("expected etcd dir %s to exist, but it did not", pathmgr.BackupEtcdDir())
}
}
})
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment