2023-07-12 20:02:06 +02:00
|
|
|
package azuredns
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
2024-03-20 05:36:35 +02:00
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
|
2023-07-12 20:02:06 +02:00
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns"
|
|
|
|
"github.com/go-acme/lego/v4/challenge/dns01"
|
|
|
|
)
|
|
|
|
|
|
|
|
// DNSProviderPrivate implements the challenge.Provider interface for Azure Private Zone DNS.
|
|
|
|
type DNSProviderPrivate struct {
|
2024-03-20 05:36:35 +02:00
|
|
|
config *Config
|
|
|
|
credentials azcore.TokenCredential
|
|
|
|
serviceDiscoveryZones map[string]ServiceDiscoveryZone
|
2023-07-12 20:02:06 +02:00
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
// NewDNSProviderPrivate creates a DNSProviderPrivate structure.
|
2023-07-12 20:02:06 +02:00
|
|
|
func NewDNSProviderPrivate(config *Config, credentials azcore.TokenCredential) (*DNSProviderPrivate, error) {
|
2024-03-20 05:36:35 +02:00
|
|
|
zones, err := discoverDNSZones(context.Background(), config, credentials)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
2024-03-20 05:36:35 +02:00
|
|
|
return nil, fmt.Errorf("discover DNS zones: %w", err)
|
2023-07-12 20:02:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return &DNSProviderPrivate{
|
2024-03-20 05:36:35 +02:00
|
|
|
config: config,
|
|
|
|
credentials: credentials,
|
|
|
|
serviceDiscoveryZones: zones,
|
2023-07-12 20:02:06 +02:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
|
|
|
// Adjusting here to cope with spikes in propagation times.
|
|
|
|
func (d *DNSProviderPrivate) Timeout() (timeout, interval time.Duration) {
|
|
|
|
return d.config.PropagationTimeout, d.config.PollingInterval
|
|
|
|
}
|
|
|
|
|
|
|
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
|
|
|
func (d *DNSProviderPrivate) Present(domain, _, keyAuth string) error {
|
|
|
|
ctx := context.Background()
|
|
|
|
info := dns01.GetChallengeInfo(domain, keyAuth)
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
zone, err := d.getHostedZone(info.EffectiveFQDN)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
client, err := newPrivateZoneClient(zone, d.credentials, d.config.Environment)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get existing record set
|
2024-03-20 05:36:35 +02:00
|
|
|
resp, err := client.Get(ctx, subDomain)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
|
|
|
var respErr *azcore.ResponseError
|
|
|
|
if !errors.As(err, &respErr) || respErr.StatusCode != http.StatusNotFound {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct unique TXT records using map
|
2024-03-20 05:36:35 +02:00
|
|
|
uniqRecords := privateUniqueRecords(resp.RecordSet, info.Value)
|
2023-07-12 20:02:06 +02:00
|
|
|
|
|
|
|
var txtRecords []*armprivatedns.TxtRecord
|
|
|
|
for txt := range uniqRecords {
|
2024-03-20 21:30:35 +02:00
|
|
|
txtRecords = append(txtRecords, &armprivatedns.TxtRecord{Value: to.SliceOfPtrs(txt)})
|
2023-07-12 20:02:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
rec := armprivatedns.RecordSet{
|
|
|
|
Name: &subDomain,
|
|
|
|
Properties: &armprivatedns.RecordSetProperties{
|
2024-03-20 05:36:35 +02:00
|
|
|
TTL: to.Ptr(int64(d.config.TTL)),
|
2023-07-12 20:02:06 +02:00
|
|
|
TxtRecords: txtRecords,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
_, err = client.CreateOrUpdate(ctx, subDomain, rec)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CleanUp removes the TXT record matching the specified parameters.
|
|
|
|
func (d *DNSProviderPrivate) CleanUp(domain, _, keyAuth string) error {
|
|
|
|
ctx := context.Background()
|
|
|
|
info := dns01.GetChallengeInfo(domain, keyAuth)
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
zone, err := d.getHostedZone(info.EffectiveFQDN)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
client, err := newPrivateZoneClient(zone, d.credentials, d.config.Environment)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, zone.Name)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = client.Delete(ctx, subDomain)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("azuredns: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Checks that azure has a zone for this domain name.
|
2024-03-20 05:36:35 +02:00
|
|
|
func (d *DNSProviderPrivate) getHostedZone(fqdn string) (ServiceDiscoveryZone, error) {
|
2024-07-21 15:06:01 +02:00
|
|
|
authZone, err := getZoneName(d.config, fqdn)
|
2024-03-20 05:36:35 +02:00
|
|
|
if err != nil {
|
|
|
|
return ServiceDiscoveryZone{}, err
|
2023-07-12 20:02:06 +02:00
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
azureZone, exists := d.serviceDiscoveryZones[dns01.UnFqdn(authZone)]
|
|
|
|
if !exists {
|
|
|
|
return ServiceDiscoveryZone{}, fmt.Errorf("could not find zone (from discovery): %s", authZone)
|
2023-07-12 20:02:06 +02:00
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
return azureZone, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// privateZoneClient provides Azure client for one DNS zone.
|
|
|
|
type privateZoneClient struct {
|
|
|
|
zone ServiceDiscoveryZone
|
|
|
|
recordClient *armprivatedns.RecordSetsClient
|
|
|
|
}
|
|
|
|
|
|
|
|
// newPrivateZoneClient creates privateZoneClient structure with initialized Azure client.
|
|
|
|
func newPrivateZoneClient(zone ServiceDiscoveryZone, credential azcore.TokenCredential, environment cloud.Configuration) (*privateZoneClient, error) {
|
|
|
|
options := &arm.ClientOptions{
|
|
|
|
ClientOptions: azcore.ClientOptions{
|
|
|
|
Cloud: environment,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
recordClient, err := armprivatedns.NewRecordSetsClient(zone.SubscriptionID, credential, options)
|
2023-07-12 20:02:06 +02:00
|
|
|
if err != nil {
|
2024-03-20 05:36:35 +02:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &privateZoneClient{
|
|
|
|
zone: zone,
|
|
|
|
recordClient: recordClient,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c privateZoneClient) Get(ctx context.Context, subDomain string) (armprivatedns.RecordSetsClientGetResponse, error) {
|
|
|
|
return c.recordClient.Get(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c privateZoneClient) CreateOrUpdate(ctx context.Context, subDomain string, rec armprivatedns.RecordSet) (armprivatedns.RecordSetsClientCreateOrUpdateResponse, error) {
|
|
|
|
return c.recordClient.CreateOrUpdate(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, rec, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c privateZoneClient) Delete(ctx context.Context, subDomain string) (armprivatedns.RecordSetsClientDeleteResponse, error) {
|
|
|
|
return c.recordClient.Delete(ctx, c.zone.ResourceGroup, c.zone.Name, armprivatedns.RecordTypeTXT, subDomain, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func privateUniqueRecords(recordSet armprivatedns.RecordSet, value string) map[string]struct{} {
|
|
|
|
uniqRecords := map[string]struct{}{value: {}}
|
|
|
|
if recordSet.Properties != nil && recordSet.Properties.TxtRecords != nil {
|
|
|
|
for _, txtRecord := range recordSet.Properties.TxtRecords {
|
|
|
|
// Assume Value doesn't contain multiple strings
|
|
|
|
if len(txtRecord.Value) > 0 {
|
|
|
|
uniqRecords[deref(txtRecord.Value[0])] = struct{}{}
|
|
|
|
}
|
|
|
|
}
|
2023-07-12 20:02:06 +02:00
|
|
|
}
|
|
|
|
|
2024-03-20 05:36:35 +02:00
|
|
|
return uniqRecords
|
2023-07-12 20:02:06 +02:00
|
|
|
}
|