15
15
package cmd
16
16
17
17
import (
18
+ "archive/zip"
19
+ "bytes"
20
+ "context"
21
+ "encoding/json"
18
22
"fmt"
19
23
"io"
20
24
"io/fs"
21
25
"os"
22
26
"path/filepath"
27
+ "sync"
23
28
29
+ "cuelabs.dev/go/oci/ociregistry"
30
+ "cuelabs.dev/go/oci/ociregistry/ociref"
31
+ "github.com/opencontainers/go-digest"
32
+ ocispecroot "github.com/opencontainers/image-spec/specs-go"
33
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24
34
"github.com/spf13/cobra"
25
35
26
36
"cuelang.org/go/internal/vcs"
37
+ "cuelang.org/go/mod/modconfig"
27
38
"cuelang.org/go/mod/modfile"
28
39
"cuelang.org/go/mod/modregistry"
29
40
"cuelang.org/go/mod/module"
@@ -44,23 +55,58 @@ no dependency or other checks at the moment.
44
55
Note: you must enable the modules experiment with:
45
56
export CUE_EXPERIMENT=modules
46
57
for this command to work.
58
+
59
+ When the --dryrun flag is specified, nothing will actually be written
60
+ to a registry, but all other checks will take place.
61
+
62
+ The --json flag can be used to find out more information about the upload.
63
+
64
+ The --out flag can be used to write the module's contents to a directory
65
+ in OCI Image Layout format. See this link for more details on the format:
66
+ https://github.com/opencontainers/image-spec/blob/8f3820ccf8f65db8744e626df17fe8a64462aece/image-layout.md
47
67
` ,
48
68
RunE : mkRunE (c , runModUpload ),
49
69
Args : cobra .ExactArgs (1 ),
50
70
}
71
+ cmd .Flags ().BoolP (string (flagDryrun ), "n" , false , "only run simulation" )
72
+ cmd .Flags ().Bool (string (flagJSON ), false , "print verbose information in JSON format (implies --dryrun)" )
73
+ cmd .Flags ().String (string (flagOut ), "" , "write module contents to specified directory in OCI Image Layout format (implies --dryrun)" )
51
74
52
75
return cmd
53
76
}
54
77
78
+ // publishInfo defines the format of the JSON printed by `cue mod publish --json`.
79
+ type publishInfo struct {
80
+ Version string `json:"version"`
81
+ Ref string `json:"ref"`
82
+ Insecure bool `json:"insecure,omitempty"`
83
+ Files []string `json:"files"`
84
+ // TODO include metadata too.
85
+ }
86
+
55
87
func runModUpload (cmd * Command , args []string ) error {
56
88
ctx := cmd .Context ()
57
- resolver , err := getRegistryResolver ()
89
+ resolver0 , err := getRegistryResolver ()
58
90
if err != nil {
59
91
return err
60
92
}
61
- if resolver == nil {
93
+ if resolver0 == nil {
62
94
return fmt .Errorf ("modules experiment not enabled (enable with CUE_EXPERIMENT=modules)" )
63
95
}
96
+ dryRun := flagDryrun .Bool (cmd )
97
+ outDir := flagOut .String (cmd )
98
+ useJSON := flagJSON .Bool (cmd )
99
+ if outDir != "" || useJSON {
100
+ dryRun = true
101
+ }
102
+ resolver := & publishRegistryResolverShim {
103
+ resolver : resolver0 ,
104
+ outDir : flagOut .String (cmd ),
105
+ dryRun : dryRun ,
106
+ // recording the files is somewhat heavyweight, so only do it
107
+ // if we're going to need them.
108
+ recordFiles : useJSON ,
109
+ }
64
110
modRoot , err := findModuleRoot ()
65
111
if err != nil {
66
112
return err
@@ -136,7 +182,38 @@ func runModUpload(cmd *Command, args []string) error {
136
182
if err := rclient .PutModuleWithMetadata (backgroundContext (), mv , zf , info .Size (), meta ); err != nil {
137
183
return fmt .Errorf ("cannot put module: %v" , err )
138
184
}
139
- fmt .Printf ("published %s\n " , mv )
185
+ ref := ociref.Reference {
186
+ Host : resolver .registryName ,
187
+ Repository : resolver .repository ,
188
+ Tag : resolver .tag ,
189
+ Digest : resolver .manifestDigest ,
190
+ }
191
+ if outDir != "" {
192
+ if err := resolver .writeIndex (); err != nil {
193
+ return err
194
+ }
195
+ }
196
+ switch {
197
+ case useJSON :
198
+ info := publishInfo {
199
+ Version : mv .String (),
200
+ Ref : ref .String (),
201
+ Insecure : resolver .insecure ,
202
+ Files : resolver .files ,
203
+ }
204
+ data , err := json .MarshalIndent (info , "" , "\t " )
205
+ if err != nil {
206
+ return err
207
+ }
208
+ data = append (data , '\n' )
209
+ os .Stdout .Write (data )
210
+ case outDir != "" :
211
+ fmt .Printf ("wrote image for %s to %s\n " , mv , outDir )
212
+ case dryRun :
213
+ fmt .Printf ("dry-run published %s to %v\n " , mv , ref )
214
+ default :
215
+ fmt .Printf ("published %s to %v\n " , mv , ref )
216
+ }
140
217
return nil
141
218
}
142
219
@@ -161,3 +238,252 @@ func (fio osFileIO) Open(f string) (io.ReadCloser, error) {
161
238
func (fio osFileIO ) absPath (f string ) string {
162
239
return filepath .Join (fio .modRoot , f )
163
240
}
241
+
242
+ // publishRegistryResolverShim implements a wrapper around
243
+ // modregistry.Resolver that records information about the module being
244
+ // published.
245
+ //
246
+ // If dryRun is true, it does not actually write to the underlying
247
+ // registry.
248
+ //
249
+ // If outDir is non-empty, it also writes the contents of the module to
250
+ // that directory.
251
+ //
252
+ // If recordFiles is true, it records which files are present in the
253
+ // module's zip file.
254
+ type publishRegistryResolverShim struct {
255
+ resolver * modconfig.Resolver
256
+ registry ociregistry.Interface
257
+ dryRun bool
258
+ recordFiles bool
259
+ outDir string
260
+
261
+ initDirOnce sync.Once
262
+ initDirError error
263
+
264
+ // mu protects the fields below it.
265
+ mu sync.Mutex
266
+ registryName string
267
+ insecure bool
268
+ repository string
269
+ tag string
270
+ manifestDigest digest.Digest
271
+ files []string
272
+ descriptors []ociregistry.Descriptor
273
+ }
274
+
275
+ // ResolveToRegistry implements [modregistry.RegistryResolver].
276
+ func (r * publishRegistryResolverShim ) ResolveToRegistry (mpath , vers string ) (modregistry.RegistryLocation , error ) {
277
+ // Make sure that we can acquire the underlying registry even
278
+ // though we are not going to use it, so we're dry-running as
279
+ // much as possible
280
+ regLoc , err := r .resolver .ResolveToRegistry (mpath , vers )
281
+ if err != nil {
282
+ return modregistry.RegistryLocation {}, err
283
+ }
284
+ loc , ok := r .resolver .ResolveToLocation (mpath , vers )
285
+ if ! ok {
286
+ panic ("unreachable: ResolveToLocation failed when ResolveToRegistry succeeded" )
287
+ }
288
+ r .mu .Lock ()
289
+ defer r .mu .Unlock ()
290
+ r .registryName = loc .Host
291
+ r .insecure = loc .Insecure
292
+ return modregistry.RegistryLocation {
293
+ Registry : & publishRegistryShim {
294
+ Funcs : & ociregistry.Funcs {
295
+ NewError : func (ctx context.Context , methodName , repo string ) error {
296
+ return fmt .Errorf ("unexpected OCI method %q invoked when publishing module" , methodName )
297
+ },
298
+ },
299
+ resolver : r ,
300
+ registry : regLoc .Registry ,
301
+ },
302
+ Repository : loc .Repository ,
303
+ Tag : loc .Tag ,
304
+ }, nil
305
+ }
306
+
307
+ func (r * publishRegistryResolverShim ) writeIndex () error {
308
+ if r .outDir == "" {
309
+ return nil
310
+ }
311
+ index := ocispec.Index {
312
+ Versioned : ocispecroot.Versioned {
313
+ SchemaVersion : 2 ,
314
+ },
315
+ MediaType : "application/vnd.oci.image.index.v1+json" ,
316
+ Manifests : r .descriptors ,
317
+ }
318
+ data , err := json .MarshalIndent (index , "" , "\t " )
319
+ if err != nil {
320
+ return err
321
+ }
322
+ data = append (data , '\n' )
323
+ if os .WriteFile (filepath .Join (r .outDir , "index.json" ), data , 0o666 ); err != nil {
324
+ return err
325
+ }
326
+ return nil
327
+ }
328
+
329
+ func (r * publishRegistryResolverShim ) setRepository (repo string ) error {
330
+ r .mu .Lock ()
331
+ defer r .mu .Unlock ()
332
+ if r .repository != "" && repo != r .repository {
333
+ return fmt .Errorf ("internal error: publish wrote to more than one OCI repository" )
334
+ }
335
+ r .repository = repo
336
+ return nil
337
+ }
338
+
339
+ func (r * publishRegistryResolverShim ) setTag (tag string , dig digest.Digest ) error {
340
+ r .mu .Lock ()
341
+ defer r .mu .Unlock ()
342
+ if r .tag != "" && tag != r .tag {
343
+ return fmt .Errorf ("internal error: publish wrote to more than one OCI tag" )
344
+ }
345
+ r .tag = tag
346
+ r .manifestDigest = dig
347
+ return nil
348
+ }
349
+
350
+ func (r * publishRegistryResolverShim ) setFiles (files []string ) {
351
+ r .mu .Lock ()
352
+ defer r .mu .Unlock ()
353
+ r .files = files
354
+ }
355
+
356
+ func (r * publishRegistryResolverShim ) addManifest (tag string , data []byte , mediaType string ) {
357
+ r .mu .Lock ()
358
+ defer r .mu .Unlock ()
359
+ r .descriptors = append (r .descriptors , ociregistry.Descriptor {
360
+ MediaType : mediaType ,
361
+ Size : int64 (len (data )),
362
+ Digest : digest .FromBytes (data ),
363
+ Annotations : map [string ]string {
364
+ "org.opencontainers.image.ref.name" : tag ,
365
+ },
366
+ })
367
+ }
368
+
369
+ func (r * publishRegistryResolverShim ) initDir () error {
370
+ // Lazily create the directory and the oci-layout file
371
+ // so we don't create anything if no operations take place
372
+ // on the registry.
373
+ r .initDirOnce .Do (func () {
374
+ if r .outDir == "" {
375
+ return
376
+ }
377
+ if err := os .Mkdir (r .outDir , 0o777 ); err != nil {
378
+ r .initDirError = err
379
+ return
380
+ }
381
+ // Create oci-layout file.
382
+ // See https://github.com/opencontainers/image-spec/blob/8f3820ccf8f65db8744e626df17fe8a64462aece/image-layout.md#oci-layout-file
383
+ r .initDirError = os .WriteFile (filepath .Join (r .outDir , "oci-layout" ), []byte (`
384
+ {
385
+ "imageLayoutVersion": "1.0.0"
386
+ }
387
+ ` [1 :]), 0o666 )
388
+ })
389
+ return r .initDirError
390
+ }
391
+
392
+ // publishRegistryShim implements [ociregistry.Interface] by recording
393
+ // what is written. It returns an error for methods not expected to be
394
+ // invoked as part of the publishing process.
395
+ type publishRegistryShim struct {
396
+ * ociregistry.Funcs
397
+ resolver * publishRegistryResolverShim
398
+ // registry is the real underlying registry. It is only used if
399
+ // resolver.dryRun is false.
400
+ registry ociregistry.Interface
401
+ }
402
+
403
+ func filesFromZip (content0 io.Reader , size int64 ) ([]string , error ) {
404
+ // The modregistry code (our only caller) always invokes PushBlob
405
+ // with a ReaderAt, so we can avoid copying all the data by type-asserting
406
+ // to that.
407
+ content , ok := content0 .(io.ReaderAt )
408
+ if ! ok {
409
+ return nil , fmt .Errorf ("internal error: PushBlob invoked without ReaderAt" )
410
+ }
411
+ z , err := zip .NewReader (content , size )
412
+ if err != nil {
413
+ return nil , err
414
+ }
415
+ files := make ([]string , len (z .File ))
416
+ for i , f := range z .File {
417
+ files [i ] = f .Name
418
+ }
419
+ return files , nil
420
+ }
421
+
422
+ func (r * publishRegistryShim ) PushBlob (ctx context.Context , repoName string , desc ociregistry.Descriptor , content io.Reader ) (ociregistry.Descriptor , error ) {
423
+ if err := r .resolver .setRepository (repoName ); err != nil {
424
+ return ociregistry.Descriptor {}, err
425
+ }
426
+ if r .resolver .recordFiles && desc .MediaType == "application/zip" {
427
+ files , err := filesFromZip (content , desc .Size )
428
+ if err != nil {
429
+ return ociregistry.Descriptor {}, err
430
+ }
431
+ r .resolver .setFiles (files )
432
+ }
433
+
434
+ switch {
435
+ case r .resolver .outDir != "" :
436
+ if err := r .resolver .initDir (); err != nil {
437
+ return ociregistry.Descriptor {}, err
438
+ }
439
+ outFile := filepath .Join (r .resolver .outDir , "blobs" , string (desc .Digest .Algorithm ()), desc .Digest .Encoded ())
440
+ if err := os .MkdirAll (filepath .Dir (outFile ), 0o777 ); err != nil {
441
+ return ociregistry.Descriptor {}, err
442
+ }
443
+ // TODO create as temp and atomically rename?
444
+ f , err := os .Create (outFile )
445
+ if err != nil {
446
+ return ociregistry.Descriptor {}, err
447
+ }
448
+ defer f .Close ()
449
+ if _ , err := io .Copy (f , content ); err != nil {
450
+ return ociregistry.Descriptor {}, fmt .Errorf ("cannot copy blob to %q: %v" , outFile , err )
451
+ }
452
+ if err := f .Close (); err != nil {
453
+ return ociregistry.Descriptor {}, err
454
+ }
455
+ case r .resolver .dryRun :
456
+ // Sanity check we can read the content.
457
+ if _ , err := io .Copy (io .Discard , content ); err != nil {
458
+ return ociregistry.Descriptor {}, fmt .Errorf ("error reading blob: %v" , err )
459
+ }
460
+ default :
461
+ return r .registry .PushBlob (ctx , repoName , desc , content )
462
+ }
463
+ return desc , nil
464
+ }
465
+
466
+ func (r * publishRegistryShim ) PushManifest (ctx context.Context , repoName string , tag string , data []byte , mediaType string ) (ociregistry.Descriptor , error ) {
467
+ if err := r .resolver .setRepository (repoName ); err != nil {
468
+ return ociregistry.Descriptor {}, err
469
+ }
470
+ desc := ociregistry.Descriptor {
471
+ Digest : digest .FromBytes (data ),
472
+ Size : int64 (len (data )),
473
+ }
474
+ if err := r .resolver .setTag (tag , desc .Digest ); err != nil {
475
+ return ociregistry.Descriptor {}, err
476
+ }
477
+ r .resolver .addManifest (tag , data , mediaType )
478
+ switch {
479
+ case r .resolver .outDir != "" :
480
+ // The OCI image layout does not distinguish between data blobs and
481
+ // manifest blobs, unlike the OCI registry API, so use PushBlob
482
+ // to create the blob.
483
+ return r .PushBlob (ctx , repoName , desc , bytes .NewReader (data ))
484
+ case r .resolver .dryRun :
485
+ return desc , nil
486
+ default :
487
+ return r .registry .PushManifest (ctx , repoName , tag , data , mediaType )
488
+ }
489
+ }
0 commit comments