Repository: kubernetes-sigs/external-dns (language: go)

Issue:
CoreDNS provider - ETCD authentication
<!-- Please only use this template for submitting enhancement requests. This can be something like a new provider or a new gateway.  -->

**What would you like to be added**:
A way to provide username and password for etcd authentication. Probably in a form of 2 environment vairables: `ETCD_USER` and `ETCD_PASS`

**Why is this needed**:
Currently I'm trying to deploy external-dns in combination with coredns and etcd on my bare-metal cluster. I've followed [coredns doc](https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/coredns.md) with exception of using [bitnami helm chart](https://artifacthub.io/packages/helm/bitnami/etcd) instead of archived etcd-operator. After doing all of that external-dns pod reported this error:
```
time="2022-02-21T23:09:13Z" level=error msg="etcdserver: user name is empty"
```
I've tried to add credentials directly into endpoints env variable in a form of `http://user:pass@endpoint:port` but it didn't change anything. After that I decided to look through [`provider/coredns/coredns.go`](https://github.com/kubernetes-sigs/external-dns/blob/dd870ae4c3ca88c99045e04868923b0c4d3b6187/provider/coredns/coredns.go#L206) code, and even though I don't have that much experience with Go, I'm pretty sure there is no code for authentication.

I'll try to work on that, just wanted to document my findings somewhere publicly


Discussion / hints:
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Reopen this issue with `/reopen`
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/close not-planned

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
@k8s-triage-robot: Closing this issue, marking it as "Not Planned".

<details>

In response to [this](https://github.com/kubernetes-sigs/external-dns/issues/2616#issuecomment-1284752118):

>The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.
>
>This bot triages issues according to the following rules:
>- After 90d of inactivity, `lifecycle/stale` is applied
>- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
>- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed
>
>You can:
>- Reopen this issue with `/reopen`
>- Mark this issue as fresh with `/remove-lifecycle rotten`
>- Offer to help out with [Issue Triage][1]
>
>Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).
>
>/close not-planned
>
>[1]: https://www.kubernetes.dev/docs/guide/issue-triage/


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
/reopen
@eaglesemanation: Reopened this issue.

<details>

In response to [this](https://github.com/kubernetes-sigs/external-dns/issues/2616#issuecomment-1284899413):

>/reopen


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
/remove-lifecycle rotten
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs.

This bot triages issues and PRs according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue or PR as fresh with `/remove-lifecycle stale`
- Mark this issue or PR as rotten with `/lifecycle rotten`
- Close this issue or PR with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle rotten
The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
The Kubernetes project currently lacks enough active contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle rotten`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle rotten

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle rotten

The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale
The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/
/remove-lifecycle stale


Fix patch:
diff --git a/docs/tutorials/coredns.md b/docs/tutorials/coredns.md
index 13d7877576..bc673494c5 100644
--- a/docs/tutorials/coredns.md
+++ b/docs/tutorials/coredns.md
@@ -86,6 +86,7 @@ helm install --name my-coredns --values values.yaml stable/coredns
 ## Installing ExternalDNS
 ### Install external ExternalDNS
 ETCD_URLS is configured to etcd client service address.
+Optionnally, you can configure ETCD_USERNAME and ETCD_PASSWORD for authenticating to etcd.
 
 #### Manifest (for clusters without RBAC enabled)
 
diff --git a/provider/coredns/coredns.go b/provider/coredns/coredns.go
index a514ca8659..eeb545aea5 100644
--- a/provider/coredns/coredns.go
+++ b/provider/coredns/coredns.go
@@ -205,8 +205,10 @@ func getETCDConfig() (*etcdcv3.Config, error) {
 	}
 	etcdURLs := strings.Split(etcdURLsStr, ",")
 	firstURL := strings.ToLower(etcdURLs[0])
+	etcdUsername := os.Getenv("ETCD_USERNAME")
+	etcdPassword := os.Getenv("ETCD_PASSWORD")
 	if strings.HasPrefix(firstURL, "http://") {
-		return &etcdcv3.Config{Endpoints: etcdURLs}, nil
+		return &etcdcv3.Config{Endpoints: etcdURLs, Username: etcdUsername, Password: etcdPassword}, nil
 	} else if strings.HasPrefix(firstURL, "https://") {
 		caFile := os.Getenv("ETCD_CA_FILE")
 		certFile := os.Getenv("ETCD_CERT_FILE")
@@ -221,6 +223,8 @@ func getETCDConfig() (*etcdcv3.Config, error) {
 		return &etcdcv3.Config{
 			Endpoints: etcdURLs,
 			TLS:       tlsConfig,
+			Username:  etcdUsername,
+			Password:  etcdPassword,
 		}, nil
 	} else {
 		return nil, errors.New("etcd URLs must start with either http:// or https://")


Test patch:
diff --git a/provider/coredns/coredns_test.go b/provider/coredns/coredns_test.go
index 8c9aa8f9d6..baf5652f8e 100644
--- a/provider/coredns/coredns_test.go
+++ b/provider/coredns/coredns_test.go
@@ -18,9 +18,12 @@ package coredns
 
 import (
 	"context"
+	"os"
+	"reflect"
 	"strings"
 	"testing"
 
+	etcdcv3 "go.etcd.io/etcd/client/v3"
 	"sigs.k8s.io/external-dns/endpoint"
 	"sigs.k8s.io/external-dns/plan"
 
@@ -55,6 +58,68 @@ func (c fakeETCDClient) DeleteService(key string) error {
 	return nil
 }
 
+func TestETCDConfig(t *testing.T) {
+	var tests = []struct {
+		name  string
+		input map[string]string
+		want  *etcdcv3.Config
+	}{
+		{
+			"default config",
+			map[string]string{},
+			&etcdcv3.Config{Endpoints: []string{"http://localhost:2379"}},
+		},
+		{
+			"config with ETCD_URLS",
+			map[string]string{"ETCD_URLS": "http://example.com:2379"},
+			&etcdcv3.Config{Endpoints: []string{"http://example.com:2379"}},
+		},
+		{
+			"config with ETCD_USERNAME and ETCD_PASSWORD",
+			map[string]string{"ETCD_USERNAME": "root", "ETCD_PASSWORD": "test"},
+			&etcdcv3.Config{
+				Endpoints: []string{"http://localhost:2379"},
+				Username:  "root",
+				Password:  "test",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			closer := envSetter(tt.input)
+			cfg, _ := getETCDConfig()
+			if !reflect.DeepEqual(cfg, tt.want) {
+				t.Errorf("unexpected config. Got %v, want %v", cfg, tt.want)
+			}
+			t.Cleanup(closer)
+		})
+	}
+
+}
+
+func envSetter(envs map[string]string) (closer func()) {
+	originalEnvs := map[string]string{}
+
+	for name, value := range envs {
+		if originalValue, ok := os.LookupEnv(name); ok {
+			originalEnvs[name] = originalValue
+		}
+		_ = os.Setenv(name, value)
+	}
+
+	return func() {
+		for name := range envs {
+			origValue, has := originalEnvs[name]
+			if has {
+				_ = os.Setenv(name, origValue)
+			} else {
+				_ = os.Unsetenv(name)
+			}
+		}
+	}
+}
+
 func TestAServiceTranslation(t *testing.T) {
 	expectedTarget := "1.2.3.4"
 	expectedDNSName := "example.com"