From be7e5b47fe20b4d9ded4496448aae03e21bb2b5e Mon Sep 17 00:00:00 2001 From: Ed Bartosh Date: Thu, 6 Feb 2020 14:40:19 +0200 Subject: [PATCH 1/3] kubeadm config images list: update output API --- cmd/kubeadm/app/apis/output/register.go | 1 + cmd/kubeadm/app/apis/output/types.go | 9 ++++++ .../app/apis/output/v1alpha1/register.go | 1 + cmd/kubeadm/app/apis/output/v1alpha1/types.go | 9 ++++++ .../v1alpha1/zz_generated.conversion.go | 32 +++++++++++++++++++ .../output/v1alpha1/zz_generated.deepcopy.go | 30 +++++++++++++++++ .../app/apis/output/zz_generated.deepcopy.go | 30 +++++++++++++++++ 7 files changed, 112 insertions(+) diff --git a/cmd/kubeadm/app/apis/output/register.go b/cmd/kubeadm/app/apis/output/register.go index 111f32df3b0..ada0599efee 100644 --- a/cmd/kubeadm/app/apis/output/register.go +++ b/cmd/kubeadm/app/apis/output/register.go @@ -47,6 +47,7 @@ func Resource(resource string) schema.GroupResource { func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &BootstrapToken{}, + &Images{}, ) return nil } diff --git a/cmd/kubeadm/app/apis/output/types.go b/cmd/kubeadm/app/apis/output/types.go index b0fd52962ad..033836303d0 100644 --- a/cmd/kubeadm/app/apis/output/types.go +++ b/cmd/kubeadm/app/apis/output/types.go @@ -31,3 +31,12 @@ type BootstrapToken struct { kubeadmapiv1beta2.BootstrapToken } + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Images represents information for the output produced by 'kubeadm config images list' +type Images struct { + metav1.TypeMeta + + Images []string +} diff --git a/cmd/kubeadm/app/apis/output/v1alpha1/register.go b/cmd/kubeadm/app/apis/output/v1alpha1/register.go index 5aa4348521f..0d30c22278e 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha1/register.go +++ b/cmd/kubeadm/app/apis/output/v1alpha1/register.go @@ -59,6 +59,7 @@ func Resource(resource string) schema.GroupResource { func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &BootstrapToken{}, + &Images{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/cmd/kubeadm/app/apis/output/v1alpha1/types.go b/cmd/kubeadm/app/apis/output/v1alpha1/types.go index 2d8db0cac39..d20e14026e9 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha1/types.go +++ b/cmd/kubeadm/app/apis/output/v1alpha1/types.go @@ -31,3 +31,12 @@ type BootstrapToken struct { kubeadmapiv1beta2.BootstrapToken } + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Images represents information for the output produced by 'kubeadm config images list' +type Images struct { + metav1.TypeMeta `json:",inline"` + + Images []string `json:"images"` +} 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 7dd96f0b6c7..512f3dbde91 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.conversion.go +++ b/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.conversion.go @@ -21,6 +21,8 @@ limitations under the License. package v1alpha1 import ( + unsafe "unsafe" + conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" output "k8s.io/kubernetes/cmd/kubeadm/app/apis/output" @@ -43,6 +45,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Images)(nil), (*output.Images)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Images_To_output_Images(a.(*Images), b.(*output.Images), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*output.Images)(nil), (*Images)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_output_Images_To_v1alpha1_Images(a.(*output.Images), b.(*Images), scope) + }); err != nil { + return err + } return nil } @@ -65,3 +77,23 @@ func autoConvert_output_BootstrapToken_To_v1alpha1_BootstrapToken(in *output.Boo func Convert_output_BootstrapToken_To_v1alpha1_BootstrapToken(in *output.BootstrapToken, out *BootstrapToken, s conversion.Scope) error { return autoConvert_output_BootstrapToken_To_v1alpha1_BootstrapToken(in, out, s) } + +func autoConvert_v1alpha1_Images_To_output_Images(in *Images, out *output.Images, s conversion.Scope) error { + out.Images = *(*[]string)(unsafe.Pointer(&in.Images)) + return nil +} + +// Convert_v1alpha1_Images_To_output_Images is an autogenerated conversion function. +func Convert_v1alpha1_Images_To_output_Images(in *Images, out *output.Images, s conversion.Scope) error { + return autoConvert_v1alpha1_Images_To_output_Images(in, out, s) +} + +func autoConvert_output_Images_To_v1alpha1_Images(in *output.Images, out *Images, s conversion.Scope) error { + out.Images = *(*[]string)(unsafe.Pointer(&in.Images)) + return nil +} + +// Convert_output_Images_To_v1alpha1_Images is an autogenerated conversion function. +func Convert_output_Images_To_v1alpha1_Images(in *output.Images, out *Images, s conversion.Scope) error { + return autoConvert_output_Images_To_v1alpha1_Images(in, out, s) +} 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 9135af97c8f..418114c7a9a 100644 --- a/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/output/v1alpha1/zz_generated.deepcopy.go @@ -49,3 +49,33 @@ 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 *Images) DeepCopyInto(out *Images) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Images != nil { + in, out := &in.Images, &out.Images + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Images. +func (in *Images) DeepCopy() *Images { + if in == nil { + return nil + } + out := new(Images) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Images) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go index 4bfa46d9105..fb19e22ed7c 100644 --- a/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go +++ b/cmd/kubeadm/app/apis/output/zz_generated.deepcopy.go @@ -49,3 +49,33 @@ 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 *Images) DeepCopyInto(out *Images) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Images != nil { + in, out := &in.Images, &out.Images + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Images. +func (in *Images) DeepCopy() *Images { + if in == nil { + return nil + } + out := new(Images) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Images) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} From 23e4d05083b6be862fe944e023f6d08de40ab83d Mon Sep 17 00:00:00 2001 From: Ed Bartosh Date: Thu, 6 Feb 2020 14:42:21 +0200 Subject: [PATCH 2/3] kubeadm config images list: implement structured output Used cli-runtime API to print image info in 5 formats: - TEXT (identical to the current output) - YAML - JSON - JSONPATH - Go template --- cmd/kubeadm/app/cmd/config.go | 50 ++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/cmd/kubeadm/app/cmd/config.go b/cmd/kubeadm/app/cmd/config.go index 59bc5f3dd60..61befdadb06 100644 --- a/cmd/kubeadm/app/cmd/config.go +++ b/cmd/kubeadm/app/cmd/config.go @@ -23,6 +23,7 @@ import ( "io" "io/ioutil" "sort" + "strings" "github.com/lithammer/dedent" "github.com/pkg/errors" @@ -31,10 +32,14 @@ import ( "k8s.io/klog" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" clientset "k8s.io/client-go/kubernetes" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2" + outputapischeme "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/scheme" + outputapiv1alpha1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/v1alpha1" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" phaseutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" @@ -45,6 +50,7 @@ import ( "k8s.io/kubernetes/cmd/kubeadm/app/phases/uploadconfig" 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" utilruntime "k8s.io/kubernetes/cmd/kubeadm/app/util/runtime" utilsexec "k8s.io/utils/exec" ) @@ -527,6 +533,8 @@ func NewCmdConfigImagesList(out io.Writer, mockK8sVersion *string) *cobra.Comman externalcfg.KubernetesVersion = *mockK8sVersion } + outputFlags := output.NewOutputFlags(&imageTextPrintFlags{}).WithTypeSetter(outputapischeme.Scheme).WithDefaultOutput(output.TextOutput) + cmd := &cobra.Command{ Use: "list", Short: "Print a list of images kubeadm will use. The configuration file is used in case any images or image repositories are customized", @@ -536,14 +544,20 @@ func NewCmdConfigImagesList(out io.Writer, mockK8sVersion *string) *cobra.Comman return err } + printer, err := outputFlags.ToPrinter() + if err != nil { + return err + } + imagesList, err := NewImagesList(cfgPath, externalcfg) if err != nil { return err } - return imagesList.Run(out) + return imagesList.Run(out, printer) }, } + outputFlags.AddFlags(cmd) AddImagesCommonConfigFlags(cmd.PersistentFlags(), externalcfg, &cfgPath, &featureGatesString) return cmd } @@ -572,11 +586,39 @@ type ImagesList struct { cfg *kubeadmapi.InitConfiguration } +// imageTextPrinter prints image info in a text form +type imageTextPrinter struct { + output.TextPrinter +} + +// PrintObj is an implementation of ResourcePrinter.PrintObj for plain text output +func (itp *imageTextPrinter) PrintObj(obj runtime.Object, writer io.Writer) error { + var err error + if imgs, ok := obj.(*outputapiv1alpha1.Images); ok { + _, err = fmt.Fprintln(writer, strings.Join(imgs.Images, "\n")) + } else { + err = errors.New("unexpected object type") + } + return err +} + +// imageTextPrintFlags provides flags necessary for printing image in a text form. +type imageTextPrintFlags struct{} + +// ToPrinter returns kubeadm printer for the text output format +func (ipf *imageTextPrintFlags) ToPrinter(outputFormat string) (output.Printer, error) { + if outputFormat == output.TextOutput { + return &imageTextPrinter{}, nil + } + return nil, genericclioptions.NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: []string{output.TextOutput}} +} + // Run runs the images command and writes the result to the io.Writer passed in -func (i *ImagesList) Run(out io.Writer) error { +func (i *ImagesList) Run(out io.Writer, printer output.Printer) error { imgs := images.GetControlPlaneImages(&i.cfg.ClusterConfiguration) - for _, img := range imgs { - fmt.Fprintln(out, img) + + if err := printer.PrintObj(&outputapiv1alpha1.Images{Images: imgs}, out); err != nil { + return errors.Wrap(err, "unable to print images") } return nil From a31ccc7b99bc31c86e843b7a9584b11182cc02d0 Mon Sep 17 00:00:00 2001 From: Ed Bartosh Date: Thu, 6 Feb 2020 14:45:16 +0200 Subject: [PATCH 3/3] kubeadm config images list: test structured output Implemented tests for 'kubeadm config images list' structured output. --- cmd/kubeadm/app/cmd/config_test.go | 131 ++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/cmd/kubeadm/app/cmd/config_test.go b/cmd/kubeadm/app/cmd/config_test.go index 09c6b7a7261..b2ed1b05a37 100644 --- a/cmd/kubeadm/app/cmd/config_test.go +++ b/cmd/kubeadm/app/cmd/config_test.go @@ -31,9 +31,11 @@ import ( "github.com/lithammer/dedent" "github.com/spf13/cobra" kubeadmapiv1beta2 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta2" + outputapischeme "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/scheme" "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" 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" "k8s.io/utils/exec" fakeexec "k8s.io/utils/exec/testing" @@ -94,6 +96,12 @@ func TestImagesListRunWithCustomConfigPath(t *testing.T) { }, } + outputFlags := output.NewOutputFlags(&imageTextPrintFlags{}).WithTypeSetter(outputapischeme.Scheme).WithDefaultOutput(output.TextOutput) + printer, err := outputFlags.ToPrinter() + if err != nil { + t.Fatalf("can't create printer for the output format %s: %+v", output.TextOutput, err) + } + for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { tmpDir, err := ioutil.TempDir("", "kubeadm-images-test") @@ -114,7 +122,7 @@ func TestImagesListRunWithCustomConfigPath(t *testing.T) { t.Fatalf("Failed getting the kubeadm images command: %v", err) } var output bytes.Buffer - if i.Run(&output) != nil { + if err = i.Run(&output, printer); err != nil { t.Fatalf("Error from running the images command: %v", err) } actual := strings.Split(output.String(), "\n") @@ -175,6 +183,11 @@ func TestConfigImagesListRunWithoutPath(t *testing.T) { }, } + outputFlags := output.NewOutputFlags(&imageTextPrintFlags{}).WithTypeSetter(outputapischeme.Scheme).WithDefaultOutput(output.TextOutput) + printer, err := outputFlags.ToPrinter() + if err != nil { + t.Fatalf("can't create printer for the output format %s: %+v", output.TextOutput, err) + } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { i, err := NewImagesList("", &tc.cfg) @@ -183,7 +196,8 @@ func TestConfigImagesListRunWithoutPath(t *testing.T) { } var output bytes.Buffer - if i.Run(&output) != nil { + + if err = i.Run(&output, printer); err != nil { t.Fatalf("did not expect an error running the Images command: %v", err) } @@ -195,6 +209,119 @@ func TestConfigImagesListRunWithoutPath(t *testing.T) { } } +func TestConfigImagesListOutput(t *testing.T) { + testcases := []struct { + name string + cfg kubeadmapiv1beta2.ClusterConfiguration + outputFormat string + expectedOutput string + }{ + { + name: "text output", + cfg: kubeadmapiv1beta2.ClusterConfiguration{ + KubernetesVersion: dummyKubernetesVersion, + }, + outputFormat: "text", + expectedOutput: `k8s.gcr.io/kube-apiserver:v1.16.0 +k8s.gcr.io/kube-controller-manager:v1.16.0 +k8s.gcr.io/kube-scheduler:v1.16.0 +k8s.gcr.io/kube-proxy:v1.16.0 +k8s.gcr.io/pause:3.2 +k8s.gcr.io/etcd:3.3.17-0 +k8s.gcr.io/coredns:1.6.5 +`, + }, + { + name: "JSON output", + cfg: kubeadmapiv1beta2.ClusterConfiguration{ + KubernetesVersion: dummyKubernetesVersion, + }, + outputFormat: "json", + expectedOutput: `{ + "kind": "Images", + "apiVersion": "output.kubeadm.k8s.io/v1alpha1", + "images": [ + "k8s.gcr.io/kube-apiserver:v1.16.0", + "k8s.gcr.io/kube-controller-manager:v1.16.0", + "k8s.gcr.io/kube-scheduler:v1.16.0", + "k8s.gcr.io/kube-proxy:v1.16.0", + "k8s.gcr.io/pause:3.2", + "k8s.gcr.io/etcd:3.3.17-0", + "k8s.gcr.io/coredns:1.6.5" + ] +} +`, + }, + { + name: "YAML output", + cfg: kubeadmapiv1beta2.ClusterConfiguration{ + KubernetesVersion: dummyKubernetesVersion, + }, + outputFormat: "yaml", + expectedOutput: `apiVersion: output.kubeadm.k8s.io/v1alpha1 +images: +- k8s.gcr.io/kube-apiserver:v1.16.0 +- k8s.gcr.io/kube-controller-manager:v1.16.0 +- k8s.gcr.io/kube-scheduler:v1.16.0 +- k8s.gcr.io/kube-proxy:v1.16.0 +- k8s.gcr.io/pause:3.2 +- k8s.gcr.io/etcd:3.3.17-0 +- k8s.gcr.io/coredns:1.6.5 +kind: Images +`, + }, + { + name: "go-template output", + cfg: kubeadmapiv1beta2.ClusterConfiguration{ + KubernetesVersion: dummyKubernetesVersion, + }, + outputFormat: `go-template={{range .images}}{{.}}{{"\n"}}{{end}}`, + expectedOutput: `k8s.gcr.io/kube-apiserver:v1.16.0 +k8s.gcr.io/kube-controller-manager:v1.16.0 +k8s.gcr.io/kube-scheduler:v1.16.0 +k8s.gcr.io/kube-proxy:v1.16.0 +k8s.gcr.io/pause:3.2 +k8s.gcr.io/etcd:3.3.17-0 +k8s.gcr.io/coredns:1.6.5 +`, + }, + { + name: "JSONPATH output", + cfg: kubeadmapiv1beta2.ClusterConfiguration{ + KubernetesVersion: dummyKubernetesVersion, + }, + outputFormat: `jsonpath={range.images[*]}{@} {end}`, + expectedOutput: "k8s.gcr.io/kube-apiserver:v1.16.0 k8s.gcr.io/kube-controller-manager:v1.16.0 k8s.gcr.io/kube-scheduler:v1.16.0 " + + "k8s.gcr.io/kube-proxy:v1.16.0 k8s.gcr.io/pause:3.2 k8s.gcr.io/etcd:3.3.17-0 k8s.gcr.io/coredns:1.6.5 ", + }, + } + + for _, tc := range testcases { + outputFlags := output.NewOutputFlags(&imageTextPrintFlags{}).WithTypeSetter(outputapischeme.Scheme).WithDefaultOutput(tc.outputFormat) + printer, err := outputFlags.ToPrinter() + if err != nil { + t.Fatalf("can't create printer for the output format %s: %+v", tc.outputFormat, err) + } + + t.Run(tc.name, func(t *testing.T) { + i, err := NewImagesList("", &tc.cfg) + if err != nil { + t.Fatalf("did not expect an error while creating the Images command: %v", err) + } + + var output bytes.Buffer + + if err = i.Run(&output, printer); err != nil { + t.Fatalf("did not expect an error running the Images command: %v", err) + } + + if output.String() != tc.expectedOutput { + t.Fatalf("unexpected output:\n|%s|\nexpected:\n|%s|\n", output.String(), tc.expectedOutput) + } + }) + } +} + func TestImagesPull(t *testing.T) { fcmd := fakeexec.FakeCmd{ CombinedOutputScript: []fakeexec.FakeAction{