github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/edit/custom_edit.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package edit 21 22 import ( 23 "bufio" 24 "bytes" 25 "fmt" 26 "io" 27 "os" 28 "path/filepath" 29 "strings" 30 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/runtime" 33 "k8s.io/apimachinery/pkg/util/yaml" 34 "k8s.io/cli-runtime/pkg/genericclioptions" 35 "k8s.io/cli-runtime/pkg/genericiooptions" 36 cmdutil "k8s.io/kubectl/pkg/cmd/util" 37 "k8s.io/kubectl/pkg/cmd/util/editor" 38 39 "github.com/1aal/kubeblocks/pkg/cli/printer" 40 "github.com/1aal/kubeblocks/pkg/cli/util" 41 "github.com/1aal/kubeblocks/pkg/cli/util/prompt" 42 ) 43 44 // CustomEditOptions is used to edit the resource manifest when creating or updating the resource, 45 // instead of using -o yaml to output the yaml file before editing the manifest. 46 type CustomEditOptions struct { 47 Factory cmdutil.Factory 48 PrintFlags *genericclioptions.PrintFlags 49 Method string 50 TestEnv bool 51 52 genericiooptions.IOStreams 53 } 54 55 func NewCustomEditOptions(f cmdutil.Factory, streams genericiooptions.IOStreams, method string) *CustomEditOptions { 56 return &CustomEditOptions{ 57 Factory: f, 58 PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput("yaml"), 59 IOStreams: streams, 60 Method: method, 61 TestEnv: false, 62 } 63 } 64 65 func (o *CustomEditOptions) Run(originalObj runtime.Object) error { 66 buf := &bytes.Buffer{} 67 var ( 68 original []byte 69 edited []byte 70 tmpFile string 71 w io.Writer = buf 72 ) 73 editPrinter, err := o.PrintFlags.ToPrinter() 74 if err != nil { 75 return fmt.Errorf("failed to create printer: %v", err) 76 } 77 if err := editPrinter.PrintObj(originalObj, w); err != nil { 78 return fmt.Errorf("failed to print object: %v", err) 79 } 80 original = buf.Bytes() 81 82 if !o.TestEnv { 83 edited, tmpFile, err = editObject(original) 84 if err != nil { 85 return fmt.Errorf("failed to lanch editor: %v", err) 86 } 87 } else { 88 edited = original 89 } 90 91 // apply validation 92 schemaValidator, err := o.Factory.Validator(metav1.FieldValidationStrict) 93 if err != nil { 94 return fmt.Errorf("failed to get validator: %v", err) 95 } 96 err = schemaValidator.ValidateBytes(cmdutil.StripComments(edited)) 97 if err != nil { 98 return fmt.Errorf("the edited file failed validation: %v", err) 99 } 100 101 // Compare content without comments 102 if bytes.Equal(cmdutil.StripComments(original), cmdutil.StripComments(edited)) { 103 os.Remove(tmpFile) 104 _, err = fmt.Fprintln(o.ErrOut, "Edit cancelled, no changes made.") 105 if err != nil { 106 return fmt.Errorf("error writing to stderr: %v", err) 107 } 108 return nil 109 } 110 111 // Returns an error if comments are included. 112 lines, err := hasComment(bytes.NewBuffer(edited)) 113 if err != nil { 114 return fmt.Errorf("error checking for comment: %v", err) 115 } 116 if !lines { 117 os.Remove(tmpFile) 118 _, err = fmt.Fprintln(o.ErrOut, "Edit cancelled, saved file was empty.") 119 if err != nil { 120 return fmt.Errorf("error writing to stderr: %v", err) 121 } 122 } 123 124 decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(edited), len(edited)) 125 if err := decoder.Decode(originalObj); err != nil { 126 return fmt.Errorf("failed to decode edited object: %v", err) 127 } 128 129 if o.Method == "patched" { 130 diff, err := util.GetUnifiedDiffString(string(original), string(edited), "Original", "Current", 3) 131 if err != nil { 132 return fmt.Errorf("failed to get diff: %v", err) 133 } 134 util.DisplayDiffWithColor(o.IOStreams.Out, diff) 135 } else if o.Method == "create" { 136 err := editPrinter.PrintObj(originalObj, o.IOStreams.Out) 137 if err != nil { 138 return fmt.Errorf("failed to print object: %v", err) 139 } 140 } 141 return confirmToContinue(o.IOStreams) 142 } 143 144 func editObject(original []byte) ([]byte, string, error) { 145 err := addHeader(bytes.NewBuffer(original)) 146 if err != nil { 147 return nil, "", err 148 } 149 150 edit := editor.NewDefaultEditor([]string{ 151 "KUBE_EDITOR", 152 "EDITOR", 153 }) 154 // launch the editor 155 edited, tmpFile, err := edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), ".yaml", bytes.NewBuffer(original)) 156 if err != nil { 157 return nil, "", err 158 } 159 160 return edited, tmpFile, nil 161 } 162 163 // HasComment returns true if any line in the provided stream is non empty - has non-whitespace 164 // characters, or the first non-whitespace character is a '#' indicating a comment. Returns 165 // any errors encountered reading the stream. 166 func hasComment(r io.Reader) (bool, error) { 167 s := bufio.NewScanner(r) 168 for s.Scan() { 169 if line := strings.TrimSpace(s.Text()); len(line) > 0 && line[0] != '#' { 170 return true, nil 171 } 172 } 173 if err := s.Err(); err != nil && err != io.EOF { 174 return false, err 175 } 176 return false, nil 177 } 178 179 // AddHeader adds a header to the provided writer 180 func addHeader(w io.Writer) error { 181 _, err := fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored, 182 # and an empty file will abort the edit. If an error occurs while saving this file will be 183 # reopened with the relevant failures. 184 # 185 `) 186 return err 187 } 188 189 func confirmToContinue(stream genericiooptions.IOStreams) error { 190 printer.Warning(stream.Out, "Above resource will be created or changed, do you want to continue to create or change this resource?\n Only 'yes' will be accepted to confirm.\n\n") 191 entered, _ := prompt.NewPrompt("Enter a value:", nil, stream.In).Run() 192 if entered != "yes" { 193 _, err := fmt.Fprintf(stream.Out, "\nCancel resource creation.\n") 194 if err != nil { 195 return err 196 } 197 return cmdutil.ErrExit 198 } 199 return nil 200 }