diff --git a/platform/config/env/env.go b/platform/config/env/env.go index 9a1d3ce0..3fd1e3a1 100644 --- a/platform/config/env/env.go +++ b/platform/config/env/env.go @@ -78,15 +78,26 @@ func GetWithFallback(groups ...[]string) (map[string]string, error) { return values, nil } +func GetOneWithFallback[T any](main string, defaultValue T, fn func(string) (T, error), names ...string) T { + v, _ := getOneWithFallback(main, names...) + + value, err := fn(v) + if err != nil { + return defaultValue + } + + return value +} + func getOneWithFallback(main string, names ...string) (string, string) { value := GetOrFile(main) - if len(value) > 0 { + if value != "" { return value, main } for _, name := range names { value := GetOrFile(name) - if len(value) > 0 { + if value != "" { return value, main } } @@ -94,43 +105,32 @@ func getOneWithFallback(main string, names ...string) (string, string) { return "", main } -// GetOrDefaultInt returns the given environment variable value as an integer. -// Returns the default if the env var cannot be coopered to an int, or is not found. -func GetOrDefaultInt(envVar string, defaultValue int) int { - v, err := strconv.Atoi(GetOrFile(envVar)) - if err != nil { - return defaultValue - } - - return v -} - -// GetOrDefaultSecond returns the given environment variable value as a time.Duration (second). -// Returns the default if the env var cannot be coopered to an int, or is not found. -func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration { - v := GetOrDefaultInt(envVar, -1) - if v < 0 { - return defaultValue - } - - return time.Duration(v) * time.Second -} - // GetOrDefaultString returns the given environment variable value as a string. // Returns the default if the env var cannot be found. -func GetOrDefaultString(envVar, defaultValue string) string { - v := GetOrFile(envVar) - if v == "" { - return defaultValue - } - - return v +func GetOrDefaultString(envVar string, defaultValue string) string { + return getOrDefault(envVar, defaultValue, ParseString) } // GetOrDefaultBool returns the given environment variable value as a boolean. // Returns the default if the env var cannot be coopered to a boolean, or is not found. func GetOrDefaultBool(envVar string, defaultValue bool) bool { - v, err := strconv.ParseBool(GetOrFile(envVar)) + return getOrDefault(envVar, defaultValue, strconv.ParseBool) +} + +// GetOrDefaultInt returns the given environment variable value as an integer. +// Returns the default if the env var cannot be coopered to an int, or is not found. +func GetOrDefaultInt(envVar string, defaultValue int) int { + return getOrDefault(envVar, defaultValue, strconv.Atoi) +} + +// GetOrDefaultSecond returns the given environment variable value as a time.Duration (second). +// Returns the default if the env var cannot be coopered to an int, or is not found. +func GetOrDefaultSecond(envVar string, defaultValue time.Duration) time.Duration { + return getOrDefault(envVar, defaultValue, ParseSecond) +} + +func getOrDefault[T any](envVar string, defaultValue T, fn func(string) (T, error)) T { + v, err := fn(GetOrFile(envVar)) if err != nil { return defaultValue } @@ -161,3 +161,26 @@ func GetOrFile(envVar string) string { return strings.TrimSuffix(string(fileContents), "\n") } + +// ParseSecond parses env var value (string) to a second (time.Duration). +func ParseSecond(s string) (time.Duration, error) { + v, err := strconv.Atoi(s) + if err != nil { + return 0, err + } + + if v < 0 { + return 0, fmt.Errorf("unsupported value: %d", v) + } + + return time.Duration(v) * time.Second, nil +} + +// ParseString parses env var value (string) to a string but throws an error when the string is empty. +func ParseString(s string) (string, error) { + if s == "" { + return "", errors.New("empty string") + } + + return s, nil +} diff --git a/platform/config/env/env_test.go b/platform/config/env/env_test.go index be422f1d..dc022b3c 100644 --- a/platform/config/env/env_test.go +++ b/platform/config/env/env_test.go @@ -15,12 +15,12 @@ func TestGetWithFallback(t *testing.T) { var1Missing := os.Getenv("TEST_LEGO_VAR_MISSING_1") var2Missing := os.Getenv("TEST_LEGO_VAR_MISSING_2") - defer func() { + t.Cleanup(func() { _ = os.Setenv("TEST_LEGO_VAR_EXIST_1", var1Exist) _ = os.Setenv("TEST_LEGO_VAR_EXIST_2", var2Exist) _ = os.Setenv("TEST_LEGO_VAR_MISSING_1", var1Missing) _ = os.Setenv("TEST_LEGO_VAR_MISSING_2", var2Missing) - }() + }) err := os.Setenv("TEST_LEGO_VAR_EXIST_1", "VAR1") require.NoError(t, err) @@ -93,7 +93,10 @@ func TestGetWithFallback(t *testing.T) { } for _, test := range testCases { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + value, err := GetWithFallback(test.groups...) if len(test.expected.error) > 0 { assert.EqualError(t, err, test.expected.error) @@ -105,6 +108,74 @@ func TestGetWithFallback(t *testing.T) { } } +func TestGetOneWithFallback(t *testing.T) { + var1Exist := os.Getenv("TEST_LEGO_VAR_EXIST_1") + var2Exist := os.Getenv("TEST_LEGO_VAR_EXIST_2") + var1Missing := os.Getenv("TEST_LEGO_VAR_MISSING_1") + var2Missing := os.Getenv("TEST_LEGO_VAR_MISSING_2") + + t.Cleanup(func() { + _ = os.Setenv("TEST_LEGO_VAR_EXIST_1", var1Exist) + _ = os.Setenv("TEST_LEGO_VAR_EXIST_2", var2Exist) + _ = os.Setenv("TEST_LEGO_VAR_MISSING_1", var1Missing) + _ = os.Setenv("TEST_LEGO_VAR_MISSING_2", var2Missing) + }) + + err := os.Setenv("TEST_LEGO_VAR_EXIST_1", "VAR1") + require.NoError(t, err) + err = os.Setenv("TEST_LEGO_VAR_EXIST_2", "VAR2") + require.NoError(t, err) + err = os.Unsetenv("TEST_LEGO_VAR_MISSING_1") + require.NoError(t, err) + err = os.Unsetenv("TEST_LEGO_VAR_MISSING_2") + require.NoError(t, err) + + testCases := []struct { + desc string + main string + defaultValue string + alts []string + expected string + }{ + { + desc: "with value and no alternative", + main: "TEST_LEGO_VAR_EXIST_1", + defaultValue: "oops", + expected: "VAR1", + }, + { + desc: "with value and alternatives", + main: "TEST_LEGO_VAR_EXIST_1", + defaultValue: "oops", + alts: []string{"TEST_LEGO_VAR_MISSING_1"}, + expected: "VAR1", + }, + { + desc: "without value and no alternatives", + main: "TEST_LEGO_VAR_MISSING_1", + defaultValue: "oops", + expected: "oops", + }, + { + desc: "without value and alternatives", + main: "TEST_LEGO_VAR_MISSING_1", + defaultValue: "oops", + alts: []string{"TEST_LEGO_VAR_EXIST_1"}, + expected: "VAR1", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + value := GetOneWithFallback(test.main, test.defaultValue, ParseString, test.alts...) + assert.Equal(t, test.expected, value) + }) + } +} + func TestGetOrDefaultInt(t *testing.T) { testCases := []struct { desc string diff --git a/providers/dns/liquidweb/liquidweb.go b/providers/dns/liquidweb/liquidweb.go index dc8fa857..c7fd9eeb 100644 --- a/providers/dns/liquidweb/liquidweb.go +++ b/providers/dns/liquidweb/liquidweb.go @@ -20,7 +20,8 @@ const defaultBaseURL = "https://api.liquidweb.com" // Environment variables names. const ( - envNamespace = "LIQUID_WEB_" + envNamespace = "LIQUID_WEB_" + altEnvNamespace = "LWAPI_" EnvURL = envNamespace + "URL" EnvUsername = envNamespace + "USERNAME" @@ -49,10 +50,10 @@ type Config struct { func NewDefaultConfig() *Config { return &Config{ BaseURL: defaultBaseURL, - TTL: env.GetOrDefaultInt(EnvTTL, 300), - PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), - PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 2*time.Second), - HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 1*time.Minute), + TTL: env.GetOneWithFallback(EnvTTL, 300, strconv.Atoi, altEnvName(EnvTTL)), + PropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, 2*time.Minute, env.ParseSecond, altEnvName(EnvPropagationTimeout)), + PollingInterval: env.GetOneWithFallback(EnvPollingInterval, 2*time.Second, env.ParseSecond, altEnvName(EnvPollingInterval)), + HTTPTimeout: env.GetOneWithFallback(EnvHTTPTimeout, 1*time.Minute, env.ParseSecond, altEnvName(EnvHTTPTimeout)), } } @@ -66,16 +67,19 @@ type DNSProvider struct { // NewDNSProvider returns a DNSProvider instance configured for Liquid Web. func NewDNSProvider() (*DNSProvider, error) { - values, err := env.Get(EnvUsername, EnvPassword) + values, err := env.GetWithFallback( + []string{EnvUsername, altEnvName(EnvUsername)}, + []string{EnvPassword, altEnvName(EnvPassword)}, + ) if err != nil { return nil, fmt.Errorf("liquidweb: %w", err) } config := NewDefaultConfig() - config.BaseURL = env.GetOrFile(EnvURL) + config.BaseURL = env.GetOneWithFallback(EnvURL, defaultBaseURL, env.ParseString, altEnvName(EnvURL)) config.Username = values[EnvUsername] config.Password = values[EnvPassword] - config.Zone = env.GetOrDefaultString(EnvZone, "") + config.Zone = env.GetOneWithFallback(EnvZone, "", env.ParseString, altEnvName(EnvZone)) return NewDNSProviderConfig(config) } @@ -191,3 +195,7 @@ func (d *DNSProvider) findZone(domain string) (string, error) { return zs[0].Name, nil } + +func altEnvName(v string) string { + return strings.ReplaceAll(v, envNamespace, altEnvNamespace) +} diff --git a/providers/dns/liquidweb/liquidweb.toml b/providers/dns/liquidweb/liquidweb.toml index 3fc53b8e..c9116912 100644 --- a/providers/dns/liquidweb/liquidweb.toml +++ b/providers/dns/liquidweb/liquidweb.toml @@ -5,22 +5,22 @@ Code = "liquidweb" Since = "v3.1.0" Example = ''' -LIQUID_WEB_USERNAME=someuser \ -LIQUID_WEB_PASSWORD=somepass \ +LWAPI_USERNAME=someuser \ +LWAPI_PASSWORD=somepass \ lego --email you@example.com --dns liquidweb --domains my.example.org run ''' [Configuration] [Configuration.Credentials] - LIQUID_WEB_USERNAME = "Liquid Web API Username" - LIQUID_WEB_PASSWORD = "Liquid Web API Password" + LWAPI_USERNAME = "Liquid Web API Username" + LWAPI_PASSWORD = "Liquid Web API Password" [Configuration.Additional] - LIQUID_WEB_ZONE = "DNS Zone" - LIQUID_WEB_URL = "Liquid Web API endpoint" - LIQUID_WEB_TTL = "The TTL of the TXT record used for the DNS challenge" - LIQUID_WEB_POLLING_INTERVAL = "Time between DNS propagation check" - LIQUID_WEB_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" - LIQUID_WEB_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)" + LWAPI_ZONE = "DNS Zone" + LWAPI_URL = "Liquid Web API endpoint" + LWAPI_TTL = "The TTL of the TXT record used for the DNS challenge" + LWAPI_POLLING_INTERVAL = "Time between DNS propagation check" + LWAPI_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + LWAPI_HTTP_TIMEOUT = "Maximum waiting time for the DNS records to be created (not verified)" [Links] API = "https://api.liquidweb.com/docs/"