github.com/drycc/workflow-cli@v1.5.3-0.20240322092846-d4ee25983af9/cmd/config.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "fmt" 7 "os" 8 "regexp" 9 "strings" 10 11 "github.com/drycc/controller-sdk-go/api" 12 "github.com/drycc/controller-sdk-go/config" 13 ) 14 15 // ConfigList lists an app's config. 16 func (d *DryccCmd) ConfigList(appID string, format string) error { 17 s, appID, err := load(d.ConfigFile, appID) 18 if err != nil { 19 return err 20 } 21 config, err := config.List(s.Client, appID) 22 if d.checkAPICompatibility(s.Client, err) != nil { 23 return err 24 } 25 26 keys := *sortKeys(config.Values) 27 switch format { 28 case "oneline": 29 var kv []string 30 for _, key := range keys { 31 kv = append(kv, fmt.Sprintf("%s=%s", key, config.Values[key])) 32 } 33 d.Println(strings.Join(kv, " ")) 34 case "diff": 35 for _, key := range keys { 36 d.Println(fmt.Sprintf("%s=%s", key, config.Values[key])) 37 } 38 default: 39 table := d.getDefaultFormatTable([]string{"UUID", "OWNER", "NAME", "VALUE"}) 40 for _, key := range keys { 41 table.Append([]string{ 42 config.UUID, 43 config.Owner, 44 key, 45 fmt.Sprintf("%v", config.Values[key]), 46 }) 47 } 48 table.Render() 49 } 50 return nil 51 } 52 53 // ConfigSet sets an app's config variables. 54 func (d *DryccCmd) ConfigSet(appID string, configVars []string) error { 55 s, appID, err := load(d.ConfigFile, appID) 56 57 if err != nil { 58 return err 59 } 60 61 configMap, err := parseConfig(configVars) 62 if err != nil { 63 return err 64 } 65 66 if value, ok := configMap["SSH_KEY"]; ok { 67 sshKey, err := parseSSHKey(value.(string)) 68 if err != nil { 69 return err 70 } 71 configMap["SSH_KEY"] = base64.StdEncoding.EncodeToString([]byte(sshKey)) 72 } 73 74 // NOTE(bacongobbler): check if the user is using the old way to set healthchecks. If so, 75 // send them a deprecation notice. 76 for key := range configMap { 77 if strings.Contains(key, "HEALTHCHECK_") { 78 d.Println(`Hey there! We've noticed that you're using 'drycc config:set HEALTHCHECK_URL' 79 to set up healthchecks. This functionality has been deprecated. In the future, please use 80 'drycc healthchecks' to set up application health checks. Thanks!`) 81 } 82 } 83 84 d.Print("Creating config... ") 85 86 quit := progress(d.WOut) 87 configObj := api.Config{Values: configMap} 88 configObj, err = config.Set(s.Client, appID, configObj) 89 quit <- true 90 <-quit 91 if d.checkAPICompatibility(s.Client, err) != nil { 92 return err 93 } 94 95 if release, ok := configObj.Values["WORKFLOW_RELEASE"]; ok { 96 d.Printf("done, %s\n\n", release) 97 } else { 98 d.Print("done\n\n") 99 } 100 101 return d.ConfigList(appID, "") 102 } 103 104 // ConfigUnset removes a config variable from an app. 105 func (d *DryccCmd) ConfigUnset(appID string, configVars []string) error { 106 s, appID, err := load(d.ConfigFile, appID) 107 108 if err != nil { 109 return err 110 } 111 112 d.Print("Removing config... ") 113 114 quit := progress(d.WOut) 115 116 configObj := api.Config{} 117 118 valuesMap := make(map[string]interface{}) 119 120 for _, configVar := range configVars { 121 valuesMap[configVar] = nil 122 } 123 124 configObj.Values = valuesMap 125 126 _, err = config.Set(s.Client, appID, configObj) 127 quit <- true 128 <-quit 129 if d.checkAPICompatibility(s.Client, err) != nil { 130 return err 131 } 132 133 d.Print("done\n\n") 134 135 return d.ConfigList(appID, "") 136 } 137 138 // ConfigPull pulls an app's config to a file. 139 func (d *DryccCmd) ConfigPull(appID string, interactive bool, overwrite bool) error { 140 s, appID, err := load(d.ConfigFile, appID) 141 142 if err != nil { 143 return err 144 } 145 146 configVars, err := config.List(s.Client, appID) 147 if d.checkAPICompatibility(s.Client, err) != nil { 148 return err 149 } 150 151 stat, err := os.Stdout.Stat() 152 153 if err != nil { 154 return err 155 } 156 157 if (stat.Mode() & os.ModeCharDevice) == 0 { 158 d.Print(formatConfig(configVars.Values)) 159 return nil 160 } 161 162 filename := ".env" 163 164 if !overwrite { 165 if _, err := os.Stat(filename); err == nil { 166 return fmt.Errorf("%s already exists, pass -o to overwrite", filename) 167 } 168 } 169 170 if interactive { 171 contents, err := os.ReadFile(filename) 172 173 if err != nil { 174 return err 175 } 176 localConfigVars := strings.Split(string(contents), "\n") 177 178 configMap, err := parseConfig(localConfigVars[:len(localConfigVars)-1]) 179 if err != nil { 180 return err 181 } 182 183 for key, value := range configVars.Values { 184 localValue, ok := configMap[key] 185 186 if ok { 187 if value != localValue { 188 var confirm string 189 d.Printf("%s: overwrite %s with %s? (y/N) ", key, localValue, value) 190 191 fmt.Scanln(&confirm) 192 193 if strings.ToLower(confirm) == "y" { 194 configMap[key] = value 195 } 196 } 197 } else { 198 configMap[key] = value 199 } 200 } 201 202 return os.WriteFile(filename, []byte(formatConfig(configMap)), 0755) 203 } 204 205 return os.WriteFile(filename, []byte(formatConfig(configVars.Values)), 0755) 206 } 207 208 // ConfigPush pushes an app's config from a file. 209 func (d *DryccCmd) ConfigPush(appID, fileName string) error { 210 stat, err := os.Stdin.Stat() 211 212 if err != nil { 213 return err 214 } 215 216 var contents []byte 217 218 if (stat.Mode() & os.ModeCharDevice) == 0 { 219 buffer := new(bytes.Buffer) 220 buffer.ReadFrom(os.Stdin) 221 contents = buffer.Bytes() 222 } else { 223 contents, err = os.ReadFile(fileName) 224 225 if err != nil { 226 return err 227 } 228 } 229 230 file := strings.Split(string(contents), "\n") 231 config := []string{} 232 233 for _, configVar := range file { 234 // If file has CRLF encoding, the default on windows, strip the CR 235 configVar = strings.Trim(configVar, "\r") 236 if len(configVar) > 0 { 237 config = append(config, configVar) 238 } 239 } 240 241 return d.ConfigSet(appID, config) 242 } 243 244 func parseConfig(configVars []string) (map[string]interface{}, error) { 245 configMap := make(map[string]interface{}) 246 247 regex := regexp.MustCompile(`^([A-z_]+[A-z0-9_]*)=([\s\S]*)$`) 248 for _, config := range configVars { 249 // Skip config that starts with an comment 250 if config[0] == '#' { 251 continue 252 } 253 254 if regex.MatchString(config) { 255 captures := regex.FindStringSubmatch(config) 256 configMap[captures[1]] = captures[2] 257 } else { 258 return nil, fmt.Errorf("'%s' does not match the pattern 'key=var', ex: MODE=test", config) 259 } 260 } 261 262 return configMap, nil 263 } 264 265 func parseSSHKey(value string) (string, error) { 266 sshRegex := regexp.MustCompile("^-----BEGIN (DSA|RSA|EC|OPENSSH) PRIVATE KEY-----") 267 268 if sshRegex.MatchString(value) { 269 return value, nil 270 } 271 272 // NOTE(felixbuenemann): check if the current value is already a base64 encoded key. 273 // This is the case if it was fetched using "drycc config:pull". 274 contents, err := base64.StdEncoding.DecodeString(value) 275 276 if err == nil && sshRegex.MatchString(string(contents)) { 277 return string(contents), nil 278 } 279 280 // NOTE(felixbuenemann): check if the value is a path to a private key. 281 if _, err := os.Stat(value); err == nil { 282 contents, err := os.ReadFile(value) 283 284 if err != nil { 285 return "", err 286 } 287 288 if sshRegex.MatchString(string(contents)) { 289 return string(contents), nil 290 } 291 } 292 293 return "", fmt.Errorf("could not parse SSH private key:\n %s", value) 294 } 295 296 func formatConfig(configVars map[string]interface{}) string { 297 var formattedConfig string 298 299 keys := *sortKeys(configVars) 300 for _, key := range keys { 301 formattedConfig += fmt.Sprintf("%s=%v\n", key, configVars[key]) 302 } 303 304 return formattedConfig 305 }