diff --git a/docs/tutorials/azure-private-dns.md b/docs/tutorials/azure-private-dns.md index 425fb9807..4aa2a5197 100644 --- a/docs/tutorials/azure-private-dns.md +++ b/docs/tutorials/azure-private-dns.md @@ -1,12 +1,12 @@ # Set up ExternalDNS for Azure Private DNS -This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS. +This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS. It comprises of the following steps: 1) Install NGINX Ingress Controller 2) Provision Azure Private DNS 3) Configure service principal for managing the zone -4) Deploy ExternalDNS +4) Deploy ExternalDNS Everything will be deployed on Kubernetes. Therefore, please see the subsequent prerequisites. @@ -26,25 +26,27 @@ $ helm install stable/nginx-ingress \ --name nginx-ingress \ --set controller.publishService.enabled=true ``` - -The parameter `controller.publishService.enabled` needs to be set to `true.` + +The parameter `controller.publishService.enabled` needs to be set to `true.` It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller. This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources. In the subsequent parameter we will make use of this. If you don't want to work with ingress-resources in your later use, you can leave the parameter out. Verify the correct propagation of the loadbalancer's ip by listing the ingresses. + ``` $ kubectl get ingress ``` + The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information. + ``` NAME HOSTS ADDRESS PORTS AGE nginx1 sample1.aks.com 52.167.195.110 80 6d22h nginx2 sample2.aks.com 52.167.195.110 80 6d21h ``` - If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice: ``` @@ -144,6 +146,8 @@ This is per default done through the file `~/.kube/config`. For general background information on this see [kubernetes-docs](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/). Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See [Azure-Docs](https://docs.microsoft.com/de-de/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials). +Follow the steps for [azure-dns provider](./azure.md#creating-configuration-file) to create a configuration file. + Then apply one of the following manifests depending on whether you use RBAC or not. The credentials of the service principal are provided to ExternalDNS as environment-variables. @@ -175,13 +179,14 @@ spec: - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= - env: - - name: AZURE_TENANT_ID - value: "" - - name: AZURE_CLIENT_ID - value: "" - - name: AZURE_CLIENT_SECRET - value: "" + volumeMounts: + - name: azure-config-file + mountPath: /etc/kubernetes + readOnly: true + volumes: + - name: azure-config-file + secret: + secretName: azure-config-file ``` ### Manifest (for clusters with RBAC enabled, cluster access) @@ -200,7 +205,7 @@ rules: resources: ["services","endpoints","pods"] verbs: ["get","watch","list"] - apiGroups: ["extensions","networking.k8s.io"] - resources: ["ingresses"] + resources: ["ingresses"] verbs: ["get","watch","list"] - apiGroups: [""] resources: ["nodes"] @@ -245,13 +250,14 @@ spec: - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= - env: - - name: AZURE_TENANT_ID - value: "" - - name: AZURE_CLIENT_ID - value: "" - - name: AZURE_CLIENT_SECRET - value: "" + volumeMounts: + - name: azure-config-file + mountPath: /etc/kubernetes + readOnly: true + volumes: + - name: azure-config-file + secret: + secretName: azure-config-file ``` ### Manifest (for clusters with RBAC enabled, namespace access) @@ -315,13 +321,14 @@ spec: - --provider=azure-private-dns - --azure-resource-group=externaldns - --azure-subscription-id= - env: - - name: AZURE_TENANT_ID - value: "" - - name: AZURE_CLIENT_ID - value: "" - - name: AZURE_CLIENT_SECRET - value: "" + volumeMounts: + - name: azure-config-file + mountPath: /etc/kubernetes + readOnly: true + volumes: + - name: azure-config-file + secret: + secretName: azure-config-file ``` Create the deployment for ExternalDNS: diff --git a/main.go b/main.go index 3b2cf7923..a12a67300 100644 --- a/main.go +++ b/main.go @@ -191,7 +191,7 @@ func main() { case "azure-dns", "azure": p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "azure-private-dns": - p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun) + p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) case "vinyldns": p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) case "vultr": diff --git a/provider/azure/azure.go b/provider/azure/azure.go index 1c82b0136..455179d9c 100644 --- a/provider/azure/azure.go +++ b/provider/azure/azure.go @@ -19,17 +19,12 @@ package azure import ( "context" "fmt" - "io/ioutil" "strings" log "github.com/sirupsen/logrus" - yaml "gopkg.in/yaml.v2" - "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns" "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" - "github.com/Azure/go-autorest/autorest/azure" "github.com/Azure/go-autorest/autorest/to" "sigs.k8s.io/external-dns/endpoint" @@ -41,18 +36,6 @@ const ( azureRecordTTL = 300 ) -type config struct { - Cloud string `json:"cloud" yaml:"cloud"` - TenantID string `json:"tenantId" yaml:"tenantId"` - SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"` - ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"` - Location string `json:"location" yaml:"location"` - ClientID string `json:"aadClientId" yaml:"aadClientId"` - ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"` - UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"` - UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"` -} - // ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing. type ZonesClient interface { ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error) @@ -82,46 +65,22 @@ type AzureProvider struct { // // Returns the provider or an error if a provider could not be created. func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) { - contents, err := ioutil.ReadFile(configFile) - if err != nil { - return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) - } - cfg := config{} - err = yaml.Unmarshal(contents, &cfg) + cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID) if err != nil { return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) } - // If a resource group was given, override what was present in the config file - if resourceGroup != "" { - cfg.ResourceGroup = resourceGroup - } - // If userAssignedIdentityClientID is provided explicitly, override existing one in config file - if userAssignedIdentityClientID != "" { - cfg.UserAssignedIdentityID = userAssignedIdentityClientID - } - - var environment azure.Environment - if cfg.Cloud == "" { - environment = azure.PublicCloud - } else { - environment, err = azure.EnvironmentFromName(cfg.Cloud) - if err != nil { - return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err) - } - } - - token, err := getAccessToken(cfg, environment) + token, err := getAccessToken(*cfg, cfg.Environment) if err != nil { return nil, fmt.Errorf("failed to get token: %v", err) } - zonesClient := dns.NewZonesClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID) + zonesClient := dns.NewZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID) zonesClient.Authorizer = autorest.NewBearerAuthorizer(token) - recordSetsClient := dns.NewRecordSetsClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID) + recordSetsClient := dns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID) recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token) - provider := &AzureProvider{ + return &AzureProvider{ domainFilter: domainFilter, zoneNameFilter: zoneNameFilter, zoneIDFilter: zoneIDFilter, @@ -130,61 +89,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon userAssignedIdentityClientID: cfg.UserAssignedIdentityID, zonesClient: zonesClient, recordSetsClient: recordSetsClient, - } - return provider, nil -} - -// getAccessToken retrieves Azure API access token. -func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) { - // Try to retrieve token with service principal credentials. - // Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true` - // and service principal exists. In this case, we still want to use service principal to authenticate. - if len(cfg.ClientID) > 0 && - len(cfg.ClientSecret) > 0 && - // due to some historical reason, for pure MSI cluster, - // they will use "msi" as placeholder in azure.json. - // In this case, we shouldn't try to use SPN to authenticate. - !strings.EqualFold(cfg.ClientID, "msi") && - !strings.EqualFold(cfg.ClientSecret, "msi") { - log.Info("Using client_id+client_secret to retrieve access token for Azure API.") - oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID) - if err != nil { - return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err) - } - - token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint) - if err != nil { - return nil, fmt.Errorf("failed to create service principal token: %v", err) - } - return token, nil - } - - // Try to retrieve token with MSI. - if cfg.UseManagedIdentityExtension { - log.Info("Using managed identity extension to retrieve access token for Azure API.") - msiEndpoint, err := adal.GetMSIVMEndpoint() - if err != nil { - return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err) - } - - if cfg.UserAssignedIdentityID != "" { - log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID) - token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID) - if err != nil { - return nil, fmt.Errorf("failed to create the managed service identity token: %v", err) - } - return token, nil - } - - log.Info("Resolving to system assigned identity.") - token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint) - if err != nil { - return nil, fmt.Errorf("failed to create the managed service identity token: %v", err) - } - return token, nil - } - - return nil, fmt.Errorf("no credentials provided for Azure API") + }, nil } // Records gets the current records. @@ -352,20 +257,20 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMap) { // Delete records first for zone, endpoints := range deleted { - for _, endpoint := range endpoints { - name := p.recordSetNameForZone(zone, endpoint) - if !p.domainFilter.Match(endpoint.DNSName) { - log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName) + for _, ep := range endpoints { + name := p.recordSetNameForZone(zone, ep) + if !p.domainFilter.Match(ep.DNSName) { + log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", ep.DNSName) continue } if p.dryRun { - log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone) + log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone) } else { - log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone) - if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(endpoint.RecordType), ""); err != nil { + log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone) + if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(ep.RecordType), ""); err != nil { log.Errorf( "Failed to delete %s record named '%s' for Azure DNS zone '%s': %v", - endpoint.RecordType, + ep.RecordType, name, zone, err, @@ -378,18 +283,18 @@ func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMa func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) { for zone, endpoints := range updated { - for _, endpoint := range endpoints { - name := p.recordSetNameForZone(zone, endpoint) - if !p.domainFilter.Match(endpoint.DNSName) { - log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName) + for _, ep := range endpoints { + name := p.recordSetNameForZone(zone, ep) + if !p.domainFilter.Match(ep.DNSName) { + log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", ep.DNSName) continue } if p.dryRun { log.Infof( "Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.", - endpoint.RecordType, + ep.RecordType, name, - endpoint.Targets, + ep.Targets, zone, ) continue @@ -397,20 +302,20 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa log.Infof( "Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.", - endpoint.RecordType, + ep.RecordType, name, - endpoint.Targets, + ep.Targets, zone, ) - recordSet, err := p.newRecordSet(endpoint) + recordSet, err := p.newRecordSet(ep) if err == nil { _, err = p.recordSetsClient.CreateOrUpdate( ctx, p.resourceGroup, zone, name, - dns.RecordType(endpoint.RecordType), + dns.RecordType(ep.RecordType), recordSet, "", "", @@ -419,9 +324,9 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa if err != nil { log.Errorf( "Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v", - endpoint.RecordType, + ep.RecordType, name, - endpoint.Targets, + ep.Targets, zone, err, ) diff --git a/provider/azure/azure_private_dns.go b/provider/azure/azure_private_dns.go index 093e4c031..320def42a 100644 --- a/provider/azure/azure_private_dns.go +++ b/provider/azure/azure_private_dns.go @@ -21,9 +21,8 @@ import ( "fmt" "strings" - "github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns" + "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/Azure/go-autorest/autorest/to" log "github.com/sirupsen/logrus" @@ -47,44 +46,43 @@ type PrivateRecordSetsClient interface { // AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service type AzurePrivateDNSProvider struct { provider.BaseProvider - domainFilter endpoint.DomainFilter - zoneIDFilter provider.ZoneIDFilter - dryRun bool - subscriptionID string - resourceGroup string - zonesClient PrivateZonesClient - recordSetsClient PrivateRecordSetsClient + domainFilter endpoint.DomainFilter + zoneIDFilter provider.ZoneIDFilter + dryRun bool + resourceGroup string + userAssignedIdentityClientID string + zonesClient PrivateZonesClient + recordSetsClient PrivateRecordSetsClient } // NewAzurePrivateDNSProvider creates a new Azure Private DNS provider. // // Returns the provider or an error if a provider could not be created. -func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) { - authorizer, err := auth.NewAuthorizerFromEnvironment() +func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) { + cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) } - settings, err := auth.GetSettingsFromEnvironment() + token, err := getAccessToken(*cfg, cfg.Environment) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get token: %v", err) } - zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID) - zonesClient.Authorizer = authorizer - recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID) - recordSetsClient.Authorizer = authorizer + zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID) + zonesClient.Authorizer = autorest.NewBearerAuthorizer(token) + recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID) + recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token) - provider := &AzurePrivateDNSProvider{ - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - dryRun: dryRun, - subscriptionID: subscriptionID, - resourceGroup: resourceGroup, - zonesClient: zonesClient, - recordSetsClient: recordSetsClient, - } - return provider, nil + return &AzurePrivateDNSProvider{ + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + dryRun: dryRun, + resourceGroup: cfg.ResourceGroup, + userAssignedIdentityClientID: cfg.UserAssignedIdentityID, + zonesClient: zonesClient, + recordSetsClient: recordSetsClient, + }, nil } // Records gets the current records. @@ -256,16 +254,16 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu log.Debugf("Records to be deleted: %d", len(deleted)) // Delete records first for zone, endpoints := range deleted { - for _, endpoint := range endpoints { - name := p.recordSetNameForZone(zone, endpoint) + for _, ep := range endpoints { + name := p.recordSetNameForZone(zone, ep) if p.dryRun { - log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone) + log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone) } else { - log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone) - if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(endpoint.RecordType), name, ""); err != nil { + log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone) + if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(ep.RecordType), name, ""); err != nil { log.Errorf( "Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v", - endpoint.RecordType, + ep.RecordType, name, zone, err, @@ -279,14 +277,14 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) { log.Debugf("Records to be updated: %d", len(updated)) for zone, endpoints := range updated { - for _, endpoint := range endpoints { - name := p.recordSetNameForZone(zone, endpoint) + for _, ep := range endpoints { + name := p.recordSetNameForZone(zone, ep) if p.dryRun { log.Infof( "Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.", - endpoint.RecordType, + ep.RecordType, name, - endpoint.Targets, + ep.Targets, zone, ) continue @@ -294,19 +292,19 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu log.Infof( "Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.", - endpoint.RecordType, + ep.RecordType, name, - endpoint.Targets, + ep.Targets, zone, ) - recordSet, err := p.newRecordSet(endpoint) + recordSet, err := p.newRecordSet(ep) if err == nil { _, err = p.recordSetsClient.CreateOrUpdate( ctx, p.resourceGroup, zone, - privatedns.RecordType(endpoint.RecordType), + privatedns.RecordType(ep.RecordType), name, recordSet, "", @@ -316,9 +314,9 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu if err != nil { log.Errorf( "Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v", - endpoint.RecordType, + ep.RecordType, name, - endpoint.Targets, + ep.Targets, zone, err, ) diff --git a/provider/azure/azure_privatedns_test.go b/provider/azure/azure_privatedns_test.go index 1754493b2..6181524ab 100644 --- a/provider/azure/azure_privatedns_test.go +++ b/provider/azure/azure_privatedns_test.go @@ -18,16 +18,11 @@ package azure import ( "context" - "os" "testing" "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/azure" - "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/Azure/go-autorest/autorest/to" - "github.com/stretchr/testify/assert" - "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" @@ -255,36 +250,6 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter } } -func validateAzurePrivateDNSClientsResourceManager(t *testing.T, environmentName string, expectedResourceManagerEndpoint string) { - err := os.Setenv(auth.EnvironmentName, environmentName) - if err != nil { - t.Fatal(err) - } - - azurePrivateDNSProvider, err := NewAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), "k8s", "sub", true) - - if err != nil { - t.Fatal(err) - } - - zonesClientBaseURI := azurePrivateDNSProvider.zonesClient.(privatedns.PrivateZonesClient).BaseURI - recordSetsClientBaseURI := azurePrivateDNSProvider.recordSetsClient.(privatedns.RecordSetsClient).BaseURI - - assert.Equal(t, zonesClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, zonesClientBaseURI) - assert.Equal(t, recordSetsClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, recordSetsClientBaseURI) -} - -func TestNewAzurePrivateDNSProvider(t *testing.T) { - // make sure to reset the environment variables at the end again - originalEnv := os.Getenv(auth.EnvironmentName) - defer os.Setenv(auth.EnvironmentName, originalEnv) - - validateAzurePrivateDNSClientsResourceManager(t, "", azure.PublicCloud.ResourceManagerEndpoint) - validateAzurePrivateDNSClientsResourceManager(t, "AZURECHINACLOUD", azure.ChinaCloud.ResourceManagerEndpoint) - validateAzurePrivateDNSClientsResourceManager(t, "AZUREGERMANCLOUD", azure.GermanCloud.ResourceManagerEndpoint) - validateAzurePrivateDNSClientsResourceManager(t, "AZUREUSGOVERNMENTCLOUD", azure.USGovernmentCloud.ResourceManagerEndpoint) -} - func TestAzurePrivateDNSRecord(t *testing.T) { provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", &[]privatedns.PrivateZone{ diff --git a/provider/azure/common.go b/provider/azure/common.go new file mode 100644 index 000000000..02386c889 --- /dev/null +++ b/provider/azure/common.go @@ -0,0 +1,129 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/Azure/go-autorest/autorest/adal" + "github.com/Azure/go-autorest/autorest/azure" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +// Config represents common config items for Azure DNS and Azure Private DNS +type config struct { + Cloud string `json:"cloud" yaml:"cloud"` + Environment azure.Environment `json:"-" yaml:"-"` + TenantID string `json:"tenantId" yaml:"tenantId"` + SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"` + ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"` + Location string `json:"location" yaml:"location"` + ClientID string `json:"aadClientId" yaml:"aadClientId"` + ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"` + UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"` + UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"` +} + +func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*config, error) { + contents, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) + } + cfg := &config{} + err = yaml.Unmarshal(contents, &cfg) + if err != nil { + return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) + } + + // If a resource group was given, override what was present in the config file + if resourceGroup != "" { + cfg.ResourceGroup = resourceGroup + } + // If userAssignedIdentityClientID is provided explicitly, override existing one in config file + if userAssignedIdentityClientID != "" { + cfg.UserAssignedIdentityID = userAssignedIdentityClientID + } + + var environment azure.Environment + if cfg.Cloud == "" { + environment = azure.PublicCloud + } else { + environment, err = azure.EnvironmentFromName(cfg.Cloud) + if err != nil { + return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err) + } + } + cfg.Environment = environment + + return cfg, nil +} + +// getAccessToken retrieves Azure API access token. +func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) { + // Try to retrieve token with service principal credentials. + // Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true` + // and service principal exists. In this case, we still want to use service principal to authenticate. + if len(cfg.ClientID) > 0 && + len(cfg.ClientSecret) > 0 && + // due to some historical reason, for pure MSI cluster, + // they will use "msi" as placeholder in azure.json. + // In this case, we shouldn't try to use SPN to authenticate. + !strings.EqualFold(cfg.ClientID, "msi") && + !strings.EqualFold(cfg.ClientSecret, "msi") { + log.Info("Using client_id+client_secret to retrieve access token for Azure API.") + oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err) + } + + token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to create service principal token: %v", err) + } + return token, nil + } + + // Try to retrieve token with MSI. + if cfg.UseManagedIdentityExtension { + log.Info("Using managed identity extension to retrieve access token for Azure API.") + msiEndpoint, err := adal.GetMSIVMEndpoint() + if err != nil { + return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err) + } + + if cfg.UserAssignedIdentityID != "" { + log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID) + token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID) + if err != nil { + return nil, fmt.Errorf("failed to create the managed service identity token: %v", err) + } + return token, nil + } + + log.Info("Resolving to system assigned identity.") + token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to create the managed service identity token: %v", err) + } + return token, nil + } + + return nil, fmt.Errorf("no credentials provided for Azure API") +} diff --git a/provider/azure/common_test.go b/provider/azure/common_test.go new file mode 100644 index 000000000..bf591b0fc --- /dev/null +++ b/provider/azure/common_test.go @@ -0,0 +1,67 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure + +import ( + "fmt" + "github.com/Azure/go-autorest/autorest/azure" + "io/ioutil" + "os" + "reflect" + "testing" +) + +func TestGetAzureEnvironmentConfig(t *testing.T) { + tmp, err := ioutil.TempFile("", "azureconf") + if err != nil { + t.Errorf("couldn't write temp file %v", err) + } + defer os.Remove(tmp.Name()) + + tests := map[string]struct { + cloud string + err error + }{ + "AzureChinaCloud": {"AzureChinaCloud", nil}, + "AzureGermanCloud": {"AzureGermanCloud", nil}, + "AzurePublicCloud": {"", nil}, + "AzureUSGovernment": {"AzureUSGovernmentCloud", nil}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + _, _ = tmp.Seek(0, 0) + _, _ = tmp.Write([]byte(fmt.Sprintf(`{"cloud": "%s"}`, test.cloud))) + got, err := getConfig(tmp.Name(), "", "") + if err != nil { + t.Errorf("got unexpected err %v", err) + } + + if test.cloud == "" { + test.cloud = "AzurePublicCloud" + } + want, err := azure.EnvironmentFromName(test.cloud) + if err != nil { + t.Errorf("couldn't get azure environment from provided name %v", err) + } + + if !reflect.DeepEqual(want, got.Environment) { + t.Errorf("got %v, want %v", got.Environment, want) + } + }) + } +}