From a802689af8451063b4c5cde1041d67936812c91c Mon Sep 17 00:00:00 2001 From: Steven Kreitzer Date: Fri, 24 May 2024 08:03:16 -0500 Subject: [PATCH] fix: everything --- .github/workflows/release.yaml | 3 + Dockerfile | 9 +- .../init/configuration/configuration.go | 29 +++ cmd/webhook/init/dnsprovider/dnsprovider.go | 54 +++++ cmd/webhook/init/logging/log.go | 37 ++++ .../webhook/init}/server/server.go | 8 +- cmd/webhook/main.go | 38 ++++ go.mod | 5 +- go.sum | 74 +++---- .../unifi/client.go | 188 ++++++------------ internal/unifi/configuration.go | 9 + internal/unifi/provider.go | 88 ++++++++ main.go | 44 ---- pkg/webhook/mediatype.go | 41 ++++ {webhook => pkg/webhook}/webhook.go | 74 +++---- webhook/configuration/configuration.go | 29 --- webhook/logging/log.go | 41 ---- 17 files changed, 429 insertions(+), 342 deletions(-) create mode 100644 cmd/webhook/init/configuration/configuration.go create mode 100644 cmd/webhook/init/dnsprovider/dnsprovider.go create mode 100644 cmd/webhook/init/logging/log.go rename {webhook => cmd/webhook/init}/server/server.go (90%) create mode 100644 cmd/webhook/main.go rename dnsprovider/dnsprovider.go => internal/unifi/client.go (53%) create mode 100644 internal/unifi/configuration.go create mode 100644 internal/unifi/provider.go delete mode 100644 main.go create mode 100644 pkg/webhook/mediatype.go rename {webhook => pkg/webhook}/webhook.go (84%) delete mode 100644 webhook/configuration/configuration.go delete mode 100644 webhook/logging/log.go diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1c45a3f..10106dc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -56,3 +56,6 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} diff --git a/Dockerfile b/Dockerfile index fd92e32..f5d0673 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ FROM golang:1.22-alpine as builder +ARG PKG=github.com/kashalls/external-dns-unifi-webhook +ARG VERSION=dev +ARG REVISION=dev WORKDIR /build -COPY go.mod go.sum /build/ -RUN go mod download COPY . . -RUN go build -o /external-dns-unifi-webhook +RUN go build -ldflags "-s -w -X main.Version=${VERSION} -X main.Gitsha=${REVISION}" ./cmd/webhook FROM gcr.io/distroless/static-debian12:nonroot USER 8675:8675 -COPY --from=builder --chmod=555 /external-dns-unifi-webhook /usr/local/bin/external-dns-unifi-webhook +COPY --from=builder --chmod=555 /build/webhook /usr/local/bin/external-dns-unifi-webhook EXPOSE 8888/tcp ENTRYPOINT ["/usr/local/bin/external-dns-unifi-webhook"] diff --git a/cmd/webhook/init/configuration/configuration.go b/cmd/webhook/init/configuration/configuration.go new file mode 100644 index 0000000..fd5a17d --- /dev/null +++ b/cmd/webhook/init/configuration/configuration.go @@ -0,0 +1,29 @@ +package configuration + +import ( + "time" + + "github.com/caarlos0/env/v11" + log "github.com/sirupsen/logrus" +) + +// Config struct for configuration environmental variables +type Config struct { + 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"` + DomainFilter []string `env:"DOMAIN_FILTER" envDefault:""` + ExcludeDomains []string `env:"EXCLUDE_DOMAIN_FILTER" envDefault:""` + RegexDomainFilter string `env:"REGEXP_DOMAIN_FILTER" envDefault:""` + RegexDomainExclusion string `env:"REGEXP_DOMAIN_FILTER_EXCLUSION" envDefault:""` +} + +// Init sets up configuration by reading set environmental variables +func Init() Config { + cfg := Config{} + if err := env.Parse(&cfg); err != nil { + log.Fatalf("error reading configuration from environment: %v", err) + } + return cfg +} diff --git a/cmd/webhook/init/dnsprovider/dnsprovider.go b/cmd/webhook/init/dnsprovider/dnsprovider.go new file mode 100644 index 0000000..f27ef42 --- /dev/null +++ b/cmd/webhook/init/dnsprovider/dnsprovider.go @@ -0,0 +1,54 @@ +package dnsprovider + +import ( + "fmt" + "regexp" + "strings" + + "github.com/caarlos0/env/v11" + "github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/configuration" + "github.com/kashalls/external-dns-provider-unifi/internal/unifi" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/provider" + + log "github.com/sirupsen/logrus" +) + +type UnifiProviderFactory func(baseProvider *provider.BaseProvider, unifiConfig *unifi.Configuration) provider.Provider + +func Init(config configuration.Config) (provider.Provider, error) { + var domainFilter endpoint.DomainFilter + createMsg := "creating unifi provider with " + + if config.RegexDomainFilter != "" { + createMsg += fmt.Sprintf("regexp domain filter: '%s', ", config.RegexDomainFilter) + if config.RegexDomainExclusion != "" { + createMsg += fmt.Sprintf("with exclusion: '%s', ", config.RegexDomainExclusion) + } + domainFilter = endpoint.NewRegexDomainFilter( + regexp.MustCompile(config.RegexDomainFilter), + regexp.MustCompile(config.RegexDomainExclusion), + ) + } else { + if config.DomainFilter != nil && len(config.DomainFilter) > 0 { + createMsg += fmt.Sprintf("domain filter: '%s', ", strings.Join(config.DomainFilter, ",")) + } + if config.ExcludeDomains != nil && len(config.ExcludeDomains) > 0 { + createMsg += fmt.Sprintf("exclude domain filter: '%s', ", strings.Join(config.ExcludeDomains, ",")) + } + domainFilter = endpoint.NewDomainFilterWithExclusions(config.DomainFilter, config.ExcludeDomains) + } + + createMsg = strings.TrimSuffix(createMsg, ", ") + if strings.HasSuffix(createMsg, "with ") { + createMsg += "no kind of domain filters" + } + log.Info(createMsg) + + unifiConfig := unifi.Configuration{} + if err := env.Parse(&unifiConfig); err != nil { + return nil, fmt.Errorf("reading adguard configuration failed: %v", err) + } + + return unifi.NewUnifiProvider(domainFilter, &unifiConfig) +} diff --git a/cmd/webhook/init/logging/log.go b/cmd/webhook/init/logging/log.go new file mode 100644 index 0000000..9b72e1b --- /dev/null +++ b/cmd/webhook/init/logging/log.go @@ -0,0 +1,37 @@ +package logging + +import ( + "os" + + log "github.com/sirupsen/logrus" +) + +func Init() { + setLogLevel() + setLogFormat() +} + +func setLogFormat() { + format := os.Getenv("LOG_FORMAT") + if format == "test" { + log.SetFormatter(&log.TextFormatter{}) + } else { + log.SetFormatter(&log.JSONFormatter{}) + } +} + +func setLogLevel() { + level := os.Getenv("LOG_LEVEL") + switch level { + case "debug": + log.SetLevel(log.DebugLevel) + case "info": + log.SetLevel(log.InfoLevel) + case "warn": + log.SetLevel(log.WarnLevel) + case "error": + log.SetLevel(log.ErrorLevel) + default: + log.SetLevel(log.InfoLevel) + } +} diff --git a/webhook/server/server.go b/cmd/webhook/init/server/server.go similarity index 90% rename from webhook/server/server.go rename to cmd/webhook/init/server/server.go index dde25d3..f21d8e0 100644 --- a/webhook/server/server.go +++ b/cmd/webhook/init/server/server.go @@ -11,9 +11,8 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/kashalls/external-dns-unifi-webhook/webhook" - "github.com/kashalls/external-dns-unifi-webhook/webhook/configuration" + "github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/configuration" + "github.com/kashalls/external-dns-provider-unifi/pkg/webhook" log "github.com/sirupsen/logrus" ) @@ -26,8 +25,8 @@ import ( // - /adjustendpoints (POST): executes the AdjustEndpoints method func Init(config configuration.Config, p *webhook.Webhook) *http.Server { r := chi.NewRouter() + r.Use(webhook.Health) - r.Use(middleware.Logger) r.Get("/", p.Negotiate) r.Get("/records", p.Records) r.Post("/records", p.ApplyChanges) @@ -57,6 +56,7 @@ func ShutdownGracefully(srv *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) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) if err := srv.Shutdown(ctx); err != nil { diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go new file mode 100644 index 0000000..ed0e8b7 --- /dev/null +++ b/cmd/webhook/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + + "github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/configuration" + "github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/dnsprovider" + "github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/logging" + "github.com/kashalls/external-dns-provider-unifi/cmd/webhook/init/server" + "github.com/kashalls/external-dns-provider-unifi/pkg/webhook" + log "github.com/sirupsen/logrus" +) + +const banner = ` +external-dns-provider-unifi +version: %s (%s) + +` + +var ( + Version = "local" + Gitsha = "?" +) + +func main() { + fmt.Printf(banner, Version, Gitsha) + + logging.Init() + + config := configuration.Init() + provider, err := dnsprovider.Init(config) + if err != nil { + log.Fatalf("failed to initialize provider: %v", err) + } + + srv := server.Init(config, webhook.New(provider)) + server.ShutdownGracefully(srv) +} diff --git a/go.mod b/go.mod index d864066..cb463b8 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/kashalls/external-dns-unifi-webhook +module github.com/kashalls/external-dns-provider-unifi go 1.22.2 @@ -9,7 +9,6 @@ require ( github.com/go-chi/chi/v5 v5.0.12 github.com/sirupsen/logrus v1.9.3 sigs.k8s.io/external-dns v0.14.2 - ) require ( @@ -20,11 +19,13 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + 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 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/apimachinery v0.30.1 // indirect diff --git a/go.sum b/go.sum index 8ed11b5..f0df857 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,18 @@ -github.com/aws/aws-sdk-go v1.45.26 h1:PJ2NJNY5N/yeobLYe1Y+xLdavBi67ZI8gvph6ftwVCg= -github.com/aws/aws-sdk-go v1.45.26/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +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= github.com/caarlos0/env/v11 v11.0.1/go.mod h1:2RC3HQu8BQqtEK3V4iHPxj0jOdWdbPpWJ6pOueeU1xM= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +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.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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= @@ -30,87 +31,71 @@ 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= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -119,28 +104,19 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= -k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= 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.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 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-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +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.13.6 h1:3v9ycedZYE3jpyFoAQ7AXxZrPVUbAbxKTe1CVH3ujnw= -sigs.k8s.io/external-dns v0.13.6/go.mod h1:zM2hq8Kr7rStCB07/WecyveJUpYgBVjzoe+WYRjN0is= sigs.k8s.io/external-dns v0.14.2 h1:j7rYtQqDAxYfN9N1/BZcRdzUBRsnZp4tZcuZ75ekTlc= sigs.k8s.io/external-dns v0.14.2/go.mod h1:GTFER2cqUxkSpYNzzkge8USXp1wJmxqWwpdXr2lYdik= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk= -sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/dnsprovider/dnsprovider.go b/internal/unifi/client.go similarity index 53% rename from dnsprovider/dnsprovider.go rename to internal/unifi/client.go index 4c12ed3..49eeeaf 100644 --- a/dnsprovider/dnsprovider.go +++ b/internal/unifi/client.go @@ -1,8 +1,7 @@ -package dnsprovider +package unifi import ( "bytes" - "context" "crypto/tls" "encoding/json" "fmt" @@ -10,12 +9,17 @@ import ( "net/http" "net/http/cookiejar" - log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" ) +// Client is the DNS provider client. +type Client struct { + BaseURL string + HTTPClient *http.Client + csrf string + records []DNSRecord +} + // DNSRecord represents a DNS record in the API. type DNSRecord struct { ID string `json:"_id,omitempty"` @@ -29,39 +33,32 @@ type DNSRecord struct { Weight int `json:"weight,omitempty"` } -// Client is the DNS provider client. -type Client struct { - BaseURL string - HTTPClient *http.Client - csrf string -} - 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" ) -// NewClient creates a new DNS provider client and logs in to store cookies. -func NewClient(baseURL, username, password string, skipTLSVerify bool) (*Client, error) { +// newUnifiClient creates a new DNS provider client and logs in to store cookies. +func newUnifiClient(config *Configuration) (*Client, error) { jar, err := cookiejar.New(nil) if err != nil { return nil, err } transport := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTLSVerify}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: config.SkipTLSVerify}, } client := &Client{ - BaseURL: baseURL, + BaseURL: config.Host, HTTPClient: &http.Client{ Transport: transport, Jar: jar, }, } - if err := client.login(username, password); err != nil { + if err := client.login(config.User, config.Password); err != nil { return nil, err } @@ -71,10 +68,12 @@ func NewClient(baseURL, username, password string, skipTLSVerify bool) (*Client, // login authenticates the client and stores the cookies. func (c *Client) login(username, password string) error { loginURL := fmt.Sprintf(UnifiLogin, c.BaseURL) + credentials := map[string]string{ "username": username, "password": password, } + body, err := json.Marshal(credentials) if err != nil { return err @@ -107,8 +106,6 @@ func (c *Client) GetData(url string) ([]byte, error) { if err != nil { return nil, err } - log.Debugf("get data: %s", resp.Status) - log.Debugf("get data: %v", resp.Body) if csrf := resp.Header.Get("x-csrf-token"); csrf != "" { c.csrf = resp.Header.Get("x-csrf-token") @@ -129,11 +126,10 @@ func (c *Client) ShipData(url string, body []byte) ([]byte, error) { c.setHeaders(req) resp, err := c.HTTPClient.Do(req) + if err != nil { return nil, err } - log.Debugf("post data: %s", resp.Status) - log.Debugf("post data: %v", resp.Body) if csrf := resp.Header.Get("x-csrf-token"); csrf != "" { c.csrf = resp.Header.Get("x-csrf-token") @@ -157,8 +153,6 @@ func (c *Client) PutData(url string, body []byte) ([]byte, error) { if err != nil { return nil, err } - log.Debugf("put data: %s", resp.Status) - log.Debugf("put data: %v", resp.Body) if csrf := resp.Header.Get("x-csrf-token"); csrf != "" { c.csrf = resp.Header.Get("x-csrf-token") @@ -175,15 +169,14 @@ func (c *Client) PutData(url string, body []byte) ([]byte, error) { } func (c *Client) DeleteData(url string) ([]byte, error) { - req, _ := http.NewRequest(http.MethodPost, url, nil) - c.setHeaders(req) + req, _ := http.NewRequest(http.MethodDelete, url, nil) + c.setHeaders(req) resp, err := c.HTTPClient.Do(req) + if err != nil { return nil, err } - log.Debugf("delete data: %s", resp.Status) - log.Debugf("delete data: %v", resp.Body) if csrf := resp.Header.Get("x-csrf-token"); csrf != "" { c.csrf = resp.Header.Get("x-csrf-token") @@ -212,11 +205,19 @@ func (c *Client) ListRecords() ([]DNSRecord, error) { return nil, err } + c.records = records // store records for later use return records, nil } -// CreateRecord creates a new DNS record. -func (c *Client) CreateRecord(record DNSRecord) (*DNSRecord, error) { +// CreateEndpoint creates a new DNS record. +func (c *Client) CreateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, error) { + record := DNSRecord{ + Key: endpoint.DNSName, + RecordType: endpoint.RecordType, + TTL: endpoint.RecordTTL, + Value: endpoint.Targets[0], + } + body, err := json.Marshal(record) if err != nil { return nil, err @@ -236,14 +237,26 @@ func (c *Client) CreateRecord(record DNSRecord) (*DNSRecord, error) { return &newRecord, nil } -// UpdateRecord updates an existing DNS record. -func (c *Client) UpdateRecord(id string, record DNSRecord) (*DNSRecord, error) { +// UpdateEndpoint updates an existing DNS record. +func (c *Client) UpdateEndpoint(endpoint *endpoint.Endpoint) (*DNSRecord, error) { + id, err := c.LookupIdentifier(endpoint.DNSName, endpoint.RecordType) + if err != nil { + return nil, err + } + + record := DNSRecord{ + Key: endpoint.DNSName, + RecordType: endpoint.RecordType, + TTL: endpoint.RecordTTL, + Value: endpoint.Targets[0], + } + body, err := json.Marshal(record) if err != nil { return nil, err } - resp, err := c.PutData(fmt.Sprintf("%s/proxy/network/v2/api/site/default/static-dns/%s", c.BaseURL, id), body) + resp, err := c.PutData(fmt.Sprintf(UnifiDNSSelectRecord, c.BaseURL, id), body) if err != nil { return nil, err } @@ -253,117 +266,36 @@ func (c *Client) UpdateRecord(id string, record DNSRecord) (*DNSRecord, error) { if err != nil { return nil, err } + return &updatedRecord, nil } -// DeleteRecord deletes a DNS record. -func (c *Client) DeleteRecord(id string) error { - _, err := c.DeleteData(fmt.Sprintf(UnifiDNSSelectRecord, c.BaseURL, id)) +// DeleteEndpoint deletes a DNS record. +func (c *Client) DeleteEndpoint(endpoint *endpoint.Endpoint) error { + id, err := c.LookupIdentifier(endpoint.DNSName, endpoint.RecordType) if err != nil { return err } - return nil -} - -// DNSProvider implements the provider.Provider interface. -type DNSProvider struct { - client *Client -} - -// NewDNSProvider initializes a new DNSProvider. -func NewDNSProvider(baseURL, username, password string, skipTLSVerify bool) (provider.Provider, error) { - client, err := NewClient(baseURL, username, password, skipTLSVerify) + _, err = c.DeleteData(fmt.Sprintf(UnifiDNSSelectRecord, c.BaseURL, id)) if err != nil { - return nil, err - } - return &DNSProvider{ - client: client, - }, nil -} - -// Records returns the list of records in the DNS provider. -func (p *DNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - records, err := p.client.ListRecords() - if err != nil { - return nil, err + return err } - jsonData, _ := json.Marshal(records) - jsonString := string(jsonData) - fmt.Println(jsonString) - - var endpoints []*endpoint.Endpoint - for _, record := range records { - endpoints = append(endpoints, &endpoint.Endpoint{ - DNSName: record.Key, - Targets: []string{record.Value}, - RecordType: record.RecordType, - SetIdentifier: record.ID, - RecordTTL: record.TTL, - }) - } - return endpoints, nil + return nil } -// ApplyChanges applies a given set of changes in the DNS provider. -func (p *DNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - - jsonData, _ := json.Marshal(changes) - jsonString := string(jsonData) - fmt.Println(jsonString) - - for _, ep := range changes.Create { - record := DNSRecord{ - Key: ep.DNSName, - Value: ep.Targets[0], - RecordType: ep.RecordType, - TTL: ep.RecordTTL, - } - if _, err := p.client.CreateRecord(record); err != nil { - return err - } - } - - for _, ep := range changes.UpdateNew { - record := DNSRecord{ - ID: ep.SetIdentifier, - Key: ep.DNSName, - Value: ep.Targets[0], - RecordType: ep.RecordType, - TTL: ep.RecordTTL, - } - // Assuming ID can be obtained from DNS name - id := ep.DNSName // This needs to be changed to actual ID fetching logic - if _, err := p.client.UpdateRecord(id, record); err != nil { - return err - } +// LookupIdentifier finds the ID of a DNS record. +func (c *Client) LookupIdentifier(Key string, RecordType string) (string, error) { + if c.records == nil { + c.ListRecords() } - for _, ep := range changes.Delete { - // Assuming ID can be obtained from DNS name - id := ep.DNSName // This needs to be changed to actual ID fetching logic - if err := p.client.DeleteRecord(id); err != nil { - return err + for _, r := range c.records { + if r.Key == Key && r.RecordType == RecordType { + return r.ID, nil } } - return nil -} - -// AdjustEndpoints modifies the endpoints before they are sent to the DNS provider. -func (p *DNSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { - // Implement any adjustments needed to the endpoints before processing - return endpoints, nil -} - -// GetDomainFilter returns the domain filter for the provider. -func (p *DNSProvider) GetDomainFilter() endpoint.DomainFilter { - // Since we're not using domain filtering, return an empty filter that matches everything - return endpoint.NewDomainFilter([]string{}) -} - -// GetDNSName returns the DNS provider's name. -func (p *DNSProvider) GetDNSName() string { - return "external-dns-unifi-webhook" + return "", fmt.Errorf("record not found") } diff --git a/internal/unifi/configuration.go b/internal/unifi/configuration.go new file mode 100644 index 0000000..f1344c3 --- /dev/null +++ b/internal/unifi/configuration.go @@ -0,0 +1,9 @@ +package unifi + +// Configuration holds configuration from environmental variables +type Configuration struct { + Host string `env:"UNIFI_HOST"` + User string `env:"UNIFI_USER" envDefault:"external-dns-unifi"` + Password string `env:"UNIFI_PASS" envDefault:"V3ryS3cur3!!"` + SkipTLSVerify bool `env:"UNIFI_SKIP_TLS_VERIFY" envDefault:"false"` +} diff --git a/internal/unifi/provider.go b/internal/unifi/provider.go new file mode 100644 index 0000000..98a0caf --- /dev/null +++ b/internal/unifi/provider.go @@ -0,0 +1,88 @@ +package unifi + +import ( + "context" + "fmt" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +// Provider type for interfacing with Adguard +type Provider struct { + provider.BaseProvider + + client *Client + domainFilter endpoint.DomainFilter +} + +// newUnifiProvider initializes a new DNSProvider. +func NewUnifiProvider(domainFilter endpoint.DomainFilter, config *Configuration) (provider.Provider, error) { + c, err := newUnifiClient(config) + + if err != nil { + return nil, fmt.Errorf("failed to create the unifi client: %w", err) + } + + p := &Provider{ + client: c, + domainFilter: domainFilter, + } + + return p, nil +} + +// Records returns the list of records in the DNS provider. +func (p *Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + records, err := p.client.ListRecords() + if err != nil { + return nil, err + } + + var endpoints []*endpoint.Endpoint + for _, record := range records { + ep := endpoint.NewEndpointWithTTL( + record.Key, + record.RecordType, + record.TTL, + record.Value, + ) + + if !p.domainFilter.Match(ep.DNSName) { + continue + } + + endpoints = append(endpoints, ep) + } + + return endpoints, nil +} + +// ApplyChanges applies a given set of changes in the DNS provider. +func (p *Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + for _, ep := range changes.Create { + if _, err := p.client.CreateEndpoint(ep); err != nil { + return err + } + } + + for _, ep := range changes.UpdateNew { + if _, err := p.client.UpdateEndpoint(ep); err != nil { + return err + } + } + + for _, ep := range changes.Delete { + if err := p.client.DeleteEndpoint(ep); err != nil { + return err + } + } + + return nil +} + +// GetDomainFilter returns the domain filter for the provider. +func (p *Provider) GetDomainFilter() endpoint.DomainFilter { + return p.domainFilter +} diff --git a/main.go b/main.go deleted file mode 100644 index 94d7411..0000000 --- a/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/kashalls/external-dns-unifi-webhook/dnsprovider" - "github.com/kashalls/external-dns-unifi-webhook/webhook" - "github.com/kashalls/external-dns-unifi-webhook/webhook/configuration" - "github.com/kashalls/external-dns-unifi-webhook/webhook/logging" - "github.com/kashalls/external-dns-unifi-webhook/webhook/server" - log "github.com/sirupsen/logrus" -) - -const banner = ` -::: ::: :::: ::: ::::::::::: :::::::::: ::::::::::: -:+: :+: :+:+: :+: :+: :+: :+: -+:+ +:+ :+:+:+ +:+ +:+ +:+ +:+ -+#+ +:+ +#+ +:+ +#+ +#+ :#::+::# +#+ -+#+ +#+ +#+ +#+#+# +#+ +#+ +#+ -#+# #+# #+# #+#+# #+# #+# #+# - ######## ### #### ########### ### ########### - - external-dns-unifi-webhook - version: %s - -` - -var ( - Version = "v0.0.2" -) - -func main() { - fmt.Printf(banner, Version) - logging.Init() - config := configuration.Init() - - provider, err := dnsprovider.NewDNSProvider(config.UnifiHost, config.UnifiUser, config.UnifiPass, config.UnifiSkipTLSVerify) - - if err != nil { - log.Fatalf("Failed to initialize DNS provider: %v", err) - } - srv := server.Init(config, webhook.New(provider)) - server.ShutdownGracefully(srv) -} diff --git a/pkg/webhook/mediatype.go b/pkg/webhook/mediatype.go new file mode 100644 index 0000000..39f2ba8 --- /dev/null +++ b/pkg/webhook/mediatype.go @@ -0,0 +1,41 @@ +package webhook + +import ( + "fmt" + "strings" +) + +const ( + mediaTypeFormat = "application/external.dns.webhook+json;" + supportedMediaVersions = "1" +) + +var mediaTypeVersion1 = mediaTypeVersion("1") + +type mediaType string + +func mediaTypeVersion(v string) mediaType { + return mediaType(mediaTypeFormat + "version=" + v) +} + +func (m mediaType) Is(headerValue string) bool { + return string(m) == headerValue +} + +func checkAndGetMediaTypeHeaderValue(value string) (string, error) { + for _, v := range strings.Split(supportedMediaVersions, ",") { + if mediaTypeVersion(v).Is(value) { + return v, nil + } + } + + supportedMediaTypesString := "" + for i, v := range strings.Split(supportedMediaVersions, ",") { + sep := "" + if i < len(supportedMediaVersions)-1 { + sep = ", " + } + supportedMediaTypesString += string(mediaTypeVersion(v)) + sep + } + return "", fmt.Errorf("unsupported media type version: '%s'. supported media types are: '%s'", value, supportedMediaTypesString) +} diff --git a/webhook/webhook.go b/pkg/webhook/webhook.go similarity index 84% rename from webhook/webhook.go rename to pkg/webhook/webhook.go index c16e499..78bfbc3 100644 --- a/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -4,56 +4,25 @@ import ( "encoding/json" "fmt" "net/http" - "strings" log "github.com/sirupsen/logrus" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" ) const ( - mediaTypeFormat = "application/external.dns.webhook+json;" - contentTypeHeader = "Content-Type" - contentTypePlaintext = "text/plain" - acceptHeader = "Accept" - varyHeader = "Vary" - supportedMediaVersions = "1" - healthPath = "/healthz" - logFieldRequestPath = "requestPath" - logFieldRequestMethod = "requestMethod" - logFieldError = "error" + contentTypeHeader = "Content-Type" + contentTypePlaintext = "text/plain" + acceptHeader = "Accept" + varyHeader = "Vary" + healthPath = "/healthz" + logFieldRequestPath = "requestPath" + logFieldRequestMethod = "requestMethod" + logFieldError = "error" ) -var mediaTypeVersion1 = mediaTypeVersion("1") - -type mediaType string - -func mediaTypeVersion(v string) mediaType { - return mediaType(mediaTypeFormat + "version=" + v) -} - -func (m mediaType) Is(headerValue string) bool { - return string(m) == headerValue -} - -func checkAndGetMediaTypeHeaderValue(value string) (string, error) { - for _, v := range strings.Split(supportedMediaVersions, ",") { - if mediaTypeVersion(v).Is(value) { - return v, nil - } - } - supportedMediaTypesString := "" - for i, v := range strings.Split(supportedMediaVersions, ",") { - sep := "" - if i < len(supportedMediaVersions)-1 { - sep = ", " - } - supportedMediaTypesString += string(mediaTypeVersion(v)) + sep - } - return "", fmt.Errorf("unsupported media type version: '%s'. Supported media types are: '%s'", value, supportedMediaTypesString) -} - // Webhook for external dns provider type Webhook struct { provider provider.Provider @@ -65,6 +34,7 @@ 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 { @@ -90,9 +60,11 @@ func (p *Webhook) headerCheck(isContentType bool, w http.ResponseWriter, r *http } else { header = r.Header.Get(acceptHeader) } + if len(header) == 0 { w.Header().Set(contentTypeHeader, contentTypePlaintext) w.WriteHeader(http.StatusNotAcceptable) + msg := "client must provide " if isContentType { msg += "a content type" @@ -100,22 +72,26 @@ func (p *Webhook) headerCheck(isContentType bool, w http.ResponseWriter, r *http msg += "an accept header" } err := fmt.Errorf(msg) + _, writeErr := fmt.Fprint(w, err.Error()) if writeErr != nil { requestLog(r).WithField(logFieldError, writeErr).Fatalf("error writing error message to response writer") } return err } + // as we support only one media type version, we can ignore the returned value if _, err := checkAndGetMediaTypeHeaderValue(header); err != nil { w.Header().Set(contentTypeHeader, contentTypePlaintext) w.WriteHeader(http.StatusUnsupportedMediaType) + msg := "client must provide a valid versioned media type in the " if isContentType { msg += "content type" } else { msg += "accept header" } + err := fmt.Errorf(msg+": %s", err.Error()) _, writeErr := fmt.Fprint(w, err.Error()) if writeErr != nil { @@ -123,6 +99,7 @@ func (p *Webhook) headerCheck(isContentType bool, w http.ResponseWriter, r *http } return err } + return nil } @@ -132,6 +109,7 @@ func (p *Webhook) Records(w http.ResponseWriter, r *http.Request) { requestLog(r).WithField(logFieldError, err).Error("accept header check failed") return } + requestLog(r).Debug("requesting records") ctx := r.Context() records, err := p.provider.Records(ctx) @@ -140,6 +118,7 @@ func (p *Webhook) Records(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } + requestLog(r).Debugf("returning records count: %d", len(records)) w.Header().Set(contentTypeHeader, string(mediaTypeVersion1)) w.Header().Set(varyHeader, contentTypeHeader) @@ -157,11 +136,13 @@ func (p *Webhook) ApplyChanges(w http.ResponseWriter, r *http.Request) { requestLog(r).WithField(logFieldError, err).Error("content type header check failed") return } + var changes plan.Changes ctx := r.Context() if err := json.NewDecoder(r.Body).Decode(&changes); err != nil { w.Header().Set(contentTypeHeader, contentTypePlaintext) w.WriteHeader(http.StatusBadRequest) + errMsg := fmt.Sprintf("error decoding changes: %s", err.Error()) if _, writeError := fmt.Fprint(w, errMsg); writeError != nil { requestLog(r).WithField(logFieldError, writeError).Fatalf("error writing error message to response writer") @@ -169,6 +150,7 @@ func (p *Webhook) ApplyChanges(w http.ResponseWriter, r *http.Request) { requestLog(r).WithField(logFieldError, err).Info(errMsg) return } + requestLog(r).Debugf("requesting apply changes, create: %d , updateOld: %d, updateNew: %d, delete: %d", len(changes.Create), len(changes.UpdateOld), len(changes.UpdateNew), len(changes.Delete)) if err := p.provider.ApplyChanges(ctx, &changes); err != nil { @@ -194,6 +176,7 @@ func (p *Webhook) AdjustEndpoints(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&pve); err != nil { w.Header().Set(contentTypeHeader, contentTypePlaintext) w.WriteHeader(http.StatusBadRequest) + errMessage := fmt.Sprintf("failed to decode request body: %v", err) log.Infof(errMessage+" , request method: %s, request path: %s", r.Method, r.URL.Path) if _, writeError := fmt.Fprint(w, errMessage); writeError != nil { @@ -201,9 +184,16 @@ func (p *Webhook) AdjustEndpoints(w http.ResponseWriter, r *http.Request) { } return } + log.Debugf("requesting adjust endpoints count: %d", len(pve)) - pve, _ = p.provider.AdjustEndpoints(pve) + pve, err := p.provider.AdjustEndpoints(pve) + if err != nil { + w.Header().Set(contentTypeHeader, contentTypePlaintext) + w.WriteHeader(http.StatusInternalServerError) + return + } out, _ := json.Marshal(&pve) + log.Debugf("return adjust endpoints response, resultEndpointCount: %d", len(pve)) w.Header().Set(contentTypeHeader, string(mediaTypeVersion1)) w.Header().Set(varyHeader, contentTypeHeader) @@ -217,12 +207,14 @@ func (p *Webhook) Negotiate(w http.ResponseWriter, r *http.Request) { requestLog(r).WithField(logFieldError, err).Error("accept header check failed") return } + b, err := p.provider.GetDomainFilter().MarshalJSON() if err != nil { log.Errorf("failed to marshal domain filter, request method: %s, request path: %s", r.Method, r.URL.Path) w.WriteHeader(http.StatusInternalServerError) return } + w.Header().Set(contentTypeHeader, string(mediaTypeVersion1)) if _, writeError := w.Write(b); writeError != nil { requestLog(r).WithField(logFieldError, writeError).Error("error writing response") diff --git a/webhook/configuration/configuration.go b/webhook/configuration/configuration.go deleted file mode 100644 index 5dbe16b..0000000 --- a/webhook/configuration/configuration.go +++ /dev/null @@ -1,29 +0,0 @@ -package configuration - -import ( - "time" - - "github.com/caarlos0/env/v11" - log "github.com/sirupsen/logrus" -) - -// Config struct for configuration environmental variables -type Config struct { - UnifiHost string `env:"UNIFI_HOST"` - UnifiUser string `env:"UNIFI_USER" envDefault:"external-dns-unifi"` - UnifiPass string `env:"UNIFI_PASS" envDefault:"V3ryS3cur3!!"` - UnifiSkipTLSVerify bool `env:"UNIFI_SKIP_TLS_VERIFY" envDefault:"true"` - 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"` -} - -// Init sets up configuration by reading set environmental variables -func Init() Config { - cfg := Config{} - if err := env.Parse(&cfg); err != nil { - log.Fatalf("Error reading configuration from environment: %v", err) - } - return cfg -} diff --git a/webhook/logging/log.go b/webhook/logging/log.go deleted file mode 100644 index cedc547..0000000 --- a/webhook/logging/log.go +++ /dev/null @@ -1,41 +0,0 @@ -package logging - -import ( - "os" - "strconv" - - log "github.com/sirupsen/logrus" -) - -func Init() { - setLogLevel() - setLogFormat() -} - -func setLogFormat() { - format := os.Getenv("LOG_FORMAT") - if format == "json" { - log.SetFormatter(&log.JSONFormatter{}) - } else { - log.SetFormatter(&log.TextFormatter{}) - } -} - -func setLogLevel() { - level := os.Getenv("LOG_LEVEL") - if level == "" { - log.SetLevel(log.InfoLevel) - } else { - if levelInt, err := strconv.Atoi(level); err == nil { - log.SetLevel(log.Level(uint32(levelInt))) - } else { - levelInt, err := log.ParseLevel(level) - if err != nil { - log.SetLevel(log.InfoLevel) - log.Errorf("Invalid log level '%s', defaulting to info", level) - } else { - log.SetLevel(levelInt) - } - } - } -}