From 9ea949c34a616dcab2d41c8c9df57e2818f632e1 Mon Sep 17 00:00:00 2001 From: Quentin McGaw Date: Thu, 20 May 2021 13:51:57 +0000 Subject: [PATCH] Azure SDK --- README.md | 2 + docs/azure.md | 64 ++++++++ go.mod | 11 ++ go.sum | 30 +++- internal/provider/constants/providers.go | 2 + internal/provider/provider.go | 3 + internal/provider/providers/azure/api.go | 77 +++++++++ internal/provider/providers/azure/provider.go | 155 ++++++++++++++++++ 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 docs/azure.md create mode 100644 internal/provider/providers/azure/api.go create mode 100644 internal/provider/providers/azure/provider.go diff --git a/README.md b/README.md index 00c3966f7..d946f7b85 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog - Updates periodically A records for different DNS providers: - Aliyun - AllInkl + - Azure - Changeip - Cloudflare - DD24 @@ -209,6 +210,7 @@ Check the documentation for your DNS provider: - [Aliyun](docs/aliyun.md) - [Allinkl](docs/allinkl.md) +- [Azure](docs/azure.md) - [ChangeIP](docs/changeip.md) - [Cloudflare](docs/cloudflare.md) - [Custom](docs/custom.md) diff --git a/docs/azure.md b/docs/azure.md new file mode 100644 index 000000000..9cb974e1c --- /dev/null +++ b/docs/azure.md @@ -0,0 +1,64 @@ +# Azure + +## Configuration + +### Example + +```json +{ + "settings": [ + { + "provider": "azure", + "domain": "domain.com", + "host": "@", + "tenant_id": "", + "client_id": "", + "client_secret": "", + "subscription_id": "", + "resource_group_name": "" + } + ] +} +``` + +### Compulsory parameters + +- `"domain"` +- `"host"` +- `"tenant_id"` +- `"client_id"` +- `"client_secret"` +- `"subscription_id"` found in the properties section of Azure DNS +- `"resource_group_name"` found in the properties section of Azure DNS + +### Optional parameters + +- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6` + +## Domain setup + +Thanks to @danimart1991 for describing the following steps! + +1. Create Domain +1. Activate Azure DNS Zone for that domain +1. Find the following parameters in the Properties section of Azure DNS: + - The name or URL `AnyNameOrUrl` for the query below **TODO** + - `subscription_id` + - `resource_group_name` +1. In the Azure Console (inside the portal), run: + + ```sh + az ad sp create-for-rbac -n "$AnyNameOrUrl" --scopes "/subscriptions/$subscription_id/resourceGroups/$resource_group_name/providers/Microsoft.Network/dnszones/$zone_name" + ``` + + This gives you the rest of the parameters: + + ```json + { + "appId": "{app_id/client_id}", + "displayName": "not important", + "name": "not important", + "password": "{app_password}", + "tenant": "not important" + } + ``` diff --git a/go.mod b/go.mod index 168bd6535..33c4a8cdc 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/qdm12/ddns-updater go 1.22 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/breml/rootcerts v0.2.17 github.com/containrrr/shoutrrr v0.8.0 github.com/go-chi/chi/v5 v5.0.12 @@ -21,16 +24,24 @@ require ( require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.15.0 // indirect github.com/go-logr/logr v1.4.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e7105fbcd..b1dd5b344 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,15 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/breml/rootcerts v0.2.17 h1:0/M2BE2Apw0qEJCXDOkaiu7d5Sx5ObNfe1BkImJ4u1I= github.com/breml/rootcerts v0.2.17/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw= github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= @@ -15,6 +25,8 @@ 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-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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -23,8 +35,16 @@ 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -36,6 +56,8 @@ github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 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/qdm12/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc= @@ -48,6 +70,8 @@ github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c= github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4= github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw= github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -55,6 +79,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -77,6 +103,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -94,8 +121,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T 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 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/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= diff --git a/internal/provider/constants/providers.go b/internal/provider/constants/providers.go index d7050a65a..851fd4ead 100644 --- a/internal/provider/constants/providers.go +++ b/internal/provider/constants/providers.go @@ -6,6 +6,7 @@ import "github.com/qdm12/ddns-updater/internal/models" const ( Aliyun models.Provider = "aliyun" AllInkl models.Provider = "allinkl" + Azure models.Provider = "azure" Changeip models.Provider = "changeip" Cloudflare models.Provider = "cloudflare" Custom models.Provider = "custom" @@ -57,6 +58,7 @@ func ProviderChoices() []models.Provider { return []models.Provider{ Aliyun, AllInkl, + Azure, Changeip, Cloudflare, Dd24, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index db750bb6a..4c0457035 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -12,6 +12,7 @@ import ( "github.com/qdm12/ddns-updater/internal/provider/constants" "github.com/qdm12/ddns-updater/internal/provider/providers/aliyun" "github.com/qdm12/ddns-updater/internal/provider/providers/allinkl" + "github.com/qdm12/ddns-updater/internal/provider/providers/azure" "github.com/qdm12/ddns-updater/internal/provider/providers/changeip" "github.com/qdm12/ddns-updater/internal/provider/providers/cloudflare" "github.com/qdm12/ddns-updater/internal/provider/providers/custom" @@ -82,6 +83,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin return aliyun.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.AllInkl: return allinkl.New(data, domain, owner, ipVersion, ipv6Suffix) + case constants.Azure: + return azure.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.Changeip: return changeip.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.Cloudflare: diff --git a/internal/provider/providers/azure/api.go b/internal/provider/providers/azure/api.go new file mode 100644 index 000000000..f562d7777 --- /dev/null +++ b/internal/provider/providers/azure/api.go @@ -0,0 +1,77 @@ +package azure + +import ( + "context" + "fmt" + "net/netip" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" +) + +func (p *Provider) createClient() (client *armdns.RecordSetsClient, err error) { + credential, err := azidentity.NewClientSecretCredential(p.tenantID, p.clientID, p.clientSecret, nil) + if err != nil { + return nil, fmt.Errorf("creating client secret credential: %w", err) + } + + client, err = armdns.NewRecordSetsClient(p.subscriptionID, credential, nil) + if err != nil { + return nil, fmt.Errorf("creating record sets client: %w", err) + } + + return client, nil +} + +func (p *Provider) getRecordSet(ctx context.Context, client *armdns.RecordSetsClient, + recordType armdns.RecordType) (response armdns.RecordSetsClientGetResponse, err error) { + return client.Get(ctx, p.resourceGroupName, p.domain, p.owner, recordType, nil) +} + +func (p *Provider) createRecordSet(ctx context.Context, client *armdns.RecordSetsClient, + ip netip.Addr) (err error) { + rrSet := armdns.RecordSet{Properties: &armdns.RecordSetProperties{}} + recordType := armdns.RecordTypeA + if ip.Is4() { + rrSet.Properties.ARecords = []*armdns.ARecord{{IPv4Address: ptrTo(ip.String())}} + } else { + recordType = armdns.RecordTypeAAAA + rrSet.Properties.AaaaRecords = []*armdns.AaaaRecord{{IPv6Address: ptrTo(ip.String())}} + } + _, err = client.CreateOrUpdate(ctx, p.resourceGroupName, p.domain, + p.owner, recordType, rrSet, nil) + if err != nil { + return fmt.Errorf("creating record set: %w", err) + } + return nil +} + +func (p *Provider) updateRecordSet(ctx context.Context, client *armdns.RecordSetsClient, + response armdns.RecordSetsClientGetResponse, ip netip.Addr) (err error) { + properties := response.Properties + recordType := armdns.RecordTypeA + if ip.Is4() { + if len(properties.ARecords) == 0 { + properties.ARecords = make([]*armdns.ARecord, 1) + } + for i := range properties.ARecords { + properties.ARecords[i].IPv4Address = ptrTo(ip.String()) + } + } else { + recordType = armdns.RecordTypeAAAA + if len(properties.AaaaRecords) == 0 { + properties.AaaaRecords = make([]*armdns.AaaaRecord, 1) + } + for i := range properties.AaaaRecords { + properties.AaaaRecords[i].IPv6Address = ptrTo(ip.String()) + } + } + rrSet := armdns.RecordSet{ + Etag: response.Etag, + Properties: properties, + } + + _, err = client.CreateOrUpdate(ctx, p.resourceGroupName, p.domain, + p.owner, recordType, rrSet, nil) + return err +} diff --git a/internal/provider/providers/azure/provider.go b/internal/provider/providers/azure/provider.go new file mode 100644 index 000000000..8aa43b783 --- /dev/null +++ b/internal/provider/providers/azure/provider.go @@ -0,0 +1,155 @@ +package azure + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/netip" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" + "github.com/qdm12/ddns-updater/internal/models" + "github.com/qdm12/ddns-updater/internal/provider/constants" + ddnserrors "github.com/qdm12/ddns-updater/internal/provider/errors" + "github.com/qdm12/ddns-updater/internal/provider/utils" + "github.com/qdm12/ddns-updater/pkg/publicip/ipversion" +) + +type Provider struct { + domain string // aka zoneName + owner string // aka relativeRecordSetName + ipVersion ipversion.IPVersion + ipv6Suffix netip.Prefix + tenantID string + clientID string + clientSecret string + subscriptionID string + resourceGroupName string +} + +func New(data json.RawMessage, domain, owner string, + ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) ( + p *Provider, err error) { + var providerSpecificSettings struct { + TenantID string `json:"tenant_id"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + SubscriptionID string `json:"subscription_id"` + ResourceGroupName string `json:"resource_group_name"` + } + err = json.Unmarshal(data, &providerSpecificSettings) + if err != nil { + return nil, fmt.Errorf("json decoding provider specific settings: %w", err) + } + err = validateSettings(domain, owner, providerSpecificSettings.TenantID, + providerSpecificSettings.ClientID, providerSpecificSettings.ClientSecret, + providerSpecificSettings.SubscriptionID, providerSpecificSettings.ResourceGroupName) + if err != nil { + return nil, fmt.Errorf("validating settings: %w", err) + } + return &Provider{ + domain: domain, + owner: owner, + ipVersion: ipVersion, + ipv6Suffix: ipv6Suffix, + tenantID: providerSpecificSettings.TenantID, + clientID: providerSpecificSettings.ClientID, + clientSecret: providerSpecificSettings.ClientSecret, + subscriptionID: providerSpecificSettings.SubscriptionID, + resourceGroupName: providerSpecificSettings.ResourceGroupName, + }, nil +} + +func validateSettings(domain, owner, tenantID, clientID, + clientSecret, subscriptionID, resourceGroupName string) error { + switch { + case domain == "": + return fmt.Errorf("%w", ddnserrors.ErrDomainNotSet) + case owner == "": + return fmt.Errorf("%w", ddnserrors.ErrOwnerNotSet) + case tenantID == "": + return fmt.Errorf("%w: tenant id", ddnserrors.ErrCredentialsNotSet) + case clientID == "": + return fmt.Errorf("%w: client id", ddnserrors.ErrCredentialsNotSet) + case clientSecret == "": + return fmt.Errorf("%w: client secret", ddnserrors.ErrCredentialsNotSet) + case subscriptionID == "": + return fmt.Errorf("%w: subscription id", ddnserrors.ErrKeyNotSet) + case resourceGroupName == "": + return fmt.Errorf("%w: resource group name", ddnserrors.ErrKeyNotSet) + } + return nil +} + +func (p *Provider) String() string { + return utils.ToString(p.domain, p.owner, constants.Azure, p.ipVersion) +} + +func (p *Provider) Domain() string { + return p.domain +} + +func (p *Provider) Owner() string { + return p.owner +} + +func (p *Provider) IPVersion() ipversion.IPVersion { + return p.ipVersion +} + +func (p *Provider) IPv6Suffix() netip.Prefix { + return p.ipv6Suffix +} + +func (p *Provider) Proxied() bool { + return false +} + +func (p *Provider) BuildDomainName() string { + return utils.BuildDomainName(p.owner, p.domain) +} + +func (p *Provider) HTML() models.HTMLRow { + return models.HTMLRow{ + Domain: fmt.Sprintf("%s", p.BuildDomainName(), p.BuildDomainName()), + Owner: p.Owner(), + Provider: "Azure", + IPVersion: p.ipVersion.String(), + } +} + +func ptrTo[T any](v T) *T { return &v } + +func (p *Provider) Update(ctx context.Context, _ *http.Client, ip netip.Addr) (newIP netip.Addr, err error) { + var recordType armdns.RecordType + if ip.Is4() { + recordType = armdns.RecordTypeA + } else { + recordType = armdns.RecordTypeAAAA + } + + client, err := p.createClient() + if err != nil { + return netip.Addr{}, fmt.Errorf("creating client: %w", err) + } + + response, err := p.getRecordSet(ctx, client, recordType) + if err != nil { + azureErr := &azcore.ResponseError{} + if errors.As(err, &azureErr) && azureErr.StatusCode == http.StatusNotFound { + err = p.createRecordSet(ctx, client, ip) + if err != nil { + return netip.Addr{}, fmt.Errorf("creating record set: %w", err) + } + } + return netip.Addr{}, fmt.Errorf("getting record set: %w", err) + } + + err = p.updateRecordSet(ctx, client, response, ip) + if err != nil { + return netip.Addr{}, fmt.Errorf("updating record set: %w", err) + } + return ip, nil +}