env.go raw

   1  package env
   2  
   3  import (
   4  	"errors"
   5  	"fmt"
   6  	"os"
   7  	"strconv"
   8  	"strings"
   9  	"time"
  10  
  11  	"github.com/go-acme/lego/v4/log"
  12  )
  13  
  14  // Get environment variables.
  15  func Get(names ...string) (map[string]string, error) {
  16  	values := map[string]string{}
  17  
  18  	var missingEnvVars []string
  19  
  20  	for _, envVar := range names {
  21  		value := GetOrFile(envVar)
  22  		if value == "" {
  23  			missingEnvVars = append(missingEnvVars, envVar)
  24  		}
  25  
  26  		values[envVar] = value
  27  	}
  28  
  29  	if len(missingEnvVars) > 0 {
  30  		return nil, fmt.Errorf("some credentials information are missing: %s", strings.Join(missingEnvVars, ","))
  31  	}
  32  
  33  	return values, nil
  34  }
  35  
  36  // GetWithFallback Get environment variable values.
  37  // The first name in each group is use as key in the result map.
  38  //
  39  // case 1:
  40  //
  41  //	// LEGO_ONE="ONE"
  42  //	// LEGO_TWO="TWO"
  43  //	env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
  44  //	// => "LEGO_ONE" = "ONE"
  45  //
  46  // case 2:
  47  //
  48  //	// LEGO_ONE=""
  49  //	// LEGO_TWO="TWO"
  50  //	env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
  51  //	// => "LEGO_ONE" = "TWO"
  52  //
  53  // case 3:
  54  //
  55  //	// LEGO_ONE=""
  56  //	// LEGO_TWO=""
  57  //	env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
  58  //	// => error
  59  func GetWithFallback(groups ...[]string) (map[string]string, error) {
  60  	values := map[string]string{}
  61  
  62  	var missingEnvVars []string
  63  
  64  	for _, names := range groups {
  65  		if len(names) == 0 {
  66  			return nil, errors.New("undefined environment variable names")
  67  		}
  68  
  69  		value, envVar := getOneWithFallback(names[0], names[1:]...)
  70  		if value == "" {
  71  			missingEnvVars = append(missingEnvVars, envVar)
  72  			continue
  73  		}
  74  
  75  		values[envVar] = value
  76  	}
  77  
  78  	if len(missingEnvVars) > 0 {
  79  		return nil, fmt.Errorf("some credentials information are missing: %s", strings.Join(missingEnvVars, ","))
  80  	}
  81  
  82  	return values, nil
  83  }
  84  
  85  func GetOneWithFallback[T any](main string, defaultValue T, fn func(string) (T, error), names ...string) T {
  86  	v, _ := getOneWithFallback(main, names...)
  87  
  88  	value, err := fn(v)
  89  	if err != nil {
  90  		return defaultValue
  91  	}
  92  
  93  	return value
  94  }
  95  
  96  func getOneWithFallback(main string, names ...string) (string, string) {
  97  	value := GetOrFile(main)
  98  	if value != "" {
  99  		return value, main
 100  	}
 101  
 102  	for _, name := range names {
 103  		value := GetOrFile(name)
 104  		if value != "" {
 105  			return value, main
 106  		}
 107  	}
 108  
 109  	return "", main
 110  }
 111  
 112  // GetOrDefaultString returns the given environment variable value as a string.
 113  // Returns the default if the env var cannot be found.
 114  func GetOrDefaultString(envVar, defaultValue string) string {
 115  	return getOrDefault(envVar, defaultValue, ParseString)
 116  }
 117  
 118  // GetOrDefaultBool returns the given environment variable value as a boolean.
 119  // Returns the default if the env var cannot be coopered to a boolean, or is not found.
 120  func GetOrDefaultBool(envVar string, defaultValue bool) bool {
 121  	return getOrDefault(envVar, defaultValue, strconv.ParseBool)
 122  }
 123  
 124  // GetOrDefaultInt returns the given environment variable value as an integer.
 125  // Returns the default if the env var cannot be coopered to an int, or is not found.
 126  func GetOrDefaultInt(envVar string, defaultValue int) int {
 127  	return getOrDefault(envVar, defaultValue, strconv.Atoi)
 128  }
 129  
 130  // GetOrDefaultSecond returns the given environment variable value as a time.Duration (second).
 131  // Returns the default if the env var cannot be coopered to an int, or is not found.
 132  func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration {
 133  	return getOrDefault(envVar, defaultValue, ParseSecond)
 134  }
 135  
 136  func getOrDefault[T any](envVar string, defaultValue T, fn func(string) (T, error)) T {
 137  	v, err := fn(GetOrFile(envVar))
 138  	if err != nil {
 139  		return defaultValue
 140  	}
 141  
 142  	return v
 143  }
 144  
 145  // GetOrFile Attempts to resolve 'key' as an environment variable.
 146  // Failing that, it will check to see if '<key>_FILE' exists.
 147  // If so, it will attempt to read from the referenced file to populate a value.
 148  func GetOrFile(envVar string) string {
 149  	envVarValue := os.Getenv(envVar)
 150  	if envVarValue != "" {
 151  		return envVarValue
 152  	}
 153  
 154  	fileVar := envVar + "_FILE"
 155  
 156  	fileVarValue := os.Getenv(fileVar)
 157  	if fileVarValue == "" {
 158  		return envVarValue
 159  	}
 160  
 161  	fileContents, err := os.ReadFile(fileVarValue)
 162  	if err != nil {
 163  		log.Printf("Failed to read the file %s (defined by env var %s): %s", fileVarValue, fileVar, err)
 164  		return ""
 165  	}
 166  
 167  	return strings.TrimSuffix(string(fileContents), "\n")
 168  }
 169  
 170  // ParseSecond parses env var value (string) to a second (time.Duration).
 171  func ParseSecond(s string) (time.Duration, error) {
 172  	v, err := strconv.Atoi(s)
 173  	if err != nil {
 174  		return 0, err
 175  	}
 176  
 177  	if v < 0 {
 178  		return 0, fmt.Errorf("unsupported value: %d", v)
 179  	}
 180  
 181  	return time.Duration(v) * time.Second, nil
 182  }
 183  
 184  // ParseString parses env var value (string) to a string but throws an error when the string is empty.
 185  func ParseString(s string) (string, error) {
 186  	if s == "" {
 187  		return "", errors.New("empty string")
 188  	}
 189  
 190  	return s, nil
 191  }
 192  
 193  // ParsePairs parses a raw string of comma-separated key-value pairs into a map.
 194  // Keys and values are separated by a colon and are trimmed of whitespace.
 195  func ParsePairs(raw string) (map[string]string, error) {
 196  	result := make(map[string]string)
 197  
 198  	for pair := range strings.SplitSeq(strings.TrimSuffix(raw, ","), ",") {
 199  		data := strings.Split(pair, ":")
 200  		if len(data) != 2 {
 201  			return nil, fmt.Errorf("incorrect pair: %s", pair)
 202  		}
 203  
 204  		result[strings.TrimSpace(data[0])] = strings.TrimSpace(data[1])
 205  	}
 206  
 207  	return result, nil
 208  }
 209