@@ -7,6 +7,7 @@ package orchestrator
7
7
import (
8
8
"context"
9
9
"crypto/sha256"
10
+ "encoding/base64"
10
11
"encoding/json"
11
12
"errors"
12
13
"fmt"
@@ -39,6 +40,10 @@ import (
39
40
"github.com/gitpod-io/gitpod/image-builder/pkg/auth"
40
41
"github.com/gitpod-io/gitpod/image-builder/pkg/resolve"
41
42
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"
42
47
)
43
48
44
49
const (
@@ -133,6 +138,10 @@ type Orchestrator struct {
133
138
134
139
metrics * metrics
135
140
141
+ ecrAuth string
142
+ ecrAuthLastRefreshTime time.Time
143
+ ecrAuthLock sync.Mutex
144
+
136
145
protocol.UnimplementedImageBuilderServer
137
146
}
138
147
@@ -167,6 +176,15 @@ func (o *Orchestrator) ResolveWorkspaceImage(ctx context.Context, req *protocol.
167
176
tracing .LogRequestSafe (span , req )
168
177
169
178
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
+
170
188
baseref , err := o .getBaseImageRef (ctx , req .Source , reqauth )
171
189
if _ , ok := status .FromError (err ); err != nil && ok {
172
190
return nil , err
@@ -218,6 +236,15 @@ func (o *Orchestrator) Build(req *protocol.BuildRequest, resp protocol.ImageBuil
218
236
// resolve build request authentication
219
237
reqauth := o .AuthResolver .ResolveRequestAuth (req .Auth )
220
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
+
221
248
baseref , err := o .getBaseImageRef (ctx , req .Source , reqauth )
222
249
if _ , ok := status .FromError (err ); err != nil && ok {
223
250
return err
@@ -549,16 +576,21 @@ func (o *Orchestrator) checkImageExists(ctx context.Context, ref string, authent
549
576
550
577
// getAbsoluteImageRef returns the "digest" form of an image, i.e. contains no mutable image tags
551
578
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
+
552
584
auth , err := allowedAuth .GetAuthFor (o .Auth , ref )
553
585
if err != nil {
554
586
return "" , status .Errorf (codes .InvalidArgument , "cannt resolve base image ref: %v" , err )
555
587
}
556
588
557
589
ref , err = o .RefResolver .Resolve (ctx , ref , resolve .WithAuthentication (auth ))
558
- if xerrors .Is (err , resolve .ErrNotFound ) {
590
+ if errors .Is (err , resolve .ErrNotFound ) {
559
591
return "" , status .Error (codes .NotFound , "cannot resolve image" )
560
592
}
561
- if xerrors .Is (err , resolve .ErrUnauthorized ) {
593
+ if errors .Is (err , resolve .ErrUnauthorized ) {
562
594
return "" , status .Error (codes .Unauthenticated , "cannot resolve image" )
563
595
}
564
596
if err != nil {
@@ -573,6 +605,10 @@ func (o *Orchestrator) getBaseImageRef(ctx context.Context, bs *protocol.BuildSo
573
605
574
606
switch src := bs .From .(type ) {
575
607
case * protocol.BuildSource_Ref :
608
+ err := o .addAdditionalECRAuth (ctx , & allowedAuth , src .Ref .Ref )
609
+ if err != nil {
610
+ return "" , err
611
+ }
576
612
return o .getAbsoluteImageRef (ctx , src .Ref .Ref , allowedAuth )
577
613
578
614
case * protocol.BuildSource_File :
@@ -643,6 +679,70 @@ func (o *Orchestrator) getWorkspaceImageRef(ctx context.Context, baseref string)
643
679
return fmt .Sprintf ("%s:%x" , o .Config .WorkspaceImageRepository , dst ), nil
644
680
}
645
681
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
+
646
746
// parentCantCancelContext is a bit of a hack. We have some operations which we want to keep alive even after clients
647
747
// disconnect. gRPC cancels the context once a client disconnects, thus we intercept the cancelation and act as if
648
748
// nothing had happened.
0 commit comments