From 2ec35382cd4666deecb893ebd46c66823430f7bf Mon Sep 17 00:00:00 2001 From: JoshVanL Date: Wed, 1 Apr 2020 20:23:30 +0100 Subject: [PATCH] Adds ctl covert command Signed-off-by: JoshVanL --- cmd/ctl/BUILD.bazel | 2 + cmd/ctl/cmd.go | 2 + cmd/ctl/pkg/convert/BUILD.bazel | 36 +++++ cmd/ctl/pkg/convert/convert.go | 254 ++++++++++++++++++++++++++++++++ cmd/ctl/pkg/version/version.go | 3 +- pkg/api/BUILD.bazel | 1 + pkg/api/scheme.go | 5 + 7 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 cmd/ctl/pkg/convert/BUILD.bazel create mode 100644 cmd/ctl/pkg/convert/convert.go diff --git a/cmd/ctl/BUILD.bazel b/cmd/ctl/BUILD.bazel index 5cd5c198f..2f78a8863 100644 --- a/cmd/ctl/BUILD.bazel +++ b/cmd/ctl/BUILD.bazel @@ -9,6 +9,7 @@ go_library( importpath = "github.com/jetstack/cert-manager/cmd/ctl", visibility = ["//visibility:private"], deps = [ + "//cmd/ctl/pkg/convert:go_default_library", "//cmd/ctl/pkg/version:go_default_library", "//pkg/util/cmd:go_default_library", "@com_github_spf13_cobra//:go_default_library", @@ -34,6 +35,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//cmd/ctl/pkg/convert:all-srcs", "//cmd/ctl/pkg/version:all-srcs", ], tags = ["automanaged"], diff --git a/cmd/ctl/cmd.go b/cmd/ctl/cmd.go index e50698890..061de772a 100644 --- a/cmd/ctl/cmd.go +++ b/cmd/ctl/cmd.go @@ -22,6 +22,7 @@ import ( "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" + "github.com/jetstack/cert-manager/cmd/ctl/pkg/convert" "github.com/jetstack/cert-manager/cmd/ctl/pkg/version" ) @@ -36,6 +37,7 @@ cert-manager-ctl is a CLI tool manage and configure cert-manager resources for K ioStreams := genericclioptions.IOStreams{In: in, Out: out, ErrOut: err} cmds.AddCommand(version.NewCmdVersion(ioStreams)) + cmds.AddCommand(convert.NewCmdConvert(ioStreams)) return cmds } diff --git a/cmd/ctl/pkg/convert/BUILD.bazel b/cmd/ctl/pkg/convert/BUILD.bazel new file mode 100644 index 000000000..e4f9494f3 --- /dev/null +++ b/cmd/ctl/pkg/convert/BUILD.bazel @@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["convert.go"], + importpath = "github.com/jetstack/cert-manager/cmd/ctl/pkg/convert", + visibility = ["//visibility:public"], + deps = [ + "//pkg/api:go_default_library", + "@com_github_spf13_cobra//:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/internalversion:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/serializer:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/serializer/json:go_default_library", + "@io_k8s_cli_runtime//pkg/genericclioptions:go_default_library", + "@io_k8s_cli_runtime//pkg/printers:go_default_library", + "@io_k8s_cli_runtime//pkg/resource:go_default_library", + "@io_k8s_klog//:go_default_library", + "@io_k8s_kubectl//pkg/cmd/util:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/cmd/ctl/pkg/convert/convert.go b/cmd/ctl/pkg/convert/convert.go new file mode 100644 index 000000000..e5b298f85 --- /dev/null +++ b/cmd/ctl/pkg/convert/convert.go @@ -0,0 +1,254 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package convert + +import ( + "fmt" + + "github.com/spf13/cobra" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + apijson "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/klog" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/jetstack/cert-manager/pkg/api" +) + +const ( + longDesc = ` +Convert cert-manager config files between different API versions. Both YAML +and JSON formats are accepted. + +The command takes filename, directory, or URL as input, and convert it into format +of version specified by --output-version flag. If target version is not specified or +not supported, convert to latest version. + +The default output will be printed to stdout in YAML format. One can use -o option +to change to output destination.` +) + +// Options is a struct to support version command +type Options struct { + PrintFlags *genericclioptions.PrintFlags + Printer printers.ResourcePrinter + + OutputVersion string + + resource.FilenameOptions + genericclioptions.IOStreams +} + +// NewOptions returns initialized Options +func NewOptions(ioStreams genericclioptions.IOStreams) *Options { + return &Options{ + IOStreams: ioStreams, + PrintFlags: genericclioptions.NewPrintFlags("converted").WithDefaultOutput("yaml"), + } +} + +// NewCmdConvert returns a cobra command for converting cert-manager resources +func NewCmdConvert(ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewOptions(ioStreams) + + cmd := &cobra.Command{ + Use: "convert", + Short: "Convert cert-manager config files between different API versions", + Long: longDesc, + DisableFlagsInUseLine: true, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(cmd)) + cmdutil.CheckErr(o.Run()) + }, + } + + cmd.Flags().StringVar(&o.OutputVersion, "output-version", o.OutputVersion, "Output the formatted object with the given group version (for ex: 'cert-manager.io/v1alpha3').") + o.PrintFlags.AddFlags(cmd) + + cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "to need to get converted.") + + return cmd +} + +// Complete collects information required to run Convert command from command line. +func (o *Options) Complete(cmd *cobra.Command) (err error) { + err = o.FilenameOptions.RequireFilenameOrKustomize() + if err != nil { + return err + } + + // build the printer + o.Printer, err = o.PrintFlags.ToPrinter() + if err != nil { + return err + } + + return nil +} + +// Run executes version command +func (o *Options) Run() error { + builder := new(resource.Builder) + + r := builder.Unstructured().LocalParam(true).ContinueOnError(). + FilenameParam(false, &o.FilenameOptions).Flatten().Do() + + err := r.Err() + if err != nil { + return fmt.Errorf("error here: %s", err) + } + + singleItemImplied := false + infos, err := r.IntoSingleItemImplied(&singleItemImplied).Infos() + if err != nil { + return fmt.Errorf("error here instead: %s", err) + } + + if len(infos) == 0 { + return fmt.Errorf("no objects passed to convert") + } + + var specifiedOutputVersion schema.GroupVersion + if len(o.OutputVersion) > 0 { + specifiedOutputVersion, err = schema.ParseGroupVersion(o.OutputVersion) + if err != nil { + return err + } + } + + factory := serializer.NewCodecFactory(api.Scheme) + serializer := apijson.NewSerializerWithOptions(apijson.DefaultMetaFactory, api.Scheme, api.Scheme, apijson.SerializerOptions{}) + encoder := factory.WithoutConversion().EncoderForVersion(serializer, nil) + objects, err := asVersionedObject(infos, !singleItemImplied, specifiedOutputVersion, encoder) + if err != nil { + return err + } + + return o.Printer.PrintObj(objects, o.Out) +} + +// asVersionedObject converts a list of infos into a single object - either a List containing +// the objects as children, or if only a single Object is present, as that object. The provided +// version will be preferred as the conversion target, but the Object's mapping version will be +// used if that version is not present. +func asVersionedObject(infos []*resource.Info, forceList bool, specifiedOutputVersion schema.GroupVersion, encoder runtime.Encoder) (runtime.Object, error) { + objects, err := asVersionedObjects(infos, specifiedOutputVersion, encoder) + if err != nil { + return nil, err + } + + var object runtime.Object + if len(objects) == 1 && !forceList { + object = objects[0] + } else { + object = &metainternalversion.List{Items: objects} + + targetVersions := []schema.GroupVersion{} + if !specifiedOutputVersion.Empty() { + targetVersions = append(targetVersions, specifiedOutputVersion) + } + targetVersions = append(targetVersions, schema.GroupVersion{Group: "", Version: "v1"}) + + converted, err := tryConvert(object, targetVersions...) + if err != nil { + return nil, err + } + + object = converted + } + + actualVersion := object.GetObjectKind().GroupVersionKind() + + if actualVersion.Version != specifiedOutputVersion.Version { + defaultVersionInfo := "" + if len(actualVersion.Version) > 0 { + defaultVersionInfo = fmt.Sprintf("Defaulting to %q", actualVersion.Version) + } + klog.V(1).Infof("info: the output version specified is invalid. %s\n", defaultVersionInfo) + } + + return object, nil +} + +// asVersionedObjects converts a list of infos into versioned objects. The provided +// version will be preferred as the conversion target, but the Object's mapping version will be +// used if that version is not present. +func asVersionedObjects(infos []*resource.Info, specifiedOutputVersion schema.GroupVersion, encoder runtime.Encoder) ([]runtime.Object, error) { + objects := []runtime.Object{} + for _, info := range infos { + if info.Object == nil { + continue + } + + targetVersions := []schema.GroupVersion{} + // objects that are not part of api.Scheme must be converted to JSON + // TODO: convert to map[string]interface{}, attach to runtime.Unknown? + if !specifiedOutputVersion.Empty() { + if _, _, err := api.Scheme.ObjectKinds(info.Object); runtime.IsNotRegisteredError(err) { + // TODO: ideally this would encode to version, but we don't expose multiple codecs here. + data, err := runtime.Encode(encoder, info.Object) + if err != nil { + return nil, err + } + // TODO: Set ContentEncoding and ContentType. + objects = append(objects, &runtime.Unknown{Raw: data}) + continue + } + + targetVersions = append(targetVersions, specifiedOutputVersion) + } else { + gvks, _, err := api.Scheme.ObjectKinds(info.Object) + if err == nil { + for _, gvk := range gvks { + targetVersions = append(targetVersions, api.Scheme.PrioritizedVersionsForGroup(gvk.Group)...) + } + } + } + + converted, err := tryConvert(info.Object, targetVersions...) + if err != nil { + return nil, err + } + objects = append(objects, converted) + } + + return objects, nil +} + +// tryConvert attempts to convert the given object to the provided versions in order. This function assumes +// the object is in internal version. +func tryConvert(object runtime.Object, versions ...schema.GroupVersion) (runtime.Object, error) { + var last error + for _, version := range versions { + if version.Empty() { + return object, nil + } + obj, err := api.Scheme.ConvertToVersion(object, version) + if err != nil { + last = err + continue + } + return obj, nil + } + + return nil, last +} diff --git a/cmd/ctl/pkg/version/version.go b/cmd/ctl/pkg/version/version.go index 4f68f2fa5..51f1f2c1b 100644 --- a/cmd/ctl/pkg/version/version.go +++ b/cmd/ctl/pkg/version/version.go @@ -21,11 +21,12 @@ import ( "errors" "fmt" - "github.com/jetstack/cert-manager/pkg/util" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "sigs.k8s.io/yaml" + + "github.com/jetstack/cert-manager/pkg/util" ) // Options is a struct to support version command diff --git a/pkg/api/BUILD.bazel b/pkg/api/BUILD.bazel index 637e0fd26..38dd15449 100644 --- a/pkg/api/BUILD.bazel +++ b/pkg/api/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "//pkg/apis/meta/v1:go_default_library", "@io_k8s_api//auditregistration/v1alpha1:go_default_library", "@io_k8s_apiextensions_apiserver//pkg/apis/apiextensions/v1beta1:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/internalversion:go_default_library", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", "@io_k8s_apimachinery//pkg/runtime:go_default_library", "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library", diff --git a/pkg/api/scheme.go b/pkg/api/scheme.go index 9fb03c9f9..9d542ea0c 100644 --- a/pkg/api/scheme.go +++ b/pkg/api/scheme.go @@ -19,6 +19,7 @@ package api import ( auditreg "k8s.io/api/auditregistration/v1alpha1" apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -77,6 +78,10 @@ var localSchemeBuilder = runtime.SchemeBuilder{ var AddToScheme = localSchemeBuilder.AddToScheme func init() { + // This is used to add the List object type for outputing multiple input objects + coreGroupVersion := schema.GroupVersion{Group: "", Version: runtime.APIVersionInternal} + Scheme.AddKnownTypes(coreGroupVersion, &metainternalversion.List{}) + metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) utilruntime.Must(AddToScheme(Scheme)) }