azure_credentials.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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/2017-10-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. "sigs.k8s.io/yaml"
  29. "k8s.io/kubernetes/pkg/credentialprovider"
  30. "k8s.io/legacy-cloud-providers/azure/auth"
  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. func getContextWithCancel() (context.Context, context.CancelFunc) {
  52. return context.WithCancel(context.Background())
  53. }
  54. // RegistriesClient is a testable interface for the ACR client List operation.
  55. type RegistriesClient interface {
  56. List(ctx context.Context) ([]containerregistry.Registry, error)
  57. }
  58. // azRegistriesClient implements RegistriesClient.
  59. type azRegistriesClient struct {
  60. client containerregistry.RegistriesClient
  61. }
  62. func newAzRegistriesClient(subscriptionID, endpoint string, token *adal.ServicePrincipalToken) *azRegistriesClient {
  63. registryClient := containerregistry.NewRegistriesClient(subscriptionID)
  64. registryClient.BaseURI = endpoint
  65. registryClient.Authorizer = autorest.NewBearerAuthorizer(token)
  66. return &azRegistriesClient{
  67. client: registryClient,
  68. }
  69. }
  70. func (az *azRegistriesClient) List(ctx context.Context) ([]containerregistry.Registry, error) {
  71. iterator, err := az.client.ListComplete(ctx)
  72. if err != nil {
  73. return nil, err
  74. }
  75. result := make([]containerregistry.Registry, 0)
  76. for ; iterator.NotDone(); err = iterator.Next() {
  77. if err != nil {
  78. return nil, err
  79. }
  80. result = append(result, iterator.Value())
  81. }
  82. return result, nil
  83. }
  84. // NewACRProvider parses the specified configFile and returns a DockerConfigProvider
  85. func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider {
  86. return &acrProvider{
  87. file: configFile,
  88. }
  89. }
  90. type acrProvider struct {
  91. file *string
  92. config *auth.AzureAuthConfig
  93. environment *azure.Environment
  94. registryClient RegistriesClient
  95. servicePrincipalToken *adal.ServicePrincipalToken
  96. }
  97. // ParseConfig returns a parsed configuration for an Azure cloudprovider config file
  98. func parseConfig(configReader io.Reader) (*auth.AzureAuthConfig, error) {
  99. var config auth.AzureAuthConfig
  100. if configReader == nil {
  101. return &config, nil
  102. }
  103. limitedReader := &io.LimitedReader{R: configReader, N: maxReadLength}
  104. configContents, err := ioutil.ReadAll(limitedReader)
  105. if err != nil {
  106. return nil, err
  107. }
  108. if limitedReader.N <= 0 {
  109. return nil, errors.New("the read limit is reached")
  110. }
  111. err = yaml.Unmarshal(configContents, &config)
  112. if err != nil {
  113. return nil, err
  114. }
  115. return &config, nil
  116. }
  117. func (a *acrProvider) loadConfig(rdr io.Reader) error {
  118. var err error
  119. a.config, err = parseConfig(rdr)
  120. if err != nil {
  121. klog.Errorf("Failed to load azure credential file: %v", err)
  122. }
  123. a.environment, err = auth.ParseAzureEnvironment(a.config.Cloud)
  124. if err != nil {
  125. return err
  126. }
  127. return nil
  128. }
  129. func (a *acrProvider) Enabled() bool {
  130. if a.file == nil || len(*a.file) == 0 {
  131. klog.V(5).Infof("Azure config unspecified, disabling")
  132. return false
  133. }
  134. f, err := os.Open(*a.file)
  135. if err != nil {
  136. klog.Errorf("Failed to load config from file: %s", *a.file)
  137. return false
  138. }
  139. defer f.Close()
  140. err = a.loadConfig(f)
  141. if err != nil {
  142. klog.Errorf("Failed to load config from file: %s", *a.file)
  143. return false
  144. }
  145. a.servicePrincipalToken, err = auth.GetServicePrincipalToken(a.config, a.environment)
  146. if err != nil {
  147. klog.Errorf("Failed to create service principal token: %v", err)
  148. return false
  149. }
  150. a.registryClient = newAzRegistriesClient(a.config.SubscriptionID, a.environment.ResourceManagerEndpoint, a.servicePrincipalToken)
  151. return true
  152. }
  153. func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
  154. klog.V(4).Infof("try to provide secret for image %s", image)
  155. cfg := credentialprovider.DockerConfig{}
  156. if a.config.UseManagedIdentityExtension {
  157. if loginServer := parseACRLoginServerFromImage(image); loginServer == "" {
  158. klog.V(4).Infof("image(%s) is not from ACR, skip MSI authentication", image)
  159. } else {
  160. if cred, err := getACRDockerEntryFromARMToken(a, loginServer); err == nil {
  161. cfg[loginServer] = *cred
  162. }
  163. }
  164. } else {
  165. // Add our entry for each of the supported container registry URLs
  166. for _, url := range containerRegistryUrls {
  167. cred := &credentialprovider.DockerConfigEntry{
  168. Username: a.config.AADClientID,
  169. Password: a.config.AADClientSecret,
  170. Email: dummyRegistryEmail,
  171. }
  172. cfg[url] = *cred
  173. }
  174. }
  175. // add ACR anonymous repo support: use empty username and password for anonymous access
  176. cfg["*.azurecr.*"] = credentialprovider.DockerConfigEntry{
  177. Username: "",
  178. Password: "",
  179. Email: dummyRegistryEmail,
  180. }
  181. return cfg
  182. }
  183. func getLoginServer(registry containerregistry.Registry) string {
  184. return *(*registry.RegistryProperties).LoginServer
  185. }
  186. func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.DockerConfigEntry, error) {
  187. // Run EnsureFresh to make sure the token is valid and does not expire
  188. if err := a.servicePrincipalToken.EnsureFresh(); err != nil {
  189. klog.Errorf("Failed to ensure fresh service principal token: %v", err)
  190. return nil, err
  191. }
  192. armAccessToken := a.servicePrincipalToken.OAuthToken()
  193. klog.V(4).Infof("discovering auth redirects for: %s", loginServer)
  194. directive, err := receiveChallengeFromLoginServer(loginServer)
  195. if err != nil {
  196. klog.Errorf("failed to receive challenge: %s", err)
  197. return nil, err
  198. }
  199. klog.V(4).Infof("exchanging an acr refresh_token")
  200. registryRefreshToken, err := performTokenExchange(
  201. loginServer, directive, a.config.TenantID, armAccessToken)
  202. if err != nil {
  203. klog.Errorf("failed to perform token exchange: %s", err)
  204. return nil, err
  205. }
  206. klog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
  207. return &credentialprovider.DockerConfigEntry{
  208. Username: dockerTokenLoginUsernameGUID,
  209. Password: registryRefreshToken,
  210. Email: dummyRegistryEmail,
  211. }, nil
  212. }
  213. // parseACRLoginServerFromImage takes image as parameter and returns login server of it.
  214. // Parameter `image` is expected in following format: foo.azurecr.io/bar/imageName:version
  215. // If the provided image is not an acr image, this function will return an empty string.
  216. func parseACRLoginServerFromImage(image string) string {
  217. match := acrRE.FindAllString(image, -1)
  218. if len(match) == 1 {
  219. return match[0]
  220. }
  221. return ""
  222. }
  223. func (a *acrProvider) LazyProvide(image string) *credentialprovider.DockerConfigEntry {
  224. return nil
  225. }