Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for using Local UniFi Api Keys #85

Merged
merged 11 commits into from
Jan 10, 2025
89 changes: 67 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,66 @@

## ⛵ Deployment

1. Create a local user with a password in your UniFi OS, this user only needs read/write access to the UniFi Network appliance.
### Gathering your credentials

2. Add the ExternalDNS Helm repository to your cluster.
<details>
<summary>UniFi Api Key - Network v9.0.0+</summary>
<br>

1. Open your UniFi Console's Network Settings and go to `Settings > Control Plane > Admins & Users`.

2. Selecting your user to operate under. Whenever we modify the DNS Records, this user will show up under `System Log > Admin Activity`

3. Go under `Control Plane API Key` and click `Create New`. You can set the name to whatever you want, and the expiration to whatever you feel like.

4. Create a Kubernetes secret called `external-dns-unifi-secret` that will hold your `UNIFI_API_KEY` with their respected values from Step 3.

```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: external-dns-unifi-secret
data:
api-key: <your-api-key>
```
</details>

<details>
<summary>Username & Password (Deprecated)</summary>
<br>

1. Open your UniFi Console's Network Settings and go to `Settings > Control Plane > Admins & Users`.

2. Select `Create New Admin`.

3. In the menu that appears, enable `Restrict to Local Access Only`. Deselect `Use a Predefined Role`. Set `Network: Site Admin`. All other selections can be set to `None`. Click `Create`.

4. Create a Kubernetes secret called `external-dns-unifi-secret` that holds the `username` and `password` with their respected values from Step 3.

```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: external-dns-unifi-secret
data:
username: <your-username>
password: <your-password>
```
</details>

### Installing the provider

1. Add the ExternalDNS Helm repository to your cluster.

```sh
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/
```

3. Create a Kubernetes secret called `external-dns-unifi-secret` that holds `username` and `password` with their respected values from step 1.
2. Deploy your `external-dns-unifi-secret` secret that holds your authentication credentials from either of the

4. Create the helm values file, for example `external-dns-unifi-values.yaml`:
3. Create the helm values file, for example `external-dns-unifi-values.yaml`:

```yaml
fullnameOverride: external-dns-unifi
Expand All @@ -47,16 +96,11 @@
value: https://192.168.1.1 # replace with the address to your UniFi router/controller
- name: UNIFI_EXTERNAL_CONTROLLER
value: "false"
- name: UNIFI_USER
valueFrom:
secretKeyRef:
name: external-dns-unifi-secret
key: username
- name: UNIFI_PASS
- name: UNIFI_API_KEY
valueFrom:
secretKeyRef:
name: external-dns-unifi-secret
key: password
key: api-key
- name: LOG_LEVEL
value: *logLevel
livenessProbe:
Expand All @@ -80,25 +124,26 @@
domainFilters: ["example.com"] # replace with your domain
```

5. Install the Helm chart
4. Install the Helm chart

```sh
helm install external-dns-unifi external-dns/external-dns -f external-dns-unifi-values.yaml --version 1.14.3 -n external-dns
helm install external-dns-unifi external-dns/external-dns -f external-dns-unifi-values.yaml --version 1.15.0 -n external-dns
```

## Configuration

### Unifi Controller Configuration

| Environment Variable | Description | Default Value |
|------------------------------|--------------------------------------------------------------|---------------|
| `UNIFI_USER` | Username for the Unifi Controller (must be provided). | N/A |
| `UNIFI_SKIP_TLS_VERIFY` | Whether to skip TLS verification (true or false). | `true` |
| `UNIFI_SITE` | Unifi Site Identifier (used in multi-site installations) | `default` |
| `UNIFI_PASS` | Password for the Unifi Controller (must be provided). | N/A |
| `UNIFI_HOST` | Host of the Unifi Controller (must be provided). | N/A |
| `UNIFI_EXTERNAL_CONTROLLER`* | Toggles support for non-UniFi Hardware | `false` |
| `LOG_LEVEL` | Change the verbosity of logs (used when making a bug report) | `info` |
| Environment Variable | Description | Default Value |
|------------------------------|-------------------------------------------------------------------|---------------|
| `UNIFI_API_KEY` | The local api key provided for your user | N/A |
| `UNIFI_USER` | Username for the Unifi Controller (deprecated use `UNIFI_API_KEY`). | N/A |
| `UNIFI_PASS` | Password for the Unifi Controller (deprecated use `UNIFI_API_KEY`). | N/A |
| `UNIFI_SKIP_TLS_VERIFY` | Whether to skip TLS verification (true or false). | `true` |
| `UNIFI_SITE` | Unifi Site Identifier (used in multi-site installations) | `default` |
| `UNIFI_HOST` | Host of the Unifi Controller (must be provided). | N/A |
| `UNIFI_EXTERNAL_CONTROLLER`* | Toggles support for non-UniFi Hardware | `false` |
| `LOG_LEVEL` | Change the verbosity of logs (used when making a bug report) | `info` |

*`UNIFI_EXTERNAL_CONTROLLER` is used to toggle between two versions of the Network Controller API. If you are running the UniFi software outside of UniFi's official hardware (e.g., Cloud Key or Dream Machine), you'll need to set `UNIFI_EXTERNAL_CONTROLLER` to `true`

Expand Down
62 changes: 38 additions & 24 deletions internal/unifi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,17 @@ func newUnifiClient(config *Config) (*httpClient, error) {
},
}

if config.ExternalController {
if client.Config.ExternalController {
client.ClientURLs.Login = unifiLoginPathExternal
client.ClientURLs.Records = unifiRecordPathExternal
}

if client.Config.ApiKey != "" {
return client, nil
}

log.Info("UNIFI_USER and UNIFI_PASSWORD are deprecated, please switch to using UNIFI_API_KEY instead")

if err := client.login(); err != nil {
return nil, err
}
Expand Down Expand Up @@ -120,27 +126,29 @@ func (c *httpClient) doRequest(method, path string, body io.Reader) (*http.Respo
return nil, err
}

if csrf := resp.Header.Get("X-CSRF-Token"); csrf != "" {
c.csrf = csrf
}

// If the status code is 401, re-login and retry the request
if resp.StatusCode == http.StatusUnauthorized {
log.Debug("received 401 unauthorized, attempting to re-login")
if err := c.login(); err != nil {
log.Error("re-login failed", zap.Error(err))
return nil, err
// TODO: Deprecation Notice - Use UNIFI_API_KEY instead
if c.Config.ApiKey == "" {
if csrf := resp.Header.Get("X-CSRF-Token"); csrf != "" {
c.csrf = csrf
}
// Update the headers with new CSRF token
c.setHeaders(req)

// Retry the request
log.Debug("retrying request after re-login")

resp, err = c.Client.Do(req)
if err != nil {
log.Error("Retry request failed", zap.Error(err))
return nil, err
// If the status code is 401, re-login and retry the request
if resp.StatusCode == http.StatusUnauthorized {
log.Debug("received 401 unauthorized, attempting to re-login")
if err := c.login(); err != nil {
log.Error("re-login failed", zap.Error(err))
return nil, err
}
// Update the headers with new CSRF token
c.setHeaders(req)

// Retry the request
log.Debug("retrying request after re-login")

resp, err = c.Client.Do(req)
if err != nil {
log.Error("Retry request failed", zap.Error(err))
return nil, err
}
}
}

Expand Down Expand Up @@ -198,7 +206,7 @@ func (c *httpClient) GetEndpoints() ([]DNSRecord, error) {
records[i].Port = nil
}

log.Debug("retrieved records", zap.Int("count", len(records)))
log.Debug("provider retrieved records", zap.Int("count", len(records)))
return records, nil
}

Expand Down Expand Up @@ -243,6 +251,7 @@ func (c *httpClient) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, er
return nil, err
}

log.Debug("client created new record", zap.Any("record", &createdRecord))
return &createdRecord, nil
}

Expand All @@ -264,6 +273,7 @@ func (c *httpClient) DeleteEndpoint(endpoint *endpoint.Endpoint) error {
return err
}

log.Debug("client deleted record", zap.Any("record", endpoint))
return nil
}

Expand All @@ -286,8 +296,12 @@ func (c *httpClient) lookupIdentifier(key, recordType string) (*DNSRecord, error

// setHeaders sets the headers for the HTTP request.
func (c *httpClient) setHeaders(req *http.Request) {
// Add the saved CSRF header.
req.Header.Set("X-CSRF-Token", c.csrf)
if c.Config.ApiKey != "" {
req.Header.Set("X-API-KEY", c.Config.ApiKey)
} else {
req.Header.Set("X-CSRF-Token", c.csrf)
}

req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json; charset=utf-8")
}
5 changes: 3 additions & 2 deletions internal/unifi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
// Config represents the configuration for the UniFi API.
type Config struct {
Host string `env:"UNIFI_HOST,notEmpty"`
User string `env:"UNIFI_USER,notEmpty"`
Password string `env:"UNIFI_PASS,notEmpty"`
ApiKey string `env:"UNIFI_API_KEY" envDefault:""`
User string `env:"UNIFI_USER" envDefault:""`
Password string `env:"UNIFI_PASS" envDefault:""`
Site string `env:"UNIFI_SITE" envDefault:"default"`
ExternalController bool `env:"UNIFI_EXTERNAL_CONTROLLER" envDefault:"false"`
SkipTLSVerify bool `env:"UNIFI_SKIP_TLS_VERIFY" envDefault:"true"`
Expand Down
2 changes: 1 addition & 1 deletion pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (p *Webhook) AdjustEndpoints(w http.ResponseWriter, r *http.Request) {
return
}

log.Debug("adjust endpoints count", zap.Int("endpoints", len(pve)))
log.Debug("webhook adjust endpoints count", zap.Int("endpoints", len(pve)))
pve, err := p.provider.AdjustEndpoints(pve)
if err != nil {
w.Header().Set(contentTypeHeader, contentTypePlaintext)
Expand Down
Loading