k8s.io/kubernetes@v1.29.3/pkg/kubectl/cmd/convert/convert.go (about)

     1  /*
     2  Copyright 2014 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package convert
    18  
    19  import (
    20  	"fmt"
    21  
    22  	"github.com/spf13/cobra"
    23  	"k8s.io/klog/v2"
    24  
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  	"k8s.io/cli-runtime/pkg/genericclioptions"
    29  	"k8s.io/cli-runtime/pkg/genericiooptions"
    30  	"k8s.io/cli-runtime/pkg/printers"
    31  	"k8s.io/cli-runtime/pkg/resource"
    32  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    33  	"k8s.io/kubectl/pkg/util/i18n"
    34  	"k8s.io/kubectl/pkg/util/templates"
    35  	"k8s.io/kubectl/pkg/validation"
    36  	scheme "k8s.io/kubernetes/pkg/api/legacyscheme"
    37  	api "k8s.io/kubernetes/pkg/apis/core"
    38  )
    39  
    40  var (
    41  	convertLong = templates.LongDesc(i18n.T(`
    42  		Convert config files between different API versions. Both YAML
    43  		and JSON formats are accepted.
    44  
    45  		The command takes filename, directory, or URL as input, and convert it into format
    46  		of version specified by --output-version flag. If target version is not specified or
    47  		not supported, convert to latest version.
    48  
    49  		The default output will be printed to stdout in YAML format. One can use -o option
    50  		to change to output destination.`))
    51  
    52  	convertExample = templates.Examples(i18n.T(`
    53  		# Convert 'pod.yaml' to latest version and print to stdout.
    54  		kubectl convert -f pod.yaml
    55  
    56  		# Convert the live state of the resource specified by 'pod.yaml' to the latest version
    57  		# and print to stdout in JSON format.
    58  		kubectl convert -f pod.yaml --local -o json
    59  
    60  		# Convert all files under current directory to latest version and create them all.
    61  		kubectl convert -f . | kubectl create -f -`))
    62  )
    63  
    64  // ConvertOptions have the data required to perform the convert operation
    65  type ConvertOptions struct {
    66  	PrintFlags *genericclioptions.PrintFlags
    67  	Printer    printers.ResourcePrinter
    68  
    69  	OutputVersion string
    70  	Namespace     string
    71  
    72  	builder   func() *resource.Builder
    73  	local     bool
    74  	validator func() (validation.Schema, error)
    75  
    76  	resource.FilenameOptions
    77  	genericiooptions.IOStreams
    78  }
    79  
    80  func NewConvertOptions(ioStreams genericiooptions.IOStreams) *ConvertOptions {
    81  	return &ConvertOptions{
    82  		PrintFlags: genericclioptions.NewPrintFlags("converted").WithTypeSetter(scheme.Scheme).WithDefaultOutput("yaml"),
    83  		local:      true,
    84  		IOStreams:  ioStreams,
    85  	}
    86  }
    87  
    88  // NewCmdConvert creates a command object for the generic "convert" action, which
    89  // translates the config file into a given version.
    90  func NewCmdConvert(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
    91  	o := NewConvertOptions(ioStreams)
    92  
    93  	cmd := &cobra.Command{
    94  		Use:                   "convert -f FILENAME",
    95  		DisableFlagsInUseLine: true,
    96  		Short:                 i18n.T("Convert config files between different API versions"),
    97  		Long:                  convertLong,
    98  		Example:               convertExample,
    99  		Run: func(cmd *cobra.Command, args []string) {
   100  			cmdutil.CheckErr(o.Complete(f, cmd))
   101  			cmdutil.CheckErr(o.RunConvert())
   102  		},
   103  	}
   104  
   105  	cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, convert will NOT try to contact api-server but run locally.")
   106  	cmd.Flags().StringVar(&o.OutputVersion, "output-version", o.OutputVersion, i18n.T("Output the formatted object with the given group version (for ex: 'extensions/v1beta1')."))
   107  	o.PrintFlags.AddFlags(cmd)
   108  
   109  	cmdutil.AddValidateFlags(cmd)
   110  	cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, "to need to get converted.")
   111  	return cmd
   112  }
   113  
   114  // Complete collects information required to run Convert command from command line.
   115  func (o *ConvertOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) (err error) {
   116  	err = o.FilenameOptions.RequireFilenameOrKustomize()
   117  	if err != nil {
   118  		return err
   119  	}
   120  	o.builder = f.NewBuilder
   121  
   122  	o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	o.validator = func() (validation.Schema, error) {
   128  		directive, err := cmdutil.GetValidationDirective(cmd)
   129  		if err != nil {
   130  			return nil, err
   131  		}
   132  		return f.Validator(directive)
   133  	}
   134  
   135  	// build the printer
   136  	o.Printer, err = o.PrintFlags.ToPrinter()
   137  	return err
   138  }
   139  
   140  // RunConvert implements the generic Convert command
   141  func (o *ConvertOptions) RunConvert() error {
   142  	b := o.builder().
   143  		WithScheme(scheme.Scheme).
   144  		LocalParam(o.local)
   145  	if !o.local {
   146  		schema, err := o.validator()
   147  		if err != nil {
   148  			return err
   149  		}
   150  		b.Schema(schema)
   151  	}
   152  
   153  	r := b.NamespaceParam(o.Namespace).
   154  		ContinueOnError().
   155  		FilenameParam(false, &o.FilenameOptions).
   156  		Flatten().
   157  		Do()
   158  
   159  	err := r.Err()
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	singleItemImplied := false
   165  	infos, err := r.IntoSingleItemImplied(&singleItemImplied).Infos()
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	if len(infos) == 0 {
   171  		return fmt.Errorf("no objects passed to convert")
   172  	}
   173  
   174  	var specifiedOutputVersion schema.GroupVersion
   175  	if len(o.OutputVersion) > 0 {
   176  		specifiedOutputVersion, err = schema.ParseGroupVersion(o.OutputVersion)
   177  		if err != nil {
   178  			return err
   179  		}
   180  	}
   181  
   182  	internalEncoder := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   183  	internalVersionJSONEncoder := unstructured.NewJSONFallbackEncoder(internalEncoder)
   184  	objects, err := asVersionedObject(infos, !singleItemImplied, specifiedOutputVersion, internalVersionJSONEncoder, o.IOStreams)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	return o.Printer.PrintObj(objects, o.Out)
   190  }
   191  
   192  // asVersionedObject converts a list of infos into a single object - either a List containing
   193  // the objects as children, or if only a single Object is present, as that object. The provided
   194  // version will be preferred as the conversion target, but the Object's mapping version will be
   195  // used if that version is not present.
   196  func asVersionedObject(infos []*resource.Info, forceList bool, specifiedOutputVersion schema.GroupVersion, encoder runtime.Encoder, iostream genericclioptions.IOStreams) (runtime.Object, error) {
   197  	objects, err := asVersionedObjects(infos, specifiedOutputVersion, encoder, iostream)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	var object runtime.Object
   203  	if len(objects) == 1 && !forceList {
   204  		object = objects[0]
   205  	} else {
   206  		object = &api.List{Items: objects}
   207  		targetVersions := []schema.GroupVersion{}
   208  		if !specifiedOutputVersion.Empty() {
   209  			targetVersions = append(targetVersions, specifiedOutputVersion)
   210  		}
   211  		targetVersions = append(targetVersions, schema.GroupVersion{Group: "", Version: "v1"})
   212  
   213  		converted, err := tryConvert(scheme.Scheme, object, targetVersions...)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		object = converted
   218  	}
   219  
   220  	actualVersion := object.GetObjectKind().GroupVersionKind()
   221  	if actualVersion.Version != specifiedOutputVersion.Version {
   222  		defaultVersionInfo := ""
   223  		if len(actualVersion.Version) > 0 {
   224  			defaultVersionInfo = fmt.Sprintf("Defaulting to %q", actualVersion.Version)
   225  		}
   226  		klog.V(1).Infof("info: the output version specified is invalid. %s\n", defaultVersionInfo)
   227  	}
   228  	return object, nil
   229  }
   230  
   231  // asVersionedObjects converts a list of infos into versioned objects. The provided
   232  // version will be preferred as the conversion target, but the Object's mapping version will be
   233  // used if that version is not present.
   234  func asVersionedObjects(infos []*resource.Info, specifiedOutputVersion schema.GroupVersion, encoder runtime.Encoder, iostream genericclioptions.IOStreams) ([]runtime.Object, error) {
   235  	objects := []runtime.Object{}
   236  	for _, info := range infos {
   237  		if info.Object == nil {
   238  			continue
   239  		}
   240  
   241  		targetVersions := []schema.GroupVersion{}
   242  		// objects that are not part of api.Scheme must be converted to JSON
   243  		// TODO: convert to map[string]interface{}, attach to runtime.Unknown?
   244  		if !specifiedOutputVersion.Empty() {
   245  			if _, _, err := scheme.Scheme.ObjectKinds(info.Object); runtime.IsNotRegisteredError(err) {
   246  				// TODO: ideally this would encode to version, but we don't expose multiple codecs here.
   247  				data, err := runtime.Encode(encoder, info.Object)
   248  				if err != nil {
   249  					return nil, err
   250  				}
   251  				// TODO: Set ContentEncoding and ContentType.
   252  				objects = append(objects, &runtime.Unknown{Raw: data})
   253  				continue
   254  			}
   255  			targetVersions = append(targetVersions, specifiedOutputVersion)
   256  		} else {
   257  			gvks, _, err := scheme.Scheme.ObjectKinds(info.Object)
   258  			if err == nil {
   259  				for _, gvk := range gvks {
   260  					targetVersions = append(targetVersions, scheme.Scheme.PrioritizedVersionsForGroup(gvk.Group)...)
   261  				}
   262  			}
   263  		}
   264  
   265  		converted, err := tryConvert(scheme.Scheme, info.Object, targetVersions...)
   266  		if err != nil {
   267  			// Dont fail on not registered error converting objects.
   268  			// Simply warn the user with the error returned from api-machinery and continue with the rest of the file
   269  			// fail on all other errors
   270  			if runtime.IsNotRegisteredError(err) {
   271  				fmt.Fprintln(iostream.ErrOut, err.Error())
   272  				continue
   273  			}
   274  
   275  			return nil, err
   276  		}
   277  		objects = append(objects, converted)
   278  	}
   279  	return objects, nil
   280  }
   281  
   282  // tryConvert attempts to convert the given object to the provided versions in order. This function assumes
   283  // the object is in internal version.
   284  func tryConvert(converter runtime.ObjectConvertor, object runtime.Object, versions ...schema.GroupVersion) (runtime.Object, error) {
   285  	var last error
   286  	for _, version := range versions {
   287  		if version.Empty() {
   288  			return object, nil
   289  		}
   290  		obj, err := converter.ConvertToVersion(object, version)
   291  		if err != nil {
   292  			last = err
   293  			continue
   294  		}
   295  		return obj, nil
   296  	}
   297  	return nil, last
   298  }