@@ -12,6 +12,7 @@ import (
12
12
"path/filepath"
13
13
"regexp"
14
14
"strings"
15
+ "sync"
15
16
"time"
16
17
17
18
"github.com/alecthomas/atomic"
@@ -88,16 +89,12 @@ type RegistryConfig struct {
88
89
}
89
90
90
91
type OCIArtefactService struct {
91
- auth * atomic.Value [authn.AuthConfig ]
92
- puller * googleremote.Puller
93
- registry string
94
- allowInsecure bool
95
- logger * log.Logger
96
- }
97
-
98
- func (s * OCIArtefactService ) Authorization () (* authn.AuthConfig , error ) {
99
- out := s .auth .Load ()
100
- return & out , nil
92
+ originalContext context.Context
93
+ puller * googleremote.Puller
94
+ targetConfig RegistryConfig
95
+ logger * log.Logger
96
+ registries map [string ]* registryAuth
97
+ registryLock sync.Mutex
101
98
}
102
99
103
100
type ArtefactRepository struct {
@@ -114,6 +111,25 @@ type ArtefactBlobs struct {
114
111
Size int64
115
112
}
116
113
114
+ type registryAuth struct {
115
+ delegate authn.Authenticator
116
+ auth atomic.Value [* authn.AuthConfig ]
117
+ }
118
+
119
+ // Authorization implements authn.Authenticator.
120
+ func (r * registryAuth ) Authorization () (* authn.AuthConfig , error ) {
121
+ if r .delegate != nil {
122
+ auth , err := r .delegate .Authorization ()
123
+ if err != nil {
124
+ return nil , errors .Wrapf (err , "failed to authorize container registry" )
125
+ }
126
+ if auth != nil {
127
+ return auth , nil
128
+ }
129
+ }
130
+ return r .auth .Load (), nil
131
+ }
132
+
117
133
func NewForTesting () * OCIArtefactService {
118
134
storage , err := NewOCIRegistryStorage (context .TODO (), RegistryConfig {Registry : "127.0.0.1:15000/ftl-tests" , AllowInsecure : true })
119
135
if err != nil {
@@ -132,47 +148,81 @@ func NewOCIRegistryStorage(ctx context.Context, config RegistryConfig) (*OCIArte
132
148
133
149
logger := log .FromContext (ctx )
134
150
o := & OCIArtefactService {
135
- auth : & atomic. Value [authn. AuthConfig ]{} ,
136
- registry : config . Registry ,
137
- allowInsecure : config . AllowInsecure ,
138
- logger : logger ,
151
+ originalContext : ctx ,
152
+ registries : map [ string ] * registryAuth {} ,
153
+ targetConfig : config ,
154
+ logger : logger ,
139
155
}
140
- puller , err := googleremote .NewPuller (googleremote .WithAuth (o ))
156
+
157
+ puller , err := googleremote .NewPuller (googleremote .WithAuthFromKeychain (o ))
141
158
if err != nil {
142
- return nil , errors .Wrapf (err , "unable to create puller for registry '%s'" , config . Registry )
159
+ return nil , errors .Wrapf (err , "unable to create puller" )
143
160
}
144
161
o .puller = puller
162
+ return o , nil
163
+ }
164
+
165
+ // Resolve implements authn.Keychain.
166
+ func (s * OCIArtefactService ) Resolve (r authn.Resource ) (authn.Authenticator , error ) {
167
+ s .registryLock .Lock ()
168
+ defer s .registryLock .Unlock ()
145
169
146
- if isECRRepository (config .Registry ) {
170
+ logger := log .FromContext (s .originalContext )
171
+ registry := r .String ()
172
+ existing := s .registries [registry ]
173
+ if existing != nil {
174
+ return existing , nil
175
+ }
176
+ cfg := & registryAuth {}
177
+ s .registries [registry ] = cfg
178
+ cfg .auth .Store (& authn.AuthConfig {})
179
+
180
+ if registry == s .targetConfig .Registry &&
181
+ s .targetConfig .Username != "" &&
182
+ s .targetConfig .Password != "" {
183
+ // The user has explicitly supplied credentials, lets use them
184
+ cfg .auth .Store (& authn.AuthConfig {
185
+ Username : s .targetConfig .Username ,
186
+ Password : s .targetConfig .Password ,
187
+ })
188
+ return cfg , nil
189
+ }
147
190
148
- username , password , err := getECRCredentials (ctx )
191
+ dctx , err := authn .DefaultKeychain .ResolveContext (s .originalContext , r )
192
+ if err == nil {
193
+ // Local docker config takes precidence
194
+ cfg .delegate = dctx
195
+ return cfg , nil
196
+ }
197
+
198
+ if isECRRepository (registry ) {
199
+
200
+ username , password , err := getECRCredentials (s .originalContext )
149
201
if err != nil {
150
202
return nil , errors .WithStack (err )
151
203
}
152
- logger .Debugf ("Using ECR credentials for registry '%s'" , config . Registry )
153
- o .auth .Store (authn.AuthConfig {Username : username , Password : password })
204
+ logger .Debugf ("Using ECR credentials for registry '%s'" , registry )
205
+ cfg .auth .Store (& authn.AuthConfig {Username : username , Password : password })
154
206
go func () {
155
207
for {
156
208
select {
157
- case <- ctx .Done ():
209
+ case <- s . originalContext .Done ():
158
210
return
159
211
case <- time .After (time .Hour ):
160
- username , password , err := getECRCredentials (ctx )
212
+ username , password , err := getECRCredentials (s . originalContext )
161
213
if err != nil {
162
214
logger .Errorf (err , "failed to refresh ECR credentials" )
163
215
}
164
- o .auth .Store (authn.AuthConfig {Username : username , Password : password })
216
+ cfg .auth .Store (& authn.AuthConfig {Username : username , Password : password })
165
217
}
166
218
}
167
219
}()
168
- } else {
169
- o .auth .Store (authn.AuthConfig {Username : config .Username , Password : config .Password })
170
220
}
171
- return o , nil
221
+ return cfg , nil
172
222
}
173
223
174
224
func (s * OCIArtefactService ) GetRegistry () string {
175
- return s .registry
225
+ return s .targetConfig . Registry
176
226
}
177
227
178
228
func getECRCredentials (ctx context.Context ) (string , string , error ) {
@@ -212,7 +262,7 @@ func getECRCredentials(ctx context.Context) (string, string, error) {
212
262
func (s * OCIArtefactService ) GetDigestsKeys (ctx context.Context , digests []sha256.SHA256 ) (keys []ArtefactKey , missing []sha256.SHA256 , err error ) {
213
263
repo , err := s .repoFactory ()
214
264
if err != nil {
215
- return nil , nil , errors .Wrapf (err , "unable to connect to container registry '%s'" , s .registry )
265
+ return nil , nil , errors .Wrapf (err , "unable to connect to container registry '%s'" , s .targetConfig . Registry )
216
266
}
217
267
set := make (map [sha256.SHA256 ]bool )
218
268
for _ , d := range digests {
@@ -243,7 +293,7 @@ func (s *OCIArtefactService) Upload(ctx context.Context, artefact ArtefactUpload
243
293
repo , err := s .repoFactory ()
244
294
logger := log .FromContext (ctx ).Scope ("oci:" + artefact .Digest .String ())
245
295
if err != nil {
246
- return errors .Wrapf (err , "unable to connect to repository '%s'" , s .registry )
296
+ return errors .Wrapf (err , "unable to connect to repository '%s'" , s .targetConfig . Registry )
247
297
}
248
298
249
299
parseSHA256 , err := sha256 .ParseSHA256 (artefact .Digest .String ())
@@ -298,15 +348,14 @@ func (s *OCIArtefactService) Download(ctx context.Context, dg sha256.SHA256) (io
298
348
// So we are using google's go-containerregistry to do the actual download
299
349
// This is not great, we should remove oras at some point
300
350
opts := []name.Option {}
301
- if s .allowInsecure {
351
+ if s .targetConfig . AllowInsecure {
302
352
opts = append (opts , name .Insecure )
303
353
}
304
- newDigest , err := name .NewDigest (fmt .Sprintf ("%s@sha256:%s" , s .registry , dg .String ()), opts ... )
354
+ newDigest , err := name .NewDigest (fmt .Sprintf ("%s@sha256:%s" , s .targetConfig . Registry , dg .String ()), opts ... )
305
355
if err != nil {
306
356
return nil , errors .Wrapf (err , "unable to create digest '%s'" , dg )
307
357
}
308
- auth := s .auth .Load ()
309
- layer , err := googleremote .Layer (newDigest , googleremote .WithAuth (authn .FromConfig (auth )), googleremote .Reuse (s .puller ))
358
+ layer , err := googleremote .Layer (newDigest , googleremote .WithAuthFromKeychain (s ), googleremote .Reuse (s .puller ))
310
359
if err != nil {
311
360
return nil , errors .Wrapf (err , "unable to read layer '%s'" , newDigest )
312
361
}
@@ -318,24 +367,36 @@ func (s *OCIArtefactService) Download(ctx context.Context, dg sha256.SHA256) (io
318
367
}
319
368
320
369
func (s * OCIArtefactService ) repoFactory () (* remote.Repository , error ) {
321
- reg , err := remote .NewRepository (s .registry )
370
+ reg , err := remote .NewRepository (s .targetConfig .Registry )
371
+ if err != nil {
372
+ return nil , errors .Wrapf (err , "unable to connect to container registry '%s'" , s .targetConfig .Registry )
373
+ }
374
+
375
+ ref , err := name .NewRepository (s .targetConfig .Registry )
376
+ if err != nil {
377
+ return nil , errors .Wrapf (err , "failed to parse registry" )
378
+ }
379
+ a , err := s .Resolve (ref )
380
+ if err != nil {
381
+ return nil , errors .Wrapf (err , "failed to resolve authenticator" )
382
+ }
383
+ acfg , err := a .Authorization ()
322
384
if err != nil {
323
- return nil , errors .Wrapf (err , "unable to connect to container registry '%s'" , s . registry )
385
+ return nil , errors .Wrapf (err , "failed to authenticate" )
324
386
}
325
387
326
- a := s .auth .Load ()
327
- s .logger .Debugf ("Connecting to registry '%s'" , s .registry )
388
+ s .logger .Debugf ("Connecting to registry '%s'" , s .targetConfig .Registry )
328
389
reg .Client = & auth.Client {
329
390
Client : retry .DefaultClient ,
330
391
Cache : auth .NewCache (),
331
392
Credential : func (ctx context.Context , hostport string ) (auth.Credential , error ) {
332
393
return auth.Credential {
333
- Username : a .Username ,
334
- Password : a .Password ,
394
+ Username : acfg .Username ,
395
+ Password : acfg .Password ,
335
396
}, nil
336
397
},
337
398
}
338
- reg .PlainHTTP = s .allowInsecure
399
+ reg .PlainHTTP = s .targetConfig . AllowInsecure
339
400
return reg , nil
340
401
}
341
402
@@ -418,12 +479,11 @@ func (s *OCIArtefactService) DownloadArtifacts(ctx context.Context, dest string,
418
479
func WithRemotePush () ImageTarget {
419
480
return func (ctx context.Context , s * OCIArtefactService , targetImage name.Tag , imageIndex v1.ImageIndex , image v1.Image , layers []v1.Layer ) error {
420
481
logger := log .FromContext (ctx )
421
- repo , err := name .NewRepository (s .registry )
482
+ repo , err := name .NewRepository (s .targetConfig . Registry )
422
483
if err != nil {
423
484
return errors .Wrapf (err , "unable to parse repo" )
424
485
}
425
- auth := s .auth .Load ()
426
- authOpt := googleremote .WithAuth (authn .FromConfig (auth ))
486
+ authOpt := googleremote .WithAuthFromKeychain (s )
427
487
428
488
for _ , l := range layers {
429
489
if err := googleremote .WriteLayer (repo , l , authOpt ); err != nil {
@@ -489,7 +549,8 @@ func (s *OCIArtefactService) BuildOCIImage(ctx context.Context, baseImage string
489
549
}
490
550
491
551
opts := []name.Option {}
492
- if s .allowInsecure {
552
+ // TODO: use http:// scheme for allow/disallow insecure
553
+ if s .targetConfig .AllowInsecure {
493
554
opts = append (opts , name .Insecure )
494
555
}
495
556
logger := log .FromContext (ctx )
@@ -503,8 +564,7 @@ func (s *OCIArtefactService) BuildOCIImage(ctx context.Context, baseImage string
503
564
return errors .Wrapf (err , "failed to parse target image" )
504
565
}
505
566
506
- auth := s .auth .Load ()
507
- desc , err := googleremote .Get (ref , googleremote .WithContext (ctx ), googleremote .WithAuth (authn .FromConfig (auth )), googleremote .Reuse (s .puller ))
567
+ desc , err := googleremote .Get (ref , googleremote .WithContext (ctx ), googleremote .WithAuthFromKeychain (s ), googleremote .Reuse (s .puller ))
508
568
if err != nil {
509
569
return errors .Errorf ("getting base image metadata: %w" , err )
510
570
}
0 commit comments