Skip to content

Commit

Permalink
Merge pull request #25 from onedr0p/more-changes
Browse files Browse the repository at this point in the history
feat: implement healthz/readyz/metrics servers & FormatUrl func
  • Loading branch information
kashalls authored May 26, 2024
2 parents e67be81 + cbe96cc commit adf6ba9
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 59 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@
key: password
- name: LOG_LEVEL
value: debug
livenessProbe:
httpGet:
path: /healthz
port: http-wh-metrics
initialDelaySeconds: 10
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /readyz
port: http-wh-metrics
initialDelaySeconds: 10
timeoutSeconds: 5
policy: sync
sources: ["ingress", "service"]
txtOwnerId: default
Expand Down
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:"0.0.0.0"`
ServerHost string `env:"SERVER_HOST" envDefault:"localhost"`
ServerPort int `env:"SERVER_PORT" envDefault:"8888"`
ServerReadTimeout time.Duration `env:"SERVER_READ_TIMEOUT"`
ServerWriteTimeout time.Duration `env:"SERVER_WRITE_TIMEOUT"`
Expand Down
71 changes: 48 additions & 23 deletions cmd/webhook/init/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,53 @@ import (
"github.com/go-chi/chi/v5"
"github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/configuration"
"github.com/kashalls/external-dns-provider-unifi/pkg/webhook"
"github.com/prometheus/client_golang/prometheus/promhttp"

log "github.com/sirupsen/logrus"
)

// Init server initialization function
// The server will respond to the following endpoints:
// - / (GET): initialization, negotiates headers and returns the domain filter
// - /records (GET): returns the current records
// - /records (POST): applies the changes
// - /adjustendpoints (POST): executes the AdjustEndpoints method
func Init(config configuration.Config, p *webhook.Webhook) *http.Server {
r := chi.NewRouter()
// HealthCheckHandler returns the status of the service
func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}

// ReadinessHandler returns whether the service is ready to accept requests
func ReadinessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}

// Init initializes the http server
func Init(config configuration.Config, p *webhook.Webhook) (*http.Server, *http.Server) {
mainRouter := chi.NewRouter()
mainRouter.Get("/", p.Negotiate)
mainRouter.Get("/records", p.Records)
mainRouter.Post("/records", p.ApplyChanges)
mainRouter.Post("/adjustendpoints", p.AdjustEndpoints)

mainServer := createHTTPServer(fmt.Sprintf("%s:%d", config.ServerHost, config.ServerPort), mainRouter, config.ServerReadTimeout, config.ServerWriteTimeout)
go func() {
log.Infof("starting server on addr: '%s' ", mainServer.Addr)
if err := mainServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Errorf("can't serve on addr: '%s', error: %v", mainServer.Addr, err)
}
}()

r.Use(webhook.Health)
r.Get("/", p.Negotiate)
r.Get("/records", p.Records)
r.Post("/records", p.ApplyChanges)
r.Post("/adjustendpoints", p.AdjustEndpoints)
healthRouter := chi.NewRouter()
healthRouter.Get("/metrics", promhttp.Handler().ServeHTTP)
healthRouter.Get("/healthz", HealthCheckHandler)
healthRouter.Get("/readyz", ReadinessHandler)

srv := createHTTPServer(fmt.Sprintf("%s:%d", config.ServerHost, config.ServerPort), r, config.ServerReadTimeout, config.ServerWriteTimeout)
healthServer := createHTTPServer("0.0.0.0:8080", healthRouter, config.ServerReadTimeout, config.ServerWriteTimeout)
go func() {
log.Infof("starting server on addr: '%s' ", srv.Addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Errorf("can't serve on addr: '%s', error: %v", srv.Addr, err)
log.Infof("starting health server on addr: '%s' ", healthServer.Addr)
if err := healthServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Errorf("can't serve health on addr: '%s', error: %v", healthServer.Addr, err)
}
}()
return srv

return mainServer, healthServer
}

func createHTTPServer(addr string, hand http.Handler, readTimeout, writeTimeout time.Duration) *http.Server {
Expand All @@ -52,15 +72,20 @@ func createHTTPServer(addr string, hand http.Handler, readTimeout, writeTimeout
}

// ShutdownGracefully gracefully shutdown the http server
func ShutdownGracefully(srv *http.Server) {
func ShutdownGracefully(mainServer *http.Server, healthServer *http.Server) {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
sig := <-sigCh

log.Infof("shutting down server due to received signal: %v", sig)
log.Infof("shutting down servers due to received signal: %v", sig)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := srv.Shutdown(ctx); err != nil {
log.Errorf("error shutting down server: %v", err)
defer cancel()

if err := mainServer.Shutdown(ctx); err != nil {
log.Errorf("error shutting down main server: %v", err)
}

if err := healthServer.Shutdown(ctx); err != nil {
log.Errorf("error shutting down health server: %v", err)
}
cancel()
}
4 changes: 2 additions & 2 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ func main() {
log.Fatalf("failed to initialize provider: %v", err)
}

srv := server.Init(config, webhook.New(provider))
server.ShutdownGracefully(srv)
main, health := server.Init(config, webhook.New(provider))
server.ShutdownGracefully(main, health)
}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ toolchain go1.22.3
require (
github.com/caarlos0/env/v11 v11.0.1
github.com/go-chi/chi/v5 v5.0.12
github.com/prometheus/client_golang v1.19.1
github.com/sirupsen/logrus v1.9.3
golang.org/x/net v0.25.0
sigs.k8s.io/external-dns v0.14.2
)

require (
github.com/aws/aws-sdk-go v1.53.9 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand All @@ -23,9 +26,12 @@ 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
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.53.0 // indirect
github.com/prometheus/procfs v0.12.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
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/apimachinery v0.30.1 // indirect
Expand Down
17 changes: 14 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caarlos0/env/v11 v11.0.1 h1:A8dDt9Ub9ybqRSUF3fQc/TA/gTam2bKT4Pit+cwrsPs=
github.com/caarlos0/env/v11 v11.0.1/go.mod h1:2RC3HQu8BQqtEK3V4iHPxj0jOdWdbPpWJ6pOueeU1xM=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -27,11 +31,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -42,6 +43,14 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
Expand Down Expand Up @@ -89,6 +98,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down
50 changes: 34 additions & 16 deletions internal/unifi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ type httpClient struct {
}

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

// newUnifiClient creates a new DNS provider client and logs in to store cookies.
Expand Down Expand Up @@ -55,14 +54,21 @@ func newUnifiClient(config *Config) (*httpClient, error) {

// 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,
jsonBody, err := json.Marshal(Login{
Username: c.Config.User,
Password: c.Config.Password,
Remember: true,
})
if err != nil {
return err
}

// Perform the login request
resp, err := c.doRequest(http.MethodPost, fmt.Sprintf(unifiLoginPath, c.Config.Host), bytes.NewBuffer(body))
resp, err := c.doRequest(
http.MethodPost,
FormatUrl(unifiLoginPath, c.Config.Host),
bytes.NewBuffer(jsonBody),
)
if err != nil {
return err
}
Expand Down Expand Up @@ -130,7 +136,11 @@ func (c *httpClient) doRequest(method, path string, body io.Reader) (*http.Respo

// 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, c.Config.Site), nil)
resp, err := c.doRequest(
http.MethodGet,
FormatUrl(unifiRecordPath, c.Config.Host, c.Config.Site),
nil,
)
if err != nil {
return nil, err
}
Expand All @@ -155,9 +165,13 @@ func (c *httpClient) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, er
Value: endpoint.Targets[0],
})
if err != nil {
return nil, fmt.Errorf("failed to marshal DNS record: %w", err)
return nil, err
}
resp, err := c.doRequest(http.MethodPost, fmt.Sprintf(unifiRecordsPath, c.Config.Host, c.Config.Site), bytes.NewReader(jsonBody))
resp, err := c.doRequest(
http.MethodPost,
FormatUrl(unifiRecordPath, c.Config.Host, c.Config.Site),
bytes.NewReader(jsonBody),
)
if err != nil {
return nil, err
}
Expand All @@ -174,20 +188,24 @@ func (c *httpClient) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, er

// 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)
lookup, err := c.lookupIdentifier(endpoint.DNSName, endpoint.RecordType)
if err != nil {
return err
}

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

return nil
}

// LookupIdentifier finds the ID of a DNS record in the UniFi controller.
func (c *httpClient) LookupIdentifier(key, recordType string) (*DNSRecord, error) {
// 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
Expand All @@ -199,7 +217,7 @@ func (c *httpClient) LookupIdentifier(key, recordType string) (*DNSRecord, error
}
}

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

// setHeaders sets the headers for the HTTP request.
Expand Down
9 changes: 8 additions & 1 deletion internal/unifi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package unifi

import "sigs.k8s.io/external-dns/endpoint"

// Config holds configuration from environmental variables
// Config represents the configuration for the UniFi API.
type Config struct {
Host string `env:"UNIFI_HOST,notEmpty"`
User string `env:"UNIFI_USER,notEmpty"`
Expand All @@ -11,6 +11,13 @@ type Config struct {
SkipTLSVerify bool `env:"UNIFI_SKIP_TLS_VERIFY" envDefault:"true"`
}

// Login represents a login request to the UniFi API.
type Login struct {
Username string `json:"username"`
Password string `json:"password"`
Remember bool `json:"remember"`
}

// DNSRecord represents a DNS record in the UniFi API.
type DNSRecord struct {
ID string `json:"_id,omitempty"`
Expand Down
14 changes: 14 additions & 0 deletions internal/unifi/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package unifi

import "strings"

// FormatUrl formats a URL with the given parameters.
func FormatUrl(path string, params ...string) string {
segments := strings.Split(path, "%s")
for i, param := range params {
if param != "" {
segments[i] += param
}
}
return strings.Join(segments, "")
}
12 changes: 0 additions & 12 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const (
contentTypePlaintext = "text/plain"
acceptHeader = "Accept"
varyHeader = "Vary"
healthPath = "/healthz"
logFieldRequestPath = "requestPath"
logFieldRequestMethod = "requestMethod"
logFieldError = "error"
Expand All @@ -34,17 +33,6 @@ func New(provider provider.Provider) *Webhook {
return &p
}

// Health handles the health request
func Health(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == healthPath {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}

func (p *Webhook) contentTypeHeaderCheck(w http.ResponseWriter, r *http.Request) error {
return p.headerCheck(true, w, r)
}
Expand Down

0 comments on commit adf6ba9

Please sign in to comment.