From 9d6e2b923a5309df75d378639303a4fdc29b97a0 Mon Sep 17 00:00:00 2001 From: "Rostislav M. Georgiev" Date: Wed, 10 Jun 2020 17:29:47 +0300 Subject: [PATCH 1/2] kubeadm upgrade plan: Use internal types of the output API `kubeadm upgrade plan` is using the external (currently `v1alpha1`) types of the kubeadm output API to collect upgrade plans. This is counter intuitive since code structure gets bound to the whatever version the output API is at. In addition to that, the versioned API is used only in the very last stages of a machine readable output (which is currently not implemented). Hence, to increase flexibility and keep up with the standard Kubernetes ecosystem practice, `kubeadm upgrade plan` is migrated to use the internal types of the output API. Signed-off-by: Rostislav M. Georgiev --- cmd/kubeadm/app/cmd/upgrade/BUILD | 2 +- cmd/kubeadm/app/cmd/upgrade/plan.go | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/cmd/kubeadm/app/cmd/upgrade/BUILD b/cmd/kubeadm/app/cmd/upgrade/BUILD index 69637538864..85925a4eb98 100644 --- a/cmd/kubeadm/app/cmd/upgrade/BUILD +++ b/cmd/kubeadm/app/cmd/upgrade/BUILD @@ -15,7 +15,7 @@ go_library( deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library", - "//cmd/kubeadm/app/apis/output/v1alpha1:go_default_library", + "//cmd/kubeadm/app/apis/output:go_default_library", "//cmd/kubeadm/app/cmd/options:go_default_library", "//cmd/kubeadm/app/cmd/phases/upgrade/node:go_default_library", "//cmd/kubeadm/app/cmd/phases/workflow:go_default_library", diff --git a/cmd/kubeadm/app/cmd/upgrade/plan.go b/cmd/kubeadm/app/cmd/upgrade/plan.go index a6e4d4bebd8..2e1505e5b09 100644 --- a/cmd/kubeadm/app/cmd/upgrade/plan.go +++ b/cmd/kubeadm/app/cmd/upgrade/plan.go @@ -26,10 +26,11 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/version" "k8s.io/klog/v2" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" - outputapiv1alpha1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha1" + outputapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/output" "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" ) @@ -96,9 +97,9 @@ func runPlan(flags *planFlags, args []string) error { return nil } -// newComponentUpgradePlan helper creates outputapiv1alpha1.ComponentUpgradePlan object -func newComponentUpgradePlan(name, currentVersion, newVersion string) outputapiv1alpha1.ComponentUpgradePlan { - return outputapiv1alpha1.ComponentUpgradePlan{ +// newComponentUpgradePlan helper creates outputapi.ComponentUpgradePlan object +func newComponentUpgradePlan(name, currentVersion, newVersion string) outputapi.ComponentUpgradePlan { + return outputapi.ComponentUpgradePlan{ Name: name, CurrentVersion: currentVersion, NewVersion: newVersion, @@ -107,7 +108,7 @@ func newComponentUpgradePlan(name, currentVersion, newVersion string) outputapiv // TODO There is currently no way to cleanly output upgrades that involve adding, removing, or changing components // https://github.com/kubernetes/kubeadm/issues/810 was created to track addressing this. -func appendDNSComponent(components []outputapiv1alpha1.ComponentUpgradePlan, up *upgrade.Upgrade, DNSType kubeadmapi.DNSAddOnType, name string) []outputapiv1alpha1.ComponentUpgradePlan { +func appendDNSComponent(components []outputapi.ComponentUpgradePlan, up *upgrade.Upgrade, DNSType kubeadmapi.DNSAddOnType, name string) []outputapi.ComponentUpgradePlan { beforeVersion, afterVersion := "", "" if up.Before.DNSType == DNSType { beforeVersion = up.Before.DNSVersion @@ -123,7 +124,7 @@ func appendDNSComponent(components []outputapiv1alpha1.ComponentUpgradePlan, up } // genUpgradePlan generates output-friendly upgrade plan out of upgrade.Upgrade structure -func genUpgradePlan(up *upgrade.Upgrade, isExternalEtcd bool) (*outputapiv1alpha1.UpgradePlan, string, error) { +func genUpgradePlan(up *upgrade.Upgrade, isExternalEtcd bool) (*outputapi.UpgradePlan, string, error) { newK8sVersion, err := version.ParseSemantic(up.After.KubeVersion) if err != nil { return nil, "", errors.Wrapf(err, "Unable to parse normalized version %q as a semantic version", up.After.KubeVersion) @@ -138,7 +139,7 @@ func genUpgradePlan(up *upgrade.Upgrade, isExternalEtcd bool) (*outputapiv1alpha } } - components := []outputapiv1alpha1.ComponentUpgradePlan{} + components := []outputapi.ComponentUpgradePlan{} if up.CanUpgradeKubelets() { // The map is of the form :. Here all the keys are put into a slice and sorted @@ -161,11 +162,11 @@ func genUpgradePlan(up *upgrade.Upgrade, isExternalEtcd bool) (*outputapiv1alpha components = append(components, newComponentUpgradePlan(constants.Etcd, up.Before.EtcdVersion, up.After.EtcdVersion)) } - return &outputapiv1alpha1.UpgradePlan{Components: components}, unstableVersionFlag, nil + return &outputapi.UpgradePlan{Components: components}, unstableVersionFlag, nil } // printUpgradePlan prints a UX-friendly overview of what versions are available to upgrade to -func printUpgradePlan(up *upgrade.Upgrade, plan *outputapiv1alpha1.UpgradePlan, unstableVersionFlag string, isExternalEtcd bool, w io.Writer) { +func printUpgradePlan(up *upgrade.Upgrade, plan *outputapi.UpgradePlan, unstableVersionFlag string, isExternalEtcd bool, w io.Writer) { // The tab writer writes to the "real" writer w tabw := tabwriter.NewWriter(w, 10, 4, 3, ' ', 0) From 709e3c3a8360e805266b745b23187092a5444ca2 Mon Sep 17 00:00:00 2001 From: "Rostislav M. Georgiev" Date: Mon, 10 Feb 2020 13:09:48 +0200 Subject: [PATCH 2/2] kubeadm upgrade plan: component config state table This change enables kubeadm upgrade plan to print a state table with information regarding known component config API groups. Most importantly this information includes current and preferred version for each group and an indication if a manual user upgrade is required. Signed-off-by: Rostislav M. Georgiev --- cmd/kubeadm/app/apis/output/types.go | 22 + cmd/kubeadm/app/apis/output/v1alpha1/types.go | 22 + .../v1alpha1/zz_generated.conversion.go | 38 ++ .../output/v1alpha1/zz_generated.deepcopy.go | 21 + .../app/apis/output/zz_generated.deepcopy.go | 21 + cmd/kubeadm/app/cmd/upgrade/BUILD | 1 + cmd/kubeadm/app/cmd/upgrade/plan.go | 91 +++- cmd/kubeadm/app/componentconfigs/BUILD | 2 + cmd/kubeadm/app/componentconfigs/configset.go | 55 +++ .../app/componentconfigs/configset_test.go | 405 ++++++++++++++++++ 10 files changed, 676 insertions(+), 2 deletions(-) diff --git a/cmd/kubeadm/app/apis/output/types.go b/cmd/kubeadm/app/apis/output/types.go index a71ddae5f80..cff41df51ec 100644 --- a/cmd/kubeadm/app/apis/output/types.go +++ b/cmd/kubeadm/app/apis/output/types.go @@ -48,6 +48,26 @@ type ComponentUpgradePlan struct { NewVersion string } +// ComponentConfigVersionState describes the current and desired version of a component config +type ComponentConfigVersionState struct { + // Group points to the Kubernetes API group that covers the config + Group string + + // CurrentVersion is the currently active component config version + // NOTE: This can be empty in case the config was not found on the cluster or it was unsupported + // kubeadm generated version + CurrentVersion string + + // PreferredVersion is the component config version that is currently preferred by kubeadm for use. + // NOTE: As of today, this is the only version supported by kubeadm. + PreferredVersion string + + // ManualUpgradeRequired indicates if users need to manually upgrade their component config versions. This happens if + // the CurrentVersion of the config is user supplied (or modified) and no longer supported. Users should upgrade + // their component configs to PreferredVersion or any other supported component config version. + ManualUpgradeRequired bool +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // UpgradePlan represents information about upgrade plan for the output @@ -56,4 +76,6 @@ type UpgradePlan struct { metav1.TypeMeta Components []ComponentUpgradePlan + + ConfigVersions []ComponentConfigVersionState } diff --git a/cmd/kubeadm/app/apis/output/v1alpha1/types.go b/cmd/kubeadm/app/apis/output/v1alpha1/types.go index 1ea18ae455e..134dfee80fd 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha1/types.go +++ b/cmd/kubeadm/app/apis/output/v1alpha1/types.go @@ -48,6 +48,26 @@ type ComponentUpgradePlan struct { NewVersion string `json:"newVersion"` } +// ComponentConfigVersionState describes the current and desired version of a component config +type ComponentConfigVersionState struct { + // Group points to the Kubernetes API group that covers the config + Group string `json:"group"` + + // CurrentVersion is the currently active component config version + // NOTE: This can be empty in case the config was not found on the cluster or it was unsupported + // kubeadm generated version + CurrentVersion string `json:"currentVersion"` + + // PreferredVersion is the component config version that is currently preferred by kubeadm for use. + // NOTE: As of today, this is the only version supported by kubeadm. + PreferredVersion string `json:"preferredVersion"` + + // ManualUpgradeRequired indicates if users need to manually upgrade their component config versions. This happens if + // the CurrentVersion of the config is user supplied (or modified) and no longer supported. Users should upgrade + // their component configs to PreferredVersion or any other supported component config version. + ManualUpgradeRequired bool `json:"manualUpgradeRequired"` +} + // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // UpgradePlan represents information about upgrade plan for the output @@ -56,4 +76,6 @@ type UpgradePlan struct { metav1.TypeMeta Components []ComponentUpgradePlan `json:"components"` + + ConfigVersions []ComponentConfigVersionState `json:"configVersions"` } diff --git a/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.conversion.go b/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.conversion.go index c675fde12d0..0e23d2546ca 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.conversion.go +++ b/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.conversion.go @@ -45,6 +45,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*ComponentConfigVersionState)(nil), (*output.ComponentConfigVersionState)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_ComponentConfigVersionState_To_output_ComponentConfigVersionState(a.(*ComponentConfigVersionState), b.(*output.ComponentConfigVersionState), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.ComponentConfigVersionState)(nil), (*ComponentConfigVersionState)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_ComponentConfigVersionState_To_v1alpha1_ComponentConfigVersionState(a.(*output.ComponentConfigVersionState), b.(*ComponentConfigVersionState), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*ComponentUpgradePlan)(nil), (*output.ComponentUpgradePlan)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_ComponentUpgradePlan_To_output_ComponentUpgradePlan(a.(*ComponentUpgradePlan), b.(*output.ComponentUpgradePlan), scope) }); err != nil { @@ -98,6 +108,32 @@ func Convert_output_BootstrapToken_To_v1alpha1_BootstrapToken(in *output.Bootstr return autoConvert_output_BootstrapToken_To_v1alpha1_BootstrapToken(in, out, s) } +func autoConvert_v1alpha1_ComponentConfigVersionState_To_output_ComponentConfigVersionState(in *ComponentConfigVersionState, out *output.ComponentConfigVersionState, s conversion.Scope) error { + out.Group = in.Group + out.CurrentVersion = in.CurrentVersion + out.PreferredVersion = in.PreferredVersion + out.ManualUpgradeRequired = in.ManualUpgradeRequired + return nil +} + +// Convert_v1alpha1_ComponentConfigVersionState_To_output_ComponentConfigVersionState is an autogenerated conversion function. +func Convert_v1alpha1_ComponentConfigVersionState_To_output_ComponentConfigVersionState(in *ComponentConfigVersionState, out *output.ComponentConfigVersionState, s conversion.Scope) error { + return autoConvert_v1alpha1_ComponentConfigVersionState_To_output_ComponentConfigVersionState(in, out, s) +} + +func autoConvert_output_ComponentConfigVersionState_To_v1alpha1_ComponentConfigVersionState(in *output.ComponentConfigVersionState, out *ComponentConfigVersionState, s conversion.Scope) error { + out.Group = in.Group + out.CurrentVersion = in.CurrentVersion + out.PreferredVersion = in.PreferredVersion + out.ManualUpgradeRequired = in.ManualUpgradeRequired + return nil +} + +// Convert_output_ComponentConfigVersionState_To_v1alpha1_ComponentConfigVersionState is an autogenerated conversion function. +func Convert_output_ComponentConfigVersionState_To_v1alpha1_ComponentConfigVersionState(in *output.ComponentConfigVersionState, out *ComponentConfigVersionState, s conversion.Scope) error { + return autoConvert_output_ComponentConfigVersionState_To_v1alpha1_ComponentConfigVersionState(in, out, s) +} + func autoConvert_v1alpha1_ComponentUpgradePlan_To_output_ComponentUpgradePlan(in *ComponentUpgradePlan, out *output.ComponentUpgradePlan, s conversion.Scope) error { out.Name = in.Name out.CurrentVersion = in.CurrentVersion @@ -144,6 +180,7 @@ func Convert_output_Images_To_v1alpha1_Images(in *output.Images, out *Images, s func autoConvert_v1alpha1_UpgradePlan_To_output_UpgradePlan(in *UpgradePlan, out *output.UpgradePlan, s conversion.Scope) error { out.Components = *(*[]output.ComponentUpgradePlan)(unsafe.Pointer(&in.Components)) + out.ConfigVersions = *(*[]output.ComponentConfigVersionState)(unsafe.Pointer(&in.ConfigVersions)) return nil } @@ -154,6 +191,7 @@ func Convert_v1alpha1_UpgradePlan_To_output_UpgradePlan(in *UpgradePlan, out *ou func autoConvert_output_UpgradePlan_To_v1alpha1_UpgradePlan(in *output.UpgradePlan, out *UpgradePlan, s conversion.Scope) error { out.Components = *(*[]ComponentUpgradePlan)(unsafe.Pointer(&in.Components)) + out.ConfigVersions = *(*[]ComponentConfigVersionState)(unsafe.Pointer(&in.ConfigVersions)) return nil } diff --git a/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.deepcopy.go index b388605a3db..c11c44675cb 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.deepcopy.go @@ -50,6 +50,22 @@ func (in *BootstrapToken) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentConfigVersionState) DeepCopyInto(out *ComponentConfigVersionState) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentConfigVersionState. +func (in *ComponentConfigVersionState) DeepCopy() *ComponentConfigVersionState { + if in == nil { + return nil + } + out := new(ComponentConfigVersionState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ComponentUpgradePlan) DeepCopyInto(out *ComponentUpgradePlan) { *out = *in @@ -105,6 +121,11 @@ func (in *UpgradePlan) DeepCopyInto(out *UpgradePlan) { *out = make([]ComponentUpgradePlan, len(*in)) copy(*out, *in) } + if in.ConfigVersions != nil { + in, out := &in.ConfigVersions, &out.ConfigVersions + *out = make([]ComponentConfigVersionState, len(*in)) + copy(*out, *in) + } return } diff --git a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go index f4a57a4859b..49788197571 100644 --- a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go @@ -50,6 +50,22 @@ func (in *BootstrapToken) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentConfigVersionState) DeepCopyInto(out *ComponentConfigVersionState) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentConfigVersionState. +func (in *ComponentConfigVersionState) DeepCopy() *ComponentConfigVersionState { + if in == nil { + return nil + } + out := new(ComponentConfigVersionState) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ComponentUpgradePlan) DeepCopyInto(out *ComponentUpgradePlan) { *out = *in @@ -105,6 +121,11 @@ func (in *UpgradePlan) DeepCopyInto(out *UpgradePlan) { *out = make([]ComponentUpgradePlan, len(*in)) copy(*out, *in) } + if in.ConfigVersions != nil { + in, out := &in.ConfigVersions, &out.ConfigVersions + *out = make([]ComponentConfigVersionState, len(*in)) + copy(*out, *in) + } return } diff --git a/cmd/kubeadm/app/cmd/upgrade/BUILD b/cmd/kubeadm/app/cmd/upgrade/BUILD index 85925a4eb98..4a8d2d1d14a 100644 --- a/cmd/kubeadm/app/cmd/upgrade/BUILD +++ b/cmd/kubeadm/app/cmd/upgrade/BUILD @@ -38,6 +38,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/util/version:go_default_library", "//staging/src/k8s.io/client-go/discovery/fake:go_default_library", "//staging/src/k8s.io/client-go/kubernetes:go_default_library", + "//vendor/github.com/lithammer/dedent:go_default_library", "//vendor/github.com/pkg/errors:go_default_library", "//vendor/github.com/pmezard/go-difflib/difflib:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", diff --git a/cmd/kubeadm/app/cmd/upgrade/plan.go b/cmd/kubeadm/app/cmd/upgrade/plan.go index 2e1505e5b09..1939f7236c3 100644 --- a/cmd/kubeadm/app/cmd/upgrade/plan.go +++ b/cmd/kubeadm/app/cmd/upgrade/plan.go @@ -19,20 +19,25 @@ package upgrade import ( "fmt" "io" + "io/ioutil" "os" "sort" "strings" "text/tabwriter" + "github.com/lithammer/dedent" "github.com/pkg/errors" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/util/version" + clientset "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" outputapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/output" + "k8s.io/kubernetes/cmd/kubeadm/app/componentconfigs" "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" ) type planFlags struct { @@ -79,6 +84,13 @@ func runPlan(flags *planFlags, args []string) error { return errors.Wrap(err, "[upgrade/versions] FATAL") } + // Fetch the current state of the component configs + klog.V(1).Infoln("[upgrade/plan] analysing component config version states") + configVersionStates, err := getComponentConfigVersionStates(&cfg.ClusterConfiguration, client, flags.cfgPath) + if err != nil { + return errors.WithMessage(err, "[upgrade/versions] FATAL") + } + // No upgrades available if len(availUpgrades) == 0 { klog.V(1).Infoln("[upgrade/plan] Awesome, you're up-to-date! Enjoy!") @@ -92,8 +104,17 @@ func runPlan(flags *planFlags, args []string) error { return err } + // Actually, this is needed for machine readable output only. + // printUpgradePlan won't output the configVersionStates as it will simply print the same table several times + // in the human readable output if it did so + plan.ConfigVersions = configVersionStates + printUpgradePlan(&up, plan, unstableVersionFlag, isExternalEtcd, os.Stdout) } + + // Finally, print the component config state table + printComponentConfigVersionStates(configVersionStates, os.Stdout) + return nil } @@ -165,6 +186,24 @@ func genUpgradePlan(up *upgrade.Upgrade, isExternalEtcd bool) (*outputapi.Upgrad return &outputapi.UpgradePlan{Components: components}, unstableVersionFlag, nil } +func getComponentConfigVersionStates(cfg *kubeadmapi.ClusterConfiguration, client clientset.Interface, cfgPath string) ([]outputapi.ComponentConfigVersionState, error) { + docmap := kubeadmapi.DocumentMap{} + + if cfgPath != "" { + bytes, err := ioutil.ReadFile(cfgPath) + if err != nil { + return nil, errors.Wrapf(err, "unable to read config file %q", cfgPath) + } + + docmap, err = kubeadmutil.SplitYAMLDocuments(bytes) + if err != nil { + return nil, err + } + } + + return componentconfigs.GetVersionStates(cfg, client, docmap) +} + // printUpgradePlan prints a UX-friendly overview of what versions are available to upgrade to func printUpgradePlan(up *upgrade.Upgrade, plan *outputapi.UpgradePlan, unstableVersionFlag string, isExternalEtcd bool, w io.Writer) { // The tab writer writes to the "real" writer w @@ -218,8 +257,7 @@ func printUpgradePlan(up *upgrade.Upgrade, plan *outputapi.UpgradePlan, unstable fmt.Fprintln(w, "") } - fmt.Fprintln(w, "_____________________________________________________________________") - fmt.Fprintln(w, "") + printLineSeparator(w) } // sortedSliceFromStringIntMap returns a slice of the keys in the map sorted alphabetically @@ -231,3 +269,52 @@ func sortedSliceFromStringIntMap(strMap map[string]uint16) []string { sort.Strings(strSlice) return strSlice } + +func strOrDash(s string) string { + if s != "" { + return s + } + return "-" +} + +func yesOrNo(b bool) string { + if b { + return "yes" + } + return "no" +} + +func printLineSeparator(w io.Writer) { + fmt.Fprintln(w, "_____________________________________________________________________") + fmt.Fprintln(w, "") +} + +func printComponentConfigVersionStates(versionStates []outputapi.ComponentConfigVersionState, w io.Writer) { + if len(versionStates) == 0 { + fmt.Fprintln(w, "No information available on component configs.") + return + } + + fmt.Fprintln(w, dedent.Dedent(` + The table below shows the current state of component configs as understood by this version of kubeadm. + Configs that have a "yes" mark in the "MANUAL UPGRADE REQUIRED" column require manual config upgrade or + resetting to kubeadm defaults before a successful upgrade can be performed. The version to manually + upgrade to is denoted in the "PREFERRED VERSION" column. + `)) + + tabw := tabwriter.NewWriter(w, 10, 4, 3, ' ', 0) + fmt.Fprintln(tabw, "API GROUP\tCURRENT VERSION\tPREFERRED VERSION\tMANUAL UPGRADE REQUIRED") + + for _, state := range versionStates { + fmt.Fprintf(tabw, + "%s\t%s\t%s\t%s\n", + state.Group, + strOrDash(state.CurrentVersion), + strOrDash(state.PreferredVersion), + yesOrNo(state.ManualUpgradeRequired), + ) + } + + tabw.Flush() + printLineSeparator(w) +} diff --git a/cmd/kubeadm/app/componentconfigs/BUILD b/cmd/kubeadm/app/componentconfigs/BUILD index f6e0016b6ee..2a0b648e8aa 100644 --- a/cmd/kubeadm/app/componentconfigs/BUILD +++ b/cmd/kubeadm/app/componentconfigs/BUILD @@ -15,6 +15,7 @@ go_library( deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library", + "//cmd/kubeadm/app/apis/output:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/features:go_default_library", "//cmd/kubeadm/app/util:go_default_library", @@ -51,6 +52,7 @@ go_test( deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/apis/kubeadm/v1beta2:go_default_library", + "//cmd/kubeadm/app/apis/output:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", "//cmd/kubeadm/app/features:go_default_library", "//cmd/kubeadm/app/util:go_default_library", diff --git a/cmd/kubeadm/app/componentconfigs/configset.go b/cmd/kubeadm/app/componentconfigs/configset.go index 10a27254787..36f96b5b73f 100644 --- a/cmd/kubeadm/app/componentconfigs/configset.go +++ b/cmd/kubeadm/app/componentconfigs/configset.go @@ -30,6 +30,7 @@ import ( "k8s.io/klog/v2" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + "k8s.io/kubernetes/cmd/kubeadm/app/apis/output" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" ) @@ -282,6 +283,60 @@ func FetchFromClusterWithLocalOverwrites(clusterCfg *kubeadmapi.ClusterConfigura return nil } +// GetVersionStates returns a slice of ComponentConfigVersionState structs +// describing all supported component config groups that were identified on the cluster +func GetVersionStates(clusterCfg *kubeadmapi.ClusterConfiguration, client clientset.Interface, docmap kubeadmapi.DocumentMap) ([]output.ComponentConfigVersionState, error) { + // We don't want to modify clusterCfg so we make a working deep copy of it. + // Also, we don't want the defaulted component configs so we get rid of them. + scratchClusterCfg := clusterCfg.DeepCopy() + scratchClusterCfg.ComponentConfigs = kubeadmapi.ComponentConfigMap{} + + // Call FetchFromClusterWithLocalOverwrites. This will populate the configs it can load and will return all + // UnsupportedConfigVersionError(s) in a sinle instance of a MultipleUnsupportedConfigVersionsError. + var multipleVerErrs UnsupportedConfigVersionsErrorMap + err := FetchFromClusterWithLocalOverwrites(scratchClusterCfg, client, docmap) + if err != nil { + if vererrs, ok := err.(UnsupportedConfigVersionsErrorMap); ok { + multipleVerErrs = vererrs + } else { + // This seems to be a genuine error so we end here + return nil, err + } + } + + results := []output.ComponentConfigVersionState{} + for _, handler := range known { + group := handler.GroupVersion.Group + if vererr, ok := multipleVerErrs[group]; ok { + // If there is an UnsupportedConfigVersionError then we are dealing with a case where the config was user + // supplied and requires manual upgrade + results = append(results, output.ComponentConfigVersionState{ + Group: group, + CurrentVersion: vererr.OldVersion.Version, + PreferredVersion: vererr.CurrentVersion.Version, + ManualUpgradeRequired: true, + }) + } else if _, ok := scratchClusterCfg.ComponentConfigs[group]; ok { + // Normally loaded component config. No manual upgrade required on behalf of users. + results = append(results, output.ComponentConfigVersionState{ + Group: group, + CurrentVersion: handler.GroupVersion.Version, // Currently kubeadm supports only one version per API + PreferredVersion: handler.GroupVersion.Version, // group so we can get away with these being the same + }) + } else { + // This config was either not present (user did not install an addon) or the config was unsupported kubeadm + // generated one and is therefore skipped so we can automatically re-generate it (no action required on + // behalf of the user). + results = append(results, output.ComponentConfigVersionState{ + Group: group, + PreferredVersion: handler.GroupVersion.Version, + }) + } + } + + return results, nil +} + // Validate is a placeholder for performing a validation on an already loaded component configs in a ClusterConfiguration // Currently it prints a warning that no validation was performed func Validate(clusterCfg *kubeadmapi.ClusterConfiguration) field.ErrorList { diff --git a/cmd/kubeadm/app/componentconfigs/configset_test.go b/cmd/kubeadm/app/componentconfigs/configset_test.go index 9c4c912784d..13652d3ee87 100644 --- a/cmd/kubeadm/app/componentconfigs/configset_test.go +++ b/cmd/kubeadm/app/componentconfigs/configset_test.go @@ -17,6 +17,7 @@ limitations under the License. package componentconfigs import ( + "reflect" "testing" "github.com/lithammer/dedent" @@ -28,6 +29,7 @@ import ( clientsetfake "k8s.io/client-go/kubernetes/fake" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" + outputapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/output" "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" ) @@ -246,3 +248,406 @@ func TestFetchFromClusterWithLocalUpgrades(t *testing.T) { }) } } + +func TestGetVersionStates(t *testing.T) { + tests := []struct { + desc string + objects []runtime.Object + substitutes string + expected []outputapi.ComponentConfigVersionState + }{ + { + desc: "Normal config", + objects: []runtime.Object{ + kubeproxyConfigMap(` + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + `), + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1beta1 + kind: KubeletConfiguration + `), + }, + }, + }, + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha1", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: false, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1beta1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: false, + }, + }, + }, + { + desc: "Normal config ignoring a current substitute", + objects: []runtime.Object{ + kubeproxyConfigMap(` + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + `), + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1beta1 + kind: KubeletConfiguration + `), + }, + }, + }, + substitutes: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + `), + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha1", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: false, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1beta1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: false, + }, + }, + }, + { + desc: "Normal config with an old substitute", + objects: []runtime.Object{ + kubeproxyConfigMap(` + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + `), + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1beta1 + kind: KubeletConfiguration + `), + }, + }, + }, + substitutes: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha0", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: true, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1beta1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: false, + }, + }, + }, + { + desc: "Old user supplied config", + objects: []runtime.Object{ + kubeproxyConfigMap(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1alpha1 + kind: KubeletConfiguration + `), + }, + }, + }, + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha0", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: true, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1alpha1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: true, + }, + }, + }, + { + desc: "Old user supplied config with a proper substitute", + objects: []runtime.Object{ + kubeproxyConfigMap(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1alpha1 + kind: KubeletConfiguration + `), + }, + }, + }, + substitutes: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + `), + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha1", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: false, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1alpha1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: true, + }, + }, + }, + { + desc: "Old user supplied config with an old substitute", + objects: []runtime.Object{ + kubeproxyConfigMap(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1alpha1 + kind: KubeletConfiguration + `), + }, + }, + }, + substitutes: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha0", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: true, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1alpha1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: true, + }, + }, + }, + { + desc: "Old kubeadm generated config", + objects: []runtime.Object{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.KubeProxyConfigMap, + Namespace: metav1.NamespaceSystem, + Annotations: map[string]string{ + constants.ComponentConfigHashAnnotationKey: "sha256:8d3dfd7abcac205f6744d8e9db44505cce0c15b0a5395501e272fc18bd54c13c", + }, + }, + Data: map[string]string{ + constants.KubeProxyConfigMapKey: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + }, + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1beta1 + kind: KubeletConfiguration + `), + }, + }, + }, + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: false, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1beta1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: false, + }, + }, + }, + { + desc: "Old kubeadm generated config with a proper substitute", + objects: []runtime.Object{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.KubeProxyConfigMap, + Namespace: metav1.NamespaceSystem, + Annotations: map[string]string{ + constants.ComponentConfigHashAnnotationKey: "sha256:8d3dfd7abcac205f6744d8e9db44505cce0c15b0a5395501e272fc18bd54c13c", + }, + }, + Data: map[string]string{ + constants.KubeProxyConfigMapKey: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + }, + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1beta1 + kind: KubeletConfiguration + `), + }, + }, + }, + substitutes: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha1 + kind: KubeProxyConfiguration + `), + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha1", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: false, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1beta1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: false, + }, + }, + }, + { + desc: "Old kubeadm generated config with an old substitute", + objects: []runtime.Object{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.KubeProxyConfigMap, + Namespace: metav1.NamespaceSystem, + Annotations: map[string]string{ + constants.ComponentConfigHashAnnotationKey: "sha256:8d3dfd7abcac205f6744d8e9db44505cce0c15b0a5395501e272fc18bd54c13c", + }, + }, + Data: map[string]string{ + constants.KubeProxyConfigMapKey: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + }, + }, + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.GetKubeletConfigMapName(constants.CurrentKubernetesVersion), + Namespace: metav1.NamespaceSystem, + }, + Data: map[string]string{ + constants.KubeletBaseConfigurationConfigMapKey: dedent.Dedent(` + apiVersion: kubelet.config.k8s.io/v1beta1 + kind: KubeletConfiguration + `), + }, + }, + }, + substitutes: dedent.Dedent(` + apiVersion: kubeproxy.config.k8s.io/v1alpha0 + kind: KubeProxyConfiguration + `), + expected: []outputapi.ComponentConfigVersionState{ + { + Group: "kubeproxy.config.k8s.io", + CurrentVersion: "v1alpha0", + PreferredVersion: "v1alpha1", + ManualUpgradeRequired: true, + }, + { + Group: "kubelet.config.k8s.io", + CurrentVersion: "v1beta1", + PreferredVersion: "v1beta1", + ManualUpgradeRequired: false, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + docmap, err := kubeadmutil.SplitYAMLDocuments([]byte(test.substitutes)) + if err != nil { + t.Fatalf("unexpected failure of SplitYAMLDocuments: %v", err) + } + + clusterCfg := &kubeadmapi.ClusterConfiguration{ + KubernetesVersion: constants.CurrentKubernetesVersion.String(), + } + client := clientsetfake.NewSimpleClientset(test.objects...) + got, err := GetVersionStates(clusterCfg, client, docmap) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(got, test.expected) { + t.Fatalf("unexpected result:\n\texpected: %v\n\tgot: %v", test.expected, got) + } + }) + } +}