Repository: go-acme/lego
Pull request #2034 (2023-10-14 16:11:01)

liquidweb: add LWAPI_ prefix for env vars
Related to https://github.com/go-acme/lego/pull/2031#issuecomment-1762962188


Patch:
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
 }
 
+// GetOrDefaultString returns the given environment variable value as a string.
+// Returns the default if the env var cannot be found.
+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 {
+	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 {
-	v, err := strconv.Atoi(GetOrFile(envVar))
-	if err != nil {
-		return defaultValue
-	}
-
-	return v
+	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 {
-	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
+	return getOrDefault(envVar, defaultValue, ParseSecond)
 }
 
-// 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))
+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/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/"


Test patch:
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