@@ -33,6 +33,8 @@ internal sealed class DockerCli
33
33
private string ? _fullCommandPath ;
34
34
#endif
35
35
36
+ private const string _blobsPath = "blobs/sha256" ;
37
+
36
38
public DockerCli ( string ? command , ILoggerFactory loggerFactory )
37
39
{
38
40
if ( ! ( command == null ||
@@ -104,8 +106,8 @@ public async Task LoadAsync(BuiltImage image, SourceImageReference sourceReferen
104
106
}
105
107
106
108
// Create new stream tarball
107
-
108
- await WriteImageToStreamAsync ( image , sourceReference , destinationReference , loadProcess . StandardInput . BaseStream , cancellationToken ) . ConfigureAwait ( false ) ;
109
+ // We want to be able to export to docker, even oci images.
110
+ await WriteDockerImageToStreamAsync ( image , sourceReference , destinationReference , loadProcess . StandardInput . BaseStream , cancellationToken ) . ConfigureAwait ( false ) ;
109
111
110
112
cancellationToken . ThrowIfCancellationRequested ( ) ;
111
113
@@ -270,13 +272,55 @@ public static bool IsInsecureRegistry(string registryDomain)
270
272
271
273
#if NET
272
274
public static async Task WriteImageToStreamAsync ( BuiltImage image , SourceImageReference sourceReference , DestinationImageReference destinationReference , Stream imageStream , CancellationToken cancellationToken )
275
+ {
276
+ if ( image . ManifestMediaType == SchemaTypes . DockerManifestV2 )
277
+ {
278
+ await WriteDockerImageToStreamAsync ( image , sourceReference , destinationReference , imageStream , cancellationToken ) ;
279
+ }
280
+ else if ( image . ManifestMediaType == SchemaTypes . OciManifestV1 )
281
+ {
282
+ await WriteOciImageToStreamAsync ( image , sourceReference , destinationReference , imageStream , cancellationToken ) ;
283
+ }
284
+ else
285
+ {
286
+ throw new ArgumentException ( Resource . FormatString ( nameof ( Strings . UnsupportedMediaTypeForTarball ) , image . Manifest . MediaType ) ) ;
287
+ }
288
+ }
289
+
290
+ private static async Task WriteDockerImageToStreamAsync (
291
+ BuiltImage image ,
292
+ SourceImageReference sourceReference ,
293
+ DestinationImageReference destinationReference ,
294
+ Stream imageStream ,
295
+ CancellationToken cancellationToken )
273
296
{
274
297
cancellationToken . ThrowIfCancellationRequested ( ) ;
275
298
using TarWriter writer = new ( imageStream , TarEntryFormat . Pax , leaveOpen : true ) ;
276
299
277
300
278
301
// Feed each layer tarball into the stream
279
302
JsonArray layerTarballPaths = new ( ) ;
303
+ await WriteImageLayers ( writer , image , sourceReference , d => $ "{ d . Substring ( "sha256:" . Length ) } /layer.tar", cancellationToken , layerTarballPaths )
304
+ . ConfigureAwait ( false ) ;
305
+
306
+ string configTarballPath = $ "{ image . ImageSha } .json";
307
+ await WriteImageConfig ( writer , image , configTarballPath , cancellationToken )
308
+ . ConfigureAwait ( false ) ;
309
+
310
+ // Add manifest
311
+ await WriteManifestForDockerImage ( writer , destinationReference , configTarballPath , layerTarballPaths , cancellationToken )
312
+ . ConfigureAwait ( false ) ;
313
+ }
314
+
315
+ private static async Task WriteImageLayers (
316
+ TarWriter writer ,
317
+ BuiltImage image ,
318
+ SourceImageReference sourceReference ,
319
+ Func < string , string > layerPathFunc ,
320
+ CancellationToken cancellationToken ,
321
+ JsonArray ? layerTarballPaths = null )
322
+ {
323
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
280
324
281
325
foreach ( var d in image . LayerDescriptors )
282
326
{
@@ -287,9 +331,9 @@ public static async Task WriteImageToStreamAsync(BuiltImage image, SourceImageRe
287
331
288
332
// Stuff that (uncompressed) tarball into the image tar stream
289
333
// TODO uncompress!!
290
- string layerTarballPath = $ " { d . Digest . Substring ( "sha256:" . Length ) } /layer.tar" ;
334
+ string layerTarballPath = layerPathFunc ( d . Digest ) ;
291
335
await writer . WriteEntryAsync ( localPath , layerTarballPath , cancellationToken ) . ConfigureAwait ( false ) ;
292
- layerTarballPaths . Add ( layerTarballPath ) ;
336
+ layerTarballPaths ? . Add ( layerTarballPath ) ;
293
337
}
294
338
else
295
339
{
@@ -299,21 +343,32 @@ public static async Task WriteImageToStreamAsync(BuiltImage image, SourceImageRe
299
343
sourceReference . Registry ? . ToString ( ) ?? "<null>" ) ) ;
300
344
}
301
345
}
346
+ }
302
347
303
- // add config
304
- string configTarballPath = $ "{ image . ImageSha } .json";
348
+ private static async Task WriteImageConfig (
349
+ TarWriter writer ,
350
+ BuiltImage image ,
351
+ string configPath ,
352
+ CancellationToken cancellationToken )
353
+ {
305
354
cancellationToken . ThrowIfCancellationRequested ( ) ;
306
355
using ( MemoryStream configStream = new ( Encoding . UTF8 . GetBytes ( image . Config ) ) )
307
356
{
308
- PaxTarEntry configEntry = new ( TarEntryType . RegularFile , configTarballPath )
357
+ PaxTarEntry configEntry = new ( TarEntryType . RegularFile , configPath )
309
358
{
310
359
DataStream = configStream
311
360
} ;
312
-
313
361
await writer . WriteEntryAsync ( configEntry , cancellationToken ) . ConfigureAwait ( false ) ;
314
362
}
363
+ }
315
364
316
- // Add manifest
365
+ private static async Task WriteManifestForDockerImage (
366
+ TarWriter writer ,
367
+ DestinationImageReference destinationReference ,
368
+ string configTarballPath ,
369
+ JsonArray layerTarballPaths ,
370
+ CancellationToken cancellationToken )
371
+ {
317
372
JsonArray tagsNode = new ( ) ;
318
373
foreach ( string tag in destinationReference . Tags )
319
374
{
@@ -339,6 +394,100 @@ public static async Task WriteImageToStreamAsync(BuiltImage image, SourceImageRe
339
394
}
340
395
}
341
396
397
+ private static async Task WriteOciImageToStreamAsync (
398
+ BuiltImage image ,
399
+ SourceImageReference sourceReference ,
400
+ DestinationImageReference destinationReference ,
401
+ Stream imageStream ,
402
+ CancellationToken cancellationToken )
403
+ {
404
+ if ( destinationReference . Tags . Length > 1 )
405
+ {
406
+ throw new ArgumentException ( Resource . FormatString ( nameof ( Strings . OciImageMultipleTagsNotSupported ) ) ) ;
407
+ }
408
+
409
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
410
+ using TarWriter writer = new ( imageStream , TarEntryFormat . Pax , leaveOpen : true ) ;
411
+
412
+ await WriteOciLayout ( writer , cancellationToken )
413
+ . ConfigureAwait ( false ) ;
414
+
415
+ await WriteImageLayers ( writer , image , sourceReference , d => $ "{ _blobsPath } /{ d . Substring ( "sha256:" . Length ) } ", cancellationToken )
416
+ . ConfigureAwait ( false ) ;
417
+
418
+ await WriteImageConfig ( writer , image , $ "{ _blobsPath } /{ image . ImageSha } ", cancellationToken )
419
+ . ConfigureAwait ( false ) ;
420
+
421
+ await WriteManifestForOciImage ( writer , image , destinationReference , cancellationToken )
422
+ . ConfigureAwait ( false ) ;
423
+ }
424
+
425
+ private static async Task WriteOciLayout ( TarWriter writer , CancellationToken cancellationToken )
426
+ {
427
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
428
+
429
+ string ociLayoutPath = "oci-layout" ;
430
+ var ociLayoutContent = "{\" imageLayoutVersion\" : \" 1.0.0\" }" ;
431
+ using ( MemoryStream ociLayoutStream = new MemoryStream ( Encoding . UTF8 . GetBytes ( ociLayoutContent ) ) )
432
+ {
433
+ PaxTarEntry layoutEntry = new ( TarEntryType . RegularFile , ociLayoutPath )
434
+ {
435
+ DataStream = ociLayoutStream
436
+ } ;
437
+ await writer . WriteEntryAsync ( layoutEntry , cancellationToken ) . ConfigureAwait ( false ) ;
438
+ }
439
+ }
440
+
441
+ private static async Task WriteManifestForOciImage (
442
+ TarWriter writer ,
443
+ BuiltImage image ,
444
+ DestinationImageReference destinationReference ,
445
+ CancellationToken cancellationToken )
446
+ {
447
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
448
+
449
+ string manifestContent = JsonSerializer . SerializeToNode ( image . Manifest ) ! . ToJsonString ( ) ;
450
+ string manifestDigest = image . Manifest . GetDigest ( ) ;
451
+
452
+ // 1. add manifest to blobs
453
+ string manifestPath = $ "{ _blobsPath } /{ manifestDigest . Substring ( "sha256:" . Length ) } ";
454
+ using ( MemoryStream manifestStream = new MemoryStream ( Encoding . UTF8 . GetBytes ( manifestContent ) ) )
455
+ {
456
+ PaxTarEntry manifestEntry = new ( TarEntryType . RegularFile , manifestPath )
457
+ {
458
+ DataStream = manifestStream
459
+ } ;
460
+ await writer . WriteEntryAsync ( manifestEntry , cancellationToken ) . ConfigureAwait ( false ) ;
461
+ }
462
+
463
+ cancellationToken . ThrowIfCancellationRequested ( ) ;
464
+
465
+ // 2. add index.json
466
+ var index = new ImageIndexV1
467
+ {
468
+ schemaVersion = 2 ,
469
+ mediaType = SchemaTypes . OciImageIndexV1 ,
470
+ manifests =
471
+ [
472
+ new PlatformSpecificOciManifest
473
+ {
474
+ mediaType = SchemaTypes . OciManifestV1 ,
475
+ size = manifestContent . Length ,
476
+ digest = manifestDigest ,
477
+ annotations = new Dictionary < string , string > { { "org.opencontainers.image.ref.name" , $ "{ destinationReference . Repository } :{ destinationReference . Tags [ 0 ] } " } }
478
+ }
479
+ ]
480
+ } ;
481
+ using ( MemoryStream indexStream = new MemoryStream ( Encoding . UTF8 . GetBytes ( JsonSerializer . SerializeToNode ( index ) ! . ToJsonString ( ) ) ) )
482
+ {
483
+ PaxTarEntry indexEntry = new ( TarEntryType . RegularFile , "index.json" )
484
+ {
485
+ DataStream = indexStream
486
+ } ;
487
+ await writer . WriteEntryAsync ( indexEntry , cancellationToken ) . ConfigureAwait ( false ) ;
488
+ }
489
+ }
490
+
342
491
private async ValueTask < string ? > GetCommandAsync ( CancellationToken cancellationToken )
343
492
{
344
493
if ( _command != null )
0 commit comments