aws_credentials.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. /*
  2. Copyright 2014 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 credentials
  14. import (
  15. "encoding/base64"
  16. "errors"
  17. "fmt"
  18. "net/url"
  19. "regexp"
  20. "strings"
  21. "sync"
  22. "time"
  23. "github.com/aws/aws-sdk-go/aws"
  24. "github.com/aws/aws-sdk-go/aws/request"
  25. "github.com/aws/aws-sdk-go/aws/session"
  26. "github.com/aws/aws-sdk-go/service/ecr"
  27. "k8s.io/apimachinery/pkg/util/wait"
  28. "k8s.io/client-go/tools/cache"
  29. "k8s.io/component-base/version"
  30. "k8s.io/klog"
  31. "k8s.io/kubernetes/pkg/credentialprovider"
  32. )
  33. var ecrPattern = regexp.MustCompile(`^(\d{12})\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.amazonaws\.com(\.cn)?$`)
  34. // init registers a credential provider for each registryURLTemplate and creates
  35. // an ECR token getter factory with a new cache to store token getters
  36. func init() {
  37. credentialprovider.RegisterCredentialProvider("amazon-ecr",
  38. newECRProvider(&ecrTokenGetterFactory{cache: make(map[string]tokenGetter)}))
  39. }
  40. // ecrProvider is a DockerConfigProvider that gets and refreshes tokens
  41. // from AWS to access ECR.
  42. type ecrProvider struct {
  43. cache cache.Store
  44. getterFactory tokenGetterFactory
  45. }
  46. var _ credentialprovider.DockerConfigProvider = &ecrProvider{}
  47. func newECRProvider(getterFactory tokenGetterFactory) *ecrProvider {
  48. return &ecrProvider{
  49. cache: cache.NewExpirationStore(stringKeyFunc, &ecrExpirationPolicy{}),
  50. getterFactory: getterFactory,
  51. }
  52. }
  53. // Enabled implements DockerConfigProvider.Enabled. Enabled is true if AWS
  54. // credentials are found.
  55. func (p *ecrProvider) Enabled() bool {
  56. sess, err := session.NewSessionWithOptions(session.Options{
  57. SharedConfigState: session.SharedConfigEnable,
  58. })
  59. if err != nil {
  60. klog.Errorf("while validating AWS credentials %v", err)
  61. return false
  62. }
  63. if _, err := sess.Config.Credentials.Get(); err != nil {
  64. klog.Errorf("while getting AWS credentials %v", err)
  65. return false
  66. }
  67. return true
  68. }
  69. // Provide returns a DockerConfig with credentials from the cache if they are
  70. // found, or from ECR
  71. func (p *ecrProvider) Provide(image string) credentialprovider.DockerConfig {
  72. parsed, err := parseRepoURL(image)
  73. if err != nil {
  74. klog.V(3).Info(err)
  75. return credentialprovider.DockerConfig{}
  76. }
  77. if cfg, exists := p.getFromCache(parsed); exists {
  78. klog.V(6).Infof("Got ECR credentials from cache for %s", parsed.registry)
  79. return cfg
  80. }
  81. klog.V(3).Info("unable to get ECR credentials from cache, checking ECR API")
  82. cfg, err := p.getFromECR(parsed)
  83. if err != nil {
  84. klog.Errorf("error getting credentials from ECR for %s %v", parsed.registry, err)
  85. return credentialprovider.DockerConfig{}
  86. }
  87. klog.V(3).Infof("Got ECR credentials from ECR API for %s", parsed.registry)
  88. return cfg
  89. }
  90. // getFromCache attempts to get credentials from the cache
  91. func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialprovider.DockerConfig, bool) {
  92. cfg := credentialprovider.DockerConfig{}
  93. obj, exists, err := p.cache.GetByKey(parsed.registry)
  94. if err != nil {
  95. klog.Errorf("error getting ECR credentials from cache: %v", err)
  96. return cfg, false
  97. }
  98. if !exists {
  99. return cfg, false
  100. }
  101. entry := obj.(*cacheEntry)
  102. cfg[entry.registry] = entry.credentials
  103. return cfg, true
  104. }
  105. // getFromECR gets credentials from ECR since they are not in the cache
  106. func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialprovider.DockerConfig, error) {
  107. cfg := credentialprovider.DockerConfig{}
  108. getter, err := p.getterFactory.GetTokenGetterForRegion(parsed.region)
  109. if err != nil {
  110. return cfg, err
  111. }
  112. params := &ecr.GetAuthorizationTokenInput{RegistryIds: []*string{aws.String(parsed.registryID)}}
  113. output, err := getter.GetAuthorizationToken(params)
  114. if err != nil {
  115. return cfg, err
  116. }
  117. if output == nil {
  118. return cfg, errors.New("authorization token is nil")
  119. }
  120. if len(output.AuthorizationData) == 0 {
  121. return cfg, errors.New("authorization data from response is empty")
  122. }
  123. data := output.AuthorizationData[0]
  124. if data.AuthorizationToken == nil {
  125. return cfg, errors.New("authorization token in response is nil")
  126. }
  127. entry, err := makeCacheEntry(data, parsed.registry)
  128. if err != nil {
  129. return cfg, err
  130. }
  131. if err := p.cache.Add(entry); err != nil {
  132. return cfg, err
  133. }
  134. cfg[entry.registry] = entry.credentials
  135. return cfg, nil
  136. }
  137. type parsedURL struct {
  138. registryID string
  139. region string
  140. registry string
  141. }
  142. // parseRepoURL parses and splits the registry URL into the registry ID,
  143. // region, and registry.
  144. // <registryID>.dkr.ecr(-fips).<region>.amazonaws.com(.cn)
  145. func parseRepoURL(image string) (*parsedURL, error) {
  146. parsed, err := url.Parse("https://" + image)
  147. if err != nil {
  148. return nil, fmt.Errorf("error parsing image %s %v", image, err)
  149. }
  150. splitURL := ecrPattern.FindStringSubmatch(parsed.Hostname())
  151. if len(splitURL) == 0 {
  152. return nil, fmt.Errorf("%s is not a valid ECR repository URL", parsed.Hostname())
  153. }
  154. return &parsedURL{
  155. registryID: splitURL[1],
  156. region: splitURL[3],
  157. registry: parsed.Hostname(),
  158. }, nil
  159. }
  160. // tokenGetter is for testing purposes
  161. type tokenGetter interface {
  162. GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
  163. }
  164. // tokenGetterFactory is for testing purposes
  165. type tokenGetterFactory interface {
  166. GetTokenGetterForRegion(string) (tokenGetter, error)
  167. }
  168. // ecrTokenGetterFactory stores a token getter per region
  169. type ecrTokenGetterFactory struct {
  170. cache map[string]tokenGetter
  171. mutex sync.Mutex
  172. }
  173. // awsHandlerLogger is a handler that logs all AWS SDK requests
  174. // Copied from pkg/cloudprovider/providers/aws/log_handler.go
  175. func awsHandlerLogger(req *request.Request) {
  176. service := req.ClientInfo.ServiceName
  177. region := req.Config.Region
  178. name := "?"
  179. if req.Operation != nil {
  180. name = req.Operation.Name
  181. }
  182. klog.V(3).Infof("AWS request: %s:%s in %s", service, name, *region)
  183. }
  184. func newECRTokenGetter(region string) (tokenGetter, error) {
  185. sess, err := session.NewSessionWithOptions(session.Options{
  186. Config: aws.Config{Region: aws.String(region)},
  187. SharedConfigState: session.SharedConfigEnable,
  188. })
  189. if err != nil {
  190. return nil, err
  191. }
  192. getter := &ecrTokenGetter{svc: ecr.New(sess)}
  193. getter.svc.Handlers.Build.PushFrontNamed(request.NamedHandler{
  194. Name: "k8s/user-agent",
  195. Fn: request.MakeAddToUserAgentHandler("kubernetes", version.Get().String()),
  196. })
  197. getter.svc.Handlers.Sign.PushFrontNamed(request.NamedHandler{
  198. Name: "k8s/logger",
  199. Fn: awsHandlerLogger,
  200. })
  201. return getter, nil
  202. }
  203. // GetTokenGetterForRegion gets the token getter for the requested region. If it
  204. // doesn't exist, it creates a new ECR token getter
  205. func (f *ecrTokenGetterFactory) GetTokenGetterForRegion(region string) (tokenGetter, error) {
  206. f.mutex.Lock()
  207. defer f.mutex.Unlock()
  208. if getter, ok := f.cache[region]; ok {
  209. return getter, nil
  210. }
  211. getter, err := newECRTokenGetter(region)
  212. if err != nil {
  213. return nil, fmt.Errorf("unable to create token getter for region %v %v", region, err)
  214. }
  215. f.cache[region] = getter
  216. return getter, nil
  217. }
  218. // The canonical implementation
  219. type ecrTokenGetter struct {
  220. svc *ecr.ECR
  221. }
  222. // GetAuthorizationToken gets the ECR authorization token using the ECR API
  223. func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
  224. return p.svc.GetAuthorizationToken(input)
  225. }
  226. type cacheEntry struct {
  227. expiresAt time.Time
  228. credentials credentialprovider.DockerConfigEntry
  229. registry string
  230. }
  231. // makeCacheEntry decodes the ECR authorization entry and re-packages it into a
  232. // cacheEntry.
  233. func makeCacheEntry(data *ecr.AuthorizationData, registry string) (*cacheEntry, error) {
  234. decodedToken, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken))
  235. if err != nil {
  236. return nil, fmt.Errorf("error decoding ECR authorization token: %v", err)
  237. }
  238. parts := strings.SplitN(string(decodedToken), ":", 2)
  239. if len(parts) < 2 {
  240. return nil, errors.New("error getting username and password from authorization token")
  241. }
  242. creds := credentialprovider.DockerConfigEntry{
  243. Username: parts[0],
  244. Password: parts[1],
  245. Email: "not@val.id", // ECR doesn't care and Docker is about to obsolete it
  246. }
  247. if data.ExpiresAt == nil {
  248. return nil, errors.New("authorization data expiresAt is nil")
  249. }
  250. return &cacheEntry{
  251. expiresAt: data.ExpiresAt.Add(-1 * wait.Jitter(30*time.Minute, 0.2)),
  252. credentials: creds,
  253. registry: registry,
  254. }, nil
  255. }
  256. // ecrExpirationPolicy implements ExpirationPolicy from client-go.
  257. type ecrExpirationPolicy struct{}
  258. // stringKeyFunc returns the cache key as a string
  259. func stringKeyFunc(obj interface{}) (string, error) {
  260. key := obj.(*cacheEntry).registry
  261. return key, nil
  262. }
  263. // IsExpired checks if the ECR credentials are expired.
  264. func (p *ecrExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool {
  265. return time.Now().After(entry.Obj.(*cacheEntry).expiresAt)
  266. }