github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/cmd/orbctl/patch.go (about) 1 // Inspired by https://samrapdev.com/capturing-sensitive-input-with-editor-in-golang-from-the-cli/ 2 3 package main 4 5 import ( 6 "errors" 7 "fmt" 8 "strconv" 9 "strings" 10 11 "github.com/AlecAivazis/survey/v2" 12 "github.com/spf13/cobra" 13 "gopkg.in/yaml.v3" 14 15 "github.com/caos/orbos/internal/operator/common" 16 "github.com/caos/orbos/pkg/git" 17 ) 18 19 func PatchCommand(getRv GetRootValues) *cobra.Command { 20 21 cmd := &cobra.Command{ 22 Use: "patch <filepath> [yamlpath]", 23 Short: "Patch a yaml property", 24 Args: cobra.MinimumNArgs(1), 25 Example: `Overwiting a file: orbctl file patch orbiter.yml --exact 26 Patching an edge property interactively: orbctl file patch orbiter.yml 27 Patching a node property non-interactively: orbctl file path orbiter.yml clusters.k8s --exact --file /path/to/my/cluster/definition.yml`, 28 } 29 flags := cmd.Flags() 30 var ( 31 value string 32 file string 33 stdin bool 34 exact bool 35 ) 36 flags.StringVar(&value, "value", "", "Content value") 37 flags.StringVar(&file, "file", "", "File containing the content value") 38 flags.BoolVar(&stdin, "stdin", false, "Read content value by stdin") 39 flags.BoolVar(&exact, "exact", false, "Write the content exactly at the path given without further prompting") 40 41 cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { 42 43 var path []string 44 if len(args) > 1 { 45 path = strings.Split(args[1], ".") 46 } 47 48 filePath := args[0] 49 50 rv := getRv("patch", "", map[string]interface{}{"value": value, "filePath": filePath, "valuePath": file, "stdin": stdin, "exact": exact}) 51 defer rv.ErrFunc(err) 52 53 if !rv.Gitops { 54 return errors.New("patch command is only supported with the --gitops flag") 55 } 56 57 if err := initRepo(rv.OrbConfig, rv.GitClient); err != nil { 58 return err 59 } 60 61 contentStr, err := content(value, file, stdin) 62 if err != nil { 63 return err 64 } 65 66 contentYaml := yamlTypedValue(contentStr) 67 68 var result interface{} 69 if len(path) == 0 && exact { 70 result = contentYaml 71 } else { 72 structure := map[string]interface{}{} 73 if err := yaml.Unmarshal(rv.GitClient.Read(filePath), structure); err != nil { 74 return err 75 } 76 if err := updateMap(structure, path, contentYaml, exact); err != nil { 77 return err 78 } 79 result = structure 80 } 81 82 return rv.GitClient.UpdateRemote(fmt.Sprintf("Overwrite %s", filePath), func() []git.File { 83 return []git.File{{ 84 Path: filePath, 85 Content: common.MarshalYAML(result), 86 }} 87 }) 88 } 89 90 return cmd 91 } 92 93 func updateMap(structure map[string]interface{}, path []string, value interface{}, exact bool) error { 94 95 if len(path) == 1 && exact { 96 structure[path[0]] = value 97 return nil 98 } 99 100 path, nextStep := drillPath(path) 101 102 if nextStep == "" { 103 104 var keys []string 105 for key := range structure { 106 keys = append(keys, key) 107 } 108 109 var err error 110 nextStep, err = prompt(keys) 111 if err != nil { 112 return err 113 } 114 } 115 116 child, ok := structure[nextStep] 117 if !ok { 118 return fmt.Errorf("path element %s not found", nextStep) 119 } 120 121 drilled, err := drillContent(child, path, value, exact) 122 if err != nil { 123 return err 124 } 125 if !drilled { 126 structure[nextStep] = value 127 } 128 129 return nil 130 } 131 132 func prompt(keys []string) (string, error) { 133 var key string 134 return key, survey.AskOne(&survey.Select{ 135 Message: "Select key:", 136 Options: keys, 137 }, &key, survey.WithValidator(survey.Required)) 138 } 139 140 func updateSlice(slice []interface{}, path []string, value interface{}, exact bool) error { 141 142 pos := func(pathNode string) (int, error) { 143 idx, err := strconv.Atoi(pathNode) 144 if err != nil { 145 return -1, err 146 } 147 148 length := len(slice) 149 if length < idx { 150 return -1, fmt.Errorf("property has only %d elements", length) 151 } 152 return idx, nil 153 } 154 155 if len(path) == 1 && exact { 156 idx, err := pos(path[0]) 157 if err != nil { 158 return err 159 } 160 161 slice[idx] = value 162 return nil 163 } 164 165 path, nextStep := drillPath(path) 166 167 if nextStep == "" { 168 var keys []string 169 for key := range slice { 170 keys = append(keys, strconv.Itoa(key)) 171 } 172 173 var err error 174 nextStep, err = prompt(keys) 175 if err != nil { 176 return err 177 } 178 } 179 180 idx, err := pos(nextStep) 181 182 drilled, err := drillContent(slice[idx], path, value, exact) 183 if err != nil { 184 return err 185 } 186 if !drilled { 187 slice[idx] = value 188 } 189 190 return nil 191 } 192 193 func drillPath(path []string) ([]string, string) { 194 var next string 195 196 if len(path) > 0 { 197 next = path[0] 198 path = path[1:] 199 } 200 return path, next 201 } 202 203 func drillContent(child interface{}, path []string, value interface{}, exact bool) (bool, error) { 204 205 switch typedNext := child.(type) { 206 case map[string]interface{}: 207 return true, updateMap(typedNext, path, value, exact) 208 case []interface{}: 209 return true, updateSlice(typedNext, path, value, exact) 210 } 211 212 if len(path) > 0 { 213 return false, fmt.Errorf("invalid path %s", strings.Join(path, ".")) 214 } 215 216 return false, nil 217 } 218 219 func yamlTypedValue(value string) interface{} { 220 221 var out interface{} 222 if err := yaml.Unmarshal([]byte(value), &out); err != nil { 223 panic(err) 224 } 225 return out 226 }