Skip to content

Commit e7feb0f

Browse files
committed
Add ECR auth support
1 parent 92853b5 commit e7feb0f

File tree

5 files changed

+164
-7
lines changed

5 files changed

+164
-7
lines changed

components/image-builder-api/go/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ type Configuration struct {
4141

4242
// BuilderImage is an image ref to the workspace builder image
4343
BuilderImage string `json:"builderImage"`
44+
45+
// EnableAdditionalECRAuth adds additional ECR auth using IRSA.
46+
// This will attempt to add ECR auth for any ECR repo a user is
47+
// trying to access.
48+
EnableAdditionalECRAuth bool
4449
}
4550

4651
type TLS struct {

components/image-builder-mk3/go.mod

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ require (
3131
)
3232

3333
require (
34+
github.com/aws/aws-sdk-go-v2/config v1.18.33
3435
github.com/beorn7/perks v1.0.1 // indirect
3536
github.com/cespare/xxhash/v2 v2.2.0 // indirect
3637
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -73,6 +74,23 @@ require (
7374
gopkg.in/yaml.v3 v3.0.1 // indirect
7475
)
7576

77+
require (
78+
github.com/aws/aws-sdk-go-v2 v1.20.1 // indirect
79+
github.com/aws/aws-sdk-go-v2/credentials v1.13.32 // indirect
80+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 // indirect
81+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 // indirect
82+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.32 // indirect
83+
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.39 // indirect
84+
github.com/aws/aws-sdk-go-v2/service/ecr v1.19.2
85+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.32 // indirect
86+
github.com/aws/aws-sdk-go-v2/service/sso v1.13.2 // indirect
87+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.2 // indirect
88+
github.com/aws/aws-sdk-go-v2/service/sts v1.21.2 // indirect
89+
github.com/aws/smithy-go v1.14.1 // indirect
90+
)
91+
92+
require github.com/jmespath/go-jmespath v0.4.0 // indirect
93+
7694
replace github.com/gitpod-io/gitpod/common-go => ../common-go // leeway
7795

7896
replace github.com/gitpod-io/gitpod/components/scrubber => ../scrubber // leeway

components/image-builder-mk3/go.sum

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/image-builder-mk3/pkg/auth/auth.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,15 @@ func (a AllowedAuthFor) GetAuthFor(auth RegistryAuthenticator, refstr string) (r
211211
// If we haven't found authentication using the built-in way, we'll resort to additional auth
212212
// the user sent us.
213213
defer func() {
214-
if err == nil && res == nil {
215-
res = a.additionalAuth(reg)
214+
if err != nil || (res != nil && (res.Auth != "" || res.Password != "")) {
215+
return
216+
}
216217

217-
if res != nil {
218-
log.WithField("reg", reg).Debug("found additional auth")
219-
}
218+
log.WithField("reg", reg).Debug("checking for additional auth")
219+
res = a.additionalAuth(reg)
220+
221+
if res != nil {
222+
log.WithField("reg", reg).Debug("found additional auth")
220223
}
221224
}()
222225

components/image-builder-mk3/pkg/orchestrator/orchestrator.go

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package orchestrator
77
import (
88
"context"
99
"crypto/sha256"
10+
"encoding/base64"
1011
"encoding/json"
1112
"errors"
1213
"fmt"
@@ -39,6 +40,10 @@ import (
3940
"github.com/gitpod-io/gitpod/image-builder/pkg/auth"
4041
"github.com/gitpod-io/gitpod/image-builder/pkg/resolve"
4142
wsmanapi "github.com/gitpod-io/gitpod/ws-manager/api"
43+
44+
"github.com/aws/aws-sdk-go-v2/aws"
45+
awsconfig "github.com/aws/aws-sdk-go-v2/config"
46+
"github.com/aws/aws-sdk-go-v2/service/ecr"
4247
)
4348

4449
const (
@@ -133,6 +138,10 @@ type Orchestrator struct {
133138

134139
metrics *metrics
135140

141+
ecrAuth string
142+
ecrAuthLastRefreshTime time.Time
143+
ecrAuthLock sync.Mutex
144+
136145
protocol.UnimplementedImageBuilderServer
137146
}
138147

@@ -167,6 +176,15 @@ func (o *Orchestrator) ResolveWorkspaceImage(ctx context.Context, req *protocol.
167176
tracing.LogRequestSafe(span, req)
168177

169178
reqauth := o.AuthResolver.ResolveRequestAuth(req.Auth)
179+
// The user might want to pull from ECR in a Dockerfile. Use the explicitely listed repos
180+
// to get the auth for that operation.
181+
for _, explicitRef := range reqauth.Explicit {
182+
err := o.addAdditionalECRAuth(ctx, &reqauth, explicitRef)
183+
if err != nil {
184+
log.WithError(err).WithField("ref", explicitRef).Warn("cannot add additional ECR auth")
185+
}
186+
}
187+
170188
baseref, err := o.getBaseImageRef(ctx, req.Source, reqauth)
171189
if _, ok := status.FromError(err); err != nil && ok {
172190
return nil, err
@@ -218,6 +236,15 @@ func (o *Orchestrator) Build(req *protocol.BuildRequest, resp protocol.ImageBuil
218236
// resolve build request authentication
219237
reqauth := o.AuthResolver.ResolveRequestAuth(req.Auth)
220238

239+
// The user might want to pull from ECR in a Dockerfile. Use the explicitely listed repos
240+
// to get the auth for that operation.
241+
for _, explicitRef := range reqauth.Explicit {
242+
err := o.addAdditionalECRAuth(ctx, &reqauth, explicitRef)
243+
if err != nil {
244+
log.WithError(err).WithField("ref", explicitRef).Warn("cannot add additional ECR auth")
245+
}
246+
}
247+
221248
baseref, err := o.getBaseImageRef(ctx, req.Source, reqauth)
222249
if _, ok := status.FromError(err); err != nil && ok {
223250
return err
@@ -549,16 +576,21 @@ func (o *Orchestrator) checkImageExists(ctx context.Context, ref string, authent
549576

550577
// getAbsoluteImageRef returns the "digest" form of an image, i.e. contains no mutable image tags
551578
func (o *Orchestrator) getAbsoluteImageRef(ctx context.Context, ref string, allowedAuth auth.AllowedAuthFor) (res string, err error) {
579+
err = o.addAdditionalECRAuth(ctx, &allowedAuth, ref)
580+
if err != nil {
581+
return "", err
582+
}
583+
552584
auth, err := allowedAuth.GetAuthFor(o.Auth, ref)
553585
if err != nil {
554586
return "", status.Errorf(codes.InvalidArgument, "cannt resolve base image ref: %v", err)
555587
}
556588

557589
ref, err = o.RefResolver.Resolve(ctx, ref, resolve.WithAuthentication(auth))
558-
if xerrors.Is(err, resolve.ErrNotFound) {
590+
if errors.Is(err, resolve.ErrNotFound) {
559591
return "", status.Error(codes.NotFound, "cannot resolve image")
560592
}
561-
if xerrors.Is(err, resolve.ErrUnauthorized) {
593+
if errors.Is(err, resolve.ErrUnauthorized) {
562594
return "", status.Error(codes.Unauthenticated, "cannot resolve image")
563595
}
564596
if err != nil {
@@ -573,6 +605,10 @@ func (o *Orchestrator) getBaseImageRef(ctx context.Context, bs *protocol.BuildSo
573605

574606
switch src := bs.From.(type) {
575607
case *protocol.BuildSource_Ref:
608+
err := o.addAdditionalECRAuth(ctx, &allowedAuth, src.Ref.Ref)
609+
if err != nil {
610+
return "", err
611+
}
576612
return o.getAbsoluteImageRef(ctx, src.Ref.Ref, allowedAuth)
577613

578614
case *protocol.BuildSource_File:
@@ -643,6 +679,70 @@ func (o *Orchestrator) getWorkspaceImageRef(ctx context.Context, baseref string)
643679
return fmt.Sprintf("%s:%x", o.Config.WorkspaceImageRepository, dst), nil
644680
}
645681

682+
func (o *Orchestrator) addAdditionalECRAuth(ctx context.Context, allowedAuth *auth.AllowedAuthFor, ref string) (err error) {
683+
if !o.Config.EnableAdditionalECRAuth {
684+
return nil
685+
}
686+
defer func() {
687+
if err == nil {
688+
return
689+
}
690+
err = fmt.Errorf("cannot add additional ECR credentials: %w", err)
691+
}()
692+
693+
// Total hack because the explicit auth entries (defaultBaseImageRegistryWhitelist) lists domains, not
694+
// repositories or references.
695+
domain := ref
696+
if strings.Contains(ref, "/") {
697+
refp, err := reference.ParseNamed(ref)
698+
if err != nil {
699+
return fmt.Errorf("cannot parse %s: %w", ref, err)
700+
}
701+
domain = reference.Domain(refp)
702+
}
703+
704+
log.WithField("domain", domain).Debug("checking if additional ECR auth needs to be added")
705+
706+
// TODO(cw): find better way to detect if ref is an ECR repo
707+
if !strings.Contains(domain, ".dkr.") || !strings.Contains(domain, ".amazonaws.com") {
708+
return nil
709+
}
710+
711+
o.ecrAuthLock.Lock()
712+
defer o.ecrAuthLock.Unlock()
713+
// ECR tokens are valid for 12h: https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html
714+
if time.Since(o.ecrAuthLastRefreshTime) > 10*time.Hour {
715+
awsCfg, err := awsconfig.LoadDefaultConfig(context.Background())
716+
if err != nil {
717+
return err
718+
}
719+
ecrc := ecr.NewFromConfig(awsCfg)
720+
tknout, err := ecrc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
721+
if err != nil {
722+
return err
723+
}
724+
if len(tknout.AuthorizationData) == 0 {
725+
return fmt.Errorf("no ECR authorization data received")
726+
}
727+
728+
pwd, err := base64.StdEncoding.DecodeString(aws.ToString(tknout.AuthorizationData[0].AuthorizationToken))
729+
if err != nil {
730+
return err
731+
}
732+
733+
o.ecrAuth = string(pwd)
734+
o.ecrAuthLastRefreshTime = time.Now()
735+
log.Debug("refreshed ECR token")
736+
}
737+
738+
if allowedAuth.Additional == nil {
739+
allowedAuth.Additional = make(map[string]string)
740+
}
741+
allowedAuth.Additional[domain] = base64.StdEncoding.EncodeToString([]byte(o.ecrAuth))
742+
log.WithField("domain", domain).Debug("added additional auth")
743+
return nil
744+
}
745+
646746
// parentCantCancelContext is a bit of a hack. We have some operations which we want to keep alive even after clients
647747
// disconnect. gRPC cancels the context once a client disconnects, thus we intercept the cancelation and act as if
648748
// nothing had happened.

0 commit comments

Comments
 (0)