2017-04-18 12:44:25 +00:00
/ *
Copyright 2017 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 editor
import (
"bufio"
"bytes"
2017-02-26 12:36:20 +00:00
"errors"
2017-04-18 12:44:25 +00:00
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/evanphx/json-patch"
"github.com/golang/glog"
2017-08-11 06:21:44 +00:00
"github.com/spf13/cobra"
2017-04-18 12:44:25 +00:00
2017-02-26 12:36:20 +00:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2017-04-18 12:44:25 +00:00
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/mergepatch"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/yaml"
2017-11-08 22:34:54 +00:00
api "k8s.io/kubernetes/pkg/apis/core"
2017-04-18 12:44:25 +00:00
"k8s.io/kubernetes/pkg/kubectl"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
2017-10-28 01:31:42 +00:00
"k8s.io/kubernetes/pkg/kubectl/scheme"
2017-06-29 21:57:06 +00:00
"k8s.io/kubernetes/pkg/kubectl/util/crlf"
2017-04-18 12:44:25 +00:00
"k8s.io/kubernetes/pkg/printers"
)
// EditOptions contains all the options for running edit cli command.
type EditOptions struct {
resource . FilenameOptions
Output string
2017-05-19 09:47:07 +00:00
OutputPatch bool
2017-04-18 12:44:25 +00:00
WindowsLineEndings bool
cmdutil . ValidateOptions
ResourceMapper * resource . Mapper
OriginalResult * resource . Result
EditMode EditMode
CmdNamespace string
ApplyAnnotation bool
Record bool
ChangeCause string
Include3rdParty bool
Out io . Writer
ErrOut io . Writer
f cmdutil . Factory
editPrinterOptions * editPrinterOptions
updatedResultGetter func ( data [ ] byte ) * resource . Result
}
type editPrinterOptions struct {
printer printers . ResourcePrinter
ext string
addHeader bool
}
// Complete completes all the required options
2017-08-11 06:21:44 +00:00
func ( o * EditOptions ) Complete ( f cmdutil . Factory , out , errOut io . Writer , args [ ] string , cmd * cobra . Command ) error {
2017-02-26 12:36:20 +00:00
if o . EditMode != NormalEditMode && o . EditMode != EditBeforeCreateMode && o . EditMode != ApplyEditMode {
2017-04-18 12:44:25 +00:00
return fmt . Errorf ( "unsupported edit mode %q" , o . EditMode )
}
if o . Output != "" {
if o . Output != "yaml" && o . Output != "json" {
return fmt . Errorf ( "invalid output format %s, only yaml|json supported" , o . Output )
}
}
o . editPrinterOptions = getPrinter ( o . Output )
2017-05-19 09:47:07 +00:00
if o . OutputPatch && o . EditMode != NormalEditMode {
return fmt . Errorf ( "the edit mode doesn't support output the patch" )
}
2017-04-18 12:44:25 +00:00
cmdNamespace , enforceNamespace , err := f . DefaultNamespace ( )
if err != nil {
return err
}
2017-11-15 06:10:30 +00:00
b := f . NewBuilder ( ) .
Unstructured ( )
2017-02-26 12:36:20 +00:00
if o . EditMode == NormalEditMode || o . EditMode == ApplyEditMode {
// when do normal edit or apply edit we need to always retrieve the latest resource from server
2017-04-18 12:44:25 +00:00
b = b . ResourceTypeOrNameArgs ( true , args ... ) . Latest ( )
}
2017-08-11 06:21:44 +00:00
includeUninitialized := cmdutil . ShouldIncludeUninitialized ( cmd , false )
2017-04-18 12:44:25 +00:00
r := b . NamespaceParam ( cmdNamespace ) . DefaultNamespace ( ) .
FilenameParam ( enforceNamespace , & o . FilenameOptions ) .
2017-08-11 06:21:44 +00:00
IncludeUninitialized ( includeUninitialized ) .
2017-04-18 12:44:25 +00:00
ContinueOnError ( ) .
Flatten ( ) .
Do ( )
err = r . Err ( )
if err != nil {
return err
}
o . OriginalResult = r
o . updatedResultGetter = func ( data [ ] byte ) * resource . Result {
// resource builder to read objects from edited data
2017-11-14 04:01:51 +00:00
return f . NewBuilder ( ) .
Unstructured ( ) .
2017-04-18 12:44:25 +00:00
Stream ( bytes . NewReader ( data ) , "edited-file" ) .
2017-08-11 06:21:44 +00:00
IncludeUninitialized ( includeUninitialized ) .
2017-04-18 12:44:25 +00:00
ContinueOnError ( ) .
Flatten ( ) .
Do ( )
}
o . CmdNamespace = cmdNamespace
o . f = f
// Set up writer
o . Out = out
o . ErrOut = errOut
return nil
}
// Validate checks the EditOptions to see if there is sufficient information to run the command.
func ( o * EditOptions ) Validate ( ) error {
return nil
}
func ( o * EditOptions ) Run ( ) error {
edit := NewDefaultEditor ( o . f . EditorEnvs ( ) )
// editFn is invoked for each edit session (once with a list for normal edit, once for each individual resource in a edit-on-create invocation)
editFn := func ( infos [ ] * resource . Info ) error {
var (
results = editResults { }
original = [ ] byte { }
edited = [ ] byte { }
file string
err error
)
containsError := false
// loop until we succeed or cancel editing
for {
// get the object we're going to serialize as input to the editor
var originalObj runtime . Object
switch len ( infos ) {
case 1 :
originalObj = infos [ 0 ] . Object
default :
l := & unstructured . UnstructuredList {
Object : map [ string ] interface { } {
"kind" : "List" ,
"apiVersion" : "v1" ,
"metadata" : map [ string ] interface { } { } ,
} ,
}
for _ , info := range infos {
l . Items = append ( l . Items , * info . Object . ( * unstructured . Unstructured ) )
}
originalObj = l
}
// generate the file to edit
buf := & bytes . Buffer { }
var w io . Writer = buf
if o . WindowsLineEndings {
w = crlf . NewCRLFWriter ( w )
}
if o . editPrinterOptions . addHeader {
2017-02-26 12:36:20 +00:00
results . header . writeTo ( w , o . EditMode )
2017-04-18 12:44:25 +00:00
}
if ! containsError {
if err := o . editPrinterOptions . printer . PrintObj ( originalObj , w ) ; err != nil {
return preservedFile ( err , results . file , o . ErrOut )
}
original = buf . Bytes ( )
} else {
// In case of an error, preserve the edited file.
// Remove the comments (header) from it since we already
// have included the latest header in the buffer above.
buf . Write ( cmdutil . ManualStrip ( edited ) )
}
// launch the editor
editedDiff := edited
edited , file , err = edit . LaunchTempFile ( fmt . Sprintf ( "%s-edit-" , filepath . Base ( os . Args [ 0 ] ) ) , o . editPrinterOptions . ext , buf )
if err != nil {
return preservedFile ( err , results . file , o . ErrOut )
}
// If we're retrying the loop because of an error, and no change was made in the file, short-circuit
if containsError && bytes . Equal ( cmdutil . StripComments ( editedDiff ) , cmdutil . StripComments ( edited ) ) {
return preservedFile ( fmt . Errorf ( "%s" , "Edit cancelled, no valid changes were saved." ) , file , o . ErrOut )
}
// cleanup any file from the previous pass
if len ( results . file ) > 0 {
os . Remove ( results . file )
}
glog . V ( 4 ) . Infof ( "User edited:\n%s" , string ( edited ) )
// Apply validation
2017-09-28 22:39:17 +00:00
schema , err := o . f . Validator ( o . EnableValidation )
2017-04-18 12:44:25 +00:00
if err != nil {
return preservedFile ( err , file , o . ErrOut )
}
err = schema . ValidateBytes ( cmdutil . StripComments ( edited ) )
if err != nil {
results = editResults {
file : file ,
}
containsError = true
2017-02-26 12:36:20 +00:00
fmt . Fprintln ( o . ErrOut , results . addError ( apierrors . NewInvalid ( api . Kind ( "" ) , "" , field . ErrorList { field . Invalid ( nil , "The edited file failed validation" , fmt . Sprintf ( "%v" , err ) ) } ) , infos [ 0 ] ) )
2017-04-18 12:44:25 +00:00
continue
}
// Compare content without comments
if bytes . Equal ( cmdutil . StripComments ( original ) , cmdutil . StripComments ( edited ) ) {
os . Remove ( file )
fmt . Fprintln ( o . ErrOut , "Edit cancelled, no changes made." )
return nil
}
lines , err := hasLines ( bytes . NewBuffer ( edited ) )
if err != nil {
return preservedFile ( err , file , o . ErrOut )
}
if ! lines {
os . Remove ( file )
fmt . Fprintln ( o . ErrOut , "Edit cancelled, saved file was empty." )
return nil
}
results = editResults {
file : file ,
}
// parse the edited file
updatedInfos , err := o . updatedResultGetter ( edited ) . Infos ( )
if err != nil {
// syntax error
containsError = true
results . header . reasons = append ( results . header . reasons , editReason { head : fmt . Sprintf ( "The edited file had a syntax error: %v" , err ) } )
continue
}
// not a syntax error as it turns out...
containsError = false
updatedVisitor := resource . InfoListVisitor ( updatedInfos )
// need to make sure the original namespace wasn't changed while editing
if err := updatedVisitor . Visit ( resource . RequireNamespace ( o . CmdNamespace ) ) ; err != nil {
return preservedFile ( err , file , o . ErrOut )
}
// iterate through all items to apply annotations
2017-05-10 08:46:04 +00:00
if err := o . visitAnnotation ( updatedVisitor ) ; err != nil {
2017-04-18 12:44:25 +00:00
return preservedFile ( err , file , o . ErrOut )
}
switch o . EditMode {
case NormalEditMode :
2017-05-10 08:46:04 +00:00
err = o . visitToPatch ( infos , updatedVisitor , & results )
2017-02-26 12:36:20 +00:00
case ApplyEditMode :
err = o . visitToApplyEditPatch ( infos , updatedVisitor )
2017-04-18 12:44:25 +00:00
case EditBeforeCreateMode :
2017-05-10 08:46:04 +00:00
err = o . visitToCreate ( updatedVisitor )
2017-04-18 12:44:25 +00:00
default :
err = fmt . Errorf ( "unsupported edit mode %q" , o . EditMode )
}
if err != nil {
return preservedFile ( err , results . file , o . ErrOut )
}
// Handle all possible errors
//
// 1. retryable: propose kubectl replace -f
// 2. notfound: indicate the location of the saved configuration of the deleted resource
// 3. invalid: retry those on the spot by looping ie. reloading the editor
if results . retryable > 0 {
fmt . Fprintf ( o . ErrOut , "You can run `%s replace -f %s` to try this update again.\n" , filepath . Base ( os . Args [ 0 ] ) , file )
return cmdutil . ErrExit
}
if results . notfound > 0 {
fmt . Fprintf ( o . ErrOut , "The edits you made on deleted resources have been saved to %q\n" , file )
return cmdutil . ErrExit
}
if len ( results . edit ) == 0 {
if results . notfound == 0 {
os . Remove ( file )
} else {
fmt . Fprintf ( o . Out , "The edits you made on deleted resources have been saved to %q\n" , file )
}
return nil
}
if len ( results . header . reasons ) > 0 {
containsError = true
}
}
}
switch o . EditMode {
// If doing normal edit we cannot use Visit because we need to edit a list for convenience. Ref: #20519
case NormalEditMode :
infos , err := o . OriginalResult . Infos ( )
if err != nil {
return err
}
2017-08-06 02:59:33 +00:00
if len ( infos ) == 0 {
return errors . New ( "edit cancelled, no objects found." )
}
2017-04-18 12:44:25 +00:00
return editFn ( infos )
2017-02-26 12:36:20 +00:00
case ApplyEditMode :
infos , err := o . OriginalResult . Infos ( )
if err != nil {
return err
}
var annotationInfos [ ] * resource . Info
for i := range infos {
data , err := kubectl . GetOriginalConfiguration ( infos [ i ] . Mapping , infos [ i ] . Object )
if err != nil {
return err
}
if data == nil {
continue
}
tempInfos , err := o . updatedResultGetter ( data ) . Infos ( )
if err != nil {
return err
}
annotationInfos = append ( annotationInfos , tempInfos [ 0 ] )
}
if len ( annotationInfos ) == 0 {
return errors . New ( "no last-applied-configuration annotation found on resources, to create the annotation, use command `kubectl apply set-last-applied --create-annotation`" )
}
return editFn ( annotationInfos )
2017-04-18 12:44:25 +00:00
// If doing an edit before created, we don't want a list and instead want the normal behavior as kubectl create.
case EditBeforeCreateMode :
return o . OriginalResult . Visit ( func ( info * resource . Info , err error ) error {
return editFn ( [ ] * resource . Info { info } )
} )
default :
return fmt . Errorf ( "unsupported edit mode %q" , o . EditMode )
}
}
2017-02-26 12:36:20 +00:00
func ( o * EditOptions ) visitToApplyEditPatch ( originalInfos [ ] * resource . Info , patchVisitor resource . Visitor ) error {
err := patchVisitor . Visit ( func ( info * resource . Info , incomingErr error ) error {
editObjUID , err := meta . NewAccessor ( ) . UID ( info . Object )
if err != nil {
return err
}
var originalInfo * resource . Info
for _ , i := range originalInfos {
originalObjUID , err := meta . NewAccessor ( ) . UID ( i . Object )
if err != nil {
return err
}
if editObjUID == originalObjUID {
originalInfo = i
break
}
}
if originalInfo == nil {
return fmt . Errorf ( "no original object found for %#v" , info . Object )
}
2018-02-21 17:10:38 +00:00
originalJS , err := encodeToJson ( cmdutil . InternalVersionJSONEncoder ( ) , originalInfo . Object )
2017-02-26 12:36:20 +00:00
if err != nil {
return err
}
2018-02-21 17:10:38 +00:00
editedJS , err := encodeToJson ( cmdutil . InternalVersionJSONEncoder ( ) , info . Object )
2017-02-26 12:36:20 +00:00
if err != nil {
return err
}
if reflect . DeepEqual ( originalJS , editedJS ) {
2018-02-21 01:14:21 +00:00
cmdutil . PrintSuccess ( false , o . Out , info . Object , false , "skipped" )
2017-02-26 12:36:20 +00:00
return nil
} else {
err := o . annotationPatch ( info )
if err != nil {
return err
}
2018-02-21 01:14:21 +00:00
cmdutil . PrintSuccess ( false , o . Out , info . Object , false , "edited" )
2017-02-26 12:36:20 +00:00
return nil
}
} )
return err
}
func ( o * EditOptions ) annotationPatch ( update * resource . Info ) error {
2018-02-21 17:10:38 +00:00
patch , _ , patchType , err := GetApplyPatch ( update . Object , cmdutil . InternalVersionJSONEncoder ( ) )
2017-02-26 12:36:20 +00:00
if err != nil {
return err
}
mapping := update . ResourceMapping ( )
client , err := o . f . UnstructuredClientForMapping ( mapping )
if err != nil {
return err
}
helper := resource . NewHelper ( client , mapping )
_ , err = helper . Patch ( o . CmdNamespace , update . Name , patchType , patch )
if err != nil {
return err
}
return nil
}
func GetApplyPatch ( obj runtime . Object , codec runtime . Encoder ) ( [ ] byte , [ ] byte , types . PatchType , error ) {
beforeJSON , err := encodeToJson ( codec , obj )
if err != nil {
return nil , [ ] byte ( "" ) , types . MergePatchType , err
}
2017-08-15 12:13:20 +00:00
objCopy := obj . DeepCopyObject ( )
2017-02-26 12:36:20 +00:00
accessor := meta . NewAccessor ( )
annotations , err := accessor . Annotations ( objCopy )
if err != nil {
return nil , beforeJSON , types . MergePatchType , err
}
if annotations == nil {
annotations = map [ string ] string { }
}
annotations [ api . LastAppliedConfigAnnotation ] = string ( beforeJSON )
accessor . SetAnnotations ( objCopy , annotations )
afterJSON , err := encodeToJson ( codec , objCopy )
if err != nil {
return nil , beforeJSON , types . MergePatchType , err
}
patch , err := jsonpatch . CreateMergePatch ( beforeJSON , afterJSON )
return patch , beforeJSON , types . MergePatchType , err
}
func encodeToJson ( codec runtime . Encoder , obj runtime . Object ) ( [ ] byte , error ) {
serialization , err := runtime . Encode ( codec , obj )
if err != nil {
return nil , err
}
js , err := yaml . ToJSON ( serialization )
if err != nil {
return nil , err
}
return js , nil
}
2017-04-18 12:44:25 +00:00
func getPrinter ( format string ) * editPrinterOptions {
switch format {
case "json" :
return & editPrinterOptions {
printer : & printers . JSONPrinter { } ,
ext : ".json" ,
addHeader : false ,
}
case "yaml" :
return & editPrinterOptions {
printer : & printers . YAMLPrinter { } ,
ext : ".yaml" ,
addHeader : true ,
}
default :
// if format is not specified, use yaml as default
return & editPrinterOptions {
printer : & printers . YAMLPrinter { } ,
ext : ".yaml" ,
addHeader : true ,
}
}
}
2017-08-06 02:59:33 +00:00
func ( o * EditOptions ) visitToPatch ( originalInfos [ ] * resource . Info , patchVisitor resource . Visitor , results * editResults ) error {
2017-04-18 12:44:25 +00:00
err := patchVisitor . Visit ( func ( info * resource . Info , incomingErr error ) error {
editObjUID , err := meta . NewAccessor ( ) . UID ( info . Object )
if err != nil {
return err
}
var originalInfo * resource . Info
for _ , i := range originalInfos {
originalObjUID , err := meta . NewAccessor ( ) . UID ( i . Object )
if err != nil {
return err
}
if editObjUID == originalObjUID {
originalInfo = i
break
}
}
if originalInfo == nil {
return fmt . Errorf ( "no original object found for %#v" , info . Object )
}
2018-02-21 17:10:38 +00:00
originalJS , err := encodeToJson ( cmdutil . InternalVersionJSONEncoder ( ) , originalInfo . Object )
2017-04-18 12:44:25 +00:00
if err != nil {
return err
}
2018-02-21 17:10:38 +00:00
editedJS , err := encodeToJson ( cmdutil . InternalVersionJSONEncoder ( ) , info . Object )
2017-04-18 12:44:25 +00:00
if err != nil {
return err
}
if reflect . DeepEqual ( originalJS , editedJS ) {
// no edit, so just skip it.
2018-02-21 01:14:21 +00:00
cmdutil . PrintSuccess ( false , o . Out , info . Object , false , "skipped" )
2017-04-18 12:44:25 +00:00
return nil
}
preconditions := [ ] mergepatch . PreconditionFunc {
mergepatch . RequireKeyUnchanged ( "apiVersion" ) ,
mergepatch . RequireKeyUnchanged ( "kind" ) ,
mergepatch . RequireMetadataKeyUnchanged ( "name" ) ,
}
// Create the versioned struct from the type defined in the mapping
// (which is the API version we'll be submitting the patch to)
2017-10-28 01:31:42 +00:00
versionedObject , err := scheme . Scheme . New ( info . Mapping . GroupVersionKind )
2017-04-18 12:44:25 +00:00
var patchType types . PatchType
var patch [ ] byte
switch {
case runtime . IsNotRegisteredError ( err ) :
// fall back to generic JSON merge patch
patchType = types . MergePatchType
patch , err = jsonpatch . CreateMergePatch ( originalJS , editedJS )
if err != nil {
glog . V ( 4 ) . Infof ( "Unable to calculate diff, no merge is possible: %v" , err )
return err
}
for _ , precondition := range preconditions {
if ! precondition ( patch ) {
glog . V ( 4 ) . Infof ( "Unable to calculate diff, no merge is possible: %v" , err )
return fmt . Errorf ( "%s" , "At least one of apiVersion, kind and name was changed" )
}
}
case err != nil :
return err
default :
patchType = types . StrategicMergePatchType
patch , err = strategicpatch . CreateTwoWayMergePatch ( originalJS , editedJS , versionedObject , preconditions ... )
if err != nil {
glog . V ( 4 ) . Infof ( "Unable to calculate diff, no merge is possible: %v" , err )
if mergepatch . IsPreconditionFailed ( err ) {
return fmt . Errorf ( "%s" , "At least one of apiVersion, kind and name was changed" )
}
return err
}
}
2017-05-19 09:47:07 +00:00
if o . OutputPatch {
fmt . Fprintf ( o . Out , "Patch: %s\n" , string ( patch ) )
}
2017-04-18 12:44:25 +00:00
patched , err := resource . NewHelper ( info . Client , info . Mapping ) . Patch ( info . Namespace , info . Name , patchType , patch )
if err != nil {
2017-05-10 08:46:04 +00:00
fmt . Fprintln ( o . ErrOut , results . addError ( err , info ) )
2017-04-18 12:44:25 +00:00
return nil
}
info . Refresh ( patched , true )
2018-02-21 01:14:21 +00:00
cmdutil . PrintSuccess ( false , o . Out , info . Object , false , "edited" )
2017-04-18 12:44:25 +00:00
return nil
} )
return err
}
2017-05-10 08:46:04 +00:00
func ( o * EditOptions ) visitToCreate ( createVisitor resource . Visitor ) error {
2017-04-18 12:44:25 +00:00
err := createVisitor . Visit ( func ( info * resource . Info , incomingErr error ) error {
if err := resource . CreateAndRefresh ( info ) ; err != nil {
return err
}
2018-02-21 01:14:21 +00:00
cmdutil . PrintSuccess ( false , o . Out , info . Object , false , "created" )
2017-04-18 12:44:25 +00:00
return nil
} )
return err
}
2017-05-10 08:46:04 +00:00
func ( o * EditOptions ) visitAnnotation ( annotationVisitor resource . Visitor ) error {
2017-04-18 12:44:25 +00:00
// iterate through all items to apply annotations
err := annotationVisitor . Visit ( func ( info * resource . Info , incomingErr error ) error {
// put configuration annotation in "updates"
2017-05-10 08:46:04 +00:00
if o . ApplyAnnotation {
2018-02-21 17:10:38 +00:00
if err := kubectl . CreateOrUpdateAnnotation ( true , info , cmdutil . InternalVersionJSONEncoder ( ) ) ; err != nil {
2017-04-18 12:44:25 +00:00
return err
}
}
2017-05-10 08:46:04 +00:00
if o . Record || cmdutil . ContainsChangeCause ( info ) {
if err := cmdutil . RecordChangeCause ( info . Object , o . ChangeCause ) ; err != nil {
2017-04-18 12:44:25 +00:00
return err
}
}
return nil
} )
return err
}
type EditMode string
const (
NormalEditMode EditMode = "normal_mode"
EditBeforeCreateMode EditMode = "edit_before_create_mode"
2017-02-26 12:36:20 +00:00
ApplyEditMode EditMode = "edit_last_applied_mode"
2017-04-18 12:44:25 +00:00
)
// editReason preserves a message about the reason this file must be edited again
type editReason struct {
head string
other [ ] string
}
// editHeader includes a list of reasons the edit must be retried
type editHeader struct {
reasons [ ] editReason
}
// writeTo outputs the current header information into a stream
2017-02-26 12:36:20 +00:00
func ( h * editHeader ) writeTo ( w io . Writer , editMode EditMode ) error {
if editMode == ApplyEditMode {
fmt . Fprint ( w , ` # Please edit the ' last - applied - configuration ' annotations below .
# Lines beginning with a '#' will be ignored , and an empty file will abort the edit .
#
` )
} else {
fmt . Fprint ( w , ` # Please edit the object below . Lines beginning with a '#' will be ignored ,
2017-04-18 12:44:25 +00:00
# and an empty file will abort the edit . If an error occurs while saving this file will be
# reopened with the relevant failures .
#
` )
2017-02-26 12:36:20 +00:00
}
2017-04-18 12:44:25 +00:00
for _ , r := range h . reasons {
if len ( r . other ) > 0 {
2018-03-10 13:08:50 +00:00
fmt . Fprintf ( w , "# %s:\n" , hashOnLineBreak ( r . head ) )
2017-04-18 12:44:25 +00:00
} else {
2018-03-10 13:08:50 +00:00
fmt . Fprintf ( w , "# %s\n" , hashOnLineBreak ( r . head ) )
2017-04-18 12:44:25 +00:00
}
for _ , o := range r . other {
2018-03-10 13:08:50 +00:00
fmt . Fprintf ( w , "# * %s\n" , hashOnLineBreak ( o ) )
2017-04-18 12:44:25 +00:00
}
fmt . Fprintln ( w , "#" )
}
return nil
}
func ( h * editHeader ) flush ( ) {
h . reasons = [ ] editReason { }
}
// editResults capture the result of an update
type editResults struct {
header editHeader
retryable int
notfound int
edit [ ] * resource . Info
file string
version schema . GroupVersion
}
func ( r * editResults ) addError ( err error , info * resource . Info ) string {
switch {
2017-02-26 12:36:20 +00:00
case apierrors . IsInvalid ( err ) :
2017-04-18 12:44:25 +00:00
r . edit = append ( r . edit , info )
reason := editReason {
head : fmt . Sprintf ( "%s %q was not valid" , info . Mapping . Resource , info . Name ) ,
}
2017-02-26 12:36:20 +00:00
if err , ok := err . ( apierrors . APIStatus ) ; ok {
2017-04-18 12:44:25 +00:00
if details := err . Status ( ) . Details ; details != nil {
for _ , cause := range details . Causes {
reason . other = append ( reason . other , fmt . Sprintf ( "%s: %s" , cause . Field , cause . Message ) )
}
}
}
r . header . reasons = append ( r . header . reasons , reason )
return fmt . Sprintf ( "error: %s %q is invalid" , info . Mapping . Resource , info . Name )
2017-02-26 12:36:20 +00:00
case apierrors . IsNotFound ( err ) :
2017-04-18 12:44:25 +00:00
r . notfound ++
return fmt . Sprintf ( "error: %s %q could not be found on the server" , info . Mapping . Resource , info . Name )
default :
r . retryable ++
return fmt . Sprintf ( "error: %s %q could not be patched: %v" , info . Mapping . Resource , info . Name , err )
}
}
// preservedFile writes out a message about the provided file if it exists to the
// provided output stream when an error happens. Used to notify the user where
// their updates were preserved.
func preservedFile ( err error , path string , out io . Writer ) error {
if len ( path ) > 0 {
if _ , err := os . Stat ( path ) ; ! os . IsNotExist ( err ) {
fmt . Fprintf ( out , "A copy of your changes has been stored to %q\n" , path )
}
}
return err
}
// hasLines returns true if any line in the provided stream is non empty - has non-whitespace
// characters, or the first non-whitespace character is a '#' indicating a comment. Returns
// any errors encountered reading the stream.
func hasLines ( r io . Reader ) ( bool , error ) {
// TODO: if any files we read have > 64KB lines, we'll need to switch to bytes.ReadLine
// TODO: probably going to be secrets
s := bufio . NewScanner ( r )
for s . Scan ( ) {
if line := strings . TrimSpace ( s . Text ( ) ) ; len ( line ) > 0 && line [ 0 ] != '#' {
return true , nil
}
}
if err := s . Err ( ) ; err != nil && err != io . EOF {
return false , err
}
return false , nil
}
2018-03-10 13:08:50 +00:00
// hashOnLineBreak returns a string built from the provided string by inserting any necessary '#'
// characters after '\n' characters, indicating a comment.
func hashOnLineBreak ( s string ) string {
r := ""
for i , ch := range s {
j := i + 1
if j < len ( s ) && ch == '\n' && s [ j ] != '#' {
r += "\n# "
} else {
r += string ( ch )
}
}
return r
}