k8s.io/kubernetes@v1.29.3/pkg/credentialprovider/azure/azure_credentials.go (about)

     1  //go:build !providerless
     2  // +build !providerless
     3  
     4  /*
     5  Copyright 2016 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package azure
    21  
    22  import (
    23  	"context"
    24  	"errors"
    25  	"io"
    26  	"os"
    27  	"regexp"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	"github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry"
    33  	"github.com/Azure/go-autorest/autorest/adal"
    34  	"github.com/Azure/go-autorest/autorest/azure"
    35  	"github.com/spf13/pflag"
    36  
    37  	"k8s.io/client-go/tools/cache"
    38  	"k8s.io/klog/v2"
    39  	"k8s.io/kubernetes/pkg/credentialprovider"
    40  	"k8s.io/legacy-cloud-providers/azure/auth"
    41  	"sigs.k8s.io/yaml"
    42  )
    43  
    44  var flagConfigFile = pflag.String("azure-container-registry-config", "",
    45  	"Path to the file containing Azure container registry configuration information.")
    46  
    47  const (
    48  	dummyRegistryEmail = "name@contoso.com"
    49  	maxReadLength      = 10 * 1 << 20 // 10MB
    50  )
    51  
    52  var (
    53  	containerRegistryUrls = []string{"*.azurecr.io", "*.azurecr.cn", "*.azurecr.de", "*.azurecr.us"}
    54  	acrRE                 = regexp.MustCompile(`.*\.azurecr\.io|.*\.azurecr\.cn|.*\.azurecr\.de|.*\.azurecr\.us`)
    55  	warnOnce              sync.Once
    56  )
    57  
    58  // init registers the various means by which credentials may
    59  // be resolved on Azure.
    60  func init() {
    61  	credentialprovider.RegisterCredentialProvider(
    62  		"azure",
    63  		NewACRProvider(flagConfigFile),
    64  	)
    65  }
    66  
    67  type cacheEntry struct {
    68  	expiresAt   time.Time
    69  	credentials credentialprovider.DockerConfigEntry
    70  	registry    string
    71  }
    72  
    73  // acrExpirationPolicy implements ExpirationPolicy from client-go.
    74  type acrExpirationPolicy struct{}
    75  
    76  // stringKeyFunc returns the cache key as a string
    77  func stringKeyFunc(obj interface{}) (string, error) {
    78  	key := obj.(*cacheEntry).registry
    79  	return key, nil
    80  }
    81  
    82  // IsExpired checks if the ACR credentials are expired.
    83  func (p *acrExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool {
    84  	return time.Now().After(entry.Obj.(*cacheEntry).expiresAt)
    85  }
    86  
    87  // RegistriesClient is a testable interface for the ACR client List operation.
    88  type RegistriesClient interface {
    89  	List(ctx context.Context) ([]containerregistry.Registry, error)
    90  }
    91  
    92  // NewACRProvider parses the specified configFile and returns a DockerConfigProvider
    93  func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider {
    94  	return &acrProvider{
    95  		file:  configFile,
    96  		cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}),
    97  	}
    98  }
    99  
   100  type acrProvider struct {
   101  	file                  *string
   102  	config                *auth.AzureAuthConfig
   103  	environment           *azure.Environment
   104  	servicePrincipalToken *adal.ServicePrincipalToken
   105  	cache                 cache.Store
   106  }
   107  
   108  // ParseConfig returns a parsed configuration for an Azure cloudprovider config file
   109  func parseConfig(configReader io.Reader) (*auth.AzureAuthConfig, error) {
   110  	var config auth.AzureAuthConfig
   111  
   112  	if configReader == nil {
   113  		return &config, nil
   114  	}
   115  
   116  	limitedReader := &io.LimitedReader{R: configReader, N: maxReadLength}
   117  	configContents, err := io.ReadAll(limitedReader)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	if limitedReader.N <= 0 {
   122  		return nil, errors.New("the read limit is reached")
   123  	}
   124  	err = yaml.Unmarshal(configContents, &config)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	return &config, nil
   130  }
   131  
   132  func (a *acrProvider) loadConfig(rdr io.Reader) error {
   133  	var err error
   134  	a.config, err = parseConfig(rdr)
   135  	if err != nil {
   136  		klog.Errorf("Failed to load azure credential file: %v", err)
   137  	}
   138  
   139  	a.environment, err = auth.ParseAzureEnvironment(a.config.Cloud, a.config.ResourceManagerEndpoint, a.config.IdentitySystem)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	return nil
   145  }
   146  
   147  func (a *acrProvider) Enabled() bool {
   148  	if a.file == nil || len(*a.file) == 0 {
   149  		klog.V(5).Infof("Azure config unspecified, disabling")
   150  		return false
   151  	}
   152  
   153  	if credentialprovider.AreLegacyCloudCredentialProvidersDisabled() {
   154  		warnOnce.Do(func() {
   155  			klog.V(4).Infof("Azure credential provider is now disabled. Please refer to sig-cloud-provider for guidance on external credential provider integration for Azure")
   156  		})
   157  		return false
   158  	}
   159  
   160  	f, err := os.Open(*a.file)
   161  	if err != nil {
   162  		klog.Errorf("Failed to load config from file: %s", *a.file)
   163  		return false
   164  	}
   165  	defer f.Close()
   166  
   167  	err = a.loadConfig(f)
   168  	if err != nil {
   169  		klog.Errorf("Failed to load config from file: %s", *a.file)
   170  		return false
   171  	}
   172  
   173  	a.servicePrincipalToken, err = auth.GetServicePrincipalToken(a.config, a.environment)
   174  	if err != nil {
   175  		klog.Errorf("Failed to create service principal token: %v", err)
   176  	}
   177  	return true
   178  }
   179  
   180  // getFromCache attempts to get credentials from the cache
   181  func (a *acrProvider) getFromCache(loginServer string) (credentialprovider.DockerConfig, bool) {
   182  	cfg := credentialprovider.DockerConfig{}
   183  	obj, exists, err := a.cache.GetByKey(loginServer)
   184  	if err != nil {
   185  		klog.Errorf("error getting ACR credentials from cache: %v", err)
   186  		return cfg, false
   187  	}
   188  	if !exists {
   189  		return cfg, false
   190  	}
   191  
   192  	entry := obj.(*cacheEntry)
   193  	cfg[entry.registry] = entry.credentials
   194  	return cfg, true
   195  }
   196  
   197  // getFromACR gets credentials from ACR since they are not in the cache
   198  func (a *acrProvider) getFromACR(loginServer string) (credentialprovider.DockerConfig, error) {
   199  	cfg := credentialprovider.DockerConfig{}
   200  	cred, err := getACRDockerEntryFromARMToken(a, loginServer)
   201  	if err != nil {
   202  		return cfg, err
   203  	}
   204  
   205  	entry := &cacheEntry{
   206  		expiresAt:   time.Now().Add(10 * time.Minute),
   207  		credentials: *cred,
   208  		registry:    loginServer,
   209  	}
   210  	if err := a.cache.Add(entry); err != nil {
   211  		return cfg, err
   212  	}
   213  	cfg[loginServer] = *cred
   214  	return cfg, nil
   215  }
   216  
   217  func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
   218  	loginServer := a.parseACRLoginServerFromImage(image)
   219  	if loginServer == "" {
   220  		klog.V(2).Infof("image(%s) is not from ACR, return empty authentication", image)
   221  		return credentialprovider.DockerConfig{}
   222  	}
   223  
   224  	cfg := credentialprovider.DockerConfig{}
   225  	if a.config != nil && a.config.UseManagedIdentityExtension {
   226  		var exists bool
   227  		cfg, exists = a.getFromCache(loginServer)
   228  		if exists {
   229  			klog.V(4).Infof("Got ACR credentials from cache for %s", loginServer)
   230  		} else {
   231  			klog.V(2).Infof("unable to get ACR credentials from cache for %s, checking ACR API", loginServer)
   232  			var err error
   233  			cfg, err = a.getFromACR(loginServer)
   234  			if err != nil {
   235  				klog.Errorf("error getting credentials from ACR for %s %v", loginServer, err)
   236  			}
   237  		}
   238  	} else {
   239  		// Add our entry for each of the supported container registry URLs
   240  		for _, url := range containerRegistryUrls {
   241  			cred := &credentialprovider.DockerConfigEntry{
   242  				Username: a.config.AADClientID,
   243  				Password: a.config.AADClientSecret,
   244  				Email:    dummyRegistryEmail,
   245  			}
   246  			cfg[url] = *cred
   247  		}
   248  
   249  		// Handle the custom cloud case
   250  		// In clouds where ACR is not yet deployed, the string will be empty
   251  		if a.environment != nil && strings.Contains(a.environment.ContainerRegistryDNSSuffix, ".azurecr.") {
   252  			customAcrSuffix := "*" + a.environment.ContainerRegistryDNSSuffix
   253  			hasBeenAdded := false
   254  			for _, url := range containerRegistryUrls {
   255  				if strings.EqualFold(url, customAcrSuffix) {
   256  					hasBeenAdded = true
   257  					break
   258  				}
   259  			}
   260  
   261  			if !hasBeenAdded {
   262  				cred := &credentialprovider.DockerConfigEntry{
   263  					Username: a.config.AADClientID,
   264  					Password: a.config.AADClientSecret,
   265  					Email:    dummyRegistryEmail,
   266  				}
   267  				cfg[customAcrSuffix] = *cred
   268  			}
   269  		}
   270  	}
   271  
   272  	// add ACR anonymous repo support: use empty username and password for anonymous access
   273  	defaultConfigEntry := credentialprovider.DockerConfigEntry{
   274  		Username: "",
   275  		Password: "",
   276  		Email:    dummyRegistryEmail,
   277  	}
   278  	cfg["*.azurecr.*"] = defaultConfigEntry
   279  	return cfg
   280  }
   281  
   282  func getLoginServer(registry containerregistry.Registry) string {
   283  	return *(*registry.RegistryProperties).LoginServer
   284  }
   285  
   286  func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.DockerConfigEntry, error) {
   287  	if a.servicePrincipalToken == nil {
   288  		token, err := auth.GetServicePrincipalToken(a.config, a.environment)
   289  		if err != nil {
   290  			klog.Errorf("Failed to create service principal token: %v", err)
   291  			return nil, err
   292  		}
   293  		a.servicePrincipalToken = token
   294  	} else {
   295  		// Run EnsureFresh to make sure the token is valid and does not expire
   296  		if err := a.servicePrincipalToken.EnsureFresh(); err != nil {
   297  			klog.Errorf("Failed to ensure fresh service principal token: %v", err)
   298  			return nil, err
   299  		}
   300  	}
   301  
   302  	armAccessToken := a.servicePrincipalToken.OAuthToken()
   303  
   304  	klog.V(4).Infof("discovering auth redirects for: %s", loginServer)
   305  	directive, err := receiveChallengeFromLoginServer(loginServer)
   306  	if err != nil {
   307  		klog.Errorf("failed to receive challenge: %s", err)
   308  		return nil, err
   309  	}
   310  
   311  	klog.V(4).Infof("exchanging an acr refresh_token")
   312  	registryRefreshToken, err := performTokenExchange(
   313  		loginServer, directive, a.config.TenantID, armAccessToken)
   314  	if err != nil {
   315  		klog.Errorf("failed to perform token exchange: %s", err)
   316  		return nil, err
   317  	}
   318  
   319  	klog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
   320  	return &credentialprovider.DockerConfigEntry{
   321  		Username: dockerTokenLoginUsernameGUID,
   322  		Password: registryRefreshToken,
   323  		Email:    dummyRegistryEmail,
   324  	}, nil
   325  }
   326  
   327  // parseACRLoginServerFromImage takes image as parameter and returns login server of it.
   328  // Parameter `image` is expected in following format: foo.azurecr.io/bar/imageName:version
   329  // If the provided image is not an acr image, this function will return an empty string.
   330  func (a *acrProvider) parseACRLoginServerFromImage(image string) string {
   331  	match := acrRE.FindAllString(image, -1)
   332  	if len(match) == 1 {
   333  		return match[0]
   334  	}
   335  
   336  	// handle the custom cloud case
   337  	if a != nil && a.environment != nil {
   338  		cloudAcrSuffix := a.environment.ContainerRegistryDNSSuffix
   339  		cloudAcrSuffixLength := len(cloudAcrSuffix)
   340  		if cloudAcrSuffixLength > 0 {
   341  			customAcrSuffixIndex := strings.Index(image, cloudAcrSuffix)
   342  			if customAcrSuffixIndex != -1 {
   343  				endIndex := customAcrSuffixIndex + cloudAcrSuffixLength
   344  				return image[0:endIndex]
   345  			}
   346  		}
   347  	}
   348  
   349  	return ""
   350  }