1
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
56 type LabelOptions struct {
57
58 resource.FilenameOptions
59 RecordFlags *genericclioptions.RecordFlags
60
61 PrintFlags *genericclioptions.PrintFlags
62 ToPrinter func(string) (printers.ResourcePrinter, error)
63
64
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
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
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
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
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
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
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
271 if !one && len(o.resourceVersion) > 0 {
272 return fmt.Errorf("--resource-version may only be used with a single resource")
273 }
274
275
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
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