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