diff --git a/.gitignore b/.gitignore
index 83c5afc..6abe6c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
.idea
###
.private/
-external-dns-unifi-webhook
\ No newline at end of file
+external-dns-unifi-webhook
+.DS_Store
diff --git a/README.md b/README.md
index 0d7f660..1575d80 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@
```yaml
fullnameOverride: external-dns-unifi
- logLevel: debug
+ logLevel: &logLevel debug
provider:
name: webhook
webhook:
@@ -47,7 +47,9 @@
tag: main # replace with a versioned release tag
env:
- name: UNIFI_HOST
- value: https://192.168.1.1 # replace with the address to your UniFi router
+ 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:
@@ -59,7 +61,7 @@
name: external-dns-unifi-secret
key: password
- name: LOG_LEVEL
- value: debug
+ value: *logLevel
livenessProbe:
httpGet:
path: /healthz
@@ -87,6 +89,33 @@
helm install external-dns-unifi external-dns/external-dns -f external-dns-unifi-values.yaml --version 1.14.3 -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` | Whether your controller is supported by official Ubiquiti hardware. | `false` |
+| `LOG_LEVEL` | Change the verbosity of logs (used when making a bug report) | `info` |
+
+### Server Configuration
+
+| Environment Variable | Description | Default Value |
+|----------------------------------|------------------------------------------------------------------|---------------|
+| `SERVER_HOST` | The host address where the server listens. | `localhost` |
+| `SERVER_PORT` | The port where the server listens. | `8888` |
+| `SERVER_READ_TIMEOUT` | Duration the server waits before timing out on read operations. | N/A |
+| `SERVER_WRITE_TIMEOUT` | Duration the server waits before timing out on write operations. | N/A |
+| `DOMAIN_FILTER` | List of domains to include in the filter. | Empty |
+| `EXCLUDE_DOMAIN_FILTER` | List of domains to exclude from filtering. | Empty |
+| `REGEXP_DOMAIN_FILTER` | Regular expression for filtering domains. | Empty |
+| `REGEXP_DOMAIN_FILTER_EXCLUSION` | Regular expression for excluding domains from the filter. | Empty |
+
## ⭐ Stargazers
diff --git a/internal/unifi/client.go b/internal/unifi/client.go
index 6f1f934..dfb943d 100644
--- a/internal/unifi/client.go
+++ b/internal/unifi/client.go
@@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/http/cookiejar"
- "net/url"
"github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/log"
"golang.org/x/net/publicsuffix"
@@ -17,16 +16,24 @@ import (
"go.uber.org/zap"
)
+type ClientURLs struct {
+ Login string
+ Records string
+}
+
// httpClient is the DNS provider client.
type httpClient struct {
*Config
*http.Client
- csrf string
+ csrf string
+ ClientURLs *ClientURLs
}
const (
- unifiLoginPath = "%s/api/auth/login"
- unifiRecordPath = "%s/proxy/network/v2/api/site/%s/static-dns/%s"
+ unifiLoginPath = "%s/api/auth/login"
+ unifiLoginPathExternal = "%s/api/login"
+ unifiRecordPath = "%s/proxy/network/v2/api/site/%s/static-dns/%s"
+ unifiRecordPathExternal = "%s/v2/api/site/%s/static-dns/%s"
)
// newUnifiClient creates a new DNS provider client and logs in to store cookies.
@@ -45,6 +52,15 @@ func newUnifiClient(config *Config) (*httpClient, error) {
},
Jar: jar,
},
+ ClientURLs: &ClientURLs{
+ Login: unifiLoginPath,
+ Records: unifiRecordPath,
+ },
+ }
+
+ if config.ExternalController {
+ client.ClientURLs.Login = unifiLoginPathExternal
+ client.ClientURLs.Records = unifiRecordPathExternal
}
if err := client.login(); err != nil {
@@ -68,7 +84,7 @@ func (c *httpClient) login() error {
// Perform the login request
resp, err := c.doRequest(
http.MethodPost,
- FormatUrl(unifiLoginPath, c.Config.Host),
+ FormatUrl(c.ClientURLs.Login, c.Config.Host),
bytes.NewBuffer(jsonBody),
)
if err != nil {
@@ -88,14 +104,10 @@ func (c *httpClient) login() error {
if csrf := resp.Header.Get("x-csrf-token"); csrf != "" {
c.csrf = resp.Header.Get("x-csrf-token")
}
-
return nil
}
-// doRequest makes an HTTP request to the UniFi controller.
func (c *httpClient) doRequest(method, path string, body io.Reader) (*http.Response, error) {
- log.Debug(fmt.Sprintf("making %s request to %s", method, path))
-
req, err := http.NewRequest(method, path, body)
if err != nil {
return nil, err
@@ -112,19 +124,22 @@ func (c *httpClient) doRequest(method, path string, body io.Reader) (*http.Respo
c.csrf = csrf
}
- log.Debug(fmt.Sprintf("response code from %s request to %s: %d", method, path, resp.StatusCode))
-
// If the status code is 401, re-login and retry the request
if resp.StatusCode == http.StatusUnauthorized {
- log.Debug("Received 401 Unauthorized, re-login required")
+ 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
}
}
@@ -140,7 +155,7 @@ func (c *httpClient) doRequest(method, path string, body io.Reader) (*http.Respo
func (c *httpClient) GetEndpoints() ([]DNSRecord, error) {
resp, err := c.doRequest(
http.MethodGet,
- FormatUrl(unifiRecordPath, c.Config.Host, c.Config.Site),
+ FormatUrl(c.ClientURLs.Records, c.Config.Host, c.Config.Site),
nil,
)
if err != nil {
@@ -150,29 +165,33 @@ func (c *httpClient) GetEndpoints() ([]DNSRecord, error) {
var records []DNSRecord
if err = json.NewDecoder(resp.Body).Decode(&records); err != nil {
+ log.Error("Failed to decode response", zap.Error(err))
return nil, err
}
- log.Debug(fmt.Sprintf("retrieved records: %+v", records))
+ log.Debug("retrieved records", zap.Int("count", len(records)))
return records, nil
}
// CreateEndpoint creates a new DNS record in the UniFi controller.
func (c *httpClient) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, error) {
- jsonBody, err := json.Marshal(DNSRecord{
+ record := DNSRecord{
Enabled: true,
Key: endpoint.DNSName,
RecordType: endpoint.RecordType,
TTL: endpoint.RecordTTL,
Value: endpoint.Targets[0],
- })
+ }
+
+ jsonBody, err := json.Marshal(record)
if err != nil {
return nil, err
}
+
resp, err := c.doRequest(
http.MethodPost,
- FormatUrl(unifiRecordPath, c.Config.Host, c.Config.Site),
+ FormatUrl(c.ClientURLs.Records, c.Config.Host, c.Config.Site),
bytes.NewReader(jsonBody),
)
if err != nil {
@@ -180,14 +199,12 @@ func (c *httpClient) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, er
}
defer resp.Body.Close()
- var record DNSRecord
- if err = json.NewDecoder(resp.Body).Decode(&record); err != nil {
+ var createdRecord DNSRecord
+ if err = json.NewDecoder(resp.Body).Decode(&createdRecord); err != nil {
return nil, err
}
- log.Debug(fmt.Sprintf("created record: %+v", record))
-
- return &record, nil
+ return &createdRecord, nil
}
// DeleteEndpoint deletes a DNS record from the UniFi controller.
@@ -197,11 +214,14 @@ func (c *httpClient) DeleteEndpoint(endpoint *endpoint.Endpoint) error {
return err
}
- if _, err = c.doRequest(
+ deleteURL := FormatUrl(c.ClientURLs.Records, c.Config.Host, c.Config.Site, lookup.ID)
+
+ _, err = c.doRequest(
http.MethodDelete,
- FormatUrl(unifiRecordPath, c.Config.Host, c.Config.Site, lookup.ID),
+ deleteURL,
nil,
- ); err != nil {
+ )
+ if err != nil {
return err
}
@@ -210,6 +230,7 @@ func (c *httpClient) DeleteEndpoint(endpoint *endpoint.Endpoint) error {
// lookupIdentifier finds the ID of a DNS record in the UniFi controller.
func (c *httpClient) lookupIdentifier(key, recordType string) (*DNSRecord, error) {
+ log.Debug("Looking up identifier", zap.String("key", key), zap.String("recordType", recordType))
records, err := c.GetEndpoints()
if err != nil {
return nil, err
@@ -221,7 +242,7 @@ func (c *httpClient) lookupIdentifier(key, recordType string) (*DNSRecord, error
}
}
- return nil, err
+ return nil, fmt.Errorf("record not found: %s", key)
}
// setHeaders sets the headers for the HTTP request.
@@ -230,12 +251,4 @@ func (c *httpClient) setHeaders(req *http.Request) {
req.Header.Set("X-CSRF-Token", c.csrf)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/json; charset=utf-8")
-
- // Log the request URL and cookies
- if c.Client.Jar != nil {
- parsedURL, _ := url.Parse(req.URL.String())
- log.Debug(fmt.Sprintf("Requesting %s cookies: %d", req.URL, len(c.Client.Jar.Cookies(parsedURL))))
- } else {
- log.Debug(fmt.Sprintf("Requesting %s", req.URL))
- }
}
diff --git a/internal/unifi/provider.go b/internal/unifi/provider.go
index 33b1b65..863918e 100644
--- a/internal/unifi/provider.go
+++ b/internal/unifi/provider.go
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
+ "github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/log"
+ "go.uber.org/zap"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
@@ -62,13 +64,19 @@ func (p *Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
// ApplyChanges applies a given set of changes in the DNS provider.
func (p *Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
for _, endpoint := range append(changes.UpdateOld, changes.Delete...) {
+ log.Debug("deleting endpoint", zap.String("name", endpoint.DNSName), zap.String("type", endpoint.RecordType))
+
if err := p.client.DeleteEndpoint(endpoint); err != nil {
+ log.Error("failed to delete endpoint", zap.String("name", endpoint.DNSName), zap.String("type", endpoint.RecordType), zap.Error(err))
return err
}
}
for _, endpoint := range append(changes.Create, changes.UpdateNew...) {
+ log.Debug("creating endpoint", zap.String("name", endpoint.DNSName), zap.String("type", endpoint.RecordType))
+
if _, err := p.client.CreateEndpoint(endpoint); err != nil {
+ log.Error("failed to create endpoint", zap.String("name", endpoint.DNSName), zap.String("type", endpoint.RecordType), zap.Error(err))
return err
}
}
diff --git a/internal/unifi/types.go b/internal/unifi/types.go
index 391bc0b..b4c536a 100644
--- a/internal/unifi/types.go
+++ b/internal/unifi/types.go
@@ -4,11 +4,12 @@ import "sigs.k8s.io/external-dns/endpoint"
// 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"`
- Site string `env:"UNIFI_SITE" envDefault:"default"`
- SkipTLSVerify bool `env:"UNIFI_SKIP_TLS_VERIFY" envDefault:"true"`
+ Host string `env:"UNIFI_HOST,notEmpty"`
+ User string `env:"UNIFI_USER,notEmpty"`
+ Password string `env:"UNIFI_PASS,notEmpty"`
+ Site string `env:"UNIFI_SITE" envDefault:"default"`
+ ExternalController bool `env:"UNIFI_EXTERNAL_CONTROLLER" envDefault:"false"`
+ SkipTLSVerify bool `env:"UNIFI_SKIP_TLS_VERIFY" envDefault:"true"`
}
// Login represents a login request to the UniFi API.
diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go
index 36461d3..d61a04c 100644
--- a/pkg/webhook/webhook.go
+++ b/pkg/webhook/webhook.go
@@ -97,7 +97,6 @@ func (p *Webhook) Records(w http.ResponseWriter, r *http.Request) {
return
}
- requestLog(r).Debug("requesting records")
ctx := r.Context()
records, err := p.provider.Records(ctx)
if err != nil {
@@ -106,7 +105,6 @@ func (p *Webhook) Records(w http.ResponseWriter, r *http.Request) {
return
}
- requestLog(r).With(zap.Int("count", len(records))).Debug("returning records")
w.Header().Set(contentTypeHeader, string(mediaTypeVersion1))
w.Header().Set(varyHeader, contentTypeHeader)
err = json.NewEncoder(w).Encode(records)
@@ -145,6 +143,7 @@ func (p *Webhook) ApplyChanges(w http.ResponseWriter, r *http.Request) {
zap.Int("delete", len(changes.Delete)),
).Debug("requesting apply changes")
if err := p.provider.ApplyChanges(ctx, &changes); err != nil {
+ requestLog(r).Error("error when applying changes", zap.Error(err))
w.Header().Set(contentTypeHeader, contentTypePlaintext)
w.WriteHeader(http.StatusInternalServerError)
return