servicediscovery.go raw

   1  package azuredns
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"fmt"
   7  
   8  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
   9  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
  10  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
  11  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph"
  12  	"github.com/go-acme/lego/v4/providers/dns/internal/ptr"
  13  )
  14  
  15  type ServiceDiscoveryZone struct {
  16  	Name           string
  17  	SubscriptionID string
  18  	ResourceGroup  string
  19  }
  20  
  21  const (
  22  	ResourceGraphTypePublicDNSZone  = "microsoft.network/dnszones"
  23  	ResourceGraphTypePrivateDNSZone = "microsoft.network/privatednszones"
  24  )
  25  
  26  const ResourceGraphQueryOptionsTop int32 = 1000
  27  
  28  // discoverDNSZones finds all visible Azure DNS zones based on optional subscriptionID, resourceGroup and serviceDiscovery filter using Kusto query.
  29  func discoverDNSZones(ctx context.Context, config *Config, credentials azcore.TokenCredential) (map[string]ServiceDiscoveryZone, error) {
  30  	options := &arm.ClientOptions{
  31  		ClientOptions: azcore.ClientOptions{
  32  			Cloud: config.Environment,
  33  		},
  34  	}
  35  
  36  	client, err := armresourcegraph.NewClient(credentials, options)
  37  	if err != nil {
  38  		return nil, err
  39  	}
  40  
  41  	// Set options
  42  	requestOptions := &armresourcegraph.QueryRequestOptions{
  43  		ResultFormat: to.Ptr(armresourcegraph.ResultFormatObjectArray),
  44  		Top:          to.Ptr(ResourceGraphQueryOptionsTop),
  45  		Skip:         to.Ptr[int32](0),
  46  	}
  47  
  48  	zones := map[string]ServiceDiscoveryZone{}
  49  
  50  	for {
  51  		// create the query request
  52  		request := armresourcegraph.QueryRequest{
  53  			Query:   to.Ptr(createGraphQuery(config)),
  54  			Options: requestOptions,
  55  		}
  56  
  57  		result, err := client.Resources(ctx, request, nil)
  58  		if err != nil {
  59  			return zones, err
  60  		}
  61  
  62  		resultList, ok := result.Data.([]any)
  63  		if !ok {
  64  			// got invalid or empty data, skipping
  65  			break
  66  		}
  67  
  68  		for _, row := range resultList {
  69  			rowData, ok := row.(map[string]any)
  70  			if !ok {
  71  				continue
  72  			}
  73  
  74  			zoneName, ok := rowData["name"].(string)
  75  			if !ok {
  76  				continue
  77  			}
  78  
  79  			if _, exists := zones[zoneName]; exists {
  80  				return zones, fmt.Errorf(`found duplicate dns zone "%s"`, zoneName)
  81  			}
  82  
  83  			zones[zoneName] = ServiceDiscoveryZone{
  84  				Name:           zoneName,
  85  				ResourceGroup:  rowData["resourceGroup"].(string),
  86  				SubscriptionID: rowData["subscriptionId"].(string),
  87  			}
  88  		}
  89  
  90  		*requestOptions.Skip += ResourceGraphQueryOptionsTop
  91  
  92  		if result.TotalRecords != nil {
  93  			if int64(ptr.Deref(requestOptions.Skip)) >= ptr.Deref(result.TotalRecords) {
  94  				break
  95  			}
  96  		}
  97  	}
  98  
  99  	return zones, nil
 100  }
 101  
 102  func createGraphQuery(config *Config) string {
 103  	buf := new(bytes.Buffer)
 104  	buf.WriteString("\nresources\n")
 105  
 106  	resourceType := ResourceGraphTypePublicDNSZone
 107  	if config.PrivateZone {
 108  		resourceType = ResourceGraphTypePrivateDNSZone
 109  	}
 110  
 111  	_, _ = fmt.Fprintf(buf, "| where type =~ %q\n", resourceType)
 112  
 113  	if config.SubscriptionID != "" {
 114  		_, _ = fmt.Fprintf(buf, "| where subscriptionId =~ %q\n", config.SubscriptionID)
 115  	}
 116  
 117  	if config.ResourceGroup != "" {
 118  		_, _ = fmt.Fprintf(buf, "| where resourceGroup =~ %q\n", config.ResourceGroup)
 119  	}
 120  
 121  	if config.ServiceDiscoveryFilter != "" {
 122  		_, _ = fmt.Fprintf(buf, "| %s\n", config.ServiceDiscoveryFilter)
 123  	}
 124  
 125  	buf.WriteString("| project subscriptionId, resourceGroup, name")
 126  
 127  	return buf.String()
 128  }
 129