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 }