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 69637538864..4a8d2d1d14a 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", @@ -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 a6e4d4bebd8..1939f7236c3 100644 --- a/cmd/kubeadm/app/cmd/upgrade/plan.go +++ b/cmd/kubeadm/app/cmd/upgrade/plan.go @@ -19,19 +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" - 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/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 { @@ -78,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!") @@ -91,14 +104,23 @@ 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 } -// 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 +129,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 +145,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 +160,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 +183,29 @@ 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 +} + +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 *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) @@ -217,8 +257,7 @@ func printUpgradePlan(up *upgrade.Upgrade, plan *outputapiv1alpha1.UpgradePlan, fmt.Fprintln(w, "") } - fmt.Fprintln(w, "_____________________________________________________________________") - fmt.Fprintln(w, "") + printLineSeparator(w) } // sortedSliceFromStringIntMap returns a slice of the keys in the map sorted alphabetically @@ -230,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) + } + }) + } +}