From 93daef6e77bb0f7eca9a5ed9804c34a5840fcb63 Mon Sep 17 00:00:00 2001 From: Paco Xu Date: Tue, 1 Mar 2022 15:11:23 +0800 Subject: [PATCH] feature(kubeadm): add support of json/yaml format for upgrade plan - cherry-pick 83941 and rebase --- cmd/kubeadm/app/apis/output/types.go | 4 + cmd/kubeadm/app/apis/output/v1alpha2/types.go | 4 + .../output/v1alpha2/zz_generated.deepcopy.go | 9 + .../app/apis/output/zz_generated.deepcopy.go | 9 + cmd/kubeadm/app/cmd/certs.go | 3 +- cmd/kubeadm/app/cmd/join.go | 3 +- cmd/kubeadm/app/cmd/reset.go | 3 +- cmd/kubeadm/app/cmd/upgrade/apply.go | 3 +- cmd/kubeadm/app/cmd/upgrade/common.go | 19 +- cmd/kubeadm/app/cmd/upgrade/common_test.go | 3 +- cmd/kubeadm/app/cmd/upgrade/diff.go | 3 +- cmd/kubeadm/app/cmd/upgrade/node.go | 3 +- cmd/kubeadm/app/cmd/upgrade/plan.go | 244 +++++++++++++++--- cmd/kubeadm/app/phases/upgrade/compute.go | 5 - cmd/kubeadm/app/util/config/cluster.go | 8 +- cmd/kubeadm/app/util/output/output.go | 44 +++- 16 files changed, 300 insertions(+), 67 deletions(-) diff --git a/cmd/kubeadm/app/apis/output/types.go b/cmd/kubeadm/app/apis/output/types.go index cd3b4a7746b..9dda9491425 100644 --- a/cmd/kubeadm/app/apis/output/types.go +++ b/cmd/kubeadm/app/apis/output/types.go @@ -40,8 +40,12 @@ type Images struct { Images []string } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + // ComponentUpgradePlan represents information about upgrade plan for one component type ComponentUpgradePlan struct { + metav1.TypeMeta + Name string CurrentVersion string NewVersion string diff --git a/cmd/kubeadm/app/apis/output/v1alpha2/types.go b/cmd/kubeadm/app/apis/output/v1alpha2/types.go index 71bc2fa93be..529c029dd59 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha2/types.go +++ b/cmd/kubeadm/app/apis/output/v1alpha2/types.go @@ -40,8 +40,12 @@ type Images struct { Images []string `json:"images"` } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + // ComponentUpgradePlan represents information about upgrade plan for one component type ComponentUpgradePlan struct { + metav1.TypeMeta + Name string `json:"name"` CurrentVersion string `json:"currentVersion"` NewVersion string `json:"newVersion"` diff --git a/cmd/kubeadm/app/apis/output/v1alpha2/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/output/v1alpha2/zz_generated.deepcopy.go index 54cc75256b3..73e8c32def2 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha2/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/output/v1alpha2/zz_generated.deepcopy.go @@ -70,6 +70,7 @@ func (in *ComponentConfigVersionState) DeepCopy() *ComponentConfigVersionState { // 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 + out.TypeMeta = in.TypeMeta return } @@ -83,6 +84,14 @@ func (in *ComponentUpgradePlan) DeepCopy() *ComponentUpgradePlan { return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ComponentUpgradePlan) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Images) DeepCopyInto(out *Images) { *out = *in diff --git a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go index 0d7bc006cf9..8d553d35c15 100644 --- a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go @@ -70,6 +70,7 @@ func (in *ComponentConfigVersionState) DeepCopy() *ComponentConfigVersionState { // 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 + out.TypeMeta = in.TypeMeta return } @@ -83,6 +84,14 @@ func (in *ComponentUpgradePlan) DeepCopy() *ComponentUpgradePlan { return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ComponentUpgradePlan) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Images) DeepCopyInto(out *Images) { *out = *in diff --git a/cmd/kubeadm/app/cmd/certs.go b/cmd/kubeadm/app/cmd/certs.go index 7506c8b6d0a..a5265b4b1c3 100644 --- a/cmd/kubeadm/app/cmd/certs.go +++ b/cmd/kubeadm/app/cmd/certs.go @@ -39,6 +39,7 @@ import ( kubeconfigphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubeconfig" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) var ( @@ -332,7 +333,7 @@ func getInternalCfg(cfgPath string, kubeconfigPath string, cfg kubeadmapiv1.Clus if cfgPath == "" { client, err := kubeconfigutil.ClientSetFromFile(kubeconfigPath) if err == nil { - internalcfg, err := configutil.FetchInitConfigurationFromCluster(client, out, logPrefix, false, false) + internalcfg, err := configutil.FetchInitConfigurationFromCluster(client, &output.TextPrinter{}, logPrefix, false, false) if err == nil { fmt.Println() // add empty line to separate the FetchInitConfigurationFromCluster output from the command output return internalcfg, nil diff --git a/cmd/kubeadm/app/cmd/join.go b/cmd/kubeadm/app/cmd/join.go index fa0b07e3631..61c6c36c5b1 100644 --- a/cmd/kubeadm/app/cmd/join.go +++ b/cmd/kubeadm/app/cmd/join.go @@ -47,6 +47,7 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/discovery" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) var ( @@ -618,7 +619,7 @@ func fetchInitConfiguration(tlsBootstrapCfg *clientcmdapi.Config) (*kubeadmapi.I } // Fetches the init configuration - initConfiguration, err := configutil.FetchInitConfigurationFromCluster(tlsClient, os.Stdout, "preflight", true, false) + initConfiguration, err := configutil.FetchInitConfigurationFromCluster(tlsClient, &output.TextPrinter{}, "preflight", true, false) if err != nil { return nil, errors.Wrap(err, "unable to fetch the kubeadm-config ConfigMap") } diff --git a/cmd/kubeadm/app/cmd/reset.go b/cmd/kubeadm/app/cmd/reset.go index aaccbd56cec..ef33b1ad8b8 100644 --- a/cmd/kubeadm/app/cmd/reset.go +++ b/cmd/kubeadm/app/cmd/reset.go @@ -37,6 +37,7 @@ import ( cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" utilruntime "k8s.io/kubernetes/cmd/kubeadm/app/util/runtime" ) @@ -98,7 +99,7 @@ func newResetData(cmd *cobra.Command, options *resetOptions, in io.Reader, out i client, err := getClientset(options.kubeconfigPath, false) if err == nil { klog.V(1).Infof("[reset] Loaded client set from kubeconfig file: %s", options.kubeconfigPath) - cfg, err = configutil.FetchInitConfigurationFromCluster(client, out, "reset", false, false) + cfg, err = configutil.FetchInitConfigurationFromCluster(client, &output.TextPrinter{}, "reset", false, false) if err != nil { klog.Warningf("[reset] Unable to fetch the kubeadm-config ConfigMap from cluster: %v", err) } diff --git a/cmd/kubeadm/app/cmd/upgrade/apply.go b/cmd/kubeadm/app/cmd/upgrade/apply.go index b47a7cc7eda..4b83ed65ccd 100644 --- a/cmd/kubeadm/app/cmd/upgrade/apply.go +++ b/cmd/kubeadm/app/cmd/upgrade/apply.go @@ -37,6 +37,7 @@ import ( kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) // applyFlags holds the information about the flags that can be passed to apply @@ -103,7 +104,7 @@ func runApply(flags *applyFlags, args []string) error { // Start with the basics, verify that the cluster is healthy and get the configuration from the cluster (using the ConfigMap) klog.V(1).Infoln("[upgrade/apply] verifying health of cluster") klog.V(1).Infoln("[upgrade/apply] retrieving configuration from cluster") - client, versionGetter, cfg, err := enforceRequirements(flags.applyPlanFlags, args, flags.dryRun, true) + client, versionGetter, cfg, err := enforceRequirements(flags.applyPlanFlags, args, flags.dryRun, true, &output.TextPrinter{}) if err != nil { return err } diff --git a/cmd/kubeadm/app/cmd/upgrade/common.go b/cmd/kubeadm/app/cmd/upgrade/common.go index 6cd4ee96b6c..8fc9358e486 100644 --- a/cmd/kubeadm/app/cmd/upgrade/common.go +++ b/cmd/kubeadm/app/cmd/upgrade/common.go @@ -47,6 +47,7 @@ import ( configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) // isKubeadmConfigPresent checks if a kubeadm config type is found in the provided document map @@ -63,14 +64,14 @@ func isKubeadmConfigPresent(docmap kubeadmapi.DocumentMap) bool { // are loaded. This function allows the component configs to be loaded from a file that contains only them. If the file contains any kubeadm types // in it (API group "kubeadm.kubernetes.io" present), then the supplied file is treaded as a legacy reconfiguration style "--config" use and the // returned bool value is set to true (the only case to be done so). -func loadConfig(cfgPath string, client clientset.Interface, skipComponentConfigs bool) (*kubeadmapi.InitConfiguration, bool, error) { +func loadConfig(cfgPath string, client clientset.Interface, skipComponentConfigs bool, printer output.Printer) (*kubeadmapi.InitConfiguration, bool, error) { // Used for info logs here const logPrefix = "upgrade/config" // The usual case here is to not have a config file, but rather load the config from the cluster. // This is probably 90% of the time. So we handle it first. if cfgPath == "" { - cfg, err := configutil.FetchInitConfigurationFromCluster(client, os.Stdout, logPrefix, false, skipComponentConfigs) + cfg, err := configutil.FetchInitConfigurationFromCluster(client, printer, logPrefix, false, skipComponentConfigs) return cfg, false, err } @@ -98,7 +99,7 @@ func loadConfig(cfgPath string, client clientset.Interface, skipComponentConfigs // If no kubeadm config types are present, we assume that there are manually upgraded component configs in the file. // Hence, we load the kubeadm types from the cluster. - initCfg, err := configutil.FetchInitConfigurationFromCluster(client, os.Stdout, logPrefix, false, true) + initCfg, err := configutil.FetchInitConfigurationFromCluster(client, printer, logPrefix, false, true) if err != nil { return nil, false, err } @@ -121,17 +122,17 @@ func loadConfig(cfgPath string, client clientset.Interface, skipComponentConfigs } // enforceRequirements verifies that it's okay to upgrade and then returns the variables needed for the rest of the procedure -func enforceRequirements(flags *applyPlanFlags, args []string, dryRun bool, upgradeApply bool) (clientset.Interface, upgrade.VersionGetter, *kubeadmapi.InitConfiguration, error) { +func enforceRequirements(flags *applyPlanFlags, args []string, dryRun bool, upgradeApply bool, printer output.Printer) (clientset.Interface, upgrade.VersionGetter, *kubeadmapi.InitConfiguration, error) { client, err := getClient(flags.kubeConfigPath, dryRun) if err != nil { return nil, nil, nil, errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", flags.kubeConfigPath) } // Fetch the configuration from a file or ConfigMap and validate it - fmt.Println("[upgrade/config] Making sure the configuration is correct:") + printer.Printf("[upgrade/config] Making sure the configuration is correct:\n") var newK8sVersion string - cfg, legacyReconfigure, err := loadConfig(flags.cfgPath, client, !upgradeApply) + cfg, legacyReconfigure, err := loadConfig(flags.cfgPath, client, !upgradeApply, printer) if err != nil { if apierrors.IsNotFound(err) { fmt.Printf("[upgrade/config] In order to upgrade, a ConfigMap called %q in the %s namespace must exist.\n", constants.KubeadmConfigConfigMap, metav1.NamespaceSystem) @@ -161,7 +162,7 @@ func enforceRequirements(flags *applyPlanFlags, args []string, dryRun bool, upgr // Ensure the user is root klog.V(1).Info("running preflight checks") - if err := runPreflightChecks(client, ignorePreflightErrorsSet, &cfg.ClusterConfiguration); err != nil { + if err := runPreflightChecks(client, ignorePreflightErrorsSet, &cfg.ClusterConfiguration, printer); err != nil { return nil, nil, nil, err } @@ -235,8 +236,8 @@ func printConfiguration(clustercfg *kubeadmapi.ClusterConfiguration, w io.Writer } // runPreflightChecks runs the root preflight check -func runPreflightChecks(client clientset.Interface, ignorePreflightErrors sets.String, cfg *kubeadmapi.ClusterConfiguration) error { - fmt.Println("[preflight] Running pre-flight checks.") +func runPreflightChecks(client clientset.Interface, ignorePreflightErrors sets.String, cfg *kubeadmapi.ClusterConfiguration, printer output.Printer) error { + printer.Printf("[preflight] Running pre-flight checks.\n") err := preflight.RunRootCheckOnly(ignorePreflightErrors) if err != nil { return err diff --git a/cmd/kubeadm/app/cmd/upgrade/common_test.go b/cmd/kubeadm/app/cmd/upgrade/common_test.go index ecdcf341249..357148e9afc 100644 --- a/cmd/kubeadm/app/cmd/upgrade/common_test.go +++ b/cmd/kubeadm/app/cmd/upgrade/common_test.go @@ -23,6 +23,7 @@ import ( kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) func TestEnforceRequirements(t *testing.T) { @@ -54,7 +55,7 @@ func TestEnforceRequirements(t *testing.T) { } for _, tt := range tcases { t.Run(tt.name, func(t *testing.T) { - _, _, _, err := enforceRequirements(&tt.flags, nil, tt.dryRun, false) + _, _, _, err := enforceRequirements(&tt.flags, nil, tt.dryRun, false, &output.TextPrinter{}) if err == nil && tt.expectedErr { t.Error("Expected error, but got success") diff --git a/cmd/kubeadm/app/cmd/upgrade/diff.go b/cmd/kubeadm/app/cmd/upgrade/diff.go index 73cdedcf17f..fe421e94811 100644 --- a/cmd/kubeadm/app/cmd/upgrade/diff.go +++ b/cmd/kubeadm/app/cmd/upgrade/diff.go @@ -37,6 +37,7 @@ import ( 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/cmd/kubeadm/app/util/output" ) type diffFlags struct { @@ -118,7 +119,7 @@ func runDiff(flags *diffFlags, args []string) error { if err != nil { return errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", flags.kubeConfigPath) } - cfg, err = configutil.FetchInitConfigurationFromCluster(client, flags.out, "upgrade/diff", false, false) + cfg, err = configutil.FetchInitConfigurationFromCluster(client, &output.TextPrinter{}, "upgrade/diff", false, false) } if err != nil { return err diff --git a/cmd/kubeadm/app/cmd/upgrade/node.go b/cmd/kubeadm/app/cmd/upgrade/node.go index 7072312a0b8..5224de86afb 100644 --- a/cmd/kubeadm/app/cmd/upgrade/node.go +++ b/cmd/kubeadm/app/cmd/upgrade/node.go @@ -33,6 +33,7 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow" "k8s.io/kubernetes/cmd/kubeadm/app/constants" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) // nodeOptions defines all the options exposed via flags by kubeadm upgrade node. @@ -139,7 +140,7 @@ func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions) (*node // Fetches the cluster configuration // NB in case of control-plane node, we are reading all the info for the node; in case of NOT control-plane node // (worker node), we are not reading local API address and the CRI socket from the node object - cfg, err := configutil.FetchInitConfigurationFromCluster(client, os.Stdout, "upgrade", !isControlPlaneNode, false) + cfg, err := configutil.FetchInitConfigurationFromCluster(client, &output.TextPrinter{}, "upgrade", !isControlPlaneNode, false) if err != nil { return nil, errors.Wrap(err, "unable to fetch the kubeadm-config ConfigMap") } diff --git a/cmd/kubeadm/app/cmd/upgrade/plan.go b/cmd/kubeadm/app/cmd/upgrade/plan.go index 7bd654ebbb8..92ff3d4db4b 100644 --- a/cmd/kubeadm/app/cmd/upgrade/plan.go +++ b/cmd/kubeadm/app/cmd/upgrade/plan.go @@ -28,16 +28,21 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/version" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" 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" + outputapischeme "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/scheme" + outputapiv1alpha2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha2" "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" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) type planFlags struct { @@ -50,25 +55,188 @@ func newCmdPlan(apf *applyPlanFlags) *cobra.Command { applyPlanFlags: apf, } + outputFlags := newUpgradePlanPrintFlags(output.TextOutput) + cmd := &cobra.Command{ Use: "plan [version] [flags]", Short: "Check which versions are available to upgrade to and validate whether your current cluster is upgradeable. To skip the internet check, pass in the optional [version] parameter", RunE: func(_ *cobra.Command, args []string) error { - return runPlan(flags, args) + printer, err := outputFlags.ToPrinter() + if err != nil { + return err + } + + return runPlan(flags, args, printer) }, } + outputFlags.AddFlags(cmd) + // Register the common flags for apply and plan addApplyPlanFlags(cmd.Flags(), flags.applyPlanFlags) return cmd } +// newComponentUpgradePlan helper creates outputapiv1alpha1.ComponentUpgradePlan object +func newComponentUpgradePlan(name, currentVersion, newVersion string) outputapi.ComponentUpgradePlan { + return outputapi.ComponentUpgradePlan{ + Name: name, + CurrentVersion: currentVersion, + NewVersion: newVersion, + } +} + +// upgradePlanPrintFlags defines printer flag structure for the +// upgrade plan kubeadm command and provides a method +// of retrieving a known printer based on flag values provided. +type upgradePlanPrintFlags struct { + // JSONYamlPrintFlags provides default flags necessary for json/yaml printing. + JSONYamlPrintFlags *upgradePlanJSONYamlPrintFlags + // TextPrintFlags provides default flags necessary for text printing. + TextPrintFlags *upgradePlanTextPrintFlags + // TypeSetterPrinter is an implementation of ResourcePrinter that wraps another printer with types set on the objects + TypeSetterPrinter *printers.TypeSetterPrinter + // OutputFormat contains currently set output format + OutputFormat string +} + +func newUpgradePlanPrintFlags(outputFormat string) *upgradePlanPrintFlags { + return &upgradePlanPrintFlags{ + JSONYamlPrintFlags: &upgradePlanJSONYamlPrintFlags{}, + TextPrintFlags: &upgradePlanTextPrintFlags{}, + TypeSetterPrinter: printers.NewTypeSetter(outputapischeme.Scheme), + OutputFormat: strings.ToLower(outputFormat), + } +} + +// AllowedFormats returns list of allowed output formats +func (pf *upgradePlanPrintFlags) AllowedFormats() []string { + ret := pf.TextPrintFlags.AllowedFormats() + return append(ret, pf.JSONYamlPrintFlags.AllowedFormats()...) +} + +// AddFlags receives a *cobra.Command reference and binds +// flags related to Kubeadm printing to it +func (pf *upgradePlanPrintFlags) AddFlags(cmd *cobra.Command) { + pf.TextPrintFlags.AddFlags(cmd) + pf.JSONYamlPrintFlags.AddFlags(cmd) + cmd.Flags().StringVarP(&pf.OutputFormat, "experimental-output", "o", pf.OutputFormat, fmt.Sprintf("Output format. One of: %s.", strings.Join(pf.AllowedFormats(), "|"))) +} + +// ToPrinter receives an outputFormat and returns a printer capable of +// handling format printing. +// Returns error if the specified outputFormat does not match supported formats. +func (pf *upgradePlanPrintFlags) ToPrinter() (output.Printer, error) { + switch pf.OutputFormat { + case output.TextOutput: + return pf.TextPrintFlags.ToPrinter(pf.OutputFormat) + case output.JSONOutput: + return newUpgradePlanJSONYAMLPrinter(pf.TypeSetterPrinter.WrapToPrinter(pf.JSONYamlPrintFlags.ToPrinter(output.JSONOutput))) + case output.YAMLOutput: + return newUpgradePlanJSONYAMLPrinter(pf.TypeSetterPrinter.WrapToPrinter(pf.JSONYamlPrintFlags.ToPrinter(output.YAMLOutput))) + default: + return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &pf.OutputFormat, AllowedFormats: pf.AllowedFormats()} + } +} + +type upgradePlanJSONYamlPrintFlags struct { + genericclioptions.JSONYamlPrintFlags +} + +// AllowedFormats returns list of allowed output formats +func (pf *upgradePlanJSONYamlPrintFlags) AllowedFormats() []string { + return []string{output.JSONOutput, output.YAMLOutput} +} + +// upgradePlanJSONYAMLPrinter prints upgrade plan in a JSON or YAML format +type upgradePlanJSONYAMLPrinter struct { + output.ResourcePrinterWrapper + Buffer []outputapiv1alpha2.ComponentUpgradePlan +} + +// newUpgradePlanJSONYAMLPrinter creates new upgradePlanJSONYAMLPrinter object +func newUpgradePlanJSONYAMLPrinter(resourcePrinter printers.ResourcePrinter, err error) (output.Printer, error) { + if err != nil { + return nil, err + } + return &upgradePlanJSONYAMLPrinter{ResourcePrinterWrapper: output.ResourcePrinterWrapper{Printer: resourcePrinter}}, nil +} + +// PrintObj is an implementation of ResourcePrinter.PrintObj that adds object to the buffer +func (p *upgradePlanJSONYAMLPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { + item, ok := obj.(*outputapiv1alpha2.ComponentUpgradePlan) + if !ok { + return fmt.Errorf("expected ComponentUpgradePlan, but got %+v", obj) + } + p.Buffer = append(p.Buffer, *item) + return nil +} + +// Close writes any buffered data and empties list of buffered components +func (p *upgradePlanJSONYAMLPrinter) Close(writer io.Writer) { + plan := &outputapiv1alpha2.UpgradePlan{Components: p.Buffer} + p.Printer.PrintObj(plan, writer) + p.Buffer = []outputapiv1alpha2.ComponentUpgradePlan{} +} + +// upgradePlanTextPrinter prints upgrade plan in a text form +type upgradePlanTextPrinter struct { + output.TextPrinter + columns []string + tabwriter *tabwriter.Writer +} + +// Flush writes any buffered data +func (p *upgradePlanTextPrinter) Flush(writer io.Writer) { + if p.tabwriter != nil { + p.tabwriter.Flush() + p.tabwriter = nil + } +} + +// PrintObj is an implementation of ResourcePrinter.PrintObj for upgrade plan plain text output +func (p *upgradePlanTextPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { + if p.tabwriter == nil { + p.tabwriter = tabwriter.NewWriter(writer, 10, 4, 3, ' ', 0) + // Print header + fmt.Fprintln(p.tabwriter, strings.Join(p.columns, "\t")) + } + + item, ok := obj.(*outputapiv1alpha2.ComponentUpgradePlan) + if !ok { + return fmt.Errorf("expected ComponentUpgradePlan, but got %+v", obj) + } + + // Print item + fmt.Fprintf(p.tabwriter, "%s\t%s\t%s\n", item.Name, item.CurrentVersion, item.NewVersion) + + return nil +} + +// upgradePlanTextPrintFlags provides flags necessary for printing upgrade plan in a text form. +type upgradePlanTextPrintFlags struct{} + +func (pf *upgradePlanTextPrintFlags) AddFlags(cmd *cobra.Command) {} + +// AllowedFormats returns list of allowed output formats +func (pf *upgradePlanTextPrintFlags) AllowedFormats() []string { + return []string{output.TextOutput} +} + +// ToPrinter returns kubeadm printer for the text output format +func (pf *upgradePlanTextPrintFlags) ToPrinter(outputFormat string) (output.Printer, error) { + if outputFormat == output.TextOutput { + return &upgradePlanTextPrinter{columns: []string{"COMPONENT", "CURRENT", "AVAILABLE"}}, nil + } + return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: []string{output.TextOutput}} +} + // runPlan takes care of outputting available versions to upgrade to for the user -func runPlan(flags *planFlags, args []string) error { +func runPlan(flags *planFlags, args []string, printer output.Printer) error { // Start with the basics, verify that the cluster is healthy, build a client and a versionGetter. Never dry-run when planning. klog.V(1).Infoln("[upgrade/plan] verifying health of cluster") klog.V(1).Infoln("[upgrade/plan] retrieving configuration from cluster") - client, versionGetter, cfg, err := enforceRequirements(flags.applyPlanFlags, args, false, false) + client, versionGetter, cfg, err := enforceRequirements(flags.applyPlanFlags, args, false, false, printer) if err != nil { return err } @@ -79,6 +247,7 @@ func runPlan(flags *planFlags, args []string) error { // Compute which upgrade possibilities there are klog.V(1).Infoln("[upgrade/plan] computing upgrade possibilities") + klog.V(1).Infoln("[upgrade/plan] Fetching available versions to upgrade to") availUpgrades, err := upgrade.GetAvailableUpgrades(versionGetter, flags.allowExperimentalUpgrades, flags.allowRCUpgrades, isExternalEtcd, client, constants.GetStaticPodDirectory()) if err != nil { return errors.Wrap(err, "[upgrade/versions] FATAL") @@ -109,24 +278,17 @@ func runPlan(flags *planFlags, args []string) error { // in the human readable output if it did so plan.ConfigVersions = configVersionStates - printUpgradePlan(&up, plan, unstableVersionFlag, isExternalEtcd, os.Stdout) + printUpgradePlan(&up, plan, unstableVersionFlag, isExternalEtcd, os.Stdout, printer) } // Finally, print the component config state table - printComponentConfigVersionStates(configVersionStates, os.Stdout) + printComponentConfigVersionStates(configVersionStates, os.Stdout, printer) + // Add a newline in the end of this output to leave some space to the next output section + klog.V(1).Infoln() return nil } -// newComponentUpgradePlan helper creates outputapi.ComponentUpgradePlan object -func newComponentUpgradePlan(name, currentVersion, newVersion string) outputapi.ComponentUpgradePlan { - return outputapi.ComponentUpgradePlan{ - Name: name, - CurrentVersion: currentVersion, - NewVersion: newVersion, - } -} - // 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 []outputapi.ComponentUpgradePlan, up *upgrade.Upgrade, name string) []outputapi.ComponentUpgradePlan { @@ -199,14 +361,14 @@ func getComponentConfigVersionStates(cfg *kubeadmapi.ClusterConfiguration, clien } // 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) { +func printUpgradePlan(up *upgrade.Upgrade, plan *outputapi.UpgradePlan, unstableVersionFlag string, isExternalEtcd bool, w io.Writer, printer output.Printer) { // The tab writer writes to the "real" writer w tabw := tabwriter.NewWriter(w, 10, 4, 3, ' ', 0) // endOfTable helper function flashes table writer endOfTable := func() { tabw.Flush() - fmt.Fprintln(w, "") + printer.Fprintln(w, "") } printHeader := true @@ -217,41 +379,41 @@ func printUpgradePlan(up *upgrade.Upgrade, plan *outputapi.UpgradePlan, unstable continue } else if component.Name == constants.Kubelet { if printManualUpgradeHeader { - fmt.Fprintln(w, "Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':") - fmt.Fprintln(tabw, "COMPONENT\tCURRENT\tTARGET") - fmt.Fprintf(tabw, "%s\t%s\t%s\n", component.Name, component.CurrentVersion, component.NewVersion) + printer.Fprintln(w, "Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':") + printer.Fprintln(tabw, "COMPONENT\tCURRENT\tTARGET") + printer.Fprintf(tabw, "%s\t%s\t%s\n", component.Name, component.CurrentVersion, component.NewVersion) printManualUpgradeHeader = false } else { - fmt.Fprintf(tabw, "%s\t%s\t%s\n", "", component.CurrentVersion, component.NewVersion) + printer.Fprintf(tabw, "%s\t%s\t%s\n", "", component.CurrentVersion, component.NewVersion) } } else { if printHeader { // End of manual upgrades table endOfTable() - fmt.Fprintf(w, "Upgrade to the latest %s:\n", up.Description) - fmt.Fprintln(w, "") - fmt.Fprintln(tabw, "COMPONENT\tCURRENT\tTARGET") + printer.Fprintf(w, "Upgrade to the latest %s:\n", up.Description) + printer.Fprintln(w, "") + printer.Fprintln(tabw, "COMPONENT\tCURRENT\tTARGET") printHeader = false } - fmt.Fprintf(tabw, "%s\t%s\t%s\n", component.Name, component.CurrentVersion, component.NewVersion) + printer.Fprintf(tabw, "%s\t%s\t%s\n", component.Name, component.CurrentVersion, component.NewVersion) } } // End of control plane table endOfTable() //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%s\n", up.After.KubeVersion, unstableVersionFlag) - fmt.Fprintln(w, "") + printer.Fprintln(w, "You can now apply the upgrade by executing the following command:") + printer.Fprintln(w, "") + printer.Fprintf(w, "\tkubeadm upgrade apply %s%s\n", up.After.KubeVersion, unstableVersionFlag) + printer.Fprintln(w, "") if up.Before.KubeadmVersion != up.After.KubeadmVersion { - fmt.Fprintf(w, "Note: Before you can perform this upgrade, you have to update kubeadm to %s.\n", up.After.KubeadmVersion) - fmt.Fprintln(w, "") + printer.Fprintf(w, "Note: Before you can perform this upgrade, you have to update kubeadm to %s.\n", up.After.KubeadmVersion) + printer.Fprintln(w, "") } - printLineSeparator(w) + printLineSeparator(w, printer) } // sortedSliceFromStringIntMap returns a slice of the keys in the map sorted alphabetically @@ -278,18 +440,18 @@ func yesOrNo(b bool) string { return "no" } -func printLineSeparator(w io.Writer) { - fmt.Fprintln(w, "_____________________________________________________________________") - fmt.Fprintln(w, "") +func printLineSeparator(w io.Writer, printer output.Printer) { + printer.Fprintln(w, "_____________________________________________________________________") + printer.Fprintln(w, "") } -func printComponentConfigVersionStates(versionStates []outputapi.ComponentConfigVersionState, w io.Writer) { +func printComponentConfigVersionStates(versionStates []outputapi.ComponentConfigVersionState, w io.Writer, printer output.Printer) { if len(versionStates) == 0 { - fmt.Fprintln(w, "No information available on component configs.") + printer.Fprintln(w, "No information available on component configs.") return } - fmt.Fprintln(w, dedent.Dedent(` + printer.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 @@ -297,10 +459,10 @@ func printComponentConfigVersionStates(versionStates []outputapi.ComponentConfig `)) tabw := tabwriter.NewWriter(w, 10, 4, 3, ' ', 0) - fmt.Fprintln(tabw, "API GROUP\tCURRENT VERSION\tPREFERRED VERSION\tMANUAL UPGRADE REQUIRED") + printer.Fprintln(tabw, "API GROUP\tCURRENT VERSION\tPREFERRED VERSION\tMANUAL UPGRADE REQUIRED") for _, state := range versionStates { - fmt.Fprintf(tabw, + printer.Fprintf(tabw, "%s\t%s\t%s\t%s\n", state.Group, strOrDash(state.CurrentVersion), @@ -310,5 +472,5 @@ func printComponentConfigVersionStates(versionStates []outputapi.ComponentConfig } tabw.Flush() - printLineSeparator(w) + printLineSeparator(w, printer) } diff --git a/cmd/kubeadm/app/phases/upgrade/compute.go b/cmd/kubeadm/app/phases/upgrade/compute.go index f46cecef7ab..52008b30c6a 100644 --- a/cmd/kubeadm/app/phases/upgrade/compute.go +++ b/cmd/kubeadm/app/phases/upgrade/compute.go @@ -73,8 +73,6 @@ type ClusterState struct { // GetAvailableUpgrades fetches all versions from the specified VersionGetter and computes which // kinds of upgrades can be performed func GetAvailableUpgrades(versionGetterImpl VersionGetter, experimentalUpgradesAllowed, rcUpgradesAllowed, externalEtcd bool, client clientset.Interface, manifestsDir string) ([]Upgrade, error) { - fmt.Println("[upgrade] Fetching available versions to upgrade to") - // Collect the upgrades kubeadm can do in this list upgrades := []Upgrade{} @@ -266,9 +264,6 @@ func GetAvailableUpgrades(versionGetterImpl VersionGetter, experimentalUpgradesA } } - // Add a newline in the end of this output to leave some space to the next output section - fmt.Println("") - return upgrades, nil } diff --git a/cmd/kubeadm/app/util/config/cluster.go b/cmd/kubeadm/app/util/config/cluster.go index 3728c293261..ed9648f61e0 100644 --- a/cmd/kubeadm/app/util/config/cluster.go +++ b/cmd/kubeadm/app/util/config/cluster.go @@ -20,7 +20,6 @@ import ( "context" "crypto/x509" "fmt" - "io" "path/filepath" "strings" @@ -42,12 +41,13 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient" "k8s.io/kubernetes/cmd/kubeadm/app/util/config/strict" + "k8s.io/kubernetes/cmd/kubeadm/app/util/output" ) // FetchInitConfigurationFromCluster fetches configuration from a ConfigMap in the cluster -func FetchInitConfigurationFromCluster(client clientset.Interface, w io.Writer, logPrefix string, newControlPlane, skipComponentConfigs bool) (*kubeadmapi.InitConfiguration, error) { - fmt.Fprintf(w, "[%s] Reading configuration from the cluster...\n", logPrefix) - fmt.Fprintf(w, "[%s] FYI: You can look at this config file with 'kubectl -n %s get cm %s -o yaml'\n", logPrefix, metav1.NamespaceSystem, constants.KubeadmConfigConfigMap) +func FetchInitConfigurationFromCluster(client clientset.Interface, printer output.Printer, logPrefix string, newControlPlane, skipComponentConfigs bool) (*kubeadmapi.InitConfiguration, error) { + printer.Printf("[%s] Reading configuration from the cluster...\n", logPrefix) + printer.Printf("[%s] FYI: You can look at this config file with 'kubectl -n %s get cm %s -o yaml'\n", logPrefix, metav1.NamespaceSystem, constants.KubeadmConfigConfigMap) // Fetch the actual config from cluster cfg, err := getInitConfigurationFromCluster(constants.KubernetesDir, client, newControlPlane, skipComponentConfigs) diff --git a/cmd/kubeadm/app/util/output/output.go b/cmd/kubeadm/app/util/output/output.go index a9776657784..b99f51eaac7 100644 --- a/cmd/kubeadm/app/util/output/output.go +++ b/cmd/kubeadm/app/util/output/output.go @@ -31,6 +31,12 @@ import ( // TextOutput describes the plain text output const TextOutput = "text" +// JSONOutput describes the JSON output +const JSONOutput = "json" + +// YAMLOutput describes the YAML output +const YAMLOutput = "yaml" + // TextPrintFlags is an interface to handle custom text output type TextPrintFlags interface { ToPrinter(outputFormat string) (Printer, error) @@ -44,7 +50,7 @@ type PrintFlags struct { JSONYamlPrintFlags *genericclioptions.JSONYamlPrintFlags // KubeTemplatePrintFlags composes print flags that provide both a JSONPath and a go-template printer. KubeTemplatePrintFlags *genericclioptions.KubeTemplatePrintFlags - // JSONYamlPrintFlags provides default flags necessary for kubeadm text printing. + // TextPrintFlags provides default flags necessary for kubeadm text printing. TextPrintFlags TextPrintFlags // TypeSetterPrinter is an implementation of ResourcePrinter that wraps another printer with types set on the objects TypeSetterPrinter *printers.TypeSetterPrinter @@ -133,13 +139,27 @@ func NewOutputFlags(textPrintFlags TextPrintFlags) *PrintFlags { type Printer interface { PrintObj(obj runtime.Object, writer io.Writer) error Fprintf(writer io.Writer, format string, args ...interface{}) (n int, err error) + Fprintln(writer io.Writer, args ...interface{}) (n int, err error) Printf(format string, args ...interface{}) (n int, err error) + + Flush(writer io.Writer) + Close(writer io.Writer) } // TextPrinter implements Printer interface for generic text output type TextPrinter struct { } +// Flush writes any buffered data +func (tp *TextPrinter) Flush(writer io.Writer) { + return +} + +// Close flushes any buffered data and closes the printer +func (tp *TextPrinter) Close(writer io.Writer) { + return +} + // PrintObj is an implementation of ResourcePrinter.PrintObj that prints object func (tp *TextPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { _, err := fmt.Fprintf(writer, "%+v\n", obj) @@ -151,6 +171,11 @@ func (tp *TextPrinter) Fprintf(writer io.Writer, format string, args ...interfac return fmt.Fprintf(writer, format, args...) } +// Fprintln is a wrapper around fmt.Fprintln +func (tp *TextPrinter) Fprintln(writer io.Writer, args ...interface{}) (n int, err error) { + return fmt.Fprintln(writer, args...) +} + // Printf is a wrapper around fmt.Printf func (tp *TextPrinter) Printf(format string, args ...interface{}) (n int, err error) { return fmt.Printf(format, args...) @@ -169,6 +194,16 @@ func NewResourcePrinterWrapper(resourcePrinter printers.ResourcePrinter, err err return &ResourcePrinterWrapper{Printer: resourcePrinter}, nil } +// Flush writes any buffered data +func (rpw *ResourcePrinterWrapper) Flush(writer io.Writer) { + return +} + +// Close flushes any buffered data and closes the printer +func (rpw *ResourcePrinterWrapper) Close(writer io.Writer) { + return +} + // PrintObj is an implementation of ResourcePrinter.PrintObj that calls underlying printer API func (rpw *ResourcePrinterWrapper) PrintObj(obj runtime.Object, writer io.Writer) error { return rpw.Printer.PrintObj(obj, writer) @@ -181,6 +216,13 @@ func (rpw *ResourcePrinterWrapper) Fprintf(writer io.Writer, format string, args return 0, nil } +// Fprintln is an empty method to satisfy Printer interface +// and silent info printing for structured output +// This method is usually redefined for the text output +func (rpw *ResourcePrinterWrapper) Fprintln(writer io.Writer, args ...interface{}) (n int, err error) { + return 0, nil +} + // Printf is an empty method to satisfy Printer interface // and silent info printing for structured output // This method is usually redefined for the text output