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