[feature] Initial commit

This commit is contained in:
2026-03-25 22:16:00 -04:00
parent 2814505fec
commit 7752d82001
10 changed files with 636 additions and 4 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.idea/
config.yaml

View File

@@ -1,8 +1,7 @@
ISC License:
ISC License
Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC")
Copyright (c) 1995-2003 by Internet Software Consortium
Copyright 2026 Aurora Development LLC
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

29
config.example.yaml Normal file
View File

@@ -0,0 +1,29 @@
interval: "1m"
dns:
provider: "example"
default:
ttl: "24h"
cname: "pangolin.example.com"
domains:
# example.com is using default
example.net:
ttl: "30m"
cname: "panglin.example.com"
example.org:
ttl: "30m"
address: "1.2.3.4"
pangolin:
url: "https://api.pangolin.instance"
key: ""
orgs:
- "your-org"
cloudflare:
token: ""
route53:
region: "us-east-1"
access_key: ""
secret_key: ""

24
example.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"context"
"github.com/libdns/libdns"
log "github.com/sirupsen/logrus"
)
type ExampleSetter struct {
}
func (e *ExampleSetter) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
for _, record := range recs {
rr := record.RR()
log.WithFields(log.Fields{
"zone": zone,
"name": rr.Name,
}).Info("Set/Update record")
}
return recs, nil
}

49
go.mod Normal file
View File

@@ -0,0 +1,49 @@
module PangolinDNS
go 1.26
require (
github.com/auroradevllc/apiclient v0.0.0-20260222000229-5aca4ca82829
github.com/go-playground/validator/v10 v10.30.1
github.com/joho/godotenv v1.5.1
github.com/libdns/cloudflare v0.2.2
github.com/libdns/libdns v1.1.1
github.com/libdns/route53 v1.6.0
github.com/samber/lo v1.53.0
github.com/sirupsen/logrus v1.9.4
github.com/spf13/viper v1.21.0
)
require (
github.com/aws/aws-sdk-go-v2 v1.39.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
)

117
go.sum Normal file
View File

@@ -0,0 +1,117 @@
github.com/auroradevllc/apiclient v0.0.0-20260222000229-5aca4ca82829 h1:CLoIDjfY+KBv+N7Q615+8MECxnQ55IKVUP0lWBOPBl4=
github.com/auroradevllc/apiclient v0.0.0-20260222000229-5aca4ca82829/go.mod h1:tNh5fcm93z/Y29FRzkVVKkjYKUhpN9TVid+ZrSsaH2U=
github.com/aws/aws-sdk-go-v2 v1.39.1 h1:fWZhGAwVRK/fAN2tmt7ilH4PPAE11rDj7HytrmbZ2FE=
github.com/aws/aws-sdk-go-v2 v1.39.1/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/config v1.31.10 h1:7LllDZAegXU3yk41mwM6KcPu0wmjKGQB1bg99bNdQm4=
github.com/aws/aws-sdk-go-v2/config v1.31.10/go.mod h1:Ge6gzXPjqu4v0oHvgAwvGzYcK921GU0hQM25WF/Kl+8=
github.com/aws/aws-sdk-go-v2/credentials v1.18.14 h1:TxkI7QI+sFkTItN/6cJuMZEIVMFXeu2dI1ZffkXngKI=
github.com/aws/aws-sdk-go-v2/credentials v1.18.14/go.mod h1:12x4Uw/vijC11XkctTjy92TNCQ+UnNJkT7fzX0Yd93E=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8 h1:gLD09eaJUdiszm7vd1btiQUYE0Hj+0I2b8AS+75z9AY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.8/go.mod h1:4RW3oMPt1POR74qVOC4SbubxAwdP4pCT0nSw3jycOU4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8 h1:6bgAZgRyT4RoFWhxS+aoGMFyE0cD1bSzFnEEi4bFPGI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.8/go.mod h1:KcGkXFVU8U28qS4KvLEcPxytPZPBcRawaH2Pf/0jptE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8 h1:HhJYoES3zOz34yWEpGENqJvRVPqpmJyR3+AFg9ybhdY=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.8/go.mod h1:JnA+hPWeYAVbDssp83tv+ysAG8lTfLVXvSsyKg/7xNA=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8 h1:M6JI2aGFEzYxsF6CXIuRBnkge9Wf9a2xU39rNeXgu10=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.8/go.mod h1:Fw+MyTwlwjFsSTE31mH211Np+CUslml8mzc0AFEG09s=
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.3 h1:jQzRC+0eI/l5mFXVoPTyyolrqyZtKIYaKHSuKJoIJKs=
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.3/go.mod h1:1GNaojT/gG4Ru9tT39ton6kRZ3FvptJ/QRKBoqUOVX4=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4 h1:FTdEN9dtWPB0EOURNtDPmwGp6GGvMqRJCAihkSl/1No=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.4/go.mod h1:mYubxV9Ff42fZH4kexj43gFPhgc/LyC7KqvUKt1watc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0 h1:I7ghctfGXrscr7r1Ga/mDqSJKm7Fkpl5Mwq79Z+rZqU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.0/go.mod h1:Zo9id81XP6jbayIFWNuDpA6lMBWhsVy+3ou2jLa4JnA=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 h1:+LVB0xBqEgjQoqr9bGZbRzvg212B0f17JdflleJRNR4=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.5/go.mod h1:xoaxeqnnUaZjPjaICgIy5B+MHCSb/ZSOn4MvkFNOUA0=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/route53 v1.6.0 h1:1fZcoCIxagfftw9GBhIqZ2rumEiB0K58n11X7ko2DOg=
github.com/libdns/route53 v1.6.0/go.mod h1:7QGcw/2J0VxcVwHsPYpuo1I6IJLHy77bbOvi1BVK3eE=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

261
main.go Normal file
View File

@@ -0,0 +1,261 @@
package main
import (
"PangolinDNS/pangolin"
"context"
"fmt"
"net/netip"
"os"
"reflect"
"time"
"github.com/go-playground/validator/v10"
_ "github.com/joho/godotenv/autoload"
"github.com/libdns/cloudflare"
"github.com/libdns/libdns"
"github.com/libdns/route53"
"github.com/samber/lo"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
const (
defaultTTL = 60 * 24 * time.Minute
)
type Config struct {
Interval time.Duration `mapstructure:"interval" validate:"required"`
DNS DNSConfig `mapstructure:"dns" validate:"required"`
Pangolin PangolinConfig `mapstructure:"pangolin" validate:"required"`
Cloudflare CloudflareConfig `mapstructure:"cloudflare" validate:"-"`
Route53 Route53Config `mapstructure:"route53" validate:"-"`
}
type PangolinConfig struct {
URL string `mapstructure:"url" validate:"required,url"`
Key string `mapstructure:"key" validate:"required"`
Organizations []string `mapstructure:"orgs" validate:"required"`
}
type CloudflareConfig struct {
Token string `mapstructure:"token" validate:"required"`
}
type Route53Config struct {
Region string `mapstructure:"region" validate:"required"`
AccessKey string `mapstructure:"access_key" validate:"required"`
SecretKey string `mapstructure:"secret_key" validate:"required"`
}
type DNSConfig struct {
Provider string `mapstructure:"provider" validate:"required"`
Default DNSTarget `mapstructure:"default" validate:"required"`
Domains map[string]DNSTarget `mapstructure:"domains" validate:"dive"`
}
type DNSTarget struct {
CNAME string `mapstructure:"cname" validate:"required_without=Address,omitempty,hostname|fqdn"`
Address string `mapstructure:"address" validate:"required_without=CNAME,omitempty,ip"`
TTL time.Duration `mapstructure:"ttl"`
}
type DNSSync struct {
cfg Config
pangolin *pangolin.Client
provider libdns.RecordSetter
resourceCache map[string]bool
}
func main() {
var provider libdns.RecordSetter
v := viper.NewWithOptions(viper.KeyDelimiter("::"))
v.SetConfigFile("config.yaml")
if err := v.ReadInConfig(); err != nil {
log.WithError(err).Fatal("Error reading config")
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
log.WithError(err).Fatal("Error unmarshalling config")
}
validate := validator.New()
validate.RegisterStructValidation(func(sl validator.StructLevel) {
cfg := sl.Current().Interface().(Config)
t := reflect.TypeOf(cfg)
cfgValue := reflect.ValueOf(cfg)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldName := field.Tag.Get("mapstructure")
if fieldName != cfg.DNS.Provider {
continue
}
if err := validate.Struct(cfgValue.Field(i).Interface()); err != nil {
for _, e := range err.(validator.ValidationErrors) {
sl.ReportError(
e.Value(),
fieldName+"."+e.Field(),
e.StructField(),
e.Tag(),
e.Param(),
)
}
}
break
}
}, Config{})
if err := validate.Struct(cfg); err != nil {
fmt.Println(err)
os.Exit(1)
}
switch cfg.DNS.Provider {
case "cloudflare":
provider = &cloudflare.Provider{
APIToken: cfg.Cloudflare.Token,
}
case "route53":
provider = &route53.Provider{
Region: cfg.Route53.Region,
AccessKeyId: cfg.Route53.AccessKey,
SecretAccessKey: cfg.Route53.SecretKey,
}
case "example":
provider = &ExampleSetter{}
default:
log.Fatalf("Unsupported provider %s", cfg.DNS.Provider)
}
p := pangolin.New(cfg.Pangolin.URL, cfg.Pangolin.Key)
s := &DNSSync{
cfg: cfg,
provider: provider,
pangolin: p,
resourceCache: make(map[string]bool),
}
// On initial startup, always push domains
s.checkResources(true)
if cfg.Interval <= 0 {
return
}
t := time.NewTicker(cfg.Interval)
for {
<-t.C
s.checkResources(false)
}
}
func (d *DNSSync) checkResources(force bool) {
for _, org := range d.cfg.Pangolin.Organizations {
log.Infof("Reading domains from org %s", org)
domains, err := d.pangolin.Domains(org)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"org": org,
}).Fatal("Unable to fetch domains for org")
}
resources, err := d.pangolin.Resources(org)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"org": org,
}).Fatal("Unable to fetch resources for org")
}
log.Info("Mapping domains to keys")
domainKeys := lo.Associate(domains, func(domain pangolin.Domain) (string, string) {
return domain.ID, domain.BaseDomain
})
log.Info("Mapping records to domains")
recordMap := lo.GroupBy(resources, func(record pangolin.Resource) string {
return domainKeys[record.DomainID]
})
for domain, res := range recordMap {
log.Infof("Building domain list for %s", domain)
zone := domain + "."
if !force {
res = lo.Filter(res, func(record pangolin.Resource, _ int) bool {
_, ok := d.resourceCache[record.FullDomain]
return !ok
})
}
records := lo.Map(res, func(resource pangolin.Resource, _ int) libdns.Record {
if target, ok := d.cfg.DNS.Domains[domain]; ok {
return recordFromTarget(resource.FullDomain, target)
}
return recordFromTarget(resource.FullDomain, d.cfg.DNS.Default)
})
changed, err := d.provider.SetRecords(context.Background(), zone, records)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"org": org,
"domain": domain,
}).Fatal("Unable to set records for domain")
}
for _, changed := range changed {
log.Infof("Updated record %s", changed.RR())
}
// Ensure all resources we checked are set in the cache for future runs
for _, res := range resources {
d.resourceCache[res.FullDomain] = true
}
}
}
}
func recordFromTarget(name string, target DNSTarget) libdns.Record {
ttl := defaultTTL
if target.TTL > 0 {
ttl = target.TTL
}
if target.CNAME != "" {
return libdns.CNAME{
Name: name,
TTL: ttl,
Target: target.CNAME,
}
}
if target.Address != "" {
return libdns.Address{
Name: name,
TTL: ttl,
IP: netip.MustParseAddr(target.Address),
}
}
panic("no target defined for domain")
}

17
pangolin/domain.go Normal file
View File

@@ -0,0 +1,17 @@
package pangolin
type Domain struct {
ID string `json:"domainId"`
BaseDomain string `json:"baseDomain"`
Verified bool `json:"verified"`
Type string `json:"type"`
Failed bool `json:"failed"`
Tries int `json:"tries"`
ConfigManaged bool `json:"configManaged"`
CertResolver string `json:"certResolver"`
PreferWildcardCert *bool `json:"preferWildcardCert"`
}
type DomainResponse struct {
Domains []Domain `json:"domains"`
}

103
pangolin/pangolin.go Normal file
View File

@@ -0,0 +1,103 @@
package pangolin
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/auroradevllc/apiclient"
)
func New(apiUrl, apiKey string) *Client {
return &Client{
apiUrl: apiUrl,
apiKey: apiKey,
}
}
type Client struct {
apiUrl string
apiKey string
debug bool
}
type Response[V any] struct {
Success bool `json:"success"`
Error bool `json:"error"`
Message string `json:"message"`
Pagination struct {
Total int `json:"total"`
PageSize int `json:"pageSize"`
Page int `json:"page"`
}
Data V `json:"data"`
}
func (p *Client) Domains(orgID string) ([]Domain, error) {
var response Response[DomainResponse]
if err := p.doRequestJSON(http.MethodGet, "/org/"+orgID+"/domains", nil, &response); err != nil {
return nil, err
}
return response.Data.Domains, nil
}
func (p *Client) Resources(orgID string) ([]Resource, error) {
var response Response[ResourceResponse]
if err := p.doRequestJSON(http.MethodGet, "/org/"+orgID+"/resources?pageSize=1000", nil, &response); err != nil {
return nil, err
}
return response.Data.Resources, nil
}
func (p *Client) doRequest(method, uri string, body any) (*apiclient.Response, error) {
opts := []apiclient.Option{
apiclient.WithMethod(method),
apiclient.WithHeader("Authorization", "Bearer "+p.apiKey),
}
if body != nil {
opts = append(opts, apiclient.WithJSON(body))
}
req, err := apiclient.NewRequest(p.apiUrl+"/"+strings.TrimLeft(uri, "/"), opts...)
if err != nil {
return nil, err
}
res, err := req.Send()
if err != nil {
return nil, err
}
return res, nil
}
func (p *Client) doRequestJSON(method, uri string, body any, out any) error {
res, err := p.doRequest(method, uri, body)
if err != nil {
return err
}
// If debugging, we want to read the entire body
if p.debug {
b, err := res.Bytes()
if err != nil {
return err
}
fmt.Println(string(b))
return json.Unmarshal(b, out)
}
return res.Unmarshal(out)
}

31
pangolin/resource.go Normal file
View File

@@ -0,0 +1,31 @@
package pangolin
type Resource struct {
ID int `json:"resourceId"`
NiceID string `json:"niceId"`
Name string `json:"name"`
SSL bool `json:"ssl"`
FullDomain string `json:"fullDomain"`
PasswordID *int `json:"passwordId"`
PincodeID *int `json:"pincodeId"`
Whitelist bool `json:"whitelist"`
HTTP bool `json:"http"`
Protocol string `json:"protocol"`
Enabled bool `json:"enabled"`
DomainID string `json:"domainId"`
Targets []ResourceTarget `json:"targets"`
}
type ResourceTarget struct {
ID int `json:"targetId"`
ResourceID int `json:"resourceId"`
IP string `json:"ip"`
Port int `json:"port"`
Enabled bool `json:"enabled"`
HealthStatus string `json:"healthStatus"`
HcEnabled bool `json:"hcEnabled"`
}
type ResourceResponse struct {
Resources []Resource `json:"resources"`
}