Skip to content

Commit 50edde1

Browse files
committed
Simplify ECR integration
1 parent 7e28c3a commit 50edde1

File tree

4 files changed

+105
-113
lines changed

4 files changed

+105
-113
lines changed

components/image-builder-mk3/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ require (
7575
)
7676

7777
require (
78-
github.com/aws/aws-sdk-go-v2 v1.20.1 // indirect
78+
github.com/aws/aws-sdk-go-v2 v1.20.1
7979
github.com/aws/aws-sdk-go-v2/credentials v1.13.32 // indirect
8080
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.8 // indirect
8181
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.38 // indirect

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

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313
"os"
1414
"strings"
1515
"sync"
16+
"time"
1617

18+
"github.com/aws/aws-sdk-go-v2/aws"
19+
"github.com/aws/aws-sdk-go-v2/service/ecr"
1720
"github.com/docker/cli/cli/config/configfile"
1821
"github.com/docker/distribution/reference"
1922
"github.com/docker/docker/api/types"
@@ -27,7 +30,7 @@ import (
2730
// RegistryAuthenticator can provide authentication for some registries
2831
type RegistryAuthenticator interface {
2932
// Authenticate attempts to provide authentication for Docker registry access
30-
Authenticate(registry string) (auth *Authentication, err error)
33+
Authenticate(ctx context.Context, registry string) (auth *Authentication, err error)
3134
}
3235

3336
// NewDockerConfigFileAuth reads a docker config file to provide authentication
@@ -91,7 +94,7 @@ func (a *DockerConfigFileAuth) loadFromFile(fn string) (err error) {
9194
}
9295

9396
// Authenticate attempts to provide an encoded authentication string for Docker registry access
94-
func (a *DockerConfigFileAuth) Authenticate(registry string) (auth *Authentication, err error) {
97+
func (a *DockerConfigFileAuth) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {
9598
ac, err := a.C.GetAuthConfig(registry)
9699
if err != nil {
97100
return nil, err
@@ -108,9 +111,88 @@ func (a *DockerConfigFileAuth) Authenticate(registry string) (auth *Authenticati
108111
}, nil
109112
}
110113

114+
// CompositeAuth returns the first non-empty authentication of any of its consitutents
115+
type CompositeAuth []RegistryAuthenticator
116+
117+
func (ca CompositeAuth) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {
118+
for _, ath := range ca {
119+
res, err := ath.Authenticate(ctx, registry)
120+
if err != nil {
121+
return nil, err
122+
}
123+
if !res.Empty() {
124+
return res, nil
125+
}
126+
}
127+
return &Authentication{}, nil
128+
}
129+
130+
func NewECRAuthenticator(ecrc *ecr.Client) *ECRAuthenticator {
131+
return &ECRAuthenticator{
132+
ecrc: ecrc,
133+
}
134+
}
135+
136+
type ECRAuthenticator struct {
137+
ecrc *ecr.Client
138+
139+
ecrAuth string
140+
ecrAuthLastRefreshTime time.Time
141+
ecrAuthLock sync.Mutex
142+
}
143+
144+
func (ath *ECRAuthenticator) Authenticate(ctx context.Context, registry string) (auth *Authentication, err error) {
145+
// TODO(cw): find better way to detect if ref is an ECR repo
146+
if !strings.Contains(registry, ".dkr.") || !strings.Contains(registry, ".amazonaws.com") {
147+
return nil, nil
148+
}
149+
150+
ath.ecrAuthLock.Lock()
151+
defer ath.ecrAuthLock.Unlock()
152+
// ECR tokens are valid for 12h: https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html
153+
if time.Since(ath.ecrAuthLastRefreshTime) > 10*time.Hour {
154+
tknout, err := ath.ecrc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{})
155+
if err != nil {
156+
return nil, err
157+
}
158+
if len(tknout.AuthorizationData) == 0 {
159+
return nil, fmt.Errorf("no ECR authorization data received")
160+
}
161+
162+
pwd, err := base64.StdEncoding.DecodeString(aws.ToString(tknout.AuthorizationData[0].AuthorizationToken))
163+
if err != nil {
164+
return nil, err
165+
}
166+
167+
ath.ecrAuth = string(pwd)
168+
ath.ecrAuthLastRefreshTime = time.Now()
169+
log.Debug("refreshed ECR token")
170+
}
171+
172+
segs := strings.Split(ath.ecrAuth, ":")
173+
if len(segs) != 2 {
174+
return nil, fmt.Errorf("cannot understand ECR token. Expected 2 segments, got %d", len(segs))
175+
}
176+
return &Authentication{
177+
Username: segs[0],
178+
Password: segs[1],
179+
Auth: base64.StdEncoding.EncodeToString([]byte(ath.ecrAuth)),
180+
}, nil
181+
}
182+
111183
// Authentication represents docker usable authentication
112184
type Authentication types.AuthConfig
113185

186+
func (a *Authentication) Empty() bool {
187+
if a == nil {
188+
return true
189+
}
190+
if a.Auth == "" && a.Password == "" {
191+
return true
192+
}
193+
return false
194+
}
195+
114196
// AllowedAuthFor describes for which repositories authentication may be provided for
115197
type AllowedAuthFor struct {
116198
All bool
@@ -197,7 +279,7 @@ func (r Resolver) ResolveRequestAuth(auth *api.BuildRegistryAuth) (authFor Allow
197279
}
198280

199281
// GetAuthFor computes the base64 encoded auth format for a Docker image pull/push
200-
func (a AllowedAuthFor) GetAuthFor(auth RegistryAuthenticator, refstr string) (res *Authentication, err error) {
282+
func (a AllowedAuthFor) GetAuthFor(ctx context.Context, auth RegistryAuthenticator, refstr string) (res *Authentication, err error) {
201283
if auth == nil {
202284
return
203285
}
@@ -240,7 +322,7 @@ func (a AllowedAuthFor) GetAuthFor(auth RegistryAuthenticator, refstr string) (r
240322
return nil, nil
241323
}
242324

243-
return auth.Authenticate(reg)
325+
return auth.Authenticate(ctx, reg)
244326
}
245327

246328
func (a AllowedAuthFor) additionalAuth(domain string) *Authentication {

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

Lines changed: 17 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package orchestrator
77
import (
88
"context"
99
"crypto/sha256"
10-
"encoding/base64"
1110
"encoding/json"
1211
"errors"
1312
"fmt"
@@ -19,6 +18,7 @@ import (
1918
"sync"
2019
"time"
2120

21+
"github.com/aws/aws-sdk-go-v2/service/ecr"
2222
"github.com/docker/distribution/reference"
2323
"github.com/google/uuid"
2424
"github.com/opentracing/opentracing-go"
@@ -30,6 +30,7 @@ import (
3030
"google.golang.org/grpc/credentials/insecure"
3131
"google.golang.org/grpc/status"
3232

33+
awsconfig "github.com/aws/aws-sdk-go-v2/config"
3334
common_grpc "github.com/gitpod-io/gitpod/common-go/grpc"
3435
"github.com/gitpod-io/gitpod/common-go/log"
3536
"github.com/gitpod-io/gitpod/common-go/tracing"
@@ -40,10 +41,6 @@ import (
4041
"github.com/gitpod-io/gitpod/image-builder/pkg/auth"
4142
"github.com/gitpod-io/gitpod/image-builder/pkg/resolve"
4243
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"
4744
)
4845

4946
const (
@@ -60,17 +57,26 @@ const (
6057

6158
// NewOrchestratingBuilder creates a new orchestrating image builder
6259
func NewOrchestratingBuilder(cfg config.Configuration) (res *Orchestrator, err error) {
63-
var authentication auth.RegistryAuthenticator
60+
var authentication auth.CompositeAuth
6461
if cfg.PullSecretFile != "" {
6562
fn := cfg.PullSecretFile
6663
if tproot := os.Getenv("TELEPRESENCE_ROOT"); tproot != "" {
6764
fn = filepath.Join(tproot, fn)
6865
}
6966

70-
authentication, err = auth.NewDockerConfigFileAuth(fn)
67+
ath, err := auth.NewDockerConfigFileAuth(fn)
7168
if err != nil {
72-
return
69+
return nil, err
7370
}
71+
authentication = append(authentication, ath)
72+
}
73+
if cfg.EnableAdditionalECRAuth {
74+
awsCfg, err := awsconfig.LoadDefaultConfig(context.Background())
75+
if err != nil {
76+
return nil, err
77+
}
78+
ecrc := ecr.NewFromConfig(awsCfg)
79+
authentication = append(authentication, auth.NewECRAuthenticator(ecrc))
7480
}
7581

7682
var wsman wsmanapi.WorkspaceManagerClient
@@ -138,10 +144,6 @@ type Orchestrator struct {
138144

139145
metrics *metrics
140146

141-
ecrAuth string
142-
ecrAuthLastRefreshTime time.Time
143-
ecrAuthLock sync.Mutex
144-
145147
protocol.UnimplementedImageBuilderServer
146148
}
147149

@@ -176,15 +178,6 @@ func (o *Orchestrator) ResolveWorkspaceImage(ctx context.Context, req *protocol.
176178
tracing.LogRequestSafe(span, req)
177179

178180
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-
188181
baseref, err := o.getBaseImageRef(ctx, req.Source, reqauth)
189182
if _, ok := status.FromError(err); err != nil && ok {
190183
return nil, err
@@ -200,7 +193,7 @@ func (o *Orchestrator) ResolveWorkspaceImage(ctx context.Context, req *protocol.
200193

201194
// to check if the image exists we must have access to the image caching registry and the refstr we check here does not come
202195
// from the user. Thus we can safely use auth.AllowedAuthForAll here.
203-
auth, err := auth.AllowedAuthForAll().GetAuthFor(o.Auth, refstr)
196+
auth, err := auth.AllowedAuthForAll().GetAuthFor(ctx, o.Auth, refstr)
204197
if err != nil {
205198
return nil, status.Errorf(codes.Internal, "cannot get workspace image authentication: %v", err)
206199
}
@@ -235,16 +228,6 @@ func (o *Orchestrator) Build(req *protocol.BuildRequest, resp protocol.ImageBuil
235228

236229
// resolve build request authentication
237230
reqauth := o.AuthResolver.ResolveRequestAuth(req.Auth)
238-
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-
248231
baseref, err := o.getBaseImageRef(ctx, req.Source, reqauth)
249232
if _, ok := status.FromError(err); err != nil && ok {
250233
return err
@@ -256,7 +239,7 @@ func (o *Orchestrator) Build(req *protocol.BuildRequest, resp protocol.ImageBuil
256239
if err != nil {
257240
return status.Errorf(codes.Internal, "cannot produce workspace image ref: %q", err)
258241
}
259-
wsrefAuth, err := auth.AllowedAuthForAll().GetAuthFor(o.Auth, wsrefstr)
242+
wsrefAuth, err := auth.AllowedAuthForAll().GetAuthFor(ctx, o.Auth, wsrefstr)
260243
if err != nil {
261244
return status.Errorf(codes.Internal, "cannot get workspace image authentication: %q", err)
262245
}
@@ -576,12 +559,7 @@ func (o *Orchestrator) checkImageExists(ctx context.Context, ref string, authent
576559

577560
// getAbsoluteImageRef returns the "digest" form of an image, i.e. contains no mutable image tags
578561
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-
584-
auth, err := allowedAuth.GetAuthFor(o.Auth, ref)
562+
auth, err := allowedAuth.GetAuthFor(ctx, o.Auth, ref)
585563
if err != nil {
586564
return "", status.Errorf(codes.InvalidArgument, "cannt resolve base image ref: %v", err)
587565
}
@@ -605,10 +583,6 @@ func (o *Orchestrator) getBaseImageRef(ctx context.Context, bs *protocol.BuildSo
605583

606584
switch src := bs.From.(type) {
607585
case *protocol.BuildSource_Ref:
608-
err := o.addAdditionalECRAuth(ctx, &allowedAuth, src.Ref.Ref)
609-
if err != nil {
610-
return "", err
611-
}
612586
return o.getAbsoluteImageRef(ctx, src.Ref.Ref, allowedAuth)
613587

614588
case *protocol.BuildSource_File:
@@ -679,70 +653,6 @@ func (o *Orchestrator) getWorkspaceImageRef(ctx context.Context, baseref string)
679653
return fmt.Sprintf("%s:%x", o.Config.WorkspaceImageRepository, dst), nil
680654
}
681655

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-
746656
// parentCantCancelContext is a bit of a hack. We have some operations which we want to keep alive even after clients
747657
// disconnect. gRPC cancels the context once a client disconnects, thus we intercept the cancelation and act as if
748658
// nothing had happened.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func (pr *PrecachingRefResolver) StartCaching(ctx context.Context, interval time
227227
continue
228228
}
229229

230-
auth, err := pr.Auth.Authenticate(reference.Domain(ref))
230+
auth, err := pr.Auth.Authenticate(ctx, reference.Domain(ref))
231231
if err != nil {
232232
log.WithError(err).WithField("ref", c).Warn("unable to precache reference: cannot authenticate")
233233
continue

0 commit comments

Comments
 (0)