diff --git a/cmd/kubeadm/app/BUILD b/cmd/kubeadm/app/BUILD index 83e384e8fcd9c..c3705c11052da 100644 --- a/cmd/kubeadm/app/BUILD +++ b/cmd/kubeadm/app/BUILD @@ -46,6 +46,7 @@ filegroup( "//cmd/kubeadm/app/phases/markmaster:all-srcs", "//cmd/kubeadm/app/phases/selfhosting:all-srcs", "//cmd/kubeadm/app/phases/token:all-srcs", + "//cmd/kubeadm/app/phases/upgrade:all-srcs", "//cmd/kubeadm/app/phases/uploadconfig:all-srcs", "//cmd/kubeadm/app/preflight:all-srcs", "//cmd/kubeadm/app/util:all-srcs", diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index ae031e5464652..e7bf5647fc124 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -23,6 +23,8 @@ go_library( "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library", "//cmd/kubeadm/app/cmd/phases:go_default_library", + "//cmd/kubeadm/app/cmd/upgrade:go_default_library", + "//cmd/kubeadm/app/cmd/util:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/discovery:go_default_library", "//cmd/kubeadm/app/features:go_default_library", @@ -95,6 +97,8 @@ filegroup( srcs = [ ":package-srcs", "//cmd/kubeadm/app/cmd/phases:all-srcs", + "//cmd/kubeadm/app/cmd/upgrade:all-srcs", + "//cmd/kubeadm/app/cmd/util:all-srcs", ], tags = ["automanaged"], ) diff --git a/cmd/kubeadm/app/cmd/cmd.go b/cmd/kubeadm/app/cmd/cmd.go index 6013adfa923fc..9546f0d10426e 100644 --- a/cmd/kubeadm/app/cmd/cmd.go +++ b/cmd/kubeadm/app/cmd/cmd.go @@ -17,7 +17,6 @@ limitations under the License. package cmd import ( - "fmt" "io" "github.com/renstrom/dedent" @@ -25,6 +24,7 @@ import ( "k8s.io/apiserver/pkg/util/flag" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases" + "k8s.io/kubernetes/cmd/kubeadm/app/cmd/upgrade" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) @@ -75,6 +75,7 @@ func NewKubeadmCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob cmds.AddCommand(NewCmdReset(out)) cmds.AddCommand(NewCmdVersion(out)) cmds.AddCommand(NewCmdToken(out, err)) + cmds.AddCommand(upgrade.NewCmdUpgrade(out)) // Wrap not yet fully supported commands in an alpha subcommand experimentalCmd := &cobra.Command{ @@ -86,18 +87,3 @@ func NewKubeadmCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob return cmds } - -// subCmdRunE returns a function that handles a case where a subcommand must be specified -// Without this callback, if a user runs just the command without a subcommand, -// or with an invalid subcommand, cobra will print usage information, but still exit cleanly. -// We want to return an error code in these cases so that the -// user knows that their command was invalid. -func subCmdRunE(name string) func(*cobra.Command, []string) error { - return func(_ *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("missing subcommand; %q is not meant to be run on its own", name) - } - - return fmt.Errorf("invalid subcommand: %q", args[0]) - } -} diff --git a/cmd/kubeadm/app/cmd/config.go b/cmd/kubeadm/app/cmd/config.go index f4ff47c7cacd3..f5c792ed82152 100644 --- a/cmd/kubeadm/app/cmd/config.go +++ b/cmd/kubeadm/app/cmd/config.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientset "k8s.io/client-go/kubernetes" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/features" "k8s.io/kubernetes/cmd/kubeadm/app/phases/uploadconfig" @@ -52,7 +53,7 @@ func NewCmdConfig(out io.Writer) *cobra.Command { // cobra will print usage information, but still exit cleanly. // We want to return an error code in these cases so that the // user knows that their command was invalid. - RunE: subCmdRunE("config"), + RunE: cmdutil.SubCmdRunE("config"), } cmd.PersistentFlags().StringVar(&kubeConfigFile, "kubeconfig", "/etc/kubernetes/admin.conf", "The KubeConfig file to use for talking to the cluster") @@ -67,7 +68,7 @@ func NewCmdConfigUpload(out io.Writer, kubeConfigFile *string) *cobra.Command { cmd := &cobra.Command{ Use: "upload", Short: "Upload configuration about the current state so 'kubeadm upgrade' later can know how to configure the upgraded cluster", - RunE: subCmdRunE("upload"), + RunE: cmdutil.SubCmdRunE("upload"), } cmd.AddCommand(NewCmdConfigUploadFromFile(out, kubeConfigFile)) diff --git a/cmd/kubeadm/app/cmd/phases/BUILD b/cmd/kubeadm/app/cmd/phases/BUILD index 683b2ef5ed2bb..0bf15e601eaaa 100644 --- a/cmd/kubeadm/app/cmd/phases/BUILD +++ b/cmd/kubeadm/app/cmd/phases/BUILD @@ -25,6 +25,7 @@ go_library( "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library", + "//cmd/kubeadm/app/cmd/util:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/features:go_default_library", "//cmd/kubeadm/app/phases/bootstraptoken/clusterinfo:go_default_library", @@ -54,7 +55,6 @@ go_test( "controlplane_test.go", "etcd_test.go", "kubeconfig_test.go", - "phase_test.go", ], library = ":go_default_library", deps = [ diff --git a/cmd/kubeadm/app/cmd/phases/bootstraptoken.go b/cmd/kubeadm/app/cmd/phases/bootstraptoken.go index f9b4535872000..40a5f1d0a0265 100644 --- a/cmd/kubeadm/app/cmd/phases/bootstraptoken.go +++ b/cmd/kubeadm/app/cmd/phases/bootstraptoken.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" clientset "k8s.io/client-go/kubernetes" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/clusterinfo" "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" @@ -36,7 +37,7 @@ func NewCmdBootstrapToken() *cobra.Command { Use: "bootstrap-token", Short: "Manage kubeadm-specific Bootstrap Token functions.", Aliases: []string{"bootstraptoken"}, - RunE: subCmdRunE("bootstrap-token"), + RunE: cmdutil.SubCmdRunE("bootstrap-token"), } cmd.PersistentFlags().StringVar(&kubeConfigFile, "kubeconfig", "/etc/kubernetes/admin.conf", "The KubeConfig file to use for talking to the cluster") @@ -55,7 +56,7 @@ func NewSubCmdClusterInfo(kubeConfigFile *string) *cobra.Command { Short: "Uploads and exposes the cluster-info ConfigMap publicly from the given cluster-info file", Aliases: []string{"clusterinfo"}, Run: func(cmd *cobra.Command, args []string) { - err := validateExactArgNumber(args, []string{"clusterinfo-file"}) + err := cmdutil.ValidateExactArgNumber(args, []string{"clusterinfo-file"}) kubeadmutil.CheckErr(err) client, err := kubeconfigutil.ClientSetFromFile(*kubeConfigFile) @@ -81,7 +82,7 @@ func NewSubCmdNodeBootstrapToken(kubeConfigFile *string) *cobra.Command { Use: "node", Short: "Manages Node Bootstrap Tokens", Aliases: []string{"clusterinfo"}, - RunE: subCmdRunE("node"), + RunE: cmdutil.SubCmdRunE("node"), } cmd.AddCommand(NewSubCmdNodeBootstrapTokenPostCSRs(kubeConfigFile)) diff --git a/cmd/kubeadm/app/cmd/phases/certs.go b/cmd/kubeadm/app/cmd/phases/certs.go index 6e4344d74cd25..6749552a1c9fb 100644 --- a/cmd/kubeadm/app/cmd/phases/certs.go +++ b/cmd/kubeadm/app/cmd/phases/certs.go @@ -22,6 +22,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" @@ -34,7 +35,7 @@ func NewCmdCerts() *cobra.Command { Use: "certs", Aliases: []string{"certificates"}, Short: "Generate certificates for a Kubernetes cluster.", - RunE: subCmdRunE("certs"), + RunE: cmdutil.SubCmdRunE("certs"), } cmd.AddCommand(getCertsSubCommands()...) diff --git a/cmd/kubeadm/app/cmd/phases/controlplane.go b/cmd/kubeadm/app/cmd/phases/controlplane.go index bc9bc5c9dec23..06571fed3948a 100644 --- a/cmd/kubeadm/app/cmd/phases/controlplane.go +++ b/cmd/kubeadm/app/cmd/phases/controlplane.go @@ -21,6 +21,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" controlplanephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane" "k8s.io/kubernetes/pkg/api" @@ -31,7 +32,7 @@ func NewCmdControlplane() *cobra.Command { cmd := &cobra.Command{ Use: "controlplane", Short: "Generate all static pod manifest files necessary to establish the control plane.", - RunE: subCmdRunE("controlplane"), + RunE: cmdutil.SubCmdRunE("controlplane"), } manifestPath := kubeadmconstants.GetStaticPodDirectory() diff --git a/cmd/kubeadm/app/cmd/phases/etcd.go b/cmd/kubeadm/app/cmd/phases/etcd.go index 7d205160fe518..f3b9ab865f2f6 100644 --- a/cmd/kubeadm/app/cmd/phases/etcd.go +++ b/cmd/kubeadm/app/cmd/phases/etcd.go @@ -21,6 +21,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" etcdphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/etcd" "k8s.io/kubernetes/pkg/api" @@ -31,7 +32,7 @@ func NewCmdEtcd() *cobra.Command { cmd := &cobra.Command{ Use: "etcd", Short: "Generate static pod manifest file for etcd.", - RunE: subCmdRunE("etcd"), + RunE: cmdutil.SubCmdRunE("etcd"), } manifestPath := kubeadmconstants.GetStaticPodDirectory() diff --git a/cmd/kubeadm/app/cmd/phases/kubeconfig.go b/cmd/kubeadm/app/cmd/phases/kubeconfig.go index a8b144563ea35..47b88e80f2eae 100644 --- a/cmd/kubeadm/app/cmd/phases/kubeconfig.go +++ b/cmd/kubeadm/app/cmd/phases/kubeconfig.go @@ -24,6 +24,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" "k8s.io/kubernetes/pkg/api" @@ -34,7 +35,7 @@ func NewCmdKubeConfig(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "kubeconfig", Short: "Generate all kubeconfig files necessary to establish the control plane and the admin kubeconfig file.", - RunE: subCmdRunE("kubeconfig"), + RunE: cmdutil.SubCmdRunE("kubeconfig"), } cmd.AddCommand(getKubeConfigSubCommands(out, kubeadmconstants.KubernetesDir)...) diff --git a/cmd/kubeadm/app/cmd/phases/markmaster.go b/cmd/kubeadm/app/cmd/phases/markmaster.go index 5c4e91e2b0c86..89457fdfc7640 100644 --- a/cmd/kubeadm/app/cmd/phases/markmaster.go +++ b/cmd/kubeadm/app/cmd/phases/markmaster.go @@ -19,6 +19,7 @@ package phases import ( "github.com/spf13/cobra" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" markmasterphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/markmaster" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" @@ -32,7 +33,7 @@ func NewCmdMarkMaster() *cobra.Command { Short: "Mark a node as master.", Aliases: []string{"markmaster"}, RunE: func(_ *cobra.Command, args []string) error { - err := validateExactArgNumber(args, []string{"node-name"}) + err := cmdutil.ValidateExactArgNumber(args, []string{"node-name"}) kubeadmutil.CheckErr(err) client, err := kubeconfigutil.ClientSetFromFile(kubeConfigFile) diff --git a/cmd/kubeadm/app/cmd/phases/phase.go b/cmd/kubeadm/app/cmd/phases/phase.go index fd6edbfed03d8..f0a33c2b59e6c 100644 --- a/cmd/kubeadm/app/cmd/phases/phase.go +++ b/cmd/kubeadm/app/cmd/phases/phase.go @@ -17,10 +17,10 @@ limitations under the License. package phases import ( - "fmt" "io" "github.com/spf13/cobra" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" ) // NewCmdPhase returns the cobra command for the "kubeadm phase" command (currently alpha-gated) @@ -28,7 +28,7 @@ func NewCmdPhase(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "phase", Short: "Invoke subsets of kubeadm functions separately for a manual install.", - RunE: subCmdRunE("phase"), + RunE: cmdutil.SubCmdRunE("phase"), } cmd.AddCommand(NewCmdBootstrapToken()) @@ -43,37 +43,3 @@ func NewCmdPhase(out io.Writer) *cobra.Command { return cmd } - -// subCmdRunE returns a function that handles a case where a subcommand must be specified -// Without this callback, if a user runs just the command without a subcommand, -// or with an invalid subcommand, cobra will print usage information, but still exit cleanly. -// We want to return an error code in these cases so that the -// user knows that their command was invalid. -func subCmdRunE(name string) func(*cobra.Command, []string) error { - return func(_ *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("missing subcommand; %q is not meant to be run on its own", name) - } - - return fmt.Errorf("invalid subcommand: %q", args[0]) - } -} - -// validateExactArgNumber validates that the required top-level arguments are specified -func validateExactArgNumber(args []string, supportedArgs []string) error { - validArgs := 0 - // Disregard possible "" arguments; they are invalid - for _, arg := range args { - if len(arg) > 0 { - validArgs++ - } - } - - if validArgs < len(supportedArgs) { - return fmt.Errorf("missing one or more required arguments. Required arguments: %v", supportedArgs) - } - if validArgs > len(supportedArgs) { - return fmt.Errorf("too many arguments, only %d argument(s) supported: %v", validArgs, supportedArgs) - } - return nil -} diff --git a/cmd/kubeadm/app/cmd/phases/preflight.go b/cmd/kubeadm/app/cmd/phases/preflight.go index b16a8e01e6ca1..f47e35126c891 100644 --- a/cmd/kubeadm/app/cmd/phases/preflight.go +++ b/cmd/kubeadm/app/cmd/phases/preflight.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" "k8s.io/kubernetes/cmd/kubeadm/app/preflight" ) @@ -27,7 +28,7 @@ func NewCmdPreFlight() *cobra.Command { cmd := &cobra.Command{ Use: "preflight", Short: "Run pre-flight checks", - RunE: subCmdRunE("preflight"), + RunE: cmdutil.SubCmdRunE("preflight"), } cmd.AddCommand(NewCmdPreFlightMaster()) diff --git a/cmd/kubeadm/app/cmd/phases/selfhosting.go b/cmd/kubeadm/app/cmd/phases/selfhosting.go index 2fd9c3a8206cb..09b3b2483658c 100644 --- a/cmd/kubeadm/app/cmd/phases/selfhosting.go +++ b/cmd/kubeadm/app/cmd/phases/selfhosting.go @@ -23,6 +23,7 @@ import ( kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" "k8s.io/kubernetes/cmd/kubeadm/app/features" "k8s.io/kubernetes/cmd/kubeadm/app/phases/selfhosting" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" @@ -37,7 +38,7 @@ func NewCmdSelfhosting() *cobra.Command { Use: "selfhosting", Aliases: []string{"selfhosted"}, Short: "Make a kubeadm cluster self-hosted.", - RunE: subCmdRunE("selfhosting"), + RunE: cmdutil.SubCmdRunE("selfhosting"), } cmd.AddCommand(getSelfhostingSubCommand()) diff --git a/cmd/kubeadm/app/cmd/token.go b/cmd/kubeadm/app/cmd/token.go index 6523c7cf914be..d3bbda1041dee 100644 --- a/cmd/kubeadm/app/cmd/token.go +++ b/cmd/kubeadm/app/cmd/token.go @@ -33,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/fields" clientset "k8s.io/client-go/kubernetes" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" tokenphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" @@ -77,7 +78,7 @@ func NewCmdToken(out io.Writer, errW io.Writer) *cobra.Command { // cobra will print usage information, but still exit cleanly. // We want to return an error code in these cases so that the // user knows that their command was invalid. - RunE: subCmdRunE("token"), + RunE: cmdutil.SubCmdRunE("token"), } tokenCmd.PersistentFlags().StringVar(&kubeConfigFile, diff --git a/cmd/kubeadm/app/cmd/upgrade/BUILD b/cmd/kubeadm/app/cmd/upgrade/BUILD new file mode 100644 index 0000000000000..dc34c1134b00e --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/BUILD @@ -0,0 +1,55 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "apply.go", + "common.go", + "plan.go", + "upgrade.go", + ], + visibility = ["//visibility:public"], + deps = [ + "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", + "//cmd/kubeadm/app/cmd/util:go_default_library", + "//cmd/kubeadm/app/phases/upgrade:go_default_library", + "//cmd/kubeadm/app/preflight:go_default_library", + "//cmd/kubeadm/app/util:go_default_library", + "//cmd/kubeadm/app/util/kubeconfig:go_default_library", + "//pkg/api:go_default_library", + "//pkg/util/version:go_default_library", + "//vendor/github.com/ghodss/yaml:go_default_library", + "//vendor/github.com/spf13/cobra:go_default_library", + "//vendor/k8s.io/client-go/kubernetes:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "apply_test.go", + "common_test.go", + "plan_test.go", + ], + library = ":go_default_library", + deps = [ + "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", + "//cmd/kubeadm/app/phases/upgrade:go_default_library", + "//pkg/util/version:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/kubeadm/app/cmd/upgrade/apply.go b/cmd/kubeadm/app/cmd/upgrade/apply.go new file mode 100644 index 0000000000000..319029ce07806 --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/apply.go @@ -0,0 +1,213 @@ +/* +Copyright 2017 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 upgrade + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + + clientset "k8s.io/client-go/kubernetes" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/util/version" +) + +// applyFlags holds the information about the flags that can be passed to apply +type applyFlags struct { + nonInteractiveMode bool + force bool + dryRun bool + newK8sVersionStr string + newK8sVersion *version.Version + imagePullTimeout time.Duration + parent *cmdUpgradeFlags +} + +// SessionIsInteractive returns true if the session is of an interactive type (the default, can be opted out of with -y, -f or --dry-run) +func (f *applyFlags) SessionIsInteractive() bool { + return !f.nonInteractiveMode +} + +// NewCmdApply returns the cobra command for `kubeadm upgrade apply` +func NewCmdApply(parentFlags *cmdUpgradeFlags) *cobra.Command { + flags := &applyFlags{ + parent: parentFlags, + imagePullTimeout: 15 * time.Minute, + } + + cmd := &cobra.Command{ + Use: "apply [version]", + Short: "Upgrade your Kubernetes cluster to the specified version", + Run: func(cmd *cobra.Command, args []string) { + // Ensure the user is root + err := runPreflightChecks(flags.parent.skipPreFlight) + kubeadmutil.CheckErr(err) + + err = cmdutil.ValidateExactArgNumber(args, []string{"version"}) + kubeadmutil.CheckErr(err) + + // It's safe to use args[0] here as the slice has been validated above + flags.newK8sVersionStr = args[0] + + // Default the flags dynamically, based on each others' value + err = SetImplicitFlags(flags) + kubeadmutil.CheckErr(err) + + err = RunApply(flags) + kubeadmutil.CheckErr(err) + }, + } + + // Specify the valid flags specific for apply + cmd.Flags().BoolVarP(&flags.nonInteractiveMode, "yes", "y", flags.nonInteractiveMode, "Perform the upgrade and do not prompt for confirmation (non-interactive mode).") + cmd.Flags().BoolVarP(&flags.force, "force", "f", flags.force, "Force upgrading although some requirements might not be met. This also implies non-interactive mode.") + cmd.Flags().BoolVar(&flags.dryRun, "dry-run", flags.dryRun, "Do not change any state, just output what actions would be applied.") + cmd.Flags().DurationVar(&flags.imagePullTimeout, "image-pull-timeout", flags.imagePullTimeout, "The maximum amount of time to wait for the control plane pods to be downloaded.") + + return cmd +} + +// RunApply takes care of the actual upgrade functionality +// It does the following things: +// - Checks if the cluster is healthy +// - Gets the configuration from the kubeadm-config ConfigMap in the cluster +// - Enforces all version skew policies +// - Asks the user if they really want to upgrade +// - Makes sure the control plane images are available locally on the master(s) +// - Upgrades the control plane components +// - Applies the other resources that'd be created with kubeadm init as well, like +// - Creating the RBAC rules for the Bootstrap Tokens and the cluster-info ConfigMap +// - Applying new kube-dns and kube-proxy manifests +// - Uploads the newly used configuration to the cluster ConfigMap +func RunApply(flags *applyFlags) error { + + // Start with the basics, verify that the cluster is healthy and get the configuration from the cluster (using the ConfigMap) + upgradeVars, err := enforceRequirements(flags.parent.kubeConfigPath, flags.parent.cfgPath, flags.parent.printConfig) + if err != nil { + return err + } + + // Set the upgraded version on the external config object now + upgradeVars.cfg.KubernetesVersion = flags.newK8sVersionStr + + // Grab the external, versioned configuration and convert it to the internal type for usage here later + internalcfg := &kubeadmapi.MasterConfiguration{} + api.Scheme.Convert(upgradeVars.cfg, internalcfg, nil) + + // Enforce the version skew policies + if err := EnforceVersionPolicies(flags, upgradeVars.versionGetter); err != nil { + return fmt.Errorf("[upgrade/version] FATAL: %v", err) + } + + // If the current session is interactive, ask the user whether they really want to upgrade + if flags.SessionIsInteractive() { + if err := InteractivelyConfirmUpgrade("Are you sure you want to proceed with the upgrade?"); err != nil { + return err + } + } + + // TODO: Implement a prepulling mechanism here + + // Now; perform the upgrade procedure + if err := PerformControlPlaneUpgrade(flags, upgradeVars.client, internalcfg); err != nil { + return fmt.Errorf("[upgrade/apply] FATAL: %v", err) + } + + // Upgrade RBAC rules and addons. Optionally, if needed, perform some extra task for a specific version + if err := upgrade.PerformPostUpgradeTasks(upgradeVars.client, internalcfg, flags.newK8sVersion); err != nil { + return fmt.Errorf("[upgrade/postupgrade] FATAL: %v", err) + } + + fmt.Println("") + fmt.Printf("[upgrade/successful] SUCCESS! Your cluster was upgraded to %q. Enjoy!\n", flags.newK8sVersionStr) + fmt.Println("") + fmt.Println("[upgrade/kubelet] Now that your control plane is upgraded, please proceed with upgrading your kubelets in turn.") + + return nil +} + +// SetImplicitFlags handles dynamically defaulting flags based on each other's value +func SetImplicitFlags(flags *applyFlags) error { + // If we are in dry-run or force mode; we should automatically execute this command non-interactively + if flags.dryRun || flags.force { + flags.nonInteractiveMode = true + } + + k8sVer, err := version.ParseSemantic(flags.newK8sVersionStr) + if err != nil { + return fmt.Errorf("couldn't parse version %q as a semantic version", flags.newK8sVersionStr) + } + flags.newK8sVersion = k8sVer + + // Automatically add the "v" prefix to the string representation in case it doesn't exist + if !strings.HasPrefix(flags.newK8sVersionStr, "v") { + flags.newK8sVersionStr = fmt.Sprintf("v%s", flags.newK8sVersionStr) + } + + return nil +} + +// EnforceVersionPolicies makes sure that the version the user specified is valid to upgrade to +// There are both fatal and skippable (with --force) errors +func EnforceVersionPolicies(flags *applyFlags, versionGetter upgrade.VersionGetter) error { + fmt.Printf("[upgrade/version] You have chosen to upgrade to version %q\n", flags.newK8sVersionStr) + + versionSkewErrs := upgrade.EnforceVersionPolicies(versionGetter, flags.newK8sVersionStr, flags.newK8sVersion, flags.parent.allowExperimentalUpgrades, flags.parent.allowRCUpgrades) + if versionSkewErrs != nil { + + if len(versionSkewErrs.Mandatory) > 0 { + return fmt.Errorf("The --version argument is invalid due to these fatal errors: %v", versionSkewErrs.Mandatory) + } + + if len(versionSkewErrs.Skippable) > 0 { + // Return the error if the user hasn't specified the --force flag + if !flags.force { + return fmt.Errorf("The --version argument is invalid due to these errors: %v. Can be bypassed if you pass the --force flag", versionSkewErrs.Mandatory) + } + // Soft errors found, but --force was specified + fmt.Printf("[upgrade/version] Found %d potential version compatibility errors but skipping since the --force flag is set: %v\n", len(versionSkewErrs.Skippable), versionSkewErrs.Skippable) + } + } + return nil +} + +// PerformControlPlaneUpgrade actually performs the upgrade procedure for the cluster of your type (self-hosted or static-pod-hosted) +func PerformControlPlaneUpgrade(flags *applyFlags, client clientset.Interface, internalcfg *kubeadmapi.MasterConfiguration) error { + + // Check if the cluster is self-hosted and act accordingly + if upgrade.IsControlPlaneSelfHosted(client) { + fmt.Printf("[upgrade/apply] Upgrading your Self-Hosted control plane to version %q...\n", flags.newK8sVersionStr) + + // Upgrade a self-hosted cluster + // TODO(luxas): Implement this later when we have the new upgrade strategy + return fmt.Errorf("not implemented") + } + + // OK, the cluster is hosted using static pods. Upgrade a static-pod hosted cluster + fmt.Printf("[upgrade/apply] Upgrading your Static Pod-hosted control plane to version %q...\n", flags.newK8sVersionStr) + + if err := upgrade.PerformStaticPodControlPlaneUpgrade(client, internalcfg, flags.newK8sVersion); err != nil { + return err + } + return nil +} diff --git a/cmd/kubeadm/app/cmd/upgrade/apply_test.go b/cmd/kubeadm/app/cmd/upgrade/apply_test.go new file mode 100644 index 0000000000000..8f8b15845352b --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/apply_test.go @@ -0,0 +1,194 @@ +/* +Copyright 2017 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 upgrade + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/util/version" +) + +func TestSetImplicitFlags(t *testing.T) { + var tests = []struct { + flags *applyFlags + expectedFlags applyFlags + errExpected bool + }{ + { // if not dryRun or force is set; the nonInteractiveMode field should not be touched + flags: &applyFlags{ + newK8sVersionStr: "v1.8.0", + dryRun: false, + force: false, + nonInteractiveMode: false, + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0", + newK8sVersion: version.MustParseSemantic("v1.8.0"), + dryRun: false, + force: false, + nonInteractiveMode: false, + }, + }, + { // if not dryRun or force is set; the nonInteractiveMode field should not be touched + flags: &applyFlags{ + newK8sVersionStr: "v1.8.0", + dryRun: false, + force: false, + nonInteractiveMode: true, + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0", + newK8sVersion: version.MustParseSemantic("v1.8.0"), + dryRun: false, + force: false, + nonInteractiveMode: true, + }, + }, + { // if dryRun or force is set; the nonInteractiveMode field should be set to true + flags: &applyFlags{ + newK8sVersionStr: "v1.8.0", + dryRun: true, + force: false, + nonInteractiveMode: false, + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0", + newK8sVersion: version.MustParseSemantic("v1.8.0"), + dryRun: true, + force: false, + nonInteractiveMode: true, + }, + }, + { // if dryRun or force is set; the nonInteractiveMode field should be set to true + flags: &applyFlags{ + newK8sVersionStr: "v1.8.0", + dryRun: false, + force: true, + nonInteractiveMode: false, + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0", + newK8sVersion: version.MustParseSemantic("v1.8.0"), + dryRun: false, + force: true, + nonInteractiveMode: true, + }, + }, + { // if dryRun or force is set; the nonInteractiveMode field should be set to true + flags: &applyFlags{ + newK8sVersionStr: "v1.8.0", + dryRun: true, + force: true, + nonInteractiveMode: false, + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0", + newK8sVersion: version.MustParseSemantic("v1.8.0"), + dryRun: true, + force: true, + nonInteractiveMode: true, + }, + }, + { // if dryRun or force is set; the nonInteractiveMode field should be set to true + flags: &applyFlags{ + newK8sVersionStr: "v1.8.0", + dryRun: true, + force: true, + nonInteractiveMode: true, + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0", + newK8sVersion: version.MustParseSemantic("v1.8.0"), + dryRun: true, + force: true, + nonInteractiveMode: true, + }, + }, + { // if the new version is empty; it should error out + flags: &applyFlags{ + newK8sVersionStr: "", + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "", + }, + errExpected: true, + }, + { // if the new version is invalid; it should error out + flags: &applyFlags{ + newK8sVersionStr: "foo", + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "foo", + }, + errExpected: true, + }, + { // if the new version is valid but without the "v" prefix; it parse and prepend v + flags: &applyFlags{ + newK8sVersionStr: "1.8.0", + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0", + newK8sVersion: version.MustParseSemantic("v1.8.0"), + }, + errExpected: false, + }, + { // valid version should succeed + flags: &applyFlags{ + newK8sVersionStr: "v1.8.1", + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.1", + newK8sVersion: version.MustParseSemantic("v1.8.1"), + }, + errExpected: false, + }, + { // valid version should succeed + flags: &applyFlags{ + newK8sVersionStr: "1.8.0-alpha.3", + }, + expectedFlags: applyFlags{ + newK8sVersionStr: "v1.8.0-alpha.3", + newK8sVersion: version.MustParseSemantic("v1.8.0-alpha.3"), + }, + errExpected: false, + }, + } + for _, rt := range tests { + actualErr := SetImplicitFlags(rt.flags) + + // If an error was returned; make newK8sVersion nil so it's easy to match using reflect.DeepEqual later (instead of a random pointer) + if actualErr != nil { + rt.flags.newK8sVersion = nil + } + + if !reflect.DeepEqual(*rt.flags, rt.expectedFlags) { + t.Errorf( + "failed SetImplicitFlags:\n\texpected flags: %v\n\t actual: %v", + rt.expectedFlags, + *rt.flags, + ) + } + if (actualErr != nil) != rt.errExpected { + t.Errorf( + "failed SetImplicitFlags:\n\texpected error: %t\n\t actual: %t", + rt.errExpected, + (actualErr != nil), + ) + } + } +} diff --git a/cmd/kubeadm/app/cmd/upgrade/common.go b/cmd/kubeadm/app/cmd/upgrade/common.go new file mode 100644 index 0000000000000..4a64d37011bfa --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/common.go @@ -0,0 +1,120 @@ +/* +Copyright 2017 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 upgrade + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "strings" + + "github.com/ghodss/yaml" + + clientset "k8s.io/client-go/kubernetes" + kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" + "k8s.io/kubernetes/cmd/kubeadm/app/preflight" + kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" +) + +// upgradeVariables holds variables needed for performing an upgrade or planning to do so +// TODO - Restructure or rename upgradeVariables +type upgradeVariables struct { + client clientset.Interface + cfg *kubeadmapiext.MasterConfiguration + versionGetter upgrade.VersionGetter +} + +// enforceRequirements verifies that it's okay to upgrade and then returns the variables needed for the rest of the procedure +func enforceRequirements(kubeConfigPath, cfgPath string, printConfig bool) (*upgradeVariables, error) { + client, err := kubeconfigutil.ClientSetFromFile(kubeConfigPath) + if err != nil { + return nil, fmt.Errorf("couldn't create a Kubernetes client from file %q: %v", kubeConfigPath, err) + } + + // Run healthchecks against the cluster + if err := upgrade.CheckClusterHealth(client); err != nil { + return nil, fmt.Errorf("[upgrade/health] FATAL: %v", err) + } + + // Fetch the configuration from a file or ConfigMap and validate it + cfg, err := upgrade.FetchConfiguration(client, os.Stdout, cfgPath) + if err != nil { + return nil, fmt.Errorf("[upgrade/config] FATAL: %v", err) + } + + // If the user told us to print this information out; do it! + if printConfig { + printConfiguration(cfg, os.Stdout) + } + + return &upgradeVariables{ + client: client, + cfg: cfg, + // Use a real version getter interface that queries the API server, the kubeadm client and the Kubernetes CI system for latest versions + versionGetter: upgrade.NewKubeVersionGetter(client, os.Stdout), + }, nil +} + +// printConfiguration prints the external version of the API to yaml +func printConfiguration(cfg *kubeadmapiext.MasterConfiguration, w io.Writer) { + // Short-circuit if cfg is nil, so we can safely get the value of the pointer below + if cfg == nil { + return + } + + cfgYaml, err := yaml.Marshal(*cfg) + if err == nil { + fmt.Fprintln(w, "[upgrade/config] Configuration used:") + + scanner := bufio.NewScanner(bytes.NewReader(cfgYaml)) + for scanner.Scan() { + fmt.Fprintf(w, "\t%s\n", scanner.Text()) + } + } +} + +// runPreflightChecks runs the root preflight check +func runPreflightChecks(skipPreFlight bool) error { + if skipPreFlight { + fmt.Println("[preflight] Skipping pre-flight checks") + return nil + } + + fmt.Println("[preflight] Running pre-flight checks") + return preflight.RunRootCheckOnly() +} + +// InteractivelyConfirmUpgrade asks the user whether they _really_ want to upgrade. +func InteractivelyConfirmUpgrade(question string) error { + + fmt.Printf("[upgrade/confirm] %s [y/N]: ", question) + + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + if err := scanner.Err(); err != nil { + return fmt.Errorf("couldn't read from standard input: %v", err) + } + answer := scanner.Text() + if strings.ToLower(answer) == "y" || strings.ToLower(answer) == "yes" { + return nil + } + + return fmt.Errorf("won't proceed; the user didn't answer (Y|y) in order to continue") +} diff --git a/cmd/kubeadm/app/cmd/upgrade/common_test.go b/cmd/kubeadm/app/cmd/upgrade/common_test.go new file mode 100644 index 0000000000000..4bf791d3009b5 --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/common_test.go @@ -0,0 +1,124 @@ +/* +Copyright 2017 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 upgrade + +import ( + "bytes" + "testing" + + kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" +) + +func TestPrintConfiguration(t *testing.T) { + var tests = []struct { + cfg *kubeadmapiext.MasterConfiguration + buf *bytes.Buffer + expectedBytes []byte + }{ + { + cfg: nil, + expectedBytes: []byte(""), + }, + { + cfg: &kubeadmapiext.MasterConfiguration{ + KubernetesVersion: "v1.7.1", + }, + expectedBytes: []byte(`[upgrade/config] Configuration used: + api: + advertiseAddress: "" + bindPort: 0 + apiServerCertSANs: null + apiServerExtraArgs: null + authorizationModes: null + certificatesDir: "" + cloudProvider: "" + controllerManagerExtraArgs: null + etcd: + caFile: "" + certFile: "" + dataDir: "" + endpoints: null + extraArgs: null + image: "" + keyFile: "" + featureFlags: null + imageRepository: "" + kubernetesVersion: v1.7.1 + networking: + dnsDomain: "" + podSubnet: "" + serviceSubnet: "" + nodeName: "" + schedulerExtraArgs: null + token: "" + tokenTTL: 0 + unifiedControlPlaneImage: "" +`), + }, + { + cfg: &kubeadmapiext.MasterConfiguration{ + KubernetesVersion: "v1.7.1", + Networking: kubeadmapiext.Networking{ + ServiceSubnet: "10.96.0.1/12", + }, + }, + expectedBytes: []byte(`[upgrade/config] Configuration used: + api: + advertiseAddress: "" + bindPort: 0 + apiServerCertSANs: null + apiServerExtraArgs: null + authorizationModes: null + certificatesDir: "" + cloudProvider: "" + controllerManagerExtraArgs: null + etcd: + caFile: "" + certFile: "" + dataDir: "" + endpoints: null + extraArgs: null + image: "" + keyFile: "" + featureFlags: null + imageRepository: "" + kubernetesVersion: v1.7.1 + networking: + dnsDomain: "" + podSubnet: "" + serviceSubnet: 10.96.0.1/12 + nodeName: "" + schedulerExtraArgs: null + token: "" + tokenTTL: 0 + unifiedControlPlaneImage: "" +`), + }, + } + for _, rt := range tests { + rt.buf = bytes.NewBufferString("") + printConfiguration(rt.cfg, rt.buf) + actualBytes := rt.buf.Bytes() + if !bytes.Equal(actualBytes, rt.expectedBytes) { + t.Errorf( + "failed PrintConfiguration:\n\texpected: %q\n\t actual: %q", + string(rt.expectedBytes), + string(actualBytes), + ) + } + } +} diff --git a/cmd/kubeadm/app/cmd/upgrade/plan.go b/cmd/kubeadm/app/cmd/upgrade/plan.go new file mode 100644 index 0000000000000..545b362ae85a8 --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/plan.go @@ -0,0 +1,142 @@ +/* +Copyright 2017 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 upgrade + +import ( + "fmt" + "io" + "os" + "sort" + "text/tabwriter" + + "github.com/spf13/cobra" + + "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" +) + +// NewCmdPlan returns the cobra command for `kubeadm upgrade plan` +func NewCmdPlan(parentFlags *cmdUpgradeFlags) *cobra.Command { + cmd := &cobra.Command{ + Use: "plan", + Short: "Check which versions are available to upgrade to and validate whether your current cluster is upgradeable", + Run: func(_ *cobra.Command, _ []string) { + // Ensure the user is root + err := runPreflightChecks(parentFlags.skipPreFlight) + kubeadmutil.CheckErr(err) + + err = RunPlan(parentFlags) + kubeadmutil.CheckErr(err) + }, + } + + return cmd +} + +// RunPlan takes care of outputting available versions to upgrade to for the user +func RunPlan(parentFlags *cmdUpgradeFlags) error { + + // Start with the basics, verify that the cluster is healthy, build a client and a versionGetter. + upgradeVars, err := enforceRequirements(parentFlags.kubeConfigPath, parentFlags.cfgPath, parentFlags.printConfig) + if err != nil { + return err + } + + // Compute which upgrade possibilities there are + availUpgrades, err := upgrade.GetAvailableUpgrades(upgradeVars.versionGetter, parentFlags.allowExperimentalUpgrades, parentFlags.allowRCUpgrades) + if err != nil { + return err + } + + // Tell the user which upgrades are available + printAvailableUpgrades(availUpgrades, os.Stdout) + return nil +} + +// printAvailableUpgrades prints a UX-friendly overview of what versions are available to upgrade to +// TODO look into columnize or some other formatter when time permits instead of using the tabwriter +func printAvailableUpgrades(upgrades []upgrade.Upgrade, w io.Writer) { + + // Return quickly if no upgrades can be made + if len(upgrades) == 0 { + fmt.Fprintln(w, "Awesome, you're up-to-date! Enjoy!") + return + } + // The tab writer writes to the "real" writer w + tabw := tabwriter.NewWriter(w, 10, 4, 3, ' ', 0) + + // Loop through the upgrade possibilities and output text to the command line + for _, upgrade := range upgrades { + + if upgrade.CanUpgradeKubelets() { + fmt.Fprintln(w, "Components that must be upgraded manually after you've upgraded the control plane with 'kubeadm upgrade apply':") + fmt.Fprintln(tabw, "COMPONENT\tCURRENT\tAVAILABLE") + firstPrinted := false + + // The map is of the form :. Here all the keys are put into a slice and sorted + // in order to always get the right order. Then the map value is extracted separately + for _, oldVersion := range sortedSliceFromStringIntMap(upgrade.Before.KubeletVersions) { + nodeCount := upgrade.Before.KubeletVersions[oldVersion] + if !firstPrinted { + // Output the Kubelet header only on the first version pair + fmt.Fprintf(tabw, "Kubelet\t%d x %s\t%s\n", nodeCount, oldVersion, upgrade.After.KubeVersion) + firstPrinted = true + continue + } + fmt.Fprintf(tabw, "\t\t%d x %s\t%s\n", nodeCount, oldVersion, upgrade.After.KubeVersion) + } + // We should flush the writer here at this stage; as the columns will now be of the right size, adjusted to the above content + tabw.Flush() + fmt.Fprintln(w, "") + } + + fmt.Fprintf(w, "Upgrade to the latest %s:\n", upgrade.Description) + fmt.Fprintln(w, "") + fmt.Fprintln(tabw, "COMPONENT\tCURRENT\tAVAILABLE") + fmt.Fprintf(tabw, "API Server\t%s\t%s\n", upgrade.Before.KubeVersion, upgrade.After.KubeVersion) + fmt.Fprintf(tabw, "Controller Manager\t%s\t%s\n", upgrade.Before.KubeVersion, upgrade.After.KubeVersion) + fmt.Fprintf(tabw, "Scheduler\t%s\t%s\n", upgrade.Before.KubeVersion, upgrade.After.KubeVersion) + fmt.Fprintf(tabw, "Kube Proxy\t%s\t%s\n", upgrade.Before.KubeVersion, upgrade.After.KubeVersion) + fmt.Fprintf(tabw, "Kube DNS\t%s\t%s\n", upgrade.Before.DNSVersion, upgrade.After.DNSVersion) + + // The tabwriter should be flushed at this stage as we have now put in all the required content for this time. This is required for the tabs' size to be correct. + tabw.Flush() + fmt.Fprintln(w, "") + fmt.Fprintln(w, "You can now apply the upgrade by executing the following command:") + fmt.Fprintln(w, "") + fmt.Fprintf(w, "\tkubeadm upgrade apply %s\n", upgrade.After.KubeVersion) + fmt.Fprintln(w, "") + + if upgrade.Before.KubeadmVersion != upgrade.After.KubeadmVersion { + fmt.Fprintf(w, "Note: Before you do can perform this upgrade, you have to update kubeadm to %s\n", upgrade.After.KubeadmVersion) + fmt.Fprintln(w, "") + } + + fmt.Fprintln(w, "_____________________________________________________________________") + fmt.Fprintln(w, "") + } +} + +// sortedSliceFromStringIntMap returns a slice of the keys in the map sorted alphabetically +func sortedSliceFromStringIntMap(strMap map[string]uint16) []string { + strSlice := []string{} + for k := range strMap { + strSlice = append(strSlice, k) + } + sort.Strings(strSlice) + return strSlice +} diff --git a/cmd/kubeadm/app/cmd/upgrade/plan_test.go b/cmd/kubeadm/app/cmd/upgrade/plan_test.go new file mode 100644 index 0000000000000..bddef5c393cef --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/plan_test.go @@ -0,0 +1,329 @@ +/* +Copyright 2017 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 upgrade + +import ( + "bytes" + "reflect" + "testing" + + "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" +) + +func TestSortedSliceFromStringIntMap(t *testing.T) { + var tests = []struct { + strMap map[string]uint16 + expectedSlice []string + }{ // The returned slice should be alphabetically sorted based on the string keys in the map + { + strMap: map[string]uint16{"foo": 1, "bar": 2}, + expectedSlice: []string{"bar", "foo"}, + }, + { // The int value should not affect this func + strMap: map[string]uint16{"foo": 2, "bar": 1}, + expectedSlice: []string{"bar", "foo"}, + }, + { + strMap: map[string]uint16{"b": 2, "a": 1, "cb": 0, "ca": 1000}, + expectedSlice: []string{"a", "b", "ca", "cb"}, + }, + { // This should work for version numbers as well; and the lowest version should come first + strMap: map[string]uint16{"v1.7.0": 1, "v1.6.1": 1, "v1.6.2": 1, "v1.8.0": 1, "v1.8.0-alpha.1": 1}, + expectedSlice: []string{"v1.6.1", "v1.6.2", "v1.7.0", "v1.8.0", "v1.8.0-alpha.1"}, + }, + } + for _, rt := range tests { + actualSlice := sortedSliceFromStringIntMap(rt.strMap) + if !reflect.DeepEqual(actualSlice, rt.expectedSlice) { + t.Errorf( + "failed SortedSliceFromStringIntMap:\n\texpected: %v\n\t actual: %v", + rt.expectedSlice, + actualSlice, + ) + } + } +} + +// TODO Think about modifying this test to be less verbose checking b/c it can be brittle. +func TestPrintAvailableUpgrades(t *testing.T) { + var tests = []struct { + upgrades []upgrade.Upgrade + buf *bytes.Buffer + expectedBytes []byte + }{ + { + upgrades: []upgrade.Upgrade{}, + expectedBytes: []byte(`Awesome, you're up-to-date! Enjoy! +`), + }, + { + upgrades: []upgrade.Upgrade{ + { + Description: "version in the v1.7 series", + Before: upgrade.ClusterState{ + KubeVersion: "v1.7.1", + KubeletVersions: map[string]uint16{ + "v1.7.1": 1, + }, + KubeadmVersion: "v1.7.2", + DNSVersion: "1.14.4", + }, + After: upgrade.ClusterState{ + KubeVersion: "v1.7.3", + KubeadmVersion: "v1.7.3", + DNSVersion: "1.14.4", + }, + }, + }, + expectedBytes: []byte(`Components that must be upgraded manually after you've upgraded the control plane with 'kubeadm upgrade apply': +COMPONENT CURRENT AVAILABLE +Kubelet 1 x v1.7.1 v1.7.3 + +Upgrade to the latest version in the v1.7 series: + +COMPONENT CURRENT AVAILABLE +API Server v1.7.1 v1.7.3 +Controller Manager v1.7.1 v1.7.3 +Scheduler v1.7.1 v1.7.3 +Kube Proxy v1.7.1 v1.7.3 +Kube DNS 1.14.4 1.14.4 + +You can now apply the upgrade by executing the following command: + + kubeadm upgrade apply v1.7.3 + +Note: Before you do can perform this upgrade, you have to update kubeadm to v1.7.3 + +_____________________________________________________________________ + +`), + }, + { + upgrades: []upgrade.Upgrade{ + { + Description: "stable version", + Before: upgrade.ClusterState{ + KubeVersion: "v1.7.3", + KubeletVersions: map[string]uint16{ + "v1.7.3": 1, + }, + KubeadmVersion: "v1.8.0", + DNSVersion: "1.14.4", + }, + After: upgrade.ClusterState{ + KubeVersion: "v1.8.0", + KubeadmVersion: "v1.8.0", + DNSVersion: "1.14.4", + }, + }, + }, + expectedBytes: []byte(`Components that must be upgraded manually after you've upgraded the control plane with 'kubeadm upgrade apply': +COMPONENT CURRENT AVAILABLE +Kubelet 1 x v1.7.3 v1.8.0 + +Upgrade to the latest stable version: + +COMPONENT CURRENT AVAILABLE +API Server v1.7.3 v1.8.0 +Controller Manager v1.7.3 v1.8.0 +Scheduler v1.7.3 v1.8.0 +Kube Proxy v1.7.3 v1.8.0 +Kube DNS 1.14.4 1.14.4 + +You can now apply the upgrade by executing the following command: + + kubeadm upgrade apply v1.8.0 + +_____________________________________________________________________ + +`), + }, + { + upgrades: []upgrade.Upgrade{ + { + Description: "version in the v1.7 series", + Before: upgrade.ClusterState{ + KubeVersion: "v1.7.3", + KubeletVersions: map[string]uint16{ + "v1.7.3": 1, + }, + KubeadmVersion: "v1.8.1", + DNSVersion: "1.14.4", + }, + After: upgrade.ClusterState{ + KubeVersion: "v1.7.5", + KubeadmVersion: "v1.8.1", + DNSVersion: "1.14.4", + }, + }, + { + Description: "stable version", + Before: upgrade.ClusterState{ + KubeVersion: "v1.7.3", + KubeletVersions: map[string]uint16{ + "v1.7.3": 1, + }, + KubeadmVersion: "v1.8.1", + DNSVersion: "1.14.4", + }, + After: upgrade.ClusterState{ + KubeVersion: "v1.8.2", + KubeadmVersion: "v1.8.2", + DNSVersion: "1.14.4", + }, + }, + }, + expectedBytes: []byte(`Components that must be upgraded manually after you've upgraded the control plane with 'kubeadm upgrade apply': +COMPONENT CURRENT AVAILABLE +Kubelet 1 x v1.7.3 v1.7.5 + +Upgrade to the latest version in the v1.7 series: + +COMPONENT CURRENT AVAILABLE +API Server v1.7.3 v1.7.5 +Controller Manager v1.7.3 v1.7.5 +Scheduler v1.7.3 v1.7.5 +Kube Proxy v1.7.3 v1.7.5 +Kube DNS 1.14.4 1.14.4 + +You can now apply the upgrade by executing the following command: + + kubeadm upgrade apply v1.7.5 + +_____________________________________________________________________ + +Components that must be upgraded manually after you've upgraded the control plane with 'kubeadm upgrade apply': +COMPONENT CURRENT AVAILABLE +Kubelet 1 x v1.7.3 v1.8.2 + +Upgrade to the latest stable version: + +COMPONENT CURRENT AVAILABLE +API Server v1.7.3 v1.8.2 +Controller Manager v1.7.3 v1.8.2 +Scheduler v1.7.3 v1.8.2 +Kube Proxy v1.7.3 v1.8.2 +Kube DNS 1.14.4 1.14.4 + +You can now apply the upgrade by executing the following command: + + kubeadm upgrade apply v1.8.2 + +Note: Before you do can perform this upgrade, you have to update kubeadm to v1.8.2 + +_____________________________________________________________________ + +`), + }, + { + upgrades: []upgrade.Upgrade{ + { + Description: "experimental version", + Before: upgrade.ClusterState{ + KubeVersion: "v1.7.5", + KubeletVersions: map[string]uint16{ + "v1.7.5": 1, + }, + KubeadmVersion: "v1.7.5", + DNSVersion: "1.14.4", + }, + After: upgrade.ClusterState{ + KubeVersion: "v1.8.0-beta.1", + KubeadmVersion: "v1.8.0-beta.1", + DNSVersion: "1.14.4", + }, + }, + }, + expectedBytes: []byte(`Components that must be upgraded manually after you've upgraded the control plane with 'kubeadm upgrade apply': +COMPONENT CURRENT AVAILABLE +Kubelet 1 x v1.7.5 v1.8.0-beta.1 + +Upgrade to the latest experimental version: + +COMPONENT CURRENT AVAILABLE +API Server v1.7.5 v1.8.0-beta.1 +Controller Manager v1.7.5 v1.8.0-beta.1 +Scheduler v1.7.5 v1.8.0-beta.1 +Kube Proxy v1.7.5 v1.8.0-beta.1 +Kube DNS 1.14.4 1.14.4 + +You can now apply the upgrade by executing the following command: + + kubeadm upgrade apply v1.8.0-beta.1 + +Note: Before you do can perform this upgrade, you have to update kubeadm to v1.8.0-beta.1 + +_____________________________________________________________________ + +`), + }, + { + upgrades: []upgrade.Upgrade{ + { + Description: "release candidate version", + Before: upgrade.ClusterState{ + KubeVersion: "v1.7.5", + KubeletVersions: map[string]uint16{ + "v1.7.5": 1, + }, + KubeadmVersion: "v1.7.5", + DNSVersion: "1.14.4", + }, + After: upgrade.ClusterState{ + KubeVersion: "v1.8.0-rc.1", + KubeadmVersion: "v1.8.0-rc.1", + DNSVersion: "1.14.4", + }, + }, + }, + expectedBytes: []byte(`Components that must be upgraded manually after you've upgraded the control plane with 'kubeadm upgrade apply': +COMPONENT CURRENT AVAILABLE +Kubelet 1 x v1.7.5 v1.8.0-rc.1 + +Upgrade to the latest release candidate version: + +COMPONENT CURRENT AVAILABLE +API Server v1.7.5 v1.8.0-rc.1 +Controller Manager v1.7.5 v1.8.0-rc.1 +Scheduler v1.7.5 v1.8.0-rc.1 +Kube Proxy v1.7.5 v1.8.0-rc.1 +Kube DNS 1.14.4 1.14.4 + +You can now apply the upgrade by executing the following command: + + kubeadm upgrade apply v1.8.0-rc.1 + +Note: Before you do can perform this upgrade, you have to update kubeadm to v1.8.0-rc.1 + +_____________________________________________________________________ + +`), + }, + } + for _, rt := range tests { + rt.buf = bytes.NewBufferString("") + printAvailableUpgrades(rt.upgrades, rt.buf) + actualBytes := rt.buf.Bytes() + if !bytes.Equal(actualBytes, rt.expectedBytes) { + t.Errorf( + "failed PrintAvailableUpgrades:\n\texpected: %q\n\t actual: %q", + string(rt.expectedBytes), + string(actualBytes), + ) + } + } +} diff --git a/cmd/kubeadm/app/cmd/upgrade/upgrade.go b/cmd/kubeadm/app/cmd/upgrade/upgrade.go new file mode 100644 index 0000000000000..9f9b14f08ff13 --- /dev/null +++ b/cmd/kubeadm/app/cmd/upgrade/upgrade.go @@ -0,0 +1,64 @@ +/* +Copyright 2017 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 upgrade + +import ( + "io" + + "github.com/spf13/cobra" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" +) + +// cmdUpgradeFlags holds the values for the common flags in `kubeadm upgrade` +type cmdUpgradeFlags struct { + kubeConfigPath string + cfgPath string + allowExperimentalUpgrades bool + allowRCUpgrades bool + printConfig bool + skipPreFlight bool +} + +// NewCmdUpgrade returns the cobra command for `kubeadm upgrade` +func NewCmdUpgrade(out io.Writer) *cobra.Command { + flags := &cmdUpgradeFlags{ + kubeConfigPath: "/etc/kubernetes/admin.conf", + cfgPath: "", + allowExperimentalUpgrades: false, + allowRCUpgrades: false, + printConfig: false, + skipPreFlight: false, + } + + cmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade your cluster smoothly to a newer version with this command.", + RunE: cmdutil.SubCmdRunE("upgrade"), + } + + cmd.PersistentFlags().StringVar(&flags.kubeConfigPath, "kubeconfig", flags.kubeConfigPath, "The KubeConfig file to use for talking to the cluster.") + cmd.PersistentFlags().StringVar(&flags.cfgPath, "config", flags.cfgPath, "Path to kubeadm config file (WARNING: Usage of a configuration file is experimental).") + cmd.PersistentFlags().BoolVar(&flags.allowExperimentalUpgrades, "allow-experimental-upgrades", flags.allowExperimentalUpgrades, "Show unstable versions of Kubernetes as an upgrade alternative and allow upgrading to an alpha/beta/release candidate versions of Kubernetes.") + cmd.PersistentFlags().BoolVar(&flags.allowRCUpgrades, "allow-release-candidate-upgrades", flags.allowRCUpgrades, "Show release candidate versions of Kubernetes as an upgrade alternative and allow upgrading to a release candidate versions of Kubernetes.") + cmd.PersistentFlags().BoolVar(&flags.printConfig, "print-config", flags.printConfig, "Whether the configuration file that will be used in the upgrade should be printed or not.") + cmd.PersistentFlags().BoolVar(&flags.skipPreFlight, "skip-preflight-checks", flags.skipPreFlight, "Skip preflight checks normally run before modifying the system") + + cmd.AddCommand(NewCmdApply(flags)) + cmd.AddCommand(NewCmdPlan(flags)) + + return cmd +} diff --git a/cmd/kubeadm/app/cmd/util/BUILD b/cmd/kubeadm/app/cmd/util/BUILD new file mode 100644 index 0000000000000..cb123d65d2a60 --- /dev/null +++ b/cmd/kubeadm/app/cmd/util/BUILD @@ -0,0 +1,28 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["cmdutil.go"], + visibility = ["//visibility:public"], + deps = ["//vendor/github.com/spf13/cobra:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["cmdutil_test.go"], + library = ":go_default_library", +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/kubeadm/app/cmd/util/cmdutil.go b/cmd/kubeadm/app/cmd/util/cmdutil.go new file mode 100644 index 0000000000000..3c0e7d65b1043 --- /dev/null +++ b/cmd/kubeadm/app/cmd/util/cmdutil.go @@ -0,0 +1,57 @@ +/* +Copyright 2017 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 phases + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// SubCmdRunE returns a function that handles a case where a subcommand must be specified +// Without this callback, if a user runs just the command without a subcommand, +// or with an invalid subcommand, cobra will print usage information, but still exit cleanly. +// We want to return an error code in these cases so that the +// user knows that their command was invalid. +func SubCmdRunE(name string) func(*cobra.Command, []string) error { + return func(_ *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("missing subcommand; %q is not meant to be run on its own", name) + } + + return fmt.Errorf("invalid subcommand: %q", args[0]) + } +} + +// ValidateExactArgNumber validates that the required top-level arguments are specified +func ValidateExactArgNumber(args []string, supportedArgs []string) error { + validArgs := 0 + // Disregard possible "" arguments; they are invalid + for _, arg := range args { + if len(arg) > 0 { + validArgs++ + } + } + + if validArgs < len(supportedArgs) { + return fmt.Errorf("missing one or more required arguments. Required arguments: %v", supportedArgs) + } + if validArgs > len(supportedArgs) { + return fmt.Errorf("too many arguments, only %d argument(s) supported: %v", validArgs, supportedArgs) + } + return nil +} diff --git a/cmd/kubeadm/app/cmd/phases/phase_test.go b/cmd/kubeadm/app/cmd/util/cmdutil_test.go similarity index 92% rename from cmd/kubeadm/app/cmd/phases/phase_test.go rename to cmd/kubeadm/app/cmd/util/cmdutil_test.go index 67a5283337fc6..ef4d81ce009a0 100644 --- a/cmd/kubeadm/app/cmd/phases/phase_test.go +++ b/cmd/kubeadm/app/cmd/util/cmdutil_test.go @@ -52,10 +52,10 @@ func TestValidateExactArgNumber(t *testing.T) { }, } for _, rt := range tests { - actual := validateExactArgNumber(rt.args, rt.supportedArgs) + actual := ValidateExactArgNumber(rt.args, rt.supportedArgs) if (actual != nil) != rt.expectedErr { t.Errorf( - "failed validateExactArgNumber:\n\texpected error: %t\n\t actual error: %t", + "failed ValidateExactArgNumber:\n\texpected error: %t\n\t actual error: %t", rt.expectedErr, (actual != nil), ) diff --git a/cmd/kubeadm/app/phases/upgrade/BUILD b/cmd/kubeadm/app/phases/upgrade/BUILD new file mode 100644 index 0000000000000..ae4a84b4a547c --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/BUILD @@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "compute.go", + "configuration.go", + "health.go", + "policy.go", + "postupgrade.go", + "staticpods.go", + "versiongetter.go", + ], + visibility = ["//visibility:public"], + deps = [ + "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", + "//pkg/api:go_default_library", + "//pkg/util/version:go_default_library", + "//vendor/k8s.io/client-go/kubernetes:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/kubeadm/app/phases/upgrade/compute.go b/cmd/kubeadm/app/phases/upgrade/compute.go new file mode 100644 index 0000000000000..e81eb93b9d3b2 --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/compute.go @@ -0,0 +1,63 @@ +/* +Copyright 2017 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 upgrade + +import ( + "fmt" +) + +// Upgrade defines an upgrade possibility to upgrade from a current version to a new one +type Upgrade struct { + Description string + Before ClusterState + After ClusterState +} + +// CanUpgradeKubelets returns whether an upgrade of any kubelet in the cluster is possible +func (u *Upgrade) CanUpgradeKubelets() bool { + // If there are multiple different versions now, an upgrade is possible (even if only for a subset of the nodes) + if len(u.Before.KubeletVersions) > 1 { + return true + } + // Don't report something available for upgrade if we don't know the current state + if len(u.Before.KubeletVersions) == 0 { + return false + } + + // if the same version number existed both before and after, we don't have to upgrade it + _, sameVersionFound := u.Before.KubeletVersions[u.After.KubeVersion] + return !sameVersionFound +} + +// ClusterState describes the state of certain versions for a cluster +type ClusterState struct { + // KubeVersion describes the version of the Kubernetes API Server, Controller Manager, Scheduler and Proxy. + KubeVersion string + // DNSVersion describes the version of the kube-dns images used and manifest version + DNSVersion string + // KubeadmVersion describes the version of the kubeadm CLI + KubeadmVersion string + // KubeletVersions is a map with a version number linked to the amount of kubelets running that version in the cluster + KubeletVersions map[string]uint16 +} + +// GetAvailableUpgrades fetches all versions from the specified VersionGetter and computes which +// kinds of upgrades can be performed +func GetAvailableUpgrades(_ VersionGetter, _, _ bool) ([]Upgrade, error) { + fmt.Println("[upgrade] Fetching available versions to upgrade to:") + return []Upgrade{}, nil +} diff --git a/cmd/kubeadm/app/phases/upgrade/configuration.go b/cmd/kubeadm/app/phases/upgrade/configuration.go new file mode 100644 index 0000000000000..a91e4140a8a06 --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/configuration.go @@ -0,0 +1,36 @@ +/* +Copyright 2017 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 upgrade + +import ( + "fmt" + "io" + + clientset "k8s.io/client-go/kubernetes" + kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + "k8s.io/kubernetes/pkg/api" +) + +// FetchConfiguration fetches configuration required for upgrading your cluster from a file (which has precedence) or a ConfigMap in the cluster +func FetchConfiguration(_ clientset.Interface, _ io.Writer, _ string) (*kubeadmapiext.MasterConfiguration, error) { + fmt.Println("[upgrade/config] Making sure the configuration is correct:") + + cfg := &kubeadmapiext.MasterConfiguration{} + api.Scheme.Default(cfg) + + return cfg, nil +} diff --git a/cmd/kubeadm/app/phases/upgrade/health.go b/cmd/kubeadm/app/phases/upgrade/health.go new file mode 100644 index 0000000000000..1beac03999660 --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/health.go @@ -0,0 +1,36 @@ +/* +Copyright 2017 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 upgrade + +import ( + clientset "k8s.io/client-go/kubernetes" +) + +// CheckClusterHealth makes sure: +// - the API /healthz endpoint is healthy +// - all Nodes are Ready +// - (if self-hosted) that there are DaemonSets with at least one Pod for all control plane components +// - (if static pod-hosted) that all required Static Pod manifests exist on disk +func CheckClusterHealth(_ clientset.Interface) error { + return nil +} + +// IsControlPlaneSelfHosted returns whether the control plane is self hosted or not +func IsControlPlaneSelfHosted(_ clientset.Interface) bool { + // No-op for now + return false +} diff --git a/cmd/kubeadm/app/phases/upgrade/policy.go b/cmd/kubeadm/app/phases/upgrade/policy.go new file mode 100644 index 0000000000000..6b06a4116e4f1 --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/policy.go @@ -0,0 +1,33 @@ +/* +Copyright 2017 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 upgrade + +import ( + "k8s.io/kubernetes/pkg/util/version" +) + +// VersionSkewPolicyErrors describes version skew errors that might be seen during the validation process in EnforceVersionPolicies +type VersionSkewPolicyErrors struct { + Mandatory []error + Skippable []error +} + +// EnforceVersionPolicies enforces that the proposed new version is compatible with all the different version skew policies +func EnforceVersionPolicies(_ VersionGetter, _ string, _ *version.Version, _, _ bool) *VersionSkewPolicyErrors { + // No-op now and return no skew errors + return nil +} diff --git a/cmd/kubeadm/app/phases/upgrade/postupgrade.go b/cmd/kubeadm/app/phases/upgrade/postupgrade.go new file mode 100644 index 0000000000000..3510caa57a27d --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/postupgrade.go @@ -0,0 +1,30 @@ +/* +Copyright 2017 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 upgrade + +import ( + clientset "k8s.io/client-go/kubernetes" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + "k8s.io/kubernetes/pkg/util/version" +) + +// PerformPostUpgradeTasks runs nearly the same functions as 'kubeadm init' would do +// Note that the markmaster phase is left out, not needed, and no token is created as that doesn't belong to the upgrade +func PerformPostUpgradeTasks(_ clientset.Interface, _ *kubeadmapi.MasterConfiguration, _ *version.Version) error { + // No-op; don't do anything here yet + return nil +} diff --git a/cmd/kubeadm/app/phases/upgrade/staticpods.go b/cmd/kubeadm/app/phases/upgrade/staticpods.go new file mode 100644 index 0000000000000..6970560f54137 --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/staticpods.go @@ -0,0 +1,29 @@ +/* +Copyright 2017 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 upgrade + +import ( + clientset "k8s.io/client-go/kubernetes" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + "k8s.io/kubernetes/pkg/util/version" +) + +// PerformStaticPodControlPlaneUpgrade upgrades a static pod-hosted control plane +func PerformStaticPodControlPlaneUpgrade(_ clientset.Interface, _ *kubeadmapi.MasterConfiguration, _ *version.Version) error { + // No-op for now; doesn't do anything yet + return nil +} diff --git a/cmd/kubeadm/app/phases/upgrade/versiongetter.go b/cmd/kubeadm/app/phases/upgrade/versiongetter.go new file mode 100644 index 0000000000000..879ca11276f30 --- /dev/null +++ b/cmd/kubeadm/app/phases/upgrade/versiongetter.go @@ -0,0 +1,83 @@ +/* +Copyright 2017 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 upgrade + +import ( + "fmt" + "io" + + clientset "k8s.io/client-go/kubernetes" + versionutil "k8s.io/kubernetes/pkg/util/version" +) + +// VersionGetter defines an interface for fetching different versions. +// Easy to implement a fake variant of this interface for unit testing +type VersionGetter interface { + // ClusterVersion should return the version of the cluster i.e. the API Server version + ClusterVersion() (string, *versionutil.Version, error) + // KubeadmVersion should return the version of the kubeadm CLI + KubeadmVersion() (string, *versionutil.Version, error) + // VersionFromCILabel should resolve CI labels like `latest`, `stable`, `stable-1.8`, etc. to real versions + VersionFromCILabel(string, string) (string, *versionutil.Version, error) + // KubeletVersions should return a map with a version and a number that describes how many kubelets there are for that version + KubeletVersions() (map[string]uint16, error) +} + +// KubeVersionGetter handles the version-fetching mechanism from external sources +type KubeVersionGetter struct { + client clientset.Interface + w io.Writer +} + +// Make sure KubeVersionGetter implements the VersionGetter interface +var _ VersionGetter = &KubeVersionGetter{} + +// NewKubeVersionGetter returns a new instance of KubeVersionGetter +func NewKubeVersionGetter(client clientset.Interface, writer io.Writer) *KubeVersionGetter { + return &KubeVersionGetter{ + client: client, + w: writer, + } +} + +// ClusterVersion gets API server version +func (g *KubeVersionGetter) ClusterVersion() (string, *versionutil.Version, error) { + fmt.Fprintf(g.w, "[upgrade/versions] Cluster version: ") + fmt.Fprintln(g.w, "v1.7.0") + + return "v1.7.0", versionutil.MustParseSemantic("v1.7.0"), nil +} + +// KubeadmVersion gets kubeadm version +func (g *KubeVersionGetter) KubeadmVersion() (string, *versionutil.Version, error) { + fmt.Fprintf(g.w, "[upgrade/versions] kubeadm version: %s\n", "v1.8.0") + + return "v1.8.0", versionutil.MustParseSemantic("v1.8.0"), nil +} + +// VersionFromCILabel resolves different labels like "stable" to action semver versions using the Kubernetes CI uploads to GCS +func (g *KubeVersionGetter) VersionFromCILabel(_, _ string) (string, *versionutil.Version, error) { + return "v1.8.1", versionutil.MustParseSemantic("v1.8.0"), nil +} + +// KubeletVersions gets the versions of the kubelets in the cluster +func (g *KubeVersionGetter) KubeletVersions() (map[string]uint16, error) { + // This tells kubeadm that there are two nodes in the cluster; both on the v1.7.1 version currently + return map[string]uint16{ + "v1.7.1": 2, + }, nil +}