1
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
50
51
52 type AnnotateFlags struct {
53
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
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
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
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
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
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
239
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
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
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
314
315
316
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
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
413 func parseAnnotations(annotationArgs []string) (map[string]string, []string, error) {
414 return cmdutil.ParsePairs(annotationArgs, "annotation", true)
415 }
416
417
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
436 func validateNoAnnotationOverwrites(accessor metav1.Object, annotations map[string]string) error {
437 var buf bytes.Buffer
438 for key, value := range annotations {
439
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
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