...

Source file src/k8s.io/cli-runtime/pkg/genericclioptions/record_flags.go

Documentation: k8s.io/cli-runtime/pkg/genericclioptions

     1  /*
     2  Copyright 2018 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 genericclioptions
    18  
    19  import (
    20  	"os"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	jsonpatch "github.com/evanphx/json-patch"
    25  	"github.com/spf13/cobra"
    26  	"github.com/spf13/pflag"
    27  
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/util/json"
    31  )
    32  
    33  // ChangeCauseAnnotation is the annotation indicating a guess at "why" something was changed
    34  const ChangeCauseAnnotation = "kubernetes.io/change-cause"
    35  
    36  // RecordFlags contains all flags associated with the "--record" operation
    37  type RecordFlags struct {
    38  	// Record indicates the state of the recording flag.  It is a pointer so a caller can opt out or rebind
    39  	Record *bool
    40  
    41  	changeCause string
    42  }
    43  
    44  // ToRecorder returns a ChangeCause recorder if --record=false was not
    45  // explicitly given by the user
    46  func (f *RecordFlags) ToRecorder() (Recorder, error) {
    47  	if f == nil {
    48  		return NoopRecorder{}, nil
    49  	}
    50  
    51  	shouldRecord := false
    52  	if f.Record != nil {
    53  		shouldRecord = *f.Record
    54  	}
    55  
    56  	// if flag was explicitly set to false by the user,
    57  	// do not record
    58  	if !shouldRecord {
    59  		return NoopRecorder{}, nil
    60  	}
    61  
    62  	return &ChangeCauseRecorder{
    63  		changeCause: f.changeCause,
    64  	}, nil
    65  }
    66  
    67  // Complete is called before the command is run, but after it is invoked to finish the state of the struct before use.
    68  func (f *RecordFlags) Complete(cmd *cobra.Command) error {
    69  	if f == nil {
    70  		return nil
    71  	}
    72  
    73  	f.changeCause = parseCommandArguments(cmd)
    74  	return nil
    75  }
    76  
    77  // CompleteWithChangeCause alters changeCause value with a new cause
    78  func (f *RecordFlags) CompleteWithChangeCause(cause string) error {
    79  	if f == nil {
    80  		return nil
    81  	}
    82  
    83  	f.changeCause = cause
    84  	return nil
    85  }
    86  
    87  // AddFlags binds the requested flags to the provided flagset
    88  // TODO have this only take a flagset
    89  func (f *RecordFlags) AddFlags(cmd *cobra.Command) {
    90  	if f == nil {
    91  		return
    92  	}
    93  
    94  	if f.Record != nil {
    95  		cmd.Flags().BoolVar(f.Record, "record", *f.Record, "Record current kubectl command in the resource annotation. If set to false, do not record the command. If set to true, record the command. If not set, default to updating the existing annotation value only if one already exists.")
    96  		cmd.Flags().MarkDeprecated("record", "--record will be removed in the future")
    97  	}
    98  }
    99  
   100  // NewRecordFlags provides a RecordFlags with reasonable default values set for use
   101  func NewRecordFlags() *RecordFlags {
   102  	record := false
   103  
   104  	return &RecordFlags{
   105  		Record: &record,
   106  	}
   107  }
   108  
   109  // Recorder is used to record why a runtime.Object was changed in an annotation.
   110  type Recorder interface {
   111  	// Record records why a runtime.Object was changed in an annotation.
   112  	Record(runtime.Object) error
   113  	MakeRecordMergePatch(runtime.Object) ([]byte, error)
   114  }
   115  
   116  // NoopRecorder does nothing.  It is a "do nothing" that can be returned so code doesn't switch on it.
   117  type NoopRecorder struct{}
   118  
   119  // Record implements Recorder
   120  func (r NoopRecorder) Record(obj runtime.Object) error {
   121  	return nil
   122  }
   123  
   124  // MakeRecordMergePatch implements Recorder
   125  func (r NoopRecorder) MakeRecordMergePatch(obj runtime.Object) ([]byte, error) {
   126  	return nil, nil
   127  }
   128  
   129  // ChangeCauseRecorder annotates a "change-cause" to an input runtime object
   130  type ChangeCauseRecorder struct {
   131  	changeCause string
   132  }
   133  
   134  // Record annotates a "change-cause" to a given info if either "shouldRecord" is true,
   135  // or the resource info previously contained a "change-cause" annotation.
   136  func (r *ChangeCauseRecorder) Record(obj runtime.Object) error {
   137  	accessor, err := meta.Accessor(obj)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	annotations := accessor.GetAnnotations()
   142  	if annotations == nil {
   143  		annotations = make(map[string]string)
   144  	}
   145  	annotations[ChangeCauseAnnotation] = r.changeCause
   146  	accessor.SetAnnotations(annotations)
   147  	return nil
   148  }
   149  
   150  // MakeRecordMergePatch produces a merge patch for updating the recording annotation.
   151  func (r *ChangeCauseRecorder) MakeRecordMergePatch(obj runtime.Object) ([]byte, error) {
   152  	// copy so we don't mess with the original
   153  	objCopy := obj.DeepCopyObject()
   154  	if err := r.Record(objCopy); err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	oldData, err := json.Marshal(obj)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	newData, err := json.Marshal(objCopy)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	return jsonpatch.CreateMergePatch(oldData, newData)
   168  }
   169  
   170  // parseCommandArguments will stringify and return all environment arguments ie. a command run by a client
   171  // using the factory.
   172  // Set showSecrets false to filter out stuff like secrets.
   173  func parseCommandArguments(cmd *cobra.Command) string {
   174  	if len(os.Args) == 0 {
   175  		return ""
   176  	}
   177  
   178  	flags := ""
   179  	parseFunc := func(flag *pflag.Flag, value string) error {
   180  		flags = flags + " --" + flag.Name
   181  		if set, ok := flag.Annotations["classified"]; !ok || len(set) == 0 {
   182  			flags = flags + "=" + value
   183  		} else {
   184  			flags = flags + "=CLASSIFIED"
   185  		}
   186  		return nil
   187  	}
   188  	var err error
   189  	err = cmd.Flags().ParseAll(os.Args[1:], parseFunc)
   190  	if err != nil || !cmd.Flags().Parsed() {
   191  		return ""
   192  	}
   193  
   194  	args := ""
   195  	if arguments := cmd.Flags().Args(); len(arguments) > 0 {
   196  		args = " " + strings.Join(arguments, " ")
   197  	}
   198  
   199  	base := filepath.Base(os.Args[0])
   200  	return base + args + flags
   201  }
   202  

View as plain text