Skip to content

Commit

Permalink
Merge pull request #21 from onedr0p/refactor-client-config
Browse files Browse the repository at this point in the history
refactor: implement doRequest and other changes
  • Loading branch information
kashalls authored May 26, 2024
2 parents 68e28a4 + bf57998 commit 05e518f
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 153 deletions.
2 changes: 1 addition & 1 deletion cmd/webhook/init/configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// Config struct for configuration environmental variables
type Config struct {
ServerHost string `env:"SERVER_HOST" envDefault:"localhost"`
ServerHost string `env:"SERVER_HOST" envDefault:"0.0.0.0"`
ServerPort int `env:"SERVER_PORT" envDefault:"8888"`
ServerReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT"`
ServerWriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT"`
Expand Down
6 changes: 3 additions & 3 deletions cmd/webhook/init/dnsprovider/dnsprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
log "github.com/sirupsen/logrus"
)

type UnifiProviderFactory func(baseProvider *provider.BaseProvider, unifiConfig *unifi.Configuration) provider.Provider
type UnifiProviderFactory func(baseProvider *provider.BaseProvider, unifiConfig *unifi.Config) provider.Provider

func Init(config configuration.Config) (provider.Provider, error) {
var domainFilter endpoint.DomainFilter
Expand Down Expand Up @@ -45,9 +45,9 @@ func Init(config configuration.Config) (provider.Provider, error) {
}
log.Info(createMsg)

unifiConfig := unifi.Configuration{}
unifiConfig := unifi.Config{}
if err := env.Parse(&unifiConfig); err != nil {
return nil, fmt.Errorf("reading adguard configuration failed: %v", err)
return nil, fmt.Errorf("reading unifi configuration failed: %v", err)
}

return unifi.NewUnifiProvider(domainFilter, &unifiConfig)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/caarlos0/env/v11 v11.0.1
github.com/go-chi/chi/v5 v5.0.12
github.com/sirupsen/logrus v1.9.3
golang.org/x/net v0.25.0
sigs.k8s.io/external-dns v0.14.2
)

Expand All @@ -22,7 +23,6 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
Expand Down
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/aws/aws-sdk-go v1.53.3 h1:xv0iGCCLdf6ZtlLPMCBjm+tU9UBLP5hXnSqnbKFYmto=
github.com/aws/aws-sdk-go v1.53.3/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.53.9 h1:6oipls9+L+l2Me5rklqlX3xGWNWGcMinY3F69q9Q+Cg=
github.com/aws/aws-sdk-go v1.53.9/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/caarlos0/env/v11 v11.0.1 h1:A8dDt9Ub9ybqRSUF3fQc/TA/gTam2bKT4Pit+cwrsPs=
Expand All @@ -11,8 +9,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
Expand Down Expand Up @@ -108,8 +104,6 @@ k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U=
k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22 h1:ao5hUqGhsqdm+bYbjH/pRkCs0unBGe9UyDahzs9zQzQ=
k8s.io/utils v0.0.0-20240423183400-0849a56e8f22/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak=
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/external-dns v0.14.2 h1:j7rYtQqDAxYfN9N1/BZcRdzUBRsnZp4tZcuZ75ekTlc=
Expand Down
217 changes: 88 additions & 129 deletions internal/unifi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,238 +8,197 @@ import (
"io"
"net/http"
"net/http/cookiejar"
"net/url"

log "github.com/sirupsen/logrus"
"golang.org/x/net/publicsuffix"
"sigs.k8s.io/external-dns/endpoint"
)

// Client is the DNS provider client.
type Client struct {
BaseURL string
HTTPClient *http.Client
csrf string
// httpClient is the DNS provider client.
type httpClient struct {
*Config
*http.Client
csrf string
}

// DNSRecord represents a DNS record in the API.
type DNSRecord struct {
ID string `json:"_id,omitempty"`
Enabled bool `json:"enabled,omitempty"`
Key string `json:"key"`
Port int `json:"port,omitempty"`
Priority int `json:"priority,omitempty"`
RecordType string `json:"record_type"`
TTL endpoint.TTL `json:"ttl,omitempty"`
Value string `json:"value"`
Weight int `json:"weight,omitempty"`
}

var (
UnifiLogin = "%s/api/auth/login"
UnifiDNSRecords = "%s/proxy/network/v2/api/site/default/static-dns"
UnifiDNSSelectRecord = "%s/proxy/network/v2/api/site/default/static-dns/%s"
const (
unifiLoginPath = "%s/api/auth/login"
unifiRecordsPath = "%s/proxy/network/v2/api/site/default/static-dns"
unifiRecordPath = "%s/proxy/network/v2/api/site/default/static-dns/%s"
)

// newUnifiClient creates a new DNS provider client and logs in to store cookies.
func newUnifiClient(config *Configuration) (*Client, error) {
jar, err := cookiejar.New(nil)
func newUnifiClient(config *Config) (*httpClient, error) {
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return nil, err
}

transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.SkipTLSVerify},
}

client := &Client{
BaseURL: config.Host,
HTTPClient: &http.Client{
Transport: transport,
Jar: jar,
// Create the HTTP client
client := &httpClient{
Config: config,
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: config.SkipTLSVerify},
},
Jar: jar,
},
}

if err := client.login(config.User, config.Password); err != nil {
if err := client.login(); err != nil {
return nil, err
}

return client, nil
}

// login authenticates the client and stores the cookies.
func (c *Client) login(username, password string) error {
loginURL := fmt.Sprintf(UnifiLogin, c.BaseURL)
// login performs a login request to the UniFi controller.
func (c *httpClient) login() error {
// Prepare the login request body
body, _ := json.Marshal(map[string]string{
"username": c.Config.User,
"password": c.Config.Password,
})

credentials := map[string]string{
"username": username,
"password": password,
}

body, err := json.Marshal(credentials)
// Perform the login request
resp, err := c.doRequest(http.MethodPost, fmt.Sprintf(unifiLoginPath, c.Config.Host), bytes.NewBuffer(body))
if err != nil {
return err
}

resp, err := c.HTTPClient.Post(loginURL, "application/json", bytes.NewBuffer(body))
if err != nil {
return err
}
defer resp.Body.Close()

// Check if the login was successful
if resp.StatusCode != http.StatusOK {
respBody, _ := io.ReadAll(resp.Body)
log.Errorf("login failed: %s, response: %s", resp.Status, string(respBody))
return fmt.Errorf("login failed: %s", resp.Status)
}

return nil
}

func (c *Client) 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")
}

func (c *Client) GetData(url string) ([]byte, error) {
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf(url, c.BaseURL), nil)
c.setHeaders(req)

resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}

// Retrieve CSRF token from the response headers
if csrf := resp.Header.Get("x-csrf-token"); csrf != "" {
c.csrf = resp.Header.Get("x-csrf-token")
}

defer resp.Body.Close()

byteArray, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return byteArray, nil
return nil
}

func (c *Client) ShipData(url string, body []byte) ([]byte, error) {
req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf(url, c.BaseURL), bytes.NewBuffer(body))
c.setHeaders(req)

resp, err := c.HTTPClient.Do(req)
// doRequest makes an HTTP request to the UniFi controller.
func (c *httpClient) doRequest(method, path string, body io.Reader) (*http.Response, error) {
log.Debugf("making %s request to %s", method, path)

req, err := http.NewRequest(method, path, body)
if err != nil {
return nil, err
}

if csrf := resp.Header.Get("x-csrf-token"); csrf != "" {
c.csrf = resp.Header.Get("x-csrf-token")
}

defer resp.Body.Close()

byteArray, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return byteArray, nil
}

func (c *Client) DeleteData(url string) ([]byte, error) {
req, _ := http.NewRequest(http.MethodDelete, url, nil)

c.setHeaders(req)
resp, err := c.HTTPClient.Do(req)

resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}

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

defer resp.Body.Close()
log.Debugf("response code from %s request to %s: %d", method, path, resp.StatusCode)

byteArray, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s request to %s was not successful: %d", method, path, resp.StatusCode)
}

return byteArray, nil
return resp, nil
}

// ListRecords retrieves all DNS records.
func (c *Client) ListRecords() ([]DNSRecord, error) {
resp, err := c.GetData(UnifiDNSRecords)
// GetEndpoints retrieves the list of DNS records from the UniFi controller.
func (c *httpClient) GetEndpoints() ([]DNSRecord, error) {
resp, err := c.doRequest(http.MethodGet, fmt.Sprintf(unifiRecordsPath, c.Config.Host), nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()

var records []DNSRecord
err = json.Unmarshal(resp, &records)
if err != nil {
if err = json.NewDecoder(resp.Body).Decode(&records); err != nil {
return nil, err
}
log.Debugf("retrieved records: %+v", records)

return records, nil
}

// CreateEndpoint creates a new DNS record.
func (c *Client) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, error) {
record := DNSRecord{
// CreateEndpoint creates a new DNS record in the UniFi controller.
func (c *httpClient) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, error) {
jsonBody, err := json.Marshal(DNSRecord{
Enabled: true,
Key: endpoint.DNSName,
RecordType: endpoint.RecordType,
TTL: endpoint.RecordTTL,
Value: endpoint.Targets[0],
}

body, err := json.Marshal(record)
})
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to marshal DNS record: %w", err)
}

resp, err := c.ShipData(UnifiDNSRecords, body)
resp, err := c.doRequest(http.MethodPost, fmt.Sprintf(unifiRecordsPath, c.Config.Host), bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
defer resp.Body.Close()

var newRecord DNSRecord
err = json.Unmarshal(resp, &newRecord)
if err != nil {
var record DNSRecord
if err = json.NewDecoder(resp.Body).Decode(&record); err != nil {
return nil, err
}
log.Debugf("created record: %+v", record)

return &newRecord, nil
return &record, nil
}

// DeleteEndpoint deletes a DNS record.
func (c *Client) DeleteEndpoint(endpoint *endpoint.Endpoint) error {
// DeleteEndpoint deletes a DNS record from the UniFi controller.
func (c *httpClient) DeleteEndpoint(endpoint *endpoint.Endpoint) error {
lookup, err := c.LookupIdentifier(endpoint.DNSName, endpoint.RecordType)
if err != nil {
return err
}

_, err = c.DeleteData(fmt.Sprintf(UnifiDNSSelectRecord, c.BaseURL, lookup.ID))
if err != nil {
if _, err = c.doRequest(http.MethodPost, fmt.Sprintf(unifiRecordPath, c.Config.Host, lookup.ID), nil); err != nil {
return err
}

return nil
}

// LookupIdentifier finds the ID of a DNS record.
func (c *Client) LookupIdentifier(Key string, RecordType string) (*DNSRecord, error) {
records, err := c.ListRecords()
// LookupIdentifier finds the ID of a DNS record in the UniFi controller.
func (c *httpClient) LookupIdentifier(key, recordType string) (*DNSRecord, error) {
records, err := c.GetEndpoints()
if err != nil {
return nil, err
}

for _, r := range records {
if r.Key == Key && r.RecordType == RecordType {
if r.Key == key && r.RecordType == recordType {
return &r, nil
}
}

return nil, fmt.Errorf("record not found")
}

// 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)
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.Debugf("Requesting %s cookies: %d", req.URL, len(c.Client.Jar.Cookies(parsedURL)))
} else {
log.Debugf("Requesting %s", req.URL)
}
}
Loading

0 comments on commit 05e518f

Please sign in to comment.