aws_credentials.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  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/klog"
  30. "k8s.io/kubernetes/pkg/credentialprovider"
  31. "k8s.io/kubernetes/pkg/version"
  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. // LazyProvide is lazy
  70. // TODO: the LazyProvide methods will be removed in a future PR
  71. func (p *ecrProvider) LazyProvide(image string) *credentialprovider.DockerConfigEntry {
  72. return nil
  73. }
  74. // Provide returns a DockerConfig with credentials from the cache if they are
  75. // found, or from ECR
  76. func (p *ecrProvider) Provide(image string) credentialprovider.DockerConfig {
  77. parsed, err := parseRepoURL(image)
  78. if err != nil {
  79. klog.V(3).Info(err)
  80. return credentialprovider.DockerConfig{}
  81. }
  82. if cfg, exists := p.getFromCache(parsed); exists {
  83. klog.V(6).Infof("Got ECR credentials from cache for %s", parsed.registry)
  84. return cfg
  85. }
  86. klog.V(3).Info("unable to get ECR credentials from cache, checking ECR API")
  87. cfg, err := p.getFromECR(parsed)
  88. if err != nil {
  89. klog.Errorf("error getting credentials from ECR for %s %v", parsed.registry, err)
  90. return credentialprovider.DockerConfig{}
  91. }
  92. klog.V(3).Infof("Got ECR credentials from ECR API for %s", parsed.registry)
  93. return cfg
  94. }
  95. // getFromCache attempts to get credentials from the cache
  96. func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialprovider.DockerConfig, bool) {
  97. cfg := credentialprovider.DockerConfig{}
  98. obj, exists, err := p.cache.GetByKey(parsed.registry)
  99. if err != nil {
  100. klog.Errorf("error getting ECR credentials from cache: %v", err)
  101. return cfg, false
  102. }
  103. if !exists {
  104. return cfg, false
  105. }
  106. entry := obj.(*cacheEntry)
  107. cfg[entry.registry] = entry.credentials
  108. return cfg, true
  109. }
  110. // getFromECR gets credentials from ECR since they are not in the cache
  111. func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialprovider.DockerConfig, error) {
  112. cfg := credentialprovider.DockerConfig{}
  113. getter, err := p.getterFactory.GetTokenGetterForRegion(parsed.region)
  114. if err != nil {
  115. return cfg, err
  116. }
  117. params := &ecr.GetAuthorizationTokenInput{RegistryIds: []*string{aws.String(parsed.registryID)}}
  118. output, err := getter.GetAuthorizationToken(params)
  119. if err != nil {
  120. return cfg, err
  121. }
  122. if output == nil {
  123. return cfg, errors.New("authorization token is nil")
  124. }
  125. if len(output.AuthorizationData) == 0 {
  126. return cfg, errors.New("authorization data from response is empty")
  127. }
  128. data := output.AuthorizationData[0]
  129. if data.AuthorizationToken == nil {
  130. return cfg, errors.New("authorization token in response is nil")
  131. }
  132. entry, err := makeCacheEntry(data, parsed.registry)
  133. if err != nil {
  134. return cfg, err
  135. }
  136. if err := p.cache.Add(entry); err != nil {
  137. return cfg, err
  138. }
  139. cfg[entry.registry] = entry.credentials
  140. return cfg, nil
  141. }
  142. type parsedURL struct {
  143. registryID string
  144. region string
  145. registry string
  146. }
  147. // parseRepoURL parses and splits the registry URL into the registry ID,
  148. // region, and registry.
  149. // <registryID>.dkr.ecr(-fips).<region>.amazonaws.com(.cn)
  150. func parseRepoURL(image string) (*parsedURL, error) {
  151. parsed, err := url.Parse("https://" + image)
  152. if err != nil {
  153. return nil, fmt.Errorf("error parsing image %s %v", image, err)
  154. }
  155. splitURL := ecrPattern.FindStringSubmatch(parsed.Hostname())
  156. if len(splitURL) == 0 {
  157. return nil, fmt.Errorf("%s is not a valid ECR repository URL", parsed.Hostname())
  158. }
  159. return &parsedURL{
  160. registryID: splitURL[1],
  161. region: splitURL[3],
  162. registry: parsed.Hostname(),
  163. }, nil
  164. }
  165. // tokenGetter is for testing purposes
  166. type tokenGetter interface {
  167. GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
  168. }
  169. // tokenGetterFactory is for testing purposes
  170. type tokenGetterFactory interface {
  171. GetTokenGetterForRegion(string) (tokenGetter, error)
  172. }
  173. // ecrTokenGetterFactory stores a token getter per region
  174. type ecrTokenGetterFactory struct {
  175. cache map[string]tokenGetter
  176. mutex sync.Mutex
  177. }
  178. // awsHandlerLogger is a handler that logs all AWS SDK requests
  179. // Copied from pkg/cloudprovider/providers/aws/log_handler.go
  180. func awsHandlerLogger(req *request.Request) {
  181. service := req.ClientInfo.ServiceName
  182. region := req.Config.Region
  183. name := "?"
  184. if req.Operation != nil {
  185. name = req.Operation.Name
  186. }
  187. klog.V(3).Infof("AWS request: %s:%s in %s", service, name, *region)
  188. }
  189. func newECRTokenGetter(region string) (tokenGetter, error) {
  190. sess, err := session.NewSessionWithOptions(session.Options{
  191. Config: aws.Config{Region: aws.String(region)},
  192. SharedConfigState: session.SharedConfigEnable,
  193. })
  194. if err != nil {
  195. return nil, err
  196. }
  197. getter := &ecrTokenGetter{svc: ecr.New(sess)}
  198. getter.svc.Handlers.Build.PushFrontNamed(request.NamedHandler{
  199. Name: "k8s/user-agent",
  200. Fn: request.MakeAddToUserAgentHandler("kubernetes", version.Get().String()),
  201. })
  202. getter.svc.Handlers.Sign.PushFrontNamed(request.NamedHandler{
  203. Name: "k8s/logger",
  204. Fn: awsHandlerLogger,
  205. })
  206. return getter, nil
  207. }
  208. // GetTokenGetterForRegion gets the token getter for the requested region. If it
  209. // doesn't exist, it creates a new ECR token getter
  210. func (f *ecrTokenGetterFactory) GetTokenGetterForRegion(region string) (tokenGetter, error) {
  211. f.mutex.Lock()
  212. defer f.mutex.Unlock()
  213. if getter, ok := f.cache[region]; ok {
  214. return getter, nil
  215. }
  216. getter, err := newECRTokenGetter(region)
  217. if err != nil {
  218. return nil, fmt.Errorf("unable to create token getter for region %v %v", region, err)
  219. }
  220. f.cache[region] = getter
  221. return getter, nil
  222. }
  223. // The canonical implementation
  224. type ecrTokenGetter struct {
  225. svc *ecr.ECR
  226. }
  227. // GetAuthorizationToken gets the ECR authorization token using the ECR API
  228. func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
  229. return p.svc.GetAuthorizationToken(input)
  230. }
  231. type cacheEntry struct {
  232. expiresAt time.Time
  233. credentials credentialprovider.DockerConfigEntry
  234. registry string
  235. }
  236. // makeCacheEntry decodes the ECR authorization entry and re-packages it into a
  237. // cacheEntry.
  238. func makeCacheEntry(data *ecr.AuthorizationData, registry string) (*cacheEntry, error) {
  239. decodedToken, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken))
  240. if err != nil {
  241. return nil, fmt.Errorf("error decoding ECR authorization token: %v", err)
  242. }
  243. parts := strings.SplitN(string(decodedToken), ":", 2)
  244. if len(parts) < 2 {
  245. return nil, errors.New("error getting username and password from authorization token")
  246. }
  247. creds := credentialprovider.DockerConfigEntry{
  248. Username: parts[0],
  249. Password: parts[1],
  250. Email: "not@val.id", // ECR doesn't care and Docker is about to obsolete it
  251. }
  252. if data.ExpiresAt == nil {
  253. return nil, errors.New("authorization data expiresAt is nil")
  254. }
  255. return &cacheEntry{
  256. expiresAt: data.ExpiresAt.Add(-1 * wait.Jitter(30*time.Minute, 0.2)),
  257. credentials: creds,
  258. registry: registry,
  259. }, nil
  260. }
  261. // ecrExpirationPolicy implements ExpirationPolicy from client-go.
  262. type ecrExpirationPolicy struct{}
  263. // stringKeyFunc returns the cache key as a string
  264. func stringKeyFunc(obj interface{}) (string, error) {
  265. key := obj.(*cacheEntry).registry
  266. return key, nil
  267. }
  268. // IsExpired checks if the ECR credentials are expired.
  269. func (p *ecrExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool {
  270. return time.Now().After(entry.Obj.(*cacheEntry).expiresAt)
  271. }