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  }