Repository: xenolf/lego (go)

Issue:
Allow CF_API_KEY and CF_API_EMAIL as alternative environment variables for Cloudflare
From https://github.com/caddyserver/dnsproviders/issues/22

The cloudflare tools use those environment variable names. If lego can switch, or read from both, then that makes compatibility with those tools easier. :)

https://github.com/cloudflare/python-cloudflare
https://github.com/cloudflare/cloudflare-go/tree/master/cmd/flarectl

Pull request description:
Cloudflare: support for CF_API_KEY and CF_API_EMAIL
Fixes #492 

Patch:
diff --git a/platform/config/env/env.go b/platform/config/env/env.go
index 4da1b4b6..557d3a73 100644
--- a/platform/config/env/env.go
+++ b/platform/config/env/env.go
@@ -1,6 +1,7 @@
 package env
 
 import (
+	"errors"
 	"fmt"
 	"os"
 	"strconv"
@@ -28,6 +29,68 @@ func Get(names ...string) (map[string]string, error) {
 	return values, nil
 }
 
+// GetWithFallback Get environment variable values
+// The first name in each group is use as key in the result map
+//
+//	// LEGO_ONE="ONE"
+//	// LEGO_TWO="TWO"
+//	env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
+//	// => "LEGO_ONE" = "ONE"
+//
+// ----
+//
+//	// LEGO_ONE=""
+//	// LEGO_TWO="TWO"
+//	env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
+//	// => "LEGO_ONE" = "TWO"
+//
+// ----
+//
+//	// LEGO_ONE=""
+//	// LEGO_TWO=""
+//	env.GetWithFallback([]string{"LEGO_ONE", "LEGO_TWO"})
+//	// => error
+//
+func GetWithFallback(groups ...[]string) (map[string]string, error) {
+	values := map[string]string{}
+
+	var missingEnvVars []string
+	for _, names := range groups {
+		if len(names) == 0 {
+			return nil, errors.New("undefined environment variable names")
+		}
+
+		value, envVar := getOneWithFallback(names[0], names[1:]...)
+		if len(value) == 0 {
+			missingEnvVars = append(missingEnvVars, envVar)
+			continue
+		}
+		values[envVar] = value
+	}
+
+	if len(missingEnvVars) > 0 {
+		return nil, fmt.Errorf("some credentials information are missing: %s", strings.Join(missingEnvVars, ","))
+	}
+
+	return values, nil
+}
+
+func getOneWithFallback(main string, names ...string) (string, string) {
+	value := os.Getenv(main)
+	if len(value) > 0 {
+		return value, main
+	}
+
+	for _, name := range names {
+		value := os.Getenv(name)
+		if len(value) > 0 {
+			return value, main
+		}
+	}
+
+	return "", main
+}
+
 // GetOrDefaultInt returns the given environment variable value as an integer.
 // Returns the default if the envvar cannot be coopered to an int, or is not found.
 func GetOrDefaultInt(envVar string, defaultValue int) int {
diff --git a/providers/dns/cloudflare/cloudflare.go b/providers/dns/cloudflare/cloudflare.go
index de15650f..b402bd82 100644
--- a/providers/dns/cloudflare/cloudflare.go
+++ b/providers/dns/cloudflare/cloudflare.go
@@ -47,7 +47,9 @@ type DNSProvider struct {
 // Credentials must be passed in the environment variables:
 // CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY.
 func NewDNSProvider() (*DNSProvider, error) {
-	values, err := env.Get("CLOUDFLARE_EMAIL", "CLOUDFLARE_API_KEY")
+	values, err := env.GetWithFallback(
+		[]string{"CLOUDFLARE_EMAIL", "CF_API_EMAIL"},
+		[]string{"CLOUDFLARE_API_KEY", "CF_API_KEY"})
 	if err != nil {
 		return nil, fmt.Errorf("cloudflare: %v", err)
 	}
@@ -92,8 +94,8 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 	}, nil
 }
 
-// Timeout returns the timeout and interval to use when checking for DNS
-// propagation. Adjusting here to cope with spikes in propagation times.
+// Timeout returns the timeout and interval to use when checking for DNS propagation.
+// Adjusting here to cope with spikes in propagation times.
 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 	return d.config.PropagationTimeout, d.config.PollingInterval
 }


Test patch:
diff --git a/platform/config/env/env_test.go b/platform/config/env/env_test.go
index c27e6da9..d32d80fd 100644
--- a/platform/config/env/env_test.go
+++ b/platform/config/env/env_test.go
@@ -9,6 +9,103 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestGetWithFallback(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")
+
+	defer 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)
+
+	type expected struct {
+		value map[string]string
+		error string
+	}
+
+	testCases := []struct {
+		desc     string
+		groups   [][]string
+		expected expected
+	}{
+		{
+			desc:   "no groups",
+			groups: nil,
+			expected: expected{
+				value: map[string]string{},
+			},
+		},
+		{
+			desc:   "empty groups",
+			groups: [][]string{{}, {}},
+			expected: expected{
+				error: "undefined environment variable names",
+			},
+		},
+		{
+			desc:   "missing env var",
+			groups: [][]string{{"TEST_LEGO_VAR_MISSING_1"}},
+			expected: expected{
+				error: "some credentials information are missing: TEST_LEGO_VAR_MISSING_1",
+			},
+		},
+		{
+			desc:   "all env var in a groups are missing",
+			groups: [][]string{{"TEST_LEGO_VAR_MISSING_1", "TEST_LEGO_VAR_MISSING_2"}},
+			expected: expected{
+				error: "some credentials information are missing: TEST_LEGO_VAR_MISSING_1",
+			},
+		},
+		{
+			desc:   "only the first env var have a value",
+			groups: [][]string{{"TEST_LEGO_VAR_EXIST_1", "TEST_LEGO_VAR_MISSING_1"}},
+			expected: expected{
+				value: map[string]string{"TEST_LEGO_VAR_EXIST_1": "VAR1"},
+			},
+		},
+		{
+			desc:   "only the second env var have a value",
+			groups: [][]string{{"TEST_LEGO_VAR_MISSING_1", "TEST_LEGO_VAR_EXIST_1"}},
+			expected: expected{
+				value: map[string]string{"TEST_LEGO_VAR_MISSING_1": "VAR1"},
+			},
+		},
+		{
+			desc:   "only all env vars have a value",
+			groups: [][]string{{"TEST_LEGO_VAR_EXIST_1", "TEST_LEGO_VAR_EXIST_2"}},
+			expected: expected{
+				value: map[string]string{"TEST_LEGO_VAR_EXIST_1": "VAR1"},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			value, err := GetWithFallback(test.groups...)
+			if len(test.expected.error) > 0 {
+				assert.EqualError(t, err, test.expected.error)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, test.expected.value, value)
+			}
+		})
+	}
+
+}
+
 func TestGetOrDefaultInt(t *testing.T) {
 	testCases := []struct {
 		desc         string