annotate.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. /*
  2. Copyright 2014 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package annotate
  14. import (
  15. "bytes"
  16. "fmt"
  17. "io"
  18. jsonpatch "github.com/evanphx/json-patch"
  19. "github.com/spf13/cobra"
  20. "k8s.io/klog"
  21. "k8s.io/apimachinery/pkg/api/meta"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/runtime"
  24. "k8s.io/apimachinery/pkg/types"
  25. "k8s.io/apimachinery/pkg/util/json"
  26. "k8s.io/cli-runtime/pkg/genericclioptions"
  27. "k8s.io/cli-runtime/pkg/printers"
  28. "k8s.io/cli-runtime/pkg/resource"
  29. "k8s.io/kubernetes/pkg/kubectl"
  30. cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
  31. "k8s.io/kubernetes/pkg/kubectl/scheme"
  32. "k8s.io/kubernetes/pkg/kubectl/util/i18n"
  33. "k8s.io/kubernetes/pkg/kubectl/util/templates"
  34. )
  35. // AnnotateOptions have the data required to perform the annotate operation
  36. type AnnotateOptions struct {
  37. PrintFlags *genericclioptions.PrintFlags
  38. PrintObj printers.ResourcePrinterFunc
  39. // Filename options
  40. resource.FilenameOptions
  41. RecordFlags *genericclioptions.RecordFlags
  42. // Common user flags
  43. overwrite bool
  44. local bool
  45. dryrun bool
  46. all bool
  47. resourceVersion string
  48. selector string
  49. fieldSelector string
  50. outputFormat string
  51. // results of arg parsing
  52. resources []string
  53. newAnnotations map[string]string
  54. removeAnnotations []string
  55. Recorder genericclioptions.Recorder
  56. namespace string
  57. enforceNamespace bool
  58. builder *resource.Builder
  59. unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
  60. genericclioptions.IOStreams
  61. }
  62. var (
  63. annotateLong = templates.LongDesc(`
  64. Update the annotations on one or more resources
  65. All Kubernetes objects support the ability to store additional data with the object as
  66. annotations. Annotations are key/value pairs that can be larger than labels and include
  67. arbitrary string values such as structured JSON. Tools and system extensions may use
  68. annotations to store their own data.
  69. Attempting to set an annotation that already exists will fail unless --overwrite is set.
  70. If --resource-version is specified and does not match the current resource version on
  71. the server the command will fail.`)
  72. annotateExample = templates.Examples(i18n.T(`
  73. # Update pod 'foo' with the annotation 'description' and the value 'my frontend'.
  74. # If the same annotation is set multiple times, only the last value will be applied
  75. kubectl annotate pods foo description='my frontend'
  76. # Update a pod identified by type and name in "pod.json"
  77. kubectl annotate -f pod.json description='my frontend'
  78. # Update pod 'foo' with the annotation 'description' and the value 'my frontend running nginx', overwriting any existing value.
  79. kubectl annotate --overwrite pods foo description='my frontend running nginx'
  80. # Update all pods in the namespace
  81. kubectl annotate pods --all description='my frontend running nginx'
  82. # Update pod 'foo' only if the resource is unchanged from version 1.
  83. kubectl annotate pods foo description='my frontend running nginx' --resource-version=1
  84. # Update pod 'foo' by removing an annotation named 'description' if it exists.
  85. # Does not require the --overwrite flag.
  86. kubectl annotate pods foo description-`))
  87. )
  88. // NewAnnotateOptions creates the options for annotate
  89. func NewAnnotateOptions(ioStreams genericclioptions.IOStreams) *AnnotateOptions {
  90. return &AnnotateOptions{
  91. PrintFlags: genericclioptions.NewPrintFlags("annotated").WithTypeSetter(scheme.Scheme),
  92. RecordFlags: genericclioptions.NewRecordFlags(),
  93. Recorder: genericclioptions.NoopRecorder{},
  94. IOStreams: ioStreams,
  95. }
  96. }
  97. // NewCmdAnnotate creates the `annotate` command
  98. func NewCmdAnnotate(parent string, f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
  99. o := NewAnnotateOptions(ioStreams)
  100. cmd := &cobra.Command{
  101. Use: "annotate [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]",
  102. DisableFlagsInUseLine: true,
  103. Short: i18n.T("Update the annotations on a resource"),
  104. Long: annotateLong + "\n\n" + cmdutil.SuggestAPIResources(parent),
  105. Example: annotateExample,
  106. Run: func(cmd *cobra.Command, args []string) {
  107. cmdutil.CheckErr(o.Complete(f, cmd, args))
  108. cmdutil.CheckErr(o.Validate())
  109. cmdutil.CheckErr(o.RunAnnotate())
  110. },
  111. }
  112. // bind flag structs
  113. o.RecordFlags.AddFlags(cmd)
  114. o.PrintFlags.AddFlags(cmd)
  115. cmdutil.AddIncludeUninitializedFlag(cmd)
  116. cmd.Flags().BoolVar(&o.overwrite, "overwrite", o.overwrite, "If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations.")
  117. cmd.Flags().BoolVar(&o.local, "local", o.local, "If true, annotation will NOT contact api-server but run locally.")
  118. cmd.Flags().StringVarP(&o.selector, "selector", "l", o.selector, "Selector (label query) to filter on, not including uninitialized ones, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2).")
  119. 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.")
  120. cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all resources, including uninitialized ones, in the namespace of the specified resource types.")
  121. cmd.Flags().StringVar(&o.resourceVersion, "resource-version", o.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."))
  122. usage := "identifying the resource to update the annotation"
  123. cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage)
  124. cmdutil.AddDryRunFlag(cmd)
  125. return cmd
  126. }
  127. // Complete adapts from the command line args and factory to the data required.
  128. func (o *AnnotateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
  129. var err error
  130. o.RecordFlags.Complete(cmd)
  131. o.Recorder, err = o.RecordFlags.ToRecorder()
  132. if err != nil {
  133. return err
  134. }
  135. o.outputFormat = cmdutil.GetFlagString(cmd, "output")
  136. o.dryrun = cmdutil.GetDryRunFlag(cmd)
  137. if o.dryrun {
  138. o.PrintFlags.Complete("%s (dry run)")
  139. }
  140. printer, err := o.PrintFlags.ToPrinter()
  141. if err != nil {
  142. return err
  143. }
  144. o.PrintObj = func(obj runtime.Object, out io.Writer) error {
  145. return printer.PrintObj(obj, out)
  146. }
  147. o.namespace, o.enforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
  148. if err != nil {
  149. return err
  150. }
  151. o.builder = f.NewBuilder()
  152. o.unstructuredClientForMapping = f.UnstructuredClientForMapping
  153. // retrieves resource and annotation args from args
  154. // also checks args to verify that all resources are specified before annotations
  155. resources, annotationArgs, err := cmdutil.GetResourcesAndPairs(args, "annotation")
  156. if err != nil {
  157. return err
  158. }
  159. o.resources = resources
  160. o.newAnnotations, o.removeAnnotations, err = parseAnnotations(annotationArgs)
  161. if err != nil {
  162. return err
  163. }
  164. return nil
  165. }
  166. // Validate checks to the AnnotateOptions to see if there is sufficient information run the command.
  167. func (o AnnotateOptions) Validate() error {
  168. if o.all && len(o.selector) > 0 {
  169. return fmt.Errorf("cannot set --all and --selector at the same time")
  170. }
  171. if o.all && len(o.fieldSelector) > 0 {
  172. return fmt.Errorf("cannot set --all and --field-selector at the same time")
  173. }
  174. if len(o.resources) < 1 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) {
  175. return fmt.Errorf("one or more resources must be specified as <resource> <name> or <resource>/<name>")
  176. }
  177. if len(o.newAnnotations) < 1 && len(o.removeAnnotations) < 1 {
  178. return fmt.Errorf("at least one annotation update is required")
  179. }
  180. return validateAnnotations(o.removeAnnotations, o.newAnnotations)
  181. }
  182. // RunAnnotate does the work
  183. func (o AnnotateOptions) RunAnnotate() error {
  184. b := o.builder.
  185. Unstructured().
  186. LocalParam(o.local).
  187. ContinueOnError().
  188. NamespaceParam(o.namespace).DefaultNamespace().
  189. FilenameParam(o.enforceNamespace, &o.FilenameOptions).
  190. Flatten()
  191. if !o.local {
  192. b = b.LabelSelectorParam(o.selector).
  193. FieldSelectorParam(o.fieldSelector).
  194. ResourceTypeOrNameArgs(o.all, o.resources...).
  195. Latest()
  196. }
  197. r := b.Do()
  198. if err := r.Err(); err != nil {
  199. return err
  200. }
  201. var singleItemImpliedResource bool
  202. r.IntoSingleItemImplied(&singleItemImpliedResource)
  203. // only apply resource version locking on a single resource.
  204. // we must perform this check after o.builder.Do() as
  205. // []o.resources can not accurately return the proper number
  206. // of resources when they are not passed in "resource/name" format.
  207. if !singleItemImpliedResource && len(o.resourceVersion) > 0 {
  208. return fmt.Errorf("--resource-version may only be used with a single resource")
  209. }
  210. return r.Visit(func(info *resource.Info, err error) error {
  211. if err != nil {
  212. return err
  213. }
  214. var outputObj runtime.Object
  215. obj := info.Object
  216. if o.dryrun || o.local {
  217. if err := o.updateAnnotations(obj); err != nil {
  218. return err
  219. }
  220. outputObj = obj
  221. } else {
  222. name, namespace := info.Name, info.Namespace
  223. oldData, err := json.Marshal(obj)
  224. if err != nil {
  225. return err
  226. }
  227. if err := o.Recorder.Record(info.Object); err != nil {
  228. klog.V(4).Infof("error recording current command: %v", err)
  229. }
  230. if err := o.updateAnnotations(obj); err != nil {
  231. return err
  232. }
  233. newData, err := json.Marshal(obj)
  234. if err != nil {
  235. return err
  236. }
  237. patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
  238. createdPatch := err == nil
  239. if err != nil {
  240. klog.V(2).Infof("couldn't compute patch: %v", err)
  241. }
  242. mapping := info.ResourceMapping()
  243. client, err := o.unstructuredClientForMapping(mapping)
  244. if err != nil {
  245. return err
  246. }
  247. helper := resource.NewHelper(client, mapping)
  248. if createdPatch {
  249. outputObj, err = helper.Patch(namespace, name, types.MergePatchType, patchBytes, nil)
  250. } else {
  251. outputObj, err = helper.Replace(namespace, name, false, obj)
  252. }
  253. if err != nil {
  254. return err
  255. }
  256. }
  257. return o.PrintObj(outputObj, o.Out)
  258. })
  259. }
  260. // parseAnnotations retrieves new and remove annotations from annotation args
  261. func parseAnnotations(annotationArgs []string) (map[string]string, []string, error) {
  262. return cmdutil.ParsePairs(annotationArgs, "annotation", true)
  263. }
  264. // validateAnnotations checks the format of annotation args and checks removed annotations aren't in the new annotations map
  265. func validateAnnotations(removeAnnotations []string, newAnnotations map[string]string) error {
  266. var modifyRemoveBuf bytes.Buffer
  267. for _, removeAnnotation := range removeAnnotations {
  268. if _, found := newAnnotations[removeAnnotation]; found {
  269. if modifyRemoveBuf.Len() > 0 {
  270. modifyRemoveBuf.WriteString(", ")
  271. }
  272. modifyRemoveBuf.WriteString(fmt.Sprintf(removeAnnotation))
  273. }
  274. }
  275. if modifyRemoveBuf.Len() > 0 {
  276. return fmt.Errorf("can not both modify and remove the following annotation(s) in the same command: %s", modifyRemoveBuf.String())
  277. }
  278. return nil
  279. }
  280. // validateNoAnnotationOverwrites validates that when overwrite is false, to-be-updated annotations don't exist in the object annotation map (yet)
  281. func validateNoAnnotationOverwrites(accessor metav1.Object, annotations map[string]string) error {
  282. var buf bytes.Buffer
  283. for key := range annotations {
  284. // change-cause annotation can always be overwritten
  285. if key == kubectl.ChangeCauseAnnotation {
  286. continue
  287. }
  288. if value, found := accessor.GetAnnotations()[key]; found {
  289. if buf.Len() > 0 {
  290. buf.WriteString("; ")
  291. }
  292. buf.WriteString(fmt.Sprintf("'%s' already has a value (%s)", key, value))
  293. }
  294. }
  295. if buf.Len() > 0 {
  296. return fmt.Errorf("--overwrite is false but found the following declared annotation(s): %s", buf.String())
  297. }
  298. return nil
  299. }
  300. // updateAnnotations updates annotations of obj
  301. func (o AnnotateOptions) updateAnnotations(obj runtime.Object) error {
  302. accessor, err := meta.Accessor(obj)
  303. if err != nil {
  304. return err
  305. }
  306. if !o.overwrite {
  307. if err := validateNoAnnotationOverwrites(accessor, o.newAnnotations); err != nil {
  308. return err
  309. }
  310. }
  311. annotations := accessor.GetAnnotations()
  312. if annotations == nil {
  313. annotations = make(map[string]string)
  314. }
  315. for key, value := range o.newAnnotations {
  316. annotations[key] = value
  317. }
  318. for _, annotation := range o.removeAnnotations {
  319. delete(annotations, annotation)
  320. }
  321. accessor.SetAnnotations(annotations)
  322. if len(o.resourceVersion) != 0 {
  323. accessor.SetResourceVersion(o.resourceVersion)
  324. }
  325. return nil
  326. }