...

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

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

     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 label
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    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  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    34  	"k8s.io/apimachinery/pkg/util/json"
    35  	"k8s.io/apimachinery/pkg/util/validation"
    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  	"k8s.io/client-go/tools/clientcmd"
    42  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    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  const (
    50  	MsgNotLabeled = "not labeled"
    51  	MsgLabeled    = "labeled"
    52  	MsgUnLabeled  = "unlabeled"
    53  )
    54  
    55  // LabelOptions have the data required to perform the label operation
    56  type LabelOptions struct {
    57  	// Filename options
    58  	resource.FilenameOptions
    59  	RecordFlags *genericclioptions.RecordFlags
    60  
    61  	PrintFlags *genericclioptions.PrintFlags
    62  	ToPrinter  func(string) (printers.ResourcePrinter, error)
    63  
    64  	// Common user flags
    65  	overwrite       bool
    66  	list            bool
    67  	local           bool
    68  	dryRunStrategy  cmdutil.DryRunStrategy
    69  	all             bool
    70  	allNamespaces   bool
    71  	resourceVersion string
    72  	selector        string
    73  	fieldSelector   string
    74  	outputFormat    string
    75  	fieldManager    string
    76  
    77  	// results of arg parsing
    78  	resources    []string
    79  	newLabels    map[string]string
    80  	removeLabels []string
    81  
    82  	Recorder genericclioptions.Recorder
    83  
    84  	namespace                    string
    85  	enforceNamespace             bool
    86  	builder                      *resource.Builder
    87  	unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
    88  
    89  	// Common shared fields
    90  	genericiooptions.IOStreams
    91  }
    92  
    93  var (
    94  	labelLong = templates.LongDesc(i18n.T(`
    95  		Update the labels on a resource.
    96  
    97  		* A label key and value must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to %[1]d characters each.
    98  		* Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app.
    99  		* If --overwrite is true, then existing labels can be overwritten, otherwise attempting to overwrite a label will result in an error.
   100  		* If --resource-version is specified, then updates will use this resource version, otherwise the existing resource-version will be used.`))
   101  
   102  	labelExample = templates.Examples(i18n.T(`
   103  		# Update pod 'foo' with the label 'unhealthy' and the value 'true'
   104  		kubectl label pods foo unhealthy=true
   105  
   106  		# Update pod 'foo' with the label 'status' and the value 'unhealthy', overwriting any existing value
   107  		kubectl label --overwrite pods foo status=unhealthy
   108  
   109  		# Update all pods in the namespace
   110  		kubectl label pods --all status=unhealthy
   111  
   112  		# Update a pod identified by the type and name in "pod.json"
   113  		kubectl label -f pod.json status=unhealthy
   114  
   115  		# Update pod 'foo' only if the resource is unchanged from version 1
   116  		kubectl label pods foo status=unhealthy --resource-version=1
   117  
   118  		# Update pod 'foo' by removing a label named 'bar' if it exists
   119  		# Does not require the --overwrite flag
   120  		kubectl label pods foo bar-`))
   121  )
   122  
   123  func NewLabelOptions(ioStreams genericiooptions.IOStreams) *LabelOptions {
   124  	return &LabelOptions{
   125  		RecordFlags: genericclioptions.NewRecordFlags(),
   126  		Recorder:    genericclioptions.NoopRecorder{},
   127  
   128  		PrintFlags: genericclioptions.NewPrintFlags("labeled").WithTypeSetter(scheme.Scheme),
   129  
   130  		IOStreams: ioStreams,
   131  	}
   132  }
   133  
   134  func NewCmdLabel(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
   135  	o := NewLabelOptions(ioStreams)
   136  
   137  	cmd := &cobra.Command{
   138  		Use:                   "label [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]",
   139  		DisableFlagsInUseLine: true,
   140  		Short:                 i18n.T("Update the labels on a resource"),
   141  		Long:                  fmt.Sprintf(labelLong, validation.LabelValueMaxLength),
   142  		Example:               labelExample,
   143  		ValidArgsFunction:     completion.ResourceTypeAndNameCompletionFunc(f),
   144  		Run: func(cmd *cobra.Command, args []string) {
   145  			cmdutil.CheckErr(o.Complete(f, cmd, args))
   146  			cmdutil.CheckErr(o.Validate())
   147  			cmdutil.CheckErr(o.RunLabel())
   148  		},
   149  	}
   150  
   151  	o.RecordFlags.AddFlags(cmd)
   152  	o.PrintFlags.AddFlags(cmd)
   153  
   154  	cmd.Flags().BoolVar(&o.overwrite, "overwrite", o.overwrite, "If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels.")
   155  	cmd.Flags().BoolVar(&o.list, "list", o.list, "If true, display the labels for a given resource.")
   156  	cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, label will NOT contact api-server but run locally.")
   157  	cmd.Flags().StringVar(&o.fieldSelector, "field-selector", o.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.")
   158  	cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all resources, in the namespace of the specified resource types")
   159  	cmd.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "A", o.allNamespaces, "If true, check the specified action in all namespaces.")
   160  	cmd.Flags().StringVar(&o.resourceVersion, "resource-version", o.resourceVersion, i18n.T("If non-empty, the labels update will only succeed if this is the current resource-version for the object. Only valid when specifying a single resource."))
   161  	usage := "identifying the resource to update the labels"
   162  	cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
   163  	cmdutil.AddDryRunFlag(cmd)
   164  	cmdutil.AddFieldManagerFlagVar(cmd, &o.fieldManager, "kubectl-label")
   165  	cmdutil.AddLabelSelectorFlagVar(cmd, &o.selector)
   166  
   167  	return cmd
   168  }
   169  
   170  // Complete adapts from the command line args and factory to the data required.
   171  func (o *LabelOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
   172  	var err error
   173  
   174  	o.RecordFlags.Complete(cmd)
   175  	o.Recorder, err = o.RecordFlags.ToRecorder()
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	o.outputFormat = cmdutil.GetFlagString(cmd, "output")
   181  	o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
   187  		o.PrintFlags.NamePrintFlags.Operation = operation
   188  		// PrintFlagsWithDryRunStrategy must be done after NamePrintFlags.Operation is set
   189  		cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy)
   190  		return o.PrintFlags.ToPrinter()
   191  	}
   192  
   193  	resources, labelArgs, err := cmdutil.GetResourcesAndPairs(args, "label")
   194  	if err != nil {
   195  		return err
   196  	}
   197  	o.resources = resources
   198  	o.newLabels, o.removeLabels, err = parseLabels(labelArgs)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	if o.list && len(o.outputFormat) > 0 {
   204  		return fmt.Errorf("--list and --output may not be specified together")
   205  	}
   206  
   207  	o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
   208  	if err != nil && !(o.local && clientcmd.IsEmptyConfig(err)) {
   209  		return err
   210  	}
   211  	o.builder = f.NewBuilder()
   212  	o.unstructuredClientForMapping = f.UnstructuredClientForMapping
   213  
   214  	return nil
   215  }
   216  
   217  // Validate checks to the LabelOptions to see if there is sufficient information run the command.
   218  func (o *LabelOptions) Validate() error {
   219  	if o.all && len(o.selector) > 0 {
   220  		return fmt.Errorf("cannot set --all and --selector at the same time")
   221  	}
   222  	if o.all && len(o.fieldSelector) > 0 {
   223  		return fmt.Errorf("cannot set --all and --field-selector at the same time")
   224  	}
   225  	if o.local {
   226  		if o.dryRunStrategy == cmdutil.DryRunServer {
   227  			return fmt.Errorf("cannot specify --local and --dry-run=server - did you mean --dry-run=client?")
   228  		}
   229  		if len(o.resources) > 0 {
   230  			return fmt.Errorf("can only use local files by -f pod.yaml or --filename=pod.json when --local=true is set")
   231  		}
   232  		if cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
   233  			return fmt.Errorf("one or more files must be specified as -f pod.yaml or --filename=pod.json")
   234  		}
   235  	} else {
   236  		if len(o.resources) < 1 && cmdutil.IsFilenameSliceEmpty(o.FilenameOptions.Filenames, o.FilenameOptions.Kustomize) {
   237  			return fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>")
   238  		}
   239  	}
   240  	if len(o.newLabels) < 1 && len(o.removeLabels) < 1 && !o.list {
   241  		return fmt.Errorf("at least one label update is required")
   242  	}
   243  	return nil
   244  }
   245  
   246  // RunLabel does the work
   247  func (o *LabelOptions) RunLabel() error {
   248  	b := o.builder.
   249  		Unstructured().
   250  		LocalParam(o.local).
   251  		ContinueOnError().
   252  		NamespaceParam(o.namespace).DefaultNamespace().
   253  		FilenameParam(o.enforceNamespace, &o.FilenameOptions).
   254  		Flatten()
   255  
   256  	if !o.local {
   257  		b = b.LabelSelectorParam(o.selector).
   258  			FieldSelectorParam(o.fieldSelector).
   259  			AllNamespaces(o.allNamespaces).
   260  			ResourceTypeOrNameArgs(o.all, o.resources...).
   261  			Latest()
   262  	}
   263  
   264  	one := false
   265  	r := b.Do().IntoSingleItemImplied(&one)
   266  	if err := r.Err(); err != nil {
   267  		return err
   268  	}
   269  
   270  	// only apply resource version locking on a single resource
   271  	if !one && len(o.resourceVersion) > 0 {
   272  		return fmt.Errorf("--resource-version may only be used with a single resource")
   273  	}
   274  
   275  	// TODO: support bulk generic output a la Get
   276  	return r.Visit(func(info *resource.Info, err error) error {
   277  		if err != nil {
   278  			return err
   279  		}
   280  
   281  		var outputObj runtime.Object
   282  		var dataChangeMsg string
   283  		obj := info.Object
   284  
   285  		if len(o.resourceVersion) != 0 {
   286  			// ensure resourceVersion is always sent in the patch by clearing it from the starting JSON
   287  			accessor, err := meta.Accessor(obj)
   288  			if err != nil {
   289  				return err
   290  			}
   291  			accessor.SetResourceVersion("")
   292  		}
   293  
   294  		oldData, err := json.Marshal(obj)
   295  		if err != nil {
   296  			return err
   297  		}
   298  		if o.dryRunStrategy == cmdutil.DryRunClient || o.local || o.list {
   299  			err = labelFunc(obj, o.overwrite, o.resourceVersion, o.newLabels, o.removeLabels)
   300  			if err != nil {
   301  				return err
   302  			}
   303  			newObj, err := json.Marshal(obj)
   304  			if err != nil {
   305  				return err
   306  			}
   307  			dataChangeMsg = updateDataChangeMsg(oldData, newObj, o.overwrite)
   308  			outputObj = info.Object
   309  		} else {
   310  			name, namespace := info.Name, info.Namespace
   311  			if err != nil {
   312  				return err
   313  			}
   314  			accessor, err := meta.Accessor(obj)
   315  			if err != nil {
   316  				return err
   317  			}
   318  			for _, label := range o.removeLabels {
   319  				if _, ok := accessor.GetLabels()[label]; !ok {
   320  					fmt.Fprintf(o.Out, "label %q not found.\n", label)
   321  				}
   322  			}
   323  
   324  			if err := labelFunc(obj, o.overwrite, o.resourceVersion, o.newLabels, o.removeLabels); err != nil {
   325  				return err
   326  			}
   327  			if err := o.Recorder.Record(obj); err != nil {
   328  				klog.V(4).Infof("error recording current command: %v", err)
   329  			}
   330  			newObj, err := json.Marshal(obj)
   331  			if err != nil {
   332  				return err
   333  			}
   334  			dataChangeMsg = updateDataChangeMsg(oldData, newObj, o.overwrite)
   335  			patchBytes, err := jsonpatch.CreateMergePatch(oldData, newObj)
   336  			createdPatch := err == nil
   337  			if err != nil {
   338  				klog.V(2).Infof("couldn't compute patch: %v", err)
   339  			}
   340  
   341  			mapping := info.ResourceMapping()
   342  			client, err := o.unstructuredClientForMapping(mapping)
   343  			if err != nil {
   344  				return err
   345  			}
   346  			helper := resource.NewHelper(client, mapping).
   347  				DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
   348  				WithFieldManager(o.fieldManager)
   349  
   350  			if createdPatch {
   351  				outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil)
   352  			} else {
   353  				outputObj, err = helper.Replace(namespace, name, false, obj)
   354  			}
   355  			if err != nil {
   356  				return err
   357  			}
   358  		}
   359  
   360  		if o.list {
   361  			accessor, err := meta.Accessor(outputObj)
   362  			if err != nil {
   363  				return err
   364  			}
   365  
   366  			indent := ""
   367  			if !one {
   368  				indent = " "
   369  				gvks, _, err := unstructuredscheme.NewUnstructuredObjectTyper().ObjectKinds(info.Object)
   370  				if err != nil {
   371  					return err
   372  				}
   373  				fmt.Fprintf(o.Out, "Listing labels for %s.%s/%s:\n", gvks[0].Kind, gvks[0].Group, info.Name)
   374  			}
   375  			for k, v := range accessor.GetLabels() {
   376  				fmt.Fprintf(o.Out, "%s%s=%s\n", indent, k, v)
   377  			}
   378  
   379  			return nil
   380  		}
   381  
   382  		printer, err := o.ToPrinter(dataChangeMsg)
   383  		if err != nil {
   384  			return err
   385  		}
   386  		return printer.PrintObj(info.Object, o.Out)
   387  	})
   388  }
   389  
   390  func updateDataChangeMsg(oldObj []byte, newObj []byte, overwrite bool) string {
   391  	msg := MsgNotLabeled
   392  	if !reflect.DeepEqual(oldObj, newObj) {
   393  		msg = MsgLabeled
   394  		if !overwrite && len(newObj) < len(oldObj) {
   395  			msg = MsgUnLabeled
   396  		}
   397  	}
   398  	return msg
   399  }
   400  
   401  func validateNoOverwrites(accessor metav1.Object, labels map[string]string) error {
   402  	allErrs := []error{}
   403  	for key, value := range labels {
   404  		if currValue, found := accessor.GetLabels()[key]; found && currValue != value {
   405  			allErrs = append(allErrs, fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", key, currValue))
   406  		}
   407  	}
   408  	return utilerrors.NewAggregate(allErrs)
   409  }
   410  
   411  func parseLabels(spec []string) (map[string]string, []string, error) {
   412  	labels := map[string]string{}
   413  	var remove []string
   414  	for _, labelSpec := range spec {
   415  		if strings.Contains(labelSpec, "=") {
   416  			parts := strings.Split(labelSpec, "=")
   417  			if len(parts) != 2 {
   418  				return nil, nil, fmt.Errorf("invalid label spec: %v", labelSpec)
   419  			}
   420  			if errs := validation.IsValidLabelValue(parts[1]); len(errs) != 0 {
   421  				return nil, nil, fmt.Errorf("invalid label value: %q: %s", labelSpec, strings.Join(errs, ";"))
   422  			}
   423  			labels[parts[0]] = parts[1]
   424  		} else if strings.HasSuffix(labelSpec, "-") {
   425  			remove = append(remove, labelSpec[:len(labelSpec)-1])
   426  		} else {
   427  			return nil, nil, fmt.Errorf("unknown label spec: %v", labelSpec)
   428  		}
   429  	}
   430  	for _, removeLabel := range remove {
   431  		if _, found := labels[removeLabel]; found {
   432  			return nil, nil, fmt.Errorf("can not both modify and remove a label in the same command")
   433  		}
   434  	}
   435  	return labels, remove, nil
   436  }
   437  
   438  func labelFunc(obj runtime.Object, overwrite bool, resourceVersion string, labels map[string]string, remove []string) error {
   439  	accessor, err := meta.Accessor(obj)
   440  	if err != nil {
   441  		return err
   442  	}
   443  	if !overwrite {
   444  		if err := validateNoOverwrites(accessor, labels); err != nil {
   445  			return err
   446  		}
   447  	}
   448  
   449  	objLabels := accessor.GetLabels()
   450  	if objLabels == nil {
   451  		objLabels = make(map[string]string)
   452  	}
   453  
   454  	for key, value := range labels {
   455  		objLabels[key] = value
   456  	}
   457  	for _, label := range remove {
   458  		delete(objLabels, label)
   459  	}
   460  	accessor.SetLabels(objLabels)
   461  
   462  	if len(resourceVersion) != 0 {
   463  		accessor.SetResourceVersion(resourceVersion)
   464  	}
   465  	return nil
   466  }
   467  

View as plain text