azure_credentials.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package azure
  14. import (
  15. "context"
  16. "errors"
  17. "io"
  18. "io/ioutil"
  19. "os"
  20. "regexp"
  21. "time"
  22. "github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2019-05-01/containerregistry"
  23. "github.com/Azure/go-autorest/autorest"
  24. "github.com/Azure/go-autorest/autorest/adal"
  25. "github.com/Azure/go-autorest/autorest/azure"
  26. "github.com/spf13/pflag"
  27. "k8s.io/klog"
  28. "k8s.io/kubernetes/pkg/credentialprovider"
  29. "k8s.io/legacy-cloud-providers/azure/auth"
  30. "sigs.k8s.io/yaml"
  31. )
  32. var flagConfigFile = pflag.String("azure-container-registry-config", "",
  33. "Path to the file containing Azure container registry configuration information.")
  34. const (
  35. dummyRegistryEmail = "name@contoso.com"
  36. maxReadLength = 10 * 1 << 20 // 10MB
  37. )
  38. var (
  39. containerRegistryUrls = []string{"*.azurecr.io", "*.azurecr.cn", "*.azurecr.de", "*.azurecr.us"}
  40. acrRE = regexp.MustCompile(`.*\.azurecr\.io|.*\.azurecr\.cn|.*\.azurecr\.de|.*\.azurecr\.us`)
  41. )
  42. // init registers the various means by which credentials may
  43. // be resolved on Azure.
  44. func init() {
  45. credentialprovider.RegisterCredentialProvider("azure",
  46. &credentialprovider.CachingDockerConfigProvider{
  47. Provider: NewACRProvider(flagConfigFile),
  48. Lifetime: 1 * time.Minute,
  49. })
  50. }
  51. // RegistriesClient is a testable interface for the ACR client List operation.
  52. type RegistriesClient interface {
  53. List(ctx context.Context) ([]containerregistry.Registry, error)
  54. }
  55. // azRegistriesClient implements RegistriesClient.
  56. type azRegistriesClient struct {
  57. client containerregistry.RegistriesClient
  58. }
  59. func newAzRegistriesClient(subscriptionID, endpoint string, token *adal.ServicePrincipalToken) *azRegistriesClient {
  60. registryClient := containerregistry.NewRegistriesClient(subscriptionID)
  61. registryClient.BaseURI = endpoint
  62. registryClient.Authorizer = autorest.NewBearerAuthorizer(token)
  63. return &azRegistriesClient{
  64. client: registryClient,
  65. }
  66. }
  67. func (az *azRegistriesClient) List(ctx context.Context) ([]containerregistry.Registry, error) {
  68. iterator, err := az.client.ListComplete(ctx)
  69. if err != nil {
  70. return nil, err
  71. }
  72. result := make([]containerregistry.Registry, 0)
  73. for ; iterator.NotDone(); err = iterator.Next() {
  74. if err != nil {
  75. return nil, err
  76. }
  77. result = append(result, iterator.Value())
  78. }
  79. return result, nil
  80. }
  81. // NewACRProvider parses the specified configFile and returns a DockerConfigProvider
  82. func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider {
  83. return &acrProvider{
  84. file: configFile,
  85. }
  86. }
  87. type acrProvider struct {
  88. file *string
  89. config *auth.AzureAuthConfig
  90. environment *azure.Environment
  91. registryClient RegistriesClient
  92. servicePrincipalToken *adal.ServicePrincipalToken
  93. }
  94. // ParseConfig returns a parsed configuration for an Azure cloudprovider config file
  95. func parseConfig(configReader io.Reader) (*auth.AzureAuthConfig, error) {
  96. var config auth.AzureAuthConfig
  97. if configReader == nil {
  98. return &config, nil
  99. }
  100. limitedReader := &io.LimitedReader{R: configReader, N: maxReadLength}
  101. configContents, err := ioutil.ReadAll(limitedReader)
  102. if err != nil {
  103. return nil, err
  104. }
  105. if limitedReader.N <= 0 {
  106. return nil, errors.New("the read limit is reached")
  107. }
  108. err = yaml.Unmarshal(configContents, &config)
  109. if err != nil {
  110. return nil, err
  111. }
  112. return &config, nil
  113. }
  114. func (a *acrProvider) loadConfig(rdr io.Reader) error {
  115. var err error
  116. a.config, err = parseConfig(rdr)
  117. if err != nil {
  118. klog.Errorf("Failed to load azure credential file: %v", err)
  119. }
  120. a.environment, err = auth.ParseAzureEnvironment(a.config.Cloud, a.config.ResourceManagerEndpoint, a.config.IdentitySystem)
  121. if err != nil {
  122. return err
  123. }
  124. return nil
  125. }
  126. func (a *acrProvider) Enabled() bool {
  127. if a.file == nil || len(*a.file) == 0 {
  128. klog.V(5).Infof("Azure config unspecified, disabling")
  129. return false
  130. }
  131. f, err := os.Open(*a.file)
  132. if err != nil {
  133. klog.Errorf("Failed to load config from file: %s", *a.file)
  134. return false
  135. }
  136. defer f.Close()
  137. err = a.loadConfig(f)
  138. if err != nil {
  139. klog.Errorf("Failed to load config from file: %s", *a.file)
  140. return false
  141. }
  142. a.servicePrincipalToken, err = auth.GetServicePrincipalToken(a.config, a.environment)
  143. if err != nil {
  144. klog.Errorf("Failed to create service principal token: %v", err)
  145. return false
  146. }
  147. a.registryClient = newAzRegistriesClient(a.config.SubscriptionID, a.environment.ResourceManagerEndpoint, a.servicePrincipalToken)
  148. return true
  149. }
  150. func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
  151. klog.V(4).Infof("try to provide secret for image %s", image)
  152. cfg := credentialprovider.DockerConfig{}
  153. if a.config.UseManagedIdentityExtension {
  154. if loginServer := parseACRLoginServerFromImage(image); loginServer == "" {
  155. klog.V(4).Infof("image(%s) is not from ACR, skip MSI authentication", image)
  156. } else {
  157. if cred, err := getACRDockerEntryFromARMToken(a, loginServer); err == nil {
  158. cfg[loginServer] = *cred
  159. }
  160. }
  161. } else {
  162. // Add our entry for each of the supported container registry URLs
  163. for _, url := range containerRegistryUrls {
  164. cred := &credentialprovider.DockerConfigEntry{
  165. Username: a.config.AADClientID,
  166. Password: a.config.AADClientSecret,
  167. Email: dummyRegistryEmail,
  168. }
  169. cfg[url] = *cred
  170. }
  171. }
  172. // add ACR anonymous repo support: use empty username and password for anonymous access
  173. cfg["*.azurecr.*"] = credentialprovider.DockerConfigEntry{
  174. Username: "",
  175. Password: "",
  176. Email: dummyRegistryEmail,
  177. }
  178. return cfg
  179. }
  180. func getLoginServer(registry containerregistry.Registry) string {
  181. return *(*registry.RegistryProperties).LoginServer
  182. }
  183. func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.DockerConfigEntry, error) {
  184. // Run EnsureFresh to make sure the token is valid and does not expire
  185. if err := a.servicePrincipalToken.EnsureFresh(); err != nil {
  186. klog.Errorf("Failed to ensure fresh service principal token: %v", err)
  187. return nil, err
  188. }
  189. armAccessToken := a.servicePrincipalToken.OAuthToken()
  190. klog.V(4).Infof("discovering auth redirects for: %s", loginServer)
  191. directive, err := receiveChallengeFromLoginServer(loginServer)
  192. if err != nil {
  193. klog.Errorf("failed to receive challenge: %s", err)
  194. return nil, err
  195. }
  196. klog.V(4).Infof("exchanging an acr refresh_token")
  197. registryRefreshToken, err := performTokenExchange(
  198. loginServer, directive, a.config.TenantID, armAccessToken)
  199. if err != nil {
  200. klog.Errorf("failed to perform token exchange: %s", err)
  201. return nil, err
  202. }
  203. klog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
  204. return &credentialprovider.DockerConfigEntry{
  205. Username: dockerTokenLoginUsernameGUID,
  206. Password: registryRefreshToken,
  207. Email: dummyRegistryEmail,
  208. }, nil
  209. }
  210. // parseACRLoginServerFromImage takes image as parameter and returns login server of it.
  211. // Parameter `image` is expected in following format: foo.azurecr.io/bar/imageName:version
  212. // If the provided image is not an acr image, this function will return an empty string.
  213. func parseACRLoginServerFromImage(image string) string {
  214. match := acrRE.FindAllString(image, -1)
  215. if len(match) == 1 {
  216. return match[0]
  217. }
  218. return ""
  219. }