Merge pull request #86810 from bart0sh/PR0087-kubeadm-output-images

kubeadm config images list: implement structured output
This commit is contained in:
Kubernetes Prow Robot 2020-02-17 17:07:28 -08:00 committed by GitHub
commit 5bd719b6a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 287 additions and 6 deletions

View File

@ -47,6 +47,7 @@ func Resource(resource string) schema.GroupResource {
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&BootstrapToken{},
&Images{},
)
return nil
}

View File

@ -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
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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{