...

Source file src/k8s.io/kubectl/pkg/cmd/annotate/annotate.go

Documentation: k8s.io/kubectl/pkg/cmd/annotate

     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 annotate
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  
    24  	jsonpatch "github.com/evanphx/json-patch"
    25  	"github.com/spf13/cobra"
    26  	"k8s.io/klog/v2"
    27  
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/types"
    33  	"k8s.io/apimachinery/pkg/util/json"
    34  
    35  	"k8s.io/client-go/tools/clientcmd"
    36  
    37  	"k8s.io/cli-runtime/pkg/genericclioptions"
    38  	"k8s.io/cli-runtime/pkg/genericiooptions"
    39  	"k8s.io/cli-runtime/pkg/printers"
    40  	"k8s.io/cli-runtime/pkg/resource"
    41  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    42  	"k8s.io/kubectl/pkg/polymorphichelpers"
    43  	"k8s.io/kubectl/pkg/scheme"
    44  	"k8s.io/kubectl/pkg/util/completion"
    45  	"k8s.io/kubectl/pkg/util/i18n"
    46  	"k8s.io/kubectl/pkg/util/templates"
    47  )
    48  
    49  // AnnotateFlags directly reflect the information that CLI is gathering via flags.  They will be converted to Options, which
    50  // reflect the runtime requirements for the command.  This structure reduces the transformation to wiring and makes
    51  // the logic itself easy to unit test
    52  type AnnotateFlags struct {
    53  	// Common user flags
    54  	All            bool
    55  	AllNamespaces  bool
    56  	DryRunStrategy cmdutil.DryRunStrategy
    57  	FieldManager   string
    58  	FieldSelector  string
    59  	resource.FilenameOptions
    60  	List            bool
    61  	Local           bool
    62  	OutputFormat    string
    63  	overwrite       bool
    64  	PrintFlags      *genericclioptions.PrintFlags
    65  	RecordFlags     *genericclioptions.RecordFlags
    66  	resourceVersion string
    67  	Selector        string
    68  
    69  	genericiooptions.IOStreams
    70  }
    71  
    72  // NewAnnotateFlags returns a default AnnotateFlags
    73  func NewAnnotateFlags(streams genericiooptions.IOStreams) *AnnotateFlags {
    74  	return &AnnotateFlags{
    75  		PrintFlags:  genericclioptions.NewPrintFlags("annotated").WithTypeSetter(scheme.Scheme),
    76  		RecordFlags: genericclioptions.NewRecordFlags(),
    77  		IOStreams:   streams,
    78  	}
    79  }
    80  
    81  // AnnotateOptions have the data required to perform the annotate operation
    82  type AnnotateOptions struct {
    83  	all           bool
    84  	allNamespaces bool
    85  
    86  	builder        *resource.Builder
    87  	dryRunStrategy cmdutil.DryRunStrategy
    88  
    89  	enforceNamespace bool
    90  	fieldSelector    string
    91  	fieldManager     string
    92  	resource.FilenameOptions
    93  
    94  	genericiooptions.IOStreams
    95  
    96  	list           bool
    97  	local          bool
    98  	namespace      string
    99  	newAnnotations map[string]string
   100  	overwrite      bool
   101  
   102  	PrintObj printers.ResourcePrinterFunc
   103  
   104  	Recorder          genericclioptions.Recorder
   105  	resources         []string
   106  	resourceVersion   string
   107  	removeAnnotations []string
   108  	selector          string
   109  
   110  	unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
   111  }
   112  
   113  var (
   114  	annotateLong = templates.LongDesc(i18n.T(`
   115  		Update the annotations on one or more resources.
   116  
   117  		All Kubernetes objects support the ability to store additional data with the object as
   118  		annotations. Annotations are key/value pairs that can be larger than labels and include
   119  		arbitrary string values such as structured JSON. Tools and system extensions may use
   120  		annotations to store their own data.
   121  
   122  		Attempting to set an annotation that already exists will fail unless --overwrite is set.
   123  		If --resource-version is specified and does not match the current resource version on
   124  		the server the command will fail.`))
   125  
   126  	annotateExample = templates.Examples(i18n.T(`
   127      # Update pod 'foo' with the annotation 'description' and the value 'my frontend'
   128      # If the same annotation is set multiple times, only the last value will be applied
   129      kubectl annotate pods foo description='my frontend'
   130  
   131      # Update a pod identified by type and name in "pod.json"
   132      kubectl annotate -f pod.json description='my frontend'
   133  
   134      # Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any existing value
   135      kubectl annotate --overwrite pods foo description='my frontend running nginx'
   136  
   137      # Update all pods in the namespace
   138      kubectl annotate pods --all description='my frontend running nginx'
   139  
   140      # Update pod 'foo' only if the resource is unchanged from version 1
   141      kubectl annotate pods foo description='my frontend running nginx' --resource-version=1
   142  
   143      # Update pod 'foo' by removing an annotation named 'description' if it exists
   144      # Does not require the --overwrite flag
   145      kubectl annotate pods foo description-`))
   146  )
   147  
   148  // NewCmdAnnotate creates the `annotate` command
   149  func NewCmdAnnotate(parent string, f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
   150  	flags := NewAnnotateFlags(streams)
   151  
   152  	cmd := &cobra.Command{
   153  		Use:                   "annotate [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]",
   154  		DisableFlagsInUseLine: true,
   155  		Short:                 i18n.T("Update the annotations on a resource"),
   156  		Long:                  annotateLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
   157  		Example:               annotateExample,
   158  		ValidArgsFunction:     completion.ResourceTypeAndNameCompletionFunc(f),
   159  		Run: func(cmd *cobra.Command, args []string) {
   160  			o, err := flags.ToOptions(f, cmd, args)
   161  			cmdutil.CheckErr(err)
   162  			cmdutil.CheckErr(o.RunAnnotate())
   163  		},
   164  	}
   165  
   166  	flags.AddFlags(cmd, streams)
   167  
   168  	return cmd
   169  }
   170  
   171  // AddFlags registers flags for a cli.
   172  func (flags *AnnotateFlags) AddFlags(cmd *cobra.Command, ioStreams genericiooptions.IOStreams) {
   173  	flags.PrintFlags.AddFlags(cmd)
   174  	flags.RecordFlags.AddFlags(cmd)
   175  
   176  	cmdutil.AddDryRunFlag(cmd)
   177  
   178  	usage := "identifying the resource to update the annotation"
   179  	cmdutil.AddFilenameOptionFlags(cmd, &flags.FilenameOptions, usage)
   180  	cmdutil.AddFieldManagerFlagVar(cmd, &flags.FieldManager, "kubectl-annotate")
   181  	cmdutil.AddLabelSelectorFlagVar(cmd, &flags.Selector)
   182  
   183  	cmd.Flags().BoolVar(&flags.overwrite, "overwrite", flags.overwrite, "If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.")
   184  	cmd.Flags().BoolVar(&flags.List, "list", flags.List, "If true, display the annotations for a given resource.")
   185  	cmd.Flags().BoolVar(&flags.Local, "local", flags.Local, "If true, annotation will NOT contact api-server but run locally.")
   186  	cmd.Flags().StringVar(&flags.FieldSelector, "field-selector", flags.FieldSelector, "Selector (field query) to filter on, supports '=', '==', and '!='.(e.g. --field-selector key1=value1,key2=value2). The server only supports a limited number of field queries per type.")
   187  	cmd.Flags().BoolVar(&flags.All, "all", flags.All, "Select all resources, in the namespace of the specified resource types.")
   188  	cmd.Flags().BoolVarP(&flags.AllNamespaces, "all-namespaces", "A", flags.AllNamespaces, "If true, check the specified action in all namespaces.")
   189  	cmd.Flags().StringVar(&flags.resourceVersion, "resource-version", flags.resourceVersion, i18n.T("If non-empty, the annotation update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource."))
   190  }
   191  
   192  // ToOptions converts from CLI inputs to runtime inputs.
   193  func (flags *AnnotateFlags) ToOptions(f cmdutil.Factory, cmd *cobra.Command, args []string) (*AnnotateOptions, error) {
   194  	options := &AnnotateOptions{
   195  		all:             flags.All,
   196  		allNamespaces:   flags.AllNamespaces,
   197  		FilenameOptions: flags.FilenameOptions,
   198  		fieldSelector:   flags.FieldSelector,
   199  		fieldManager:    flags.FieldManager,
   200  		IOStreams:       flags.IOStreams,
   201  		local:           flags.Local,
   202  		list:            flags.List,
   203  		overwrite:       flags.overwrite,
   204  		resourceVersion: flags.resourceVersion,
   205  		Recorder:        genericclioptions.NoopRecorder{},
   206  		selector:        flags.Selector,
   207  	}
   208  
   209  	var err error
   210  
   211  	flags.RecordFlags.Complete(cmd)
   212  	options.Recorder, err = flags.RecordFlags.ToRecorder()
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	options.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	cmdutil.PrintFlagsWithDryRunStrategy(flags.PrintFlags, options.dryRunStrategy)
   223  	printer, err := flags.PrintFlags.ToPrinter()
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	options.PrintObj = func(obj runtime.Object, out io.Writer) error {
   228  		return printer.PrintObj(obj, out)
   229  	}
   230  
   231  	options.namespace, options.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
   232  	if err != nil && !(options.local && clientcmd.IsEmptyConfig(err)) {
   233  		return nil, err
   234  	}
   235  	options.builder = f.NewBuilder()
   236  	options.unstructuredClientForMapping = f.UnstructuredClientForMapping
   237  
   238  	// retrieves resource and annotation args from args
   239  	// also checks args to verify that all resources are specified before annotations
   240  	resources, annotationArgs, err := cmdutil.GetResourcesAndPairs(args, "annotation")
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	options.resources = resources
   245  	options.newAnnotations, options.removeAnnotations, err = parseAnnotations(annotationArgs)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	// Checks the options and flags to see if there is sufficient information run the command.
   251  	if flags.List && len(flags.OutputFormat) > 0 {
   252  		return nil, fmt.Errorf("--list and --output may not be specified together")
   253  	}
   254  	if flags.All && len(flags.Selector) > 0 {
   255  		return nil, fmt.Errorf("cannot set --all and --selector at the same time")
   256  	}
   257  	if flags.All && len(flags.FieldSelector) > 0 {
   258  		return nil, fmt.Errorf("cannot set --all and --field-selector at the same time")
   259  	}
   260  
   261  	if !flags.Local {
   262  		if len(options.resources) < 1 && cmdutil.IsFilenameSliceEmpty(flags.Filenames, flags.Kustomize) {
   263  			return nil, fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>")
   264  		}
   265  	} else {
   266  		if options.dryRunStrategy == cmdutil.DryRunServer {
   267  			return nil, fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")
   268  		}
   269  		if len(options.resources) > 0 {
   270  			return nil, fmt.Errorf("can only use local files by -f rsrc.yaml or --filename=rsrc.json when --local=true is set")
   271  		}
   272  		if cmdutil.IsFilenameSliceEmpty(flags.Filenames, flags.Kustomize) {
   273  			return nil, fmt.Errorf("one or more files must be specified as -f rsrc.yaml or --filename=rsrc.json")
   274  		}
   275  	}
   276  	if len(options.newAnnotations) < 1 && len(options.removeAnnotations) < 1 && !flags.List {
   277  		return nil, fmt.Errorf("at least one annotation update is required")
   278  	}
   279  	err = validateAnnotations(options.removeAnnotations, options.newAnnotations)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  
   284  	return options, nil
   285  }
   286  
   287  // RunAnnotate does the work
   288  func (o AnnotateOptions) RunAnnotate() error {
   289  	b := o.builder.
   290  		Unstructured().
   291  		LocalParam(o.local).
   292  		ContinueOnError().
   293  		NamespaceParam(o.namespace).DefaultNamespace().
   294  		FilenameParam(o.enforceNamespace, &o.FilenameOptions).
   295  		Flatten()
   296  
   297  	if !o.local {
   298  		b = b.LabelSelectorParam(o.selector).
   299  			FieldSelectorParam(o.fieldSelector).
   300  			AllNamespaces(o.allNamespaces).
   301  			ResourceTypeOrNameArgs(o.all, o.resources...).
   302  			Latest()
   303  	}
   304  
   305  	r := b.Do()
   306  	if err := r.Err(); err != nil {
   307  		return err
   308  	}
   309  
   310  	var singleItemImpliedResource bool
   311  	r.IntoSingleItemImplied(&singleItemImpliedResource)
   312  
   313  	// only apply resource version locking on a single resource.
   314  	// we must perform this check after o.builder.Do() as
   315  	// []o.resources can not accurately return the proper number
   316  	// of resources when they are not passed in "resource/name" format.
   317  	if !singleItemImpliedResource && len(o.resourceVersion) > 0 {
   318  		return fmt.Errorf("--resource-version may only be used with a single resource")
   319  	}
   320  
   321  	return r.Visit(func(info *resource.Info, err error) error {
   322  		if err != nil {
   323  			return err
   324  		}
   325  
   326  		var outputObj runtime.Object
   327  		obj := info.Object
   328  
   329  		if o.dryRunStrategy == cmdutil.DryRunClient || o.local || o.list {
   330  			if err := o.updateAnnotations(obj); err != nil {
   331  				return err
   332  			}
   333  			outputObj = obj
   334  		} else {
   335  			mapping := info.ResourceMapping()
   336  			name, namespace := info.Name, info.Namespace
   337  
   338  			if len(o.resourceVersion) != 0 {
   339  				// ensure resourceVersion is always sent in the patch by clearing it from the starting JSON
   340  				accessor, err := meta.Accessor(obj)
   341  				if err != nil {
   342  					return err
   343  				}
   344  				accessor.SetResourceVersion("")
   345  			}
   346  
   347  			oldData, err := json.Marshal(obj)
   348  			if err != nil {
   349  				return err
   350  			}
   351  			if err := o.Recorder.Record(info.Object); err != nil {
   352  				klog.V(4).Infof("error recording current command: %v", err)
   353  			}
   354  			if err := o.updateAnnotations(obj); err != nil {
   355  				return err
   356  			}
   357  			newData, err := json.Marshal(obj)
   358  			if err != nil {
   359  				return err
   360  			}
   361  			patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
   362  			createdPatch := err == nil
   363  			if err != nil {
   364  				klog.V(2).Infof("couldn't compute patch: %v", err)
   365  			}
   366  
   367  			client, err := o.unstructuredClientForMapping(mapping)
   368  			if err != nil {
   369  				return err
   370  			}
   371  			helper := resource.
   372  				NewHelper(client, mapping).
   373  				DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
   374  				WithFieldManager(o.fieldManager)
   375  
   376  			if createdPatch {
   377  				outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil)
   378  			} else {
   379  				outputObj, err = helper.Replace(namespace, name, false, obj)
   380  			}
   381  			if err != nil {
   382  				return err
   383  			}
   384  		}
   385  
   386  		if o.list {
   387  			accessor, err := meta.Accessor(outputObj)
   388  			if err != nil {
   389  				return err
   390  			}
   391  
   392  			indent := ""
   393  			if !singleItemImpliedResource {
   394  				indent = " "
   395  				gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(info.Object)
   396  				if err != nil {
   397  					return err
   398  				}
   399  				fmt.Fprintf(o.Out, "Listing annotations for %s.%s/%s:\n", gvks[0].Kind, gvks[0].Group, info.Name)
   400  			}
   401  			for k, v := range accessor.GetAnnotations() {
   402  				fmt.Fprintf(o.Out, "%s%s=%s\n", indent, k, v)
   403  			}
   404  
   405  			return nil
   406  		}
   407  
   408  		return o.PrintObj(outputObj, o.Out)
   409  	})
   410  }
   411  
   412  // parseAnnotations retrieves new and remove annotations from annotation args
   413  func parseAnnotations(annotationArgs []string) (map[string]string, []string, error) {
   414  	return cmdutil.ParsePairs(annotationArgs, "annotation", true)
   415  }
   416  
   417  // validateAnnotations checks the format of annotation args and checks removed annotations aren't in the new annotations map
   418  func validateAnnotations(removeAnnotations []string, newAnnotations map[string]string) error {
   419  	var modifyRemoveBuf bytes.Buffer
   420  	for _, removeAnnotation := range removeAnnotations {
   421  		if _, found := newAnnotations[removeAnnotation]; found {
   422  			if modifyRemoveBuf.Len() > 0 {
   423  				modifyRemoveBuf.WriteString(", ")
   424  			}
   425  			modifyRemoveBuf.WriteString(fmt.Sprint(removeAnnotation))
   426  		}
   427  	}
   428  	if modifyRemoveBuf.Len() > 0 {
   429  		return fmt.Errorf("can not both modify and remove the following annotation(s) in the same command: %s", modifyRemoveBuf.String())
   430  	}
   431  
   432  	return nil
   433  }
   434  
   435  // validateNoAnnotationOverwrites validates that when overwrite is false, to-be-updated annotations don't exist in the object annotation map (yet)
   436  func validateNoAnnotationOverwrites(accessor metav1.Object, annotations map[string]string) error {
   437  	var buf bytes.Buffer
   438  	for key, value := range annotations {
   439  		// change-cause annotation can always be overwritten
   440  		if key == polymorphichelpers.ChangeCauseAnnotation {
   441  			continue
   442  		}
   443  		if currValue, found := accessor.GetAnnotations()[key]; found && currValue != value {
   444  			if buf.Len() > 0 {
   445  				buf.WriteString("; ")
   446  			}
   447  			buf.WriteString(fmt.Sprintf("'%s' already has a value (%s)", key, currValue))
   448  		}
   449  	}
   450  	if buf.Len() > 0 {
   451  		return fmt.Errorf("--overwrite is false but found the following declared annotation(s): %s", buf.String())
   452  	}
   453  	return nil
   454  }
   455  
   456  // updateAnnotations updates annotations of obj
   457  func (o AnnotateOptions) updateAnnotations(obj runtime.Object) error {
   458  	accessor, err := meta.Accessor(obj)
   459  	if err != nil {
   460  		return err
   461  	}
   462  	if !o.overwrite {
   463  		if err := validateNoAnnotationOverwrites(accessor, o.newAnnotations); err != nil {
   464  			return err
   465  		}
   466  	}
   467  
   468  	annotations := accessor.GetAnnotations()
   469  	if annotations == nil {
   470  		annotations = make(map[string]string)
   471  	}
   472  
   473  	for key, value := range o.newAnnotations {
   474  		annotations[key] = value
   475  	}
   476  	for _, annotation := range o.removeAnnotations {
   477  		delete(annotations, annotation)
   478  	}
   479  	accessor.SetAnnotations(annotations)
   480  
   481  	if len(o.resourceVersion) != 0 {
   482  		accessor.SetResourceVersion(o.resourceVersion)
   483  	}
   484  	return nil
   485  }
   486  

View as plain text