2019-01-12 04:58:27 +00:00
/ *
Copyright 2014 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package annotate
import (
"bytes"
"fmt"
"io"
jsonpatch "github.com/evanphx/json-patch"
"github.com/spf13/cobra"
2020-08-10 17:43:49 +00:00
"k8s.io/klog/v2"
2019-01-12 04:58:27 +00:00
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2020-08-10 17:43:49 +00:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructuredscheme"
2019-01-12 04:58:27 +00:00
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/cli-runtime/pkg/genericclioptions"
2019-04-07 17:07:55 +00:00
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/cli-runtime/pkg/resource"
2019-09-27 21:51:53 +00:00
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/polymorphichelpers"
"k8s.io/kubectl/pkg/scheme"
2021-07-02 08:43:15 +00:00
"k8s.io/kubectl/pkg/util"
2019-09-27 21:51:53 +00:00
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
2019-01-12 04:58:27 +00:00
)
// AnnotateOptions have the data required to perform the annotate operation
type AnnotateOptions struct {
PrintFlags * genericclioptions . PrintFlags
PrintObj printers . ResourcePrinterFunc
// Filename options
resource . FilenameOptions
RecordFlags * genericclioptions . RecordFlags
// Common user flags
overwrite bool
2020-08-10 17:43:49 +00:00
list bool
2019-01-12 04:58:27 +00:00
local bool
2020-03-26 21:07:15 +00:00
dryRunStrategy cmdutil . DryRunStrategy
dryRunVerifier * resource . DryRunVerifier
2020-08-10 17:43:49 +00:00
fieldManager string
2019-01-12 04:58:27 +00:00
all bool
2021-07-02 08:43:15 +00:00
allNamespaces bool
2019-01-12 04:58:27 +00:00
resourceVersion string
selector string
fieldSelector string
outputFormat string
// results of arg parsing
resources [ ] string
newAnnotations map [ string ] string
removeAnnotations [ ] string
Recorder genericclioptions . Recorder
namespace string
enforceNamespace bool
builder * resource . Builder
unstructuredClientForMapping func ( mapping * meta . RESTMapping ) ( resource . RESTClient , error )
genericclioptions . IOStreams
}
var (
2020-12-01 01:06:26 +00:00
annotateLong = templates . LongDesc ( i18n . T ( `
2021-07-02 08:43:15 +00:00
Update the annotations on one or more resources .
2019-01-12 04:58:27 +00:00
All Kubernetes objects support the ability to store additional data with the object as
annotations . Annotations are key / value pairs that can be larger than labels and include
arbitrary string values such as structured JSON . Tools and system extensions may use
annotations to store their own data .
Attempting to set an annotation that already exists will fail unless -- overwrite is set .
If -- resource - version is specified and does not match the current resource version on
2020-12-01 01:06:26 +00:00
the server the command will fail . ` ) )
2019-01-12 04:58:27 +00:00
annotateExample = templates . Examples ( i18n . T ( `
2021-07-02 08:43:15 +00:00
# Update pod ' foo ' with the annotation ' description ' and the value ' my frontend '
2019-01-12 04:58:27 +00:00
# If the same annotation is set multiple times , only the last value will be applied
kubectl annotate pods foo description = ' my frontend '
# Update a pod identified by type and name in "pod.json"
kubectl annotate - f pod . json description = ' my frontend '
2021-07-02 08:43:15 +00:00
# Update pod ' foo ' with the annotation ' description ' and the value ' my frontend running nginx ' , overwriting any existing value
2019-01-12 04:58:27 +00:00
kubectl annotate -- overwrite pods foo description = ' my frontend running nginx '
# Update all pods in the namespace
kubectl annotate pods -- all description = ' my frontend running nginx '
2021-07-02 08:43:15 +00:00
# Update pod ' foo ' only if the resource is unchanged from version 1
2019-01-12 04:58:27 +00:00
kubectl annotate pods foo description = ' my frontend running nginx ' -- resource - version = 1
2021-07-02 08:43:15 +00:00
# Update pod ' foo ' by removing an annotation named ' description ' if it exists
# Does not require the -- overwrite flag
2019-01-12 04:58:27 +00:00
kubectl annotate pods foo description - ` ) )
)
2019-04-07 17:07:55 +00:00
// NewAnnotateOptions creates the options for annotate
2019-01-12 04:58:27 +00:00
func NewAnnotateOptions ( ioStreams genericclioptions . IOStreams ) * AnnotateOptions {
return & AnnotateOptions {
PrintFlags : genericclioptions . NewPrintFlags ( "annotated" ) . WithTypeSetter ( scheme . Scheme ) ,
RecordFlags : genericclioptions . NewRecordFlags ( ) ,
Recorder : genericclioptions . NoopRecorder { } ,
IOStreams : ioStreams ,
}
}
2019-04-07 17:07:55 +00:00
// NewCmdAnnotate creates the `annotate` command
2019-01-12 04:58:27 +00:00
func NewCmdAnnotate ( parent string , f cmdutil . Factory , ioStreams genericclioptions . IOStreams ) * cobra . Command {
o := NewAnnotateOptions ( ioStreams )
cmd := & cobra . Command {
Use : "annotate [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]" ,
DisableFlagsInUseLine : true ,
Short : i18n . T ( "Update the annotations on a resource" ) ,
2019-04-07 17:07:55 +00:00
Long : annotateLong + "\n\n" + cmdutil . SuggestAPIResources ( parent ) ,
2019-01-12 04:58:27 +00:00
Example : annotateExample ,
2021-07-02 08:43:15 +00:00
ValidArgsFunction : util . ResourceTypeAndNameCompletionFunc ( f ) ,
2019-01-12 04:58:27 +00:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
cmdutil . CheckErr ( o . Complete ( f , cmd , args ) )
cmdutil . CheckErr ( o . Validate ( ) )
cmdutil . CheckErr ( o . RunAnnotate ( ) )
} ,
}
// bind flag structs
o . RecordFlags . AddFlags ( cmd )
o . PrintFlags . AddFlags ( cmd )
cmd . Flags ( ) . BoolVar ( & o . overwrite , "overwrite" , o . overwrite , "If true, allow annotations to be overwritten, otherwise reject annotation updates that overwrite existing annotations." )
2020-08-10 17:43:49 +00:00
cmd . Flags ( ) . BoolVar ( & o . list , "list" , o . list , "If true, display the annotations for a given resource." )
2019-01-12 04:58:27 +00:00
cmd . Flags ( ) . BoolVar ( & o . local , "local" , o . local , "If true, annotation will NOT contact api-server but run locally." )
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)." )
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." )
cmd . Flags ( ) . BoolVar ( & o . all , "all" , o . all , "Select all resources, including uninitialized ones, in the namespace of the specified resource types." )
2021-07-02 08:43:15 +00:00
cmd . Flags ( ) . BoolVarP ( & o . allNamespaces , "all-namespaces" , "A" , o . allNamespaces , "If true, check the specified action in all namespaces." )
2019-01-12 04:58:27 +00:00
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." ) )
usage := "identifying the resource to update the annotation"
cmdutil . AddFilenameOptionFlags ( cmd , & o . FilenameOptions , usage )
cmdutil . AddDryRunFlag ( cmd )
2020-08-10 17:43:49 +00:00
cmdutil . AddFieldManagerFlagVar ( cmd , & o . fieldManager , "kubectl-annotate" )
2019-01-12 04:58:27 +00:00
return cmd
}
// Complete adapts from the command line args and factory to the data required.
func ( o * AnnotateOptions ) Complete ( f cmdutil . Factory , cmd * cobra . Command , args [ ] string ) error {
var err error
o . RecordFlags . Complete ( cmd )
o . Recorder , err = o . RecordFlags . ToRecorder ( )
if err != nil {
return err
}
o . outputFormat = cmdutil . GetFlagString ( cmd , "output" )
2020-03-26 21:07:15 +00:00
o . dryRunStrategy , err = cmdutil . GetDryRunStrategy ( cmd )
if err != nil {
return err
}
dynamicClient , err := f . DynamicClient ( )
if err != nil {
return err
2019-01-12 04:58:27 +00:00
}
2021-03-18 22:40:29 +00:00
o . dryRunVerifier = resource . NewDryRunVerifier ( dynamicClient , f . OpenAPIGetter ( ) )
2020-03-26 21:07:15 +00:00
cmdutil . PrintFlagsWithDryRunStrategy ( o . PrintFlags , o . dryRunStrategy )
2019-01-12 04:58:27 +00:00
printer , err := o . PrintFlags . ToPrinter ( )
if err != nil {
return err
}
o . PrintObj = func ( obj runtime . Object , out io . Writer ) error {
return printer . PrintObj ( obj , out )
}
2020-08-10 17:43:49 +00:00
if o . list && len ( o . outputFormat ) > 0 {
return fmt . Errorf ( "--list and --output may not be specified together" )
}
2019-01-12 04:58:27 +00:00
o . namespace , o . enforceNamespace , err = f . ToRawKubeConfigLoader ( ) . Namespace ( )
if err != nil {
return err
}
o . builder = f . NewBuilder ( )
o . unstructuredClientForMapping = f . UnstructuredClientForMapping
// retrieves resource and annotation args from args
// also checks args to verify that all resources are specified before annotations
resources , annotationArgs , err := cmdutil . GetResourcesAndPairs ( args , "annotation" )
if err != nil {
return err
}
o . resources = resources
o . newAnnotations , o . removeAnnotations , err = parseAnnotations ( annotationArgs )
if err != nil {
return err
}
return nil
}
// Validate checks to the AnnotateOptions to see if there is sufficient information run the command.
func ( o AnnotateOptions ) Validate ( ) error {
if o . all && len ( o . selector ) > 0 {
return fmt . Errorf ( "cannot set --all and --selector at the same time" )
}
if o . all && len ( o . fieldSelector ) > 0 {
return fmt . Errorf ( "cannot set --all and --field-selector at the same time" )
}
2020-03-26 21:07:15 +00:00
if ! o . local {
if len ( o . resources ) < 1 && cmdutil . IsFilenameSliceEmpty ( o . Filenames , o . Kustomize ) {
return fmt . Errorf ( "one or more resources must be specified as <resource> <name> or <resource>/<name>" )
}
} else {
if o . dryRunStrategy == cmdutil . DryRunServer {
return fmt . Errorf ( "cannot specify --local and --dry-run=server - did you mean --dry-run=client?" )
}
if len ( o . resources ) > 0 {
return fmt . Errorf ( "can only use local files by -f rsrc.yaml or --filename=rsrc.json when --local=true is set" )
}
if cmdutil . IsFilenameSliceEmpty ( o . Filenames , o . Kustomize ) {
return fmt . Errorf ( "one or more files must be specified as -f rsrc.yaml or --filename=rsrc.json" )
}
2019-01-12 04:58:27 +00:00
}
2020-08-10 17:43:49 +00:00
if len ( o . newAnnotations ) < 1 && len ( o . removeAnnotations ) < 1 && ! o . list {
2019-01-12 04:58:27 +00:00
return fmt . Errorf ( "at least one annotation update is required" )
}
return validateAnnotations ( o . removeAnnotations , o . newAnnotations )
}
// RunAnnotate does the work
func ( o AnnotateOptions ) RunAnnotate ( ) error {
b := o . builder .
Unstructured ( ) .
LocalParam ( o . local ) .
ContinueOnError ( ) .
NamespaceParam ( o . namespace ) . DefaultNamespace ( ) .
FilenameParam ( o . enforceNamespace , & o . FilenameOptions ) .
Flatten ( )
if ! o . local {
b = b . LabelSelectorParam ( o . selector ) .
FieldSelectorParam ( o . fieldSelector ) .
2021-07-02 08:43:15 +00:00
AllNamespaces ( o . allNamespaces ) .
2019-01-12 04:58:27 +00:00
ResourceTypeOrNameArgs ( o . all , o . resources ... ) .
Latest ( )
}
r := b . Do ( )
if err := r . Err ( ) ; err != nil {
return err
}
var singleItemImpliedResource bool
r . IntoSingleItemImplied ( & singleItemImpliedResource )
// only apply resource version locking on a single resource.
// we must perform this check after o.builder.Do() as
// []o.resources can not accurately return the proper number
// of resources when they are not passed in "resource/name" format.
if ! singleItemImpliedResource && len ( o . resourceVersion ) > 0 {
return fmt . Errorf ( "--resource-version may only be used with a single resource" )
}
return r . Visit ( func ( info * resource . Info , err error ) error {
if err != nil {
return err
}
var outputObj runtime . Object
obj := info . Object
2020-08-10 17:43:49 +00:00
if o . dryRunStrategy == cmdutil . DryRunClient || o . local || o . list {
2019-01-12 04:58:27 +00:00
if err := o . updateAnnotations ( obj ) ; err != nil {
return err
}
outputObj = obj
} else {
2020-03-26 21:07:15 +00:00
mapping := info . ResourceMapping ( )
if o . dryRunStrategy == cmdutil . DryRunServer {
if err := o . dryRunVerifier . HasSupport ( mapping . GroupVersionKind ) ; err != nil {
return err
}
}
2019-01-12 04:58:27 +00:00
name , namespace := info . Name , info . Namespace
2019-12-12 01:27:03 +00:00
if len ( o . resourceVersion ) != 0 {
// ensure resourceVersion is always sent in the patch by clearing it from the starting JSON
accessor , err := meta . Accessor ( obj )
if err != nil {
return err
}
accessor . SetResourceVersion ( "" )
}
2019-01-12 04:58:27 +00:00
oldData , err := json . Marshal ( obj )
if err != nil {
return err
}
if err := o . Recorder . Record ( info . Object ) ; err != nil {
klog . V ( 4 ) . Infof ( "error recording current command: %v" , err )
}
if err := o . updateAnnotations ( obj ) ; err != nil {
return err
}
newData , err := json . Marshal ( obj )
if err != nil {
return err
}
patchBytes , err := jsonpatch . CreateMergePatch ( oldData , newData )
createdPatch := err == nil
if err != nil {
klog . V ( 2 ) . Infof ( "couldn't compute patch: %v" , err )
}
client , err := o . unstructuredClientForMapping ( mapping )
if err != nil {
return err
}
2020-03-26 21:07:15 +00:00
helper := resource .
NewHelper ( client , mapping ) .
2020-08-10 17:43:49 +00:00
DryRun ( o . dryRunStrategy == cmdutil . DryRunServer ) .
WithFieldManager ( o . fieldManager )
2019-01-12 04:58:27 +00:00
if createdPatch {
outputObj , err = helper . Patch ( namespace , name , types . MergePatchType , patchBytes , nil )
} else {
outputObj , err = helper . Replace ( namespace , name , false , obj )
}
if err != nil {
return err
}
}
2020-08-10 17:43:49 +00:00
if o . list {
accessor , err := meta . Accessor ( outputObj )
if err != nil {
return err
}
indent := ""
if ! singleItemImpliedResource {
indent = " "
gvks , _ , err := unstructuredscheme . NewUnstructuredObjectTyper ( ) . ObjectKinds ( info . Object )
if err != nil {
return err
}
fmt . Fprintf ( o . Out , "Listing annotations for %s.%s/%s:\n" , gvks [ 0 ] . Kind , gvks [ 0 ] . Group , info . Name )
}
for k , v := range accessor . GetAnnotations ( ) {
fmt . Fprintf ( o . Out , "%s%s=%s\n" , indent , k , v )
}
return nil
}
2019-01-12 04:58:27 +00:00
return o . PrintObj ( outputObj , o . Out )
} )
}
// parseAnnotations retrieves new and remove annotations from annotation args
func parseAnnotations ( annotationArgs [ ] string ) ( map [ string ] string , [ ] string , error ) {
return cmdutil . ParsePairs ( annotationArgs , "annotation" , true )
}
// validateAnnotations checks the format of annotation args and checks removed annotations aren't in the new annotations map
func validateAnnotations ( removeAnnotations [ ] string , newAnnotations map [ string ] string ) error {
var modifyRemoveBuf bytes . Buffer
for _ , removeAnnotation := range removeAnnotations {
if _ , found := newAnnotations [ removeAnnotation ] ; found {
if modifyRemoveBuf . Len ( ) > 0 {
modifyRemoveBuf . WriteString ( ", " )
}
2020-03-26 21:07:15 +00:00
modifyRemoveBuf . WriteString ( fmt . Sprint ( removeAnnotation ) )
2019-01-12 04:58:27 +00:00
}
}
if modifyRemoveBuf . Len ( ) > 0 {
return fmt . Errorf ( "can not both modify and remove the following annotation(s) in the same command: %s" , modifyRemoveBuf . String ( ) )
}
return nil
}
// validateNoAnnotationOverwrites validates that when overwrite is false, to-be-updated annotations don't exist in the object annotation map (yet)
func validateNoAnnotationOverwrites ( accessor metav1 . Object , annotations map [ string ] string ) error {
var buf bytes . Buffer
for key := range annotations {
// change-cause annotation can always be overwritten
2019-09-27 21:51:53 +00:00
if key == polymorphichelpers . ChangeCauseAnnotation {
2019-01-12 04:58:27 +00:00
continue
}
if value , found := accessor . GetAnnotations ( ) [ key ] ; found {
if buf . Len ( ) > 0 {
buf . WriteString ( "; " )
}
buf . WriteString ( fmt . Sprintf ( "'%s' already has a value (%s)" , key , value ) )
}
}
if buf . Len ( ) > 0 {
return fmt . Errorf ( "--overwrite is false but found the following declared annotation(s): %s" , buf . String ( ) )
}
return nil
}
// updateAnnotations updates annotations of obj
func ( o AnnotateOptions ) updateAnnotations ( obj runtime . Object ) error {
accessor , err := meta . Accessor ( obj )
if err != nil {
return err
}
if ! o . overwrite {
if err := validateNoAnnotationOverwrites ( accessor , o . newAnnotations ) ; err != nil {
return err
}
}
annotations := accessor . GetAnnotations ( )
if annotations == nil {
annotations = make ( map [ string ] string )
}
for key , value := range o . newAnnotations {
annotations [ key ] = value
}
for _ , annotation := range o . removeAnnotations {
delete ( annotations , annotation )
}
accessor . SetAnnotations ( annotations )
if len ( o . resourceVersion ) != 0 {
accessor . SetResourceVersion ( o . resourceVersion )
}
return nil
}