From a145cf81b095f91914140706bb6bf8cb1b004be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 25 Aug 2017 13:59:33 +0300 Subject: [PATCH 1/4] kubeadm: Resolve tech debt; move commonly used funcs to a general package instead of duplicating --- cmd/kubeadm/app/BUILD | 1 + cmd/kubeadm/app/cmd/BUILD | 4 ++ cmd/kubeadm/app/cmd/cmd.go | 18 +----- cmd/kubeadm/app/cmd/config.go | 5 +- cmd/kubeadm/app/cmd/phases/BUILD | 2 +- cmd/kubeadm/app/cmd/phases/bootstraptoken.go | 7 ++- cmd/kubeadm/app/cmd/phases/certs.go | 3 +- cmd/kubeadm/app/cmd/phases/controlplane.go | 3 +- cmd/kubeadm/app/cmd/phases/etcd.go | 3 +- cmd/kubeadm/app/cmd/phases/kubeconfig.go | 3 +- cmd/kubeadm/app/cmd/phases/markmaster.go | 3 +- cmd/kubeadm/app/cmd/phases/phase.go | 38 +------------ cmd/kubeadm/app/cmd/phases/preflight.go | 3 +- cmd/kubeadm/app/cmd/phases/selfhosting.go | 3 +- cmd/kubeadm/app/cmd/token.go | 3 +- cmd/kubeadm/app/cmd/util/BUILD | 28 +++++++++ cmd/kubeadm/app/cmd/util/cmdutil.go | 57 +++++++++++++++++++ .../phase_test.go => util/cmdutil_test.go} | 4 +- 18 files changed, 120 insertions(+), 68 deletions(-) create mode 100644 cmd/kubeadm/app/cmd/util/BUILD create mode 100644 cmd/kubeadm/app/cmd/util/cmdutil.go rename cmd/kubeadm/app/cmd/{phases/phase_test.go => util/cmdutil_test.go} (92%) diff --git a/cmd/kubeadm/app/BUILD b/cmd/kubeadm/app/BUILD index 83e384e8fcd..c3705c11052 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 ae031e54646..e7bf5647fc1 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 6013adfa923..9546f0d1042 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 f4ff47c7cac..f5c792ed821 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 683b2ef5ed2..0bf15e601ea 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 f9b45358720..40a5f1d0a02 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 6e4344d74cd..6749552a1c9 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 bc9bc5c9dec..06571fed394 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 7d205160fe5..f3b9ab865f2 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 a8b144563ea..47b88e80f2e 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 5c4e91e2b0c..89457fdfc76 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 fd6edbfed03..f0a33c2b59e 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 b16a8e01e6c..f47e35126c8 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 2fd9c3a8206..f7b20b69a3e 100644 --- a/cmd/kubeadm/app/cmd/phases/selfhosting.go +++ b/cmd/kubeadm/app/cmd/phases/selfhosting.go @@ -29,6 +29,7 @@ import ( configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" "k8s.io/kubernetes/pkg/api" + cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" ) // NewCmdSelfhosting returns the self-hosting Cobra command @@ -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 6523c7cf914..d3bbda1041d 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/util/BUILD b/cmd/kubeadm/app/cmd/util/BUILD new file mode 100644 index 00000000000..cb123d65d2a --- /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 00000000000..3c0e7d65b10 --- /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 67a5283337f..ef4d81ce009 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), ) From 65f225a2653ce2364a6bf3b7138eec530c8a6eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 25 Aug 2017 14:00:16 +0300 Subject: [PATCH 2/4] kubeadm: Add 'kubeadm upgrade plan' and 'kubeadm upgrade apply' CLI commands --- cmd/kubeadm/app/cmd/upgrade/apply.go | 213 ++++++++++++++++++ cmd/kubeadm/app/cmd/upgrade/common.go | 120 ++++++++++ cmd/kubeadm/app/cmd/upgrade/plan.go | 142 ++++++++++++ cmd/kubeadm/app/cmd/upgrade/upgrade.go | 64 ++++++ cmd/kubeadm/app/phases/upgrade/compute.go | 63 ++++++ .../app/phases/upgrade/configuration.go | 36 +++ cmd/kubeadm/app/phases/upgrade/health.go | 36 +++ cmd/kubeadm/app/phases/upgrade/policy.go | 33 +++ cmd/kubeadm/app/phases/upgrade/postupgrade.go | 30 +++ cmd/kubeadm/app/phases/upgrade/staticpods.go | 29 +++ .../app/phases/upgrade/versiongetter.go | 83 +++++++ 11 files changed, 849 insertions(+) create mode 100644 cmd/kubeadm/app/cmd/upgrade/apply.go create mode 100644 cmd/kubeadm/app/cmd/upgrade/common.go create mode 100644 cmd/kubeadm/app/cmd/upgrade/plan.go create mode 100644 cmd/kubeadm/app/cmd/upgrade/upgrade.go create mode 100644 cmd/kubeadm/app/phases/upgrade/compute.go create mode 100644 cmd/kubeadm/app/phases/upgrade/configuration.go create mode 100644 cmd/kubeadm/app/phases/upgrade/health.go create mode 100644 cmd/kubeadm/app/phases/upgrade/policy.go create mode 100644 cmd/kubeadm/app/phases/upgrade/postupgrade.go create mode 100644 cmd/kubeadm/app/phases/upgrade/staticpods.go create mode 100644 cmd/kubeadm/app/phases/upgrade/versiongetter.go diff --git a/cmd/kubeadm/app/cmd/upgrade/apply.go b/cmd/kubeadm/app/cmd/upgrade/apply.go new file mode 100644 index 00000000000..319029ce078 --- /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/common.go b/cmd/kubeadm/app/cmd/upgrade/common.go new file mode 100644 index 00000000000..4a64d37011b --- /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/plan.go b/cmd/kubeadm/app/cmd/upgrade/plan.go new file mode 100644 index 00000000000..545b362ae85 --- /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/upgrade.go b/cmd/kubeadm/app/cmd/upgrade/upgrade.go new file mode 100644 index 00000000000..9f9b14f08ff --- /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/phases/upgrade/compute.go b/cmd/kubeadm/app/phases/upgrade/compute.go new file mode 100644 index 00000000000..e81eb93b9d3 --- /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 00000000000..a91e4140a8a --- /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 00000000000..1beac039996 --- /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 00000000000..6b06a4116e4 --- /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 00000000000..3510caa57a2 --- /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 00000000000..6970560f541 --- /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 00000000000..879ca11276f --- /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 +} From f9c3148af5096292b3196472d1e85db1177c1457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 25 Aug 2017 14:00:39 +0300 Subject: [PATCH 3/4] Add unit tests for kubeadm upgrade|plan --- cmd/kubeadm/app/cmd/upgrade/apply_test.go | 194 ++++++++++++ cmd/kubeadm/app/cmd/upgrade/common_test.go | 124 ++++++++ cmd/kubeadm/app/cmd/upgrade/plan_test.go | 329 +++++++++++++++++++++ 3 files changed, 647 insertions(+) create mode 100644 cmd/kubeadm/app/cmd/upgrade/apply_test.go create mode 100644 cmd/kubeadm/app/cmd/upgrade/common_test.go create mode 100644 cmd/kubeadm/app/cmd/upgrade/plan_test.go 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 00000000000..8f8b1584535 --- /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_test.go b/cmd/kubeadm/app/cmd/upgrade/common_test.go new file mode 100644 index 00000000000..4bf791d3009 --- /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_test.go b/cmd/kubeadm/app/cmd/upgrade/plan_test.go new file mode 100644 index 00000000000..bddef5c393c --- /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), + ) + } + } +} From 396a33dd8fe112454652cdbf8618c8671c18c42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20K=C3=A4ldstr=C3=B6m?= Date: Fri, 25 Aug 2017 17:23:17 +0300 Subject: [PATCH 4/4] autogenerated bazel --- cmd/kubeadm/app/cmd/phases/selfhosting.go | 2 +- cmd/kubeadm/app/cmd/upgrade/BUILD | 55 +++++++++++++++++++++++ cmd/kubeadm/app/phases/upgrade/BUILD | 36 +++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 cmd/kubeadm/app/cmd/upgrade/BUILD create mode 100644 cmd/kubeadm/app/phases/upgrade/BUILD diff --git a/cmd/kubeadm/app/cmd/phases/selfhosting.go b/cmd/kubeadm/app/cmd/phases/selfhosting.go index f7b20b69a3e..09b3b248365 100644 --- a/cmd/kubeadm/app/cmd/phases/selfhosting.go +++ b/cmd/kubeadm/app/cmd/phases/selfhosting.go @@ -23,13 +23,13 @@ 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" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" "k8s.io/kubernetes/pkg/api" - cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" ) // NewCmdSelfhosting returns the self-hosting Cobra command diff --git a/cmd/kubeadm/app/cmd/upgrade/BUILD b/cmd/kubeadm/app/cmd/upgrade/BUILD new file mode 100644 index 00000000000..dc34c1134b0 --- /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/phases/upgrade/BUILD b/cmd/kubeadm/app/phases/upgrade/BUILD new file mode 100644 index 00000000000..ae4a84b4a54 --- /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"], +)