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  }