aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
authorGravatar Darshan Chaudhary <deathbullet@gmail.com> 2019-08-09 12:40:28 +0530
committerGravatar Miek Gieben <miek@miek.nl> 2019-08-09 08:10:28 +0100
commit879466b0288a2d11e278950375c7593b60ea0677 (patch)
tree8f277238fd0cd9b458d63fbcc156267ad2a04006 /plugin
parent5b74d0f957a2565c8ee168ad55283b0914cac9d7 (diff)
downloadcoredns-879466b0288a2d11e278950375c7593b60ea0677.tar.gz
coredns-879466b0288a2d11e278950375c7593b60ea0677.tar.zst
coredns-879466b0288a2d11e278950375c7593b60ea0677.zip
Add plugin for Azure DNS (#2945)
* Add plugin for Azure DNS Signed-off-by: darshanime <deathbullet@gmail.com> * Rename AzureDNS plugin to Azure Signed-off-by: darshanime <deathbullet@gmail.com> * remove upstream from azure syntax Signed-off-by: darshanime <deathbullet@gmail.com> * Rename azure plugin block keynames Signed-off-by: darshanime <deathbullet@gmail.com> * Normalize zone name before lookup in zones Signed-off-by: darshanime <deathbullet@gmail.com> * Update import path for caddy Signed-off-by: darshanime <deathbullet@gmail.com> * normalize azure zone name only if required Signed-off-by: darshanime <deathbullet@gmail.com> * Add support for MX, SRV, TXT, records Signed-off-by: darshanime <deathbullet@gmail.com> * Add specs for new record types Signed-off-by: darshanime <deathbullet@gmail.com> * Use sequential updates for zones Signed-off-by: darshanime <deathbullet@gmail.com> * Add OWNERS file for azure plugin Signed-off-by: darshanime <deathbullet@gmail.com> * Rename imports for third party packages Signed-off-by: darshanime <deathbullet@gmail.com> * Capitalize values in README Signed-off-by: darshanime <deathbullet@gmail.com> * Shorten keys for azure plugin config Signed-off-by: darshanime <deathbullet@gmail.com> * Fixup readme for azure plugin Signed-off-by: darshanime <deathbullet@gmail.com>
Diffstat (limited to 'plugin')
-rw-r--r--plugin/azure/OWNERS8
-rw-r--r--plugin/azure/README.md52
-rw-r--r--plugin/azure/azure.go293
-rw-r--r--plugin/azure/azure_test.go180
-rw-r--r--plugin/azure/setup.go123
-rw-r--r--plugin/azure/setup_test.go72
6 files changed, 728 insertions, 0 deletions
diff --git a/plugin/azure/OWNERS b/plugin/azure/OWNERS
new file mode 100644
index 000000000..c30e67ae3
--- /dev/null
+++ b/plugin/azure/OWNERS
@@ -0,0 +1,8 @@
+reviewers:
+ - miekg
+ - yongtang
+ - darshanime
+approvers:
+ - miekg
+ - yongtang
+ - darshanime
diff --git a/plugin/azure/README.md b/plugin/azure/README.md
new file mode 100644
index 000000000..5c7be25a8
--- /dev/null
+++ b/plugin/azure/README.md
@@ -0,0 +1,52 @@
+# azure
+
+## Name
+
+*azure* - enables serving zone data from Microsoft Azure DNS service.
+
+## Description
+
+The azure plugin is useful for serving zones from Microsoft Azure DNS.
+Thi *azure* plugin supports all the DNS records supported by Azure, viz. A, AAAA, CAA, CNAME, MX, NS, PTR, SOA, SRV, and TXT record types. For a non-existing resource record, zone's SOA response will returned.
+
+
+## Syntax
+
+~~~ txt
+azure RESOURCE_GROUP:ZONE... {
+ tenant TENANT_ID
+ client CLIENT_ID
+ secret CLIENT_SECRET
+ subscription SUBSCRIPTION_ID
+}
+~~~
+
+* **`RESOURCE_GROUP`** The resource group to which the dns hosted zones belong on Azure
+
+* **`ZONE`** the zone that contains the resource record sets to be
+ accessed.
+
+* `fallthrough` If zone matches and no record can be generated, pass request to the next plugin.
+ If **ZONES** is omitted, then fallthrough happens for all zones for which the plugin is
+ authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then
+ only queries for those zones will be subject to fallthrough.
+
+* `environment` the azure environment to use. Defaults to `AzurePublicCloud`. Possible values: `AzureChinaCloud`, `AzureGermanCloud`, `AzurePublicCloud`, `AzureUSGovernmentCloud`.
+
+## Examples
+
+Enable the *azure* plugin with Azure credentials:
+
+~~~ txt
+. {
+ azure resource_group_foo:foo.com {
+ tenant 123abc-123abc-123abc-123abc
+ client 123abc-123abc-123abc-123abc
+ secret 123abc-123abc-123abc-123abc
+ subscription 123abc-123abc-123abc-123abc
+ }
+}
+~~~
+
+## Also See
+- [Azure DNS Overview](https://docs.microsoft.com/en-us/azure/dns/dns-overview)
diff --git a/plugin/azure/azure.go b/plugin/azure/azure.go
new file mode 100644
index 000000000..f3b2fad40
--- /dev/null
+++ b/plugin/azure/azure.go
@@ -0,0 +1,293 @@
+package azure
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/file"
+ "github.com/coredns/coredns/plugin/pkg/fall"
+ "github.com/coredns/coredns/plugin/pkg/upstream"
+ "github.com/coredns/coredns/request"
+
+ azuredns "github.com/Azure/azure-sdk-for-go/profiles/latest/dns/mgmt/dns"
+ "github.com/miekg/dns"
+)
+
+type zone struct {
+ id string
+ z *file.Zone
+ zone string
+}
+
+type zones map[string][]*zone
+
+// Azure is the core struct of the azure plugin.
+type Azure struct {
+ Next plugin.Handler
+ Fall fall.F
+ zoneNames []string
+ client azuredns.RecordSetsClient
+ upstream *upstream.Upstream
+ zMu sync.RWMutex
+ zones zones
+}
+
+// New validates the input DNS zones and initializes the Azure struct.
+func New(ctx context.Context, dnsClient azuredns.RecordSetsClient, keys map[string][]string, up *upstream.Upstream) (*Azure, error) {
+ zones := make(map[string][]*zone, len(keys))
+ zoneNames := make([]string, 0, len(keys))
+ for resourceGroup, inputZoneNames := range keys {
+ for _, zoneName := range inputZoneNames {
+ _, err := dnsClient.ListAllByDNSZone(context.Background(), resourceGroup, zoneName, nil, "")
+ if err != nil {
+ return nil, err
+ }
+ // Normalizing zoneName to make it fqdn if required.
+ zoneNameFQDN := dns.Fqdn(zoneName)
+ if _, ok := zones[zoneNameFQDN]; !ok {
+ zoneNames = append(zoneNames, zoneNameFQDN)
+ }
+ zones[zoneNameFQDN] = append(zones[zoneNameFQDN], &zone{id: resourceGroup, zone: zoneName, z: file.NewZone(zoneName, "")})
+ }
+ }
+ return &Azure{
+ client: dnsClient,
+ zones: zones,
+ zoneNames: zoneNames,
+ upstream: up,
+ }, nil
+}
+
+// Run updates the zone from azure.
+func (h *Azure) Run(ctx context.Context) error {
+ if err := h.updateZones(ctx); err != nil {
+ return err
+ }
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ log.Infof("Breaking out of Azure update loop: %v", ctx.Err())
+ return
+ case <-time.After(1 * time.Minute):
+ if err := h.updateZones(ctx); err != nil && ctx.Err() == nil {
+ log.Errorf("Failed to update zones: %v", err)
+ }
+ }
+ }
+ }()
+ return nil
+}
+
+func (h *Azure) updateZones(ctx context.Context) error {
+ errs := make([]string, 0)
+ for zName, z := range h.zones {
+ for i, hostedZone := range z {
+ recordSet, err := h.client.ListByDNSZone(ctx, hostedZone.id, hostedZone.zone, nil, "")
+ if err != nil {
+ errs = append(errs, fmt.Sprintf("failed to list resource records for %v from azure: %v", hostedZone.zone, err))
+ }
+ newZ := updateZoneFromResourceSet(recordSet, zName)
+ newZ.Upstream = h.upstream
+ h.zMu.Lock()
+ (*z[i]).z = newZ
+ h.zMu.Unlock()
+ }
+ }
+
+ if len(errs) != 0 {
+ return fmt.Errorf("errors updating zones: %v", errs)
+ }
+ return nil
+
+}
+
+func updateZoneFromResourceSet(recordSet azuredns.RecordSetListResultPage, zName string) *file.Zone {
+ newZ := file.NewZone(zName, "")
+
+ for _, result := range *(recordSet.Response().Value) {
+ resultFqdn := *(result.RecordSetProperties.Fqdn)
+ resultTTL := uint32(*(result.RecordSetProperties.TTL))
+ if result.RecordSetProperties.ARecords != nil {
+ for _, A := range *(result.RecordSetProperties.ARecords) {
+ a := dns.A{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeA,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ A: net.ParseIP(*(A.Ipv4Address))}
+ newZ.Insert(&a)
+ }
+ }
+
+ if result.RecordSetProperties.AaaaRecords != nil {
+ for _, AAAA := range *(result.RecordSetProperties.AaaaRecords) {
+ aaaa := dns.AAAA{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeAAAA,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ AAAA: net.ParseIP(*(AAAA.Ipv6Address))}
+ newZ.Insert(&aaaa)
+ }
+ }
+
+ if result.RecordSetProperties.MxRecords != nil {
+ for _, MX := range *(result.RecordSetProperties.MxRecords) {
+ mx := dns.MX{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeMX,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ Preference: uint16(*(MX.Preference)),
+ Mx: dns.Fqdn(*(MX.Exchange))}
+ newZ.Insert(&mx)
+ }
+ }
+
+ if result.RecordSetProperties.PtrRecords != nil {
+ for _, PTR := range *(result.RecordSetProperties.PtrRecords) {
+ ptr := dns.PTR{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypePTR,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ Ptr: dns.Fqdn(*(PTR.Ptrdname))}
+ newZ.Insert(&ptr)
+ }
+ }
+
+ if result.RecordSetProperties.SrvRecords != nil {
+ for _, SRV := range *(result.RecordSetProperties.SrvRecords) {
+ srv := dns.SRV{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeSRV,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ Priority: uint16(*(SRV.Priority)),
+ Weight: uint16(*(SRV.Weight)),
+ Port: uint16(*(SRV.Port)),
+ Target: dns.Fqdn(*(SRV.Target))}
+ newZ.Insert(&srv)
+ }
+ }
+
+ if result.RecordSetProperties.TxtRecords != nil {
+ for _, TXT := range *(result.RecordSetProperties.TxtRecords) {
+ txt := dns.TXT{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeTXT,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ Txt: *(TXT.Value)}
+ newZ.Insert(&txt)
+ }
+ }
+
+ if result.RecordSetProperties.NsRecords != nil {
+ for _, NS := range *(result.RecordSetProperties.NsRecords) {
+ ns := dns.NS{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeNS,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ Ns: *(NS.Nsdname)}
+ newZ.Insert(&ns)
+ }
+ }
+
+ if result.RecordSetProperties.SoaRecord != nil {
+ SOA := result.RecordSetProperties.SoaRecord
+ soa := dns.SOA{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeSOA,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ Minttl: uint32(*(SOA.MinimumTTL)),
+ Expire: uint32(*(SOA.ExpireTime)),
+ Retry: uint32(*(SOA.RetryTime)),
+ Refresh: uint32(*(SOA.RefreshTime)),
+ Serial: uint32(*(SOA.SerialNumber)),
+ Mbox: dns.Fqdn(*(SOA.Email)),
+ Ns: *(SOA.Host)}
+ newZ.Insert(&soa)
+ }
+
+ if result.RecordSetProperties.CnameRecord != nil {
+ CNAME := result.RecordSetProperties.CnameRecord.Cname
+ cname := dns.CNAME{
+ Hdr: dns.RR_Header{
+ Name: resultFqdn,
+ Rrtype: dns.TypeCNAME,
+ Class: dns.ClassINET,
+ Ttl: resultTTL},
+ Target: dns.Fqdn(*CNAME)}
+ newZ.Insert(&cname)
+ }
+ }
+ return newZ
+}
+
+// ServeDNS implements the plugin.Handler interface.
+func (h *Azure) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+ qname := state.Name()
+
+ zName := plugin.Zones(h.zoneNames).Matches(qname)
+ if zName == "" {
+ return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
+ }
+
+ z, ok := h.zones[zName] // ok true if we are authoritive for the zone.
+ if !ok || z == nil {
+ return dns.RcodeServerFailure, nil
+ }
+
+ m := new(dns.Msg)
+ m.SetReply(r)
+ m.Authoritative = true
+ var result file.Result
+ for _, hostedZone := range z {
+ h.zMu.RLock()
+ m.Answer, m.Ns, m.Extra, result = hostedZone.z.Lookup(ctx, state, qname)
+ h.zMu.RUnlock()
+
+ // record type exists for this name (NODATA).
+ if len(m.Answer) != 0 || result == file.NoData {
+ break
+ }
+ }
+
+ if len(m.Answer) == 0 && result != file.NoData && h.Fall.Through(qname) {
+ return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
+ }
+
+ switch result {
+ case file.Success:
+ case file.NoData:
+ case file.NameError:
+ m.Rcode = dns.RcodeNameError
+ case file.Delegation:
+ m.Authoritative = false
+ case file.ServerFailure:
+ return dns.RcodeServerFailure, nil
+ }
+
+ w.WriteMsg(m)
+ return dns.RcodeSuccess, nil
+}
+
+// Name implements plugin.Handler.Name.
+func (h *Azure) Name() string { return "azure" }
diff --git a/plugin/azure/azure_test.go b/plugin/azure/azure_test.go
new file mode 100644
index 000000000..d006f196b
--- /dev/null
+++ b/plugin/azure/azure_test.go
@@ -0,0 +1,180 @@
+package azure
+
+import (
+ "context"
+ "reflect"
+ "testing"
+
+ "github.com/coredns/coredns/plugin/file"
+ "github.com/coredns/coredns/plugin/pkg/dnstest"
+ "github.com/coredns/coredns/plugin/pkg/fall"
+ "github.com/coredns/coredns/plugin/test"
+ "github.com/coredns/coredns/request"
+
+ "github.com/miekg/dns"
+)
+
+var demoAzure = Azure{
+ Next: testHandler(),
+ Fall: fall.Zero,
+ zoneNames: []string{"example.org.", "www.example.org.", "example.org.", "sample.example.org."},
+ zones: testZones(),
+}
+
+func testZones() zones {
+ zones := make(map[string][]*zone)
+ zones["example.org."] = append(zones["example.org."], &zone{zone: "example.org."})
+ newZ := file.NewZone("example.org.", "")
+
+ for _, rr := range []string{
+ "example.org. 300 IN A 1.2.3.4",
+ "example.org. 300 IN AAAA 2001:db8:85a3::8a2e:370:7334",
+ "www.example.org. 300 IN A 1.2.3.4",
+ "www.example.org. 300 IN A 1.2.3.4",
+ "org. 172800 IN NS ns3-06.azure-dns.org.",
+ "org. 300 IN SOA ns1-06.azure-dns.com. azuredns-hostmaster.microsoft.com. 1 3600 300 2419200 300",
+ "cname.example.org. 300 IN CNAME example.org",
+ "mail.example.org. 300 IN MX 10 mailserver.example.com",
+ "ptr.example.org. 300 IN PTR www.ptr-example.com",
+ "example.org. 300 IN SRV 1 10 5269 srv-1.example.com.",
+ "example.org. 300 IN SRV 1 10 5269 srv-2.example.com.",
+ "txt.example.org. 300 IN TXT \"TXT for example.org\"",
+ } {
+ r, _ := dns.NewRR(rr)
+ newZ.Insert(r)
+ }
+ zones["example.org."][0].z = newZ
+ return zones
+}
+
+func testHandler() test.HandlerFunc {
+ return func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ state := request.Request{W: w, Req: r}
+ qname := state.Name()
+ m := new(dns.Msg)
+ rcode := dns.RcodeServerFailure
+ if qname == "example.gov." { // No records match, test fallthrough.
+ m.SetReply(r)
+ rr := test.A("example.gov. 300 IN A 2.4.6.8")
+ m.Answer = []dns.RR{rr}
+ m.Authoritative = true
+ rcode = dns.RcodeSuccess
+ }
+ m.SetRcode(r, rcode)
+ w.WriteMsg(m)
+ return rcode, nil
+ }
+}
+
+func TestAzure(t *testing.T) {
+ tests := []struct {
+ qname string
+ qtype uint16
+ wantRetCode int
+ wantAnswer []string
+ wantMsgRCode int
+ wantNS []string
+ expectedErr error
+ }{
+ {
+ qname: "example.org.",
+ qtype: dns.TypeA,
+ wantAnswer: []string{"example.org. 300 IN A 1.2.3.4"},
+ },
+ {
+ qname: "example.org",
+ qtype: dns.TypeAAAA,
+ wantAnswer: []string{"example.org. 300 IN AAAA 2001:db8:85a3::8a2e:370:7334"},
+ },
+ {
+ qname: "example.org",
+ qtype: dns.TypeSOA,
+ wantAnswer: []string{"org. 300 IN SOA ns1-06.azure-dns.com. azuredns-hostmaster.microsoft.com. 1 3600 300 2419200 300"},
+ },
+ {
+ qname: "badexample.com",
+ qtype: dns.TypeA,
+ wantRetCode: dns.RcodeServerFailure,
+ wantMsgRCode: dns.RcodeServerFailure,
+ },
+ {
+ qname: "example.gov",
+ qtype: dns.TypeA,
+ wantAnswer: []string{"example.gov. 300 IN A 2.4.6.8"},
+ },
+ {
+ qname: "example.org",
+ qtype: dns.TypeSRV,
+ wantAnswer: []string{"example.org. 300 IN SRV 1 10 5269 srv-1.example.com.", "example.org. 300 IN SRV 1 10 5269 srv-2.example.com."},
+ },
+ {
+ qname: "cname.example.org.",
+ qtype: dns.TypeCNAME,
+ wantAnswer: []string{"cname.example.org. 300 IN CNAME example.org."},
+ },
+ {
+ qname: "cname.example.org.",
+ qtype: dns.TypeA,
+ wantAnswer: []string{"cname.example.org. 300 IN CNAME example.org.", "example.org. 300 IN A 1.2.3.4"},
+ },
+ {
+ qname: "mail.example.org.",
+ qtype: dns.TypeMX,
+ wantAnswer: []string{"mail.example.org. 300 IN MX 10 mailserver.example.com."},
+ },
+ {
+ qname: "ptr.example.org.",
+ qtype: dns.TypePTR,
+ wantAnswer: []string{"ptr.example.org. 300 IN PTR www.ptr-example.com."},
+ },
+ {
+ qname: "txt.example.org.",
+ qtype: dns.TypeTXT,
+ wantAnswer: []string{"txt.example.org. 300 IN TXT \"TXT for example.org\""},
+ },
+ }
+
+ for ti, tc := range tests {
+ req := new(dns.Msg)
+ req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype)
+
+ rec := dnstest.NewRecorder(&test.ResponseWriter{})
+ code, err := demoAzure.ServeDNS(context.Background(), rec, req)
+
+ if err != tc.expectedErr {
+ t.Fatalf("Test %d: Expected error %v, but got %v", ti, tc.expectedErr, err)
+ }
+
+ if code != int(tc.wantRetCode) {
+ t.Fatalf("Test %d: Expected returned status code %s, but got %s", ti, dns.RcodeToString[tc.wantRetCode], dns.RcodeToString[code])
+ }
+
+ if tc.wantMsgRCode != rec.Msg.Rcode {
+ t.Errorf("Test %d: Unexpected msg status code. Want: %s, got: %s", ti, dns.RcodeToString[tc.wantMsgRCode], dns.RcodeToString[rec.Msg.Rcode])
+ }
+
+ if len(tc.wantAnswer) != len(rec.Msg.Answer) {
+ t.Errorf("Test %d: Unexpected number of Answers. Want: %d, got: %d", ti, len(tc.wantAnswer), len(rec.Msg.Answer))
+ } else {
+ for i, gotAnswer := range rec.Msg.Answer {
+ if gotAnswer.String() != tc.wantAnswer[i] {
+ t.Errorf("Test %d: Unexpected answer.\nWant:\n\t%s\nGot:\n\t%s", ti, tc.wantAnswer[i], gotAnswer)
+ }
+ }
+ }
+
+ if len(tc.wantNS) != len(rec.Msg.Ns) {
+ t.Errorf("Test %d: Unexpected NS number. Want: %d, got: %d", ti, len(tc.wantNS), len(rec.Msg.Ns))
+ } else {
+ for i, ns := range rec.Msg.Ns {
+ got, ok := ns.(*dns.SOA)
+ if !ok {
+ t.Errorf("Test %d: Unexpected NS type. Want: SOA, got: %v", ti, reflect.TypeOf(got))
+ }
+ if got.String() != tc.wantNS[i] {
+ t.Errorf("Test %d: Unexpected NS.\nWant: %v\nGot: %v", ti, tc.wantNS[i], got)
+ }
+ }
+ }
+ }
+}
diff --git a/plugin/azure/setup.go b/plugin/azure/setup.go
new file mode 100644
index 000000000..1ac0cc723
--- /dev/null
+++ b/plugin/azure/setup.go
@@ -0,0 +1,123 @@
+package azure
+
+import (
+ "context"
+ "strings"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/plugin"
+ "github.com/coredns/coredns/plugin/pkg/fall"
+ clog "github.com/coredns/coredns/plugin/pkg/log"
+ "github.com/coredns/coredns/plugin/pkg/upstream"
+
+ azuredns "github.com/Azure/azure-sdk-for-go/profiles/latest/dns/mgmt/dns"
+ azurerest "github.com/Azure/go-autorest/autorest/azure"
+ "github.com/Azure/go-autorest/autorest/azure/auth"
+ "github.com/caddyserver/caddy"
+)
+
+var log = clog.NewWithPlugin("azure")
+
+func init() {
+ caddy.RegisterPlugin("azure", caddy.Plugin{
+ ServerType: "dns",
+ Action: setup,
+ })
+}
+
+func setup(c *caddy.Controller) error {
+ env, keys, fall, err := parse(c)
+ if err != nil {
+ return plugin.Error("azure", err)
+ }
+ ctx := context.Background()
+ dnsClient := azuredns.NewRecordSetsClient(env.Values[auth.SubscriptionID])
+ dnsClient.Authorizer, err = env.GetAuthorizer()
+ if err != nil {
+ return c.Errf("failed to create azure plugin: %v", err)
+ }
+ h, err := New(ctx, dnsClient, keys, upstream.New())
+ if err != nil {
+ return c.Errf("failed to initialize azure plugin: %v", err)
+ }
+ h.Fall = fall
+ if err := h.Run(ctx); err != nil {
+ return c.Errf("failed to run azure plugin: %v", err)
+ }
+ dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
+ h.Next = next
+ return h
+ })
+ return nil
+}
+
+func parse(c *caddy.Controller) (auth.EnvironmentSettings, map[string][]string, fall.F, error) {
+ resourceGroupMapping := map[string][]string{}
+ resourceGroupSet := map[string]struct{}{}
+ var err error
+ var fall fall.F
+
+ azureEnv := azurerest.PublicCloud
+ env := auth.EnvironmentSettings{Values: map[string]string{}}
+
+ for c.Next() {
+ args := c.RemainingArgs()
+
+ for i := 0; i < len(args); i++ {
+ parts := strings.SplitN(args[i], ":", 2)
+ if len(parts) != 2 {
+ return env, resourceGroupMapping, fall, c.Errf("invalid resource group / zone '%s'", args[i])
+ }
+ resourceGroup, zoneName := parts[0], parts[1]
+ if resourceGroup == "" || zoneName == "" {
+ return env, resourceGroupMapping, fall, c.Errf("invalid resource group / zone '%s'", args[i])
+ }
+ if _, ok := resourceGroupSet[args[i]]; ok {
+ return env, resourceGroupMapping, fall, c.Errf("conflict zone '%s'", args[i])
+ }
+
+ resourceGroupSet[args[i]] = struct{}{}
+ resourceGroupMapping[resourceGroup] = append(resourceGroupMapping[resourceGroup], zoneName)
+ }
+ for c.NextBlock() {
+ switch c.Val() {
+ case "subscription":
+ if !c.NextArg() {
+ return env, resourceGroupMapping, fall, c.ArgErr()
+ }
+ env.Values[auth.SubscriptionID] = c.Val()
+ case "tenant":
+ if !c.NextArg() {
+ return env, resourceGroupMapping, fall, c.ArgErr()
+ }
+ env.Values[auth.TenantID] = c.Val()
+ case "client":
+ if !c.NextArg() {
+ return env, resourceGroupMapping, fall, c.ArgErr()
+ }
+ env.Values[auth.ClientID] = c.Val()
+ case "secret":
+ if !c.NextArg() {
+ return env, resourceGroupMapping, fall, c.ArgErr()
+ }
+ env.Values[auth.ClientSecret] = c.Val()
+ case "environment":
+ if !c.NextArg() {
+ return env, resourceGroupMapping, fall, c.ArgErr()
+ }
+ env.Values[auth.ClientSecret] = c.Val()
+ azureEnv, err = azurerest.EnvironmentFromName(c.Val())
+ if err != nil {
+ return env, resourceGroupMapping, fall, c.Errf("cannot set azure environment: %s", err.Error())
+ }
+ case "fallthrough":
+ fall.SetZonesFromArgs(c.RemainingArgs())
+ default:
+ return env, resourceGroupMapping, fall, c.Errf("unknown property '%s'", c.Val())
+ }
+ }
+ }
+ env.Values[auth.Resource] = azureEnv.ResourceManagerEndpoint
+ env.Environment = azureEnv
+ return env, resourceGroupMapping, fall, nil
+}
diff --git a/plugin/azure/setup_test.go b/plugin/azure/setup_test.go
new file mode 100644
index 000000000..c0b22d581
--- /dev/null
+++ b/plugin/azure/setup_test.go
@@ -0,0 +1,72 @@
+package azure
+
+import (
+ "testing"
+
+ "github.com/caddyserver/caddy"
+)
+
+func TestSetup(t *testing.T) {
+ tests := []struct {
+ body string
+ expectedError bool
+ }{
+ {`azure`, false},
+ {`azure :`, true},
+ {`azure resource_set:zone`, false},
+ {`azure resource_set:zone {
+ tenant
+}`, true},
+ {`azure resource_set:zone {
+ tenant
+}`, true},
+ {`azure resource_set:zone {
+ client
+}`, true},
+ {`azure resource_set:zone {
+ secret
+}`, true},
+ {`azure resource_set:zone {
+ subscription
+}`, true},
+ {`azure resource_set:zone {
+ upstream 10.0.0.1
+}`, true},
+
+ {`azure resource_set:zone {
+ upstream
+}`, true},
+ {`azure resource_set:zone {
+ foobar
+}`, true},
+ {`azure resource_set:zone {
+ tenant tenant_id
+ client client_id
+ secret client_secret
+ subscription subscription_id
+}`, false},
+
+ {`azure resource_set:zone {
+ fallthrough
+}`, false},
+ {`azure resource_set:zone {
+ environment AZUREPUBLICCLOUD
+ }`, false},
+ {`azure resource_set:zone resource_set:zone {
+ fallthrough
+ }`, true},
+ {`azure resource_set:zone,zone2 {
+ fallthrough
+ }`, false},
+ {`azure resource-set {
+ fallthrough
+ }`, true},
+ }
+
+ for i, test := range tests {
+ c := caddy.NewTestController("dns", test.body)
+ if _, _, _, err := parse(c); (err == nil) == test.expectedError {
+ t.Fatalf("Unexpected errors: %v in test: %d\n\t%s", err, i, test.body)
+ }
+ }
+}