sigs.k8s.io/external-dns@v0.14.1/provider/azure/config.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package azure 18 19 import ( 20 "fmt" 21 "os" 22 "strings" 23 24 "github.com/Azure/azure-sdk-for-go/sdk/azcore" 25 "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" 26 "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" 27 "github.com/Azure/azure-sdk-for-go/sdk/azidentity" 28 log "github.com/sirupsen/logrus" 29 "gopkg.in/yaml.v2" 30 ) 31 32 // config represents common config items for Azure DNS and Azure Private DNS 33 type config struct { 34 Cloud string `json:"cloud" yaml:"cloud"` 35 TenantID string `json:"tenantId" yaml:"tenantId"` 36 SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"` 37 ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"` 38 Location string `json:"location" yaml:"location"` 39 ClientID string `json:"aadClientId" yaml:"aadClientId"` 40 ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"` 41 UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"` 42 UseWorkloadIdentityExtension bool `json:"useWorkloadIdentityExtension" yaml:"useWorkloadIdentityExtension"` 43 UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"` 44 } 45 46 func getConfig(configFile, subscriptionID, resourceGroup, userAssignedIdentityClientID string) (*config, error) { 47 contents, err := os.ReadFile(configFile) 48 if err != nil { 49 return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) 50 } 51 cfg := &config{} 52 err = yaml.Unmarshal(contents, &cfg) 53 if err != nil { 54 return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) 55 } 56 // If a subscription ID was given, override what was present in the config file 57 if subscriptionID != "" { 58 cfg.SubscriptionID = subscriptionID 59 } 60 // If a resource group was given, override what was present in the config file 61 if resourceGroup != "" { 62 cfg.ResourceGroup = resourceGroup 63 } 64 // If userAssignedIdentityClientID is provided explicitly, override existing one in config file 65 if userAssignedIdentityClientID != "" { 66 cfg.UserAssignedIdentityID = userAssignedIdentityClientID 67 } 68 return cfg, nil 69 } 70 71 // getCredentials retrieves Azure API credentials. 72 func getCredentials(cfg config) (azcore.TokenCredential, *arm.ClientOptions, error) { 73 cloudCfg, err := getCloudConfiguration(cfg.Cloud) 74 if err != nil { 75 return nil, nil, fmt.Errorf("failed to get cloud configuration: %w", err) 76 } 77 clientOpts := azcore.ClientOptions{ 78 Cloud: cloudCfg, 79 } 80 armClientOpts := &arm.ClientOptions{ 81 ClientOptions: clientOpts, 82 } 83 84 // Try to retrieve token with service principal credentials. 85 // Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true` 86 // and service principal exists. In this case, we still want to use service principal to authenticate. 87 if len(cfg.ClientID) > 0 && 88 len(cfg.ClientSecret) > 0 && 89 // due to some historical reason, for pure MSI cluster, 90 // they will use "msi" as placeholder in azure.json. 91 // In this case, we shouldn't try to use SPN to authenticate. 92 !strings.EqualFold(cfg.ClientID, "msi") && 93 !strings.EqualFold(cfg.ClientSecret, "msi") { 94 log.Info("Using client_id+client_secret to retrieve access token for Azure API.") 95 opts := &azidentity.ClientSecretCredentialOptions{ 96 ClientOptions: clientOpts, 97 } 98 cred, err := azidentity.NewClientSecretCredential(cfg.TenantID, cfg.ClientID, cfg.ClientSecret, opts) 99 if err != nil { 100 return nil, nil, fmt.Errorf("failed to create service principal token: %w", err) 101 } 102 return cred, armClientOpts, nil 103 } 104 105 // Try to retrieve token with Workload Identity. 106 if cfg.UseWorkloadIdentityExtension { 107 log.Info("Using workload identity extension to retrieve access token for Azure API.") 108 109 wiOpt := azidentity.WorkloadIdentityCredentialOptions{ 110 ClientOptions: clientOpts, 111 // In a standard scenario, Client ID and Tenant ID are expected to be read from environment variables. 112 // Though, in certain cases, it might be important to have an option to override those (e.g. when AZURE_TENANT_ID is not set 113 // through a webhook or azure.workload.identity/client-id service account annotation is absent). When any of those values are 114 // empty in our config, they will automatically be read from environment variables by azidentity 115 TenantID: cfg.TenantID, 116 ClientID: cfg.ClientID, 117 } 118 119 cred, err := azidentity.NewWorkloadIdentityCredential(&wiOpt) 120 if err != nil { 121 return nil, nil, fmt.Errorf("failed to create a workload identity token: %w", err) 122 } 123 124 return cred, armClientOpts, nil 125 } 126 127 // Try to retrieve token with MSI. 128 if cfg.UseManagedIdentityExtension { 129 log.Info("Using managed identity extension to retrieve access token for Azure API.") 130 msiOpt := azidentity.ManagedIdentityCredentialOptions{ 131 ClientOptions: clientOpts, 132 } 133 if cfg.UserAssignedIdentityID != "" { 134 msiOpt.ID = azidentity.ClientID(cfg.UserAssignedIdentityID) 135 } 136 cred, err := azidentity.NewManagedIdentityCredential(&msiOpt) 137 if err != nil { 138 return nil, nil, fmt.Errorf("failed to create the managed service identity token: %w", err) 139 } 140 return cred, armClientOpts, nil 141 } 142 143 return nil, nil, fmt.Errorf("no credentials provided for Azure API") 144 } 145 146 func getCloudConfiguration(name string) (cloud.Configuration, error) { 147 name = strings.ToUpper(name) 148 switch name { 149 case "AZURECLOUD", "AZUREPUBLICCLOUD", "": 150 return cloud.AzurePublic, nil 151 case "AZUREUSGOVERNMENT", "AZUREUSGOVERNMENTCLOUD": 152 return cloud.AzureGovernment, nil 153 case "AZURECHINACLOUD": 154 return cloud.AzureChina, nil 155 } 156 return cloud.Configuration{}, fmt.Errorf("unknown cloud name: %s", name) 157 }