37
37
38
38
import argparse
39
39
import functools
40
+ import json
40
41
import logging
41
42
import os
42
43
import pathlib
@@ -265,6 +266,59 @@ def container_build(cli, target):
265
266
run (cli , ** task )
266
267
267
268
269
+ def index_build (cli , target ):
270
+ """Construct a new index or manifest."""
271
+ logger .debug ("Building index: %s" , target )
272
+ eng = container_engine (cli )
273
+ args = [eng , "manifest" , "create" , target .image_name ()]
274
+ run (cli , args , check = True )
275
+ # add images to index
276
+ for img in target .contents :
277
+ add_args = [eng , "manifest" , "add" ]
278
+ # Currently annotations can't be used for much, but perhaps in the
279
+ # future podman/docker/etc will let you filter on annotations. Then
280
+ # you could choose base distro, etc using a single big index with
281
+ # annotations indicating various features. For now, it's mostly
282
+ # acedemic and for practice.
283
+ add_args .append (
284
+ "--annotation=org.samba.samba-container.pkg-source="
285
+ f"{ img .pkg_source } "
286
+ )
287
+ add_args .append (
288
+ "--annotation=org.samba.samba-container.distro=" f"{ img .distro } "
289
+ )
290
+ add_args += [target .image_name (), img .image_name ()]
291
+ run (cli , add_args , check = True )
292
+ # apply additional tag names
293
+ for tname in target .all_names (baseless = cli .without_repo_bases ):
294
+ tag_args = [eng , "tag" , target .image_name (), tname ]
295
+ if target .image_name () != tname :
296
+ run (cli , tag_args , check = True )
297
+ # verfication step
298
+ inspect_args = [eng , "manifest" , "inspect" , target .image_name ()]
299
+ res = run (cli , inspect_args , check = True , capture_output = True )
300
+ idx_info = json .loads (res .stdout )
301
+ if len (idx_info ["manifests" ]) != len (target .contents ):
302
+ logger .error ("unexpected index info: %r" , idx_info )
303
+ logger .error (
304
+ "saw %d entries, expected %d (%r)" ,
305
+ len (idx_info ["manifests" ]),
306
+ len (target .contents ),
307
+ target .contents ,
308
+ )
309
+ raise ValueError ("unexpected number of manifest entries" )
310
+ return target .image_name ()
311
+
312
+
313
+ def image_build (cli , target ):
314
+ if isinstance (target , TargetIndex ):
315
+ logger .debug ("target is an index (or manifest)" )
316
+ return index_build (cli , target )
317
+ else :
318
+ logger .debug ("target is a container image" )
319
+ return container_build (cli , target )
320
+
321
+
268
322
def create_common_container_engine_args (cli , target ):
269
323
args = []
270
324
pkgs_from = PACKAGES_FROM [target .pkg_source ]
@@ -294,6 +348,15 @@ def container_id(cli, target):
294
348
"""Construct and run a command to fetch a hexidecimal id for a container
295
349
image.
296
350
"""
351
+ if isinstance (target , TargetIndex ):
352
+ args = [
353
+ container_engine (cli ),
354
+ "manifest" ,
355
+ "exists" ,
356
+ target .image_name (),
357
+ ]
358
+ run (cli , args , check = True )
359
+ return target .image_name ()
297
360
args = [
298
361
container_engine (cli ),
299
362
"inspect" ,
@@ -377,24 +440,36 @@ def _split_img(image_name, max_tag_split=3):
377
440
return base , iname , tparts
378
441
379
442
380
- class TargetImage :
443
+ class Target :
381
444
def __init__ (
382
- self , name , pkg_source , distro , arch , extra_tag = "" , * , repo_base = ""
445
+ self , name , * , pkg_source , distro , extra_tag = "" , repo_base = ""
383
446
):
384
447
self .name = name
385
448
self .pkg_source = pkg_source
386
449
self .distro = distro
387
- self .arch = arch
388
450
self .extra_tag = extra_tag
389
451
self .repo_base = repo_base
390
452
self .additional_tags = []
391
453
454
+ def all_names (self , baseless = False ):
455
+ yield self .image_name ()
456
+ for tag , _ in self .additional_tags :
457
+ yield self .image_name (tag = tag )
458
+ if self .repo_base and baseless :
459
+ yield self .image_name (repo_base = "" )
460
+ for tag , qual in self .additional_tags :
461
+ if qual == QUAL_NONE :
462
+ continue
463
+ yield self .image_name (tag = tag , repo_base = "" )
464
+
465
+ def supports_arch (self , arch ):
466
+ return False
467
+
468
+ def flat_name (self ):
469
+ return f"{ self .name } .{ self .tag_name ()} "
470
+
392
471
def tag_name (self ):
393
- tag_parts = [self .pkg_source , self .distro , self .arch ]
394
- if self .extra_tag :
395
- tag_parts .append (self .extra_tag )
396
- tag = "-" .join (tag_parts )
397
- return tag
472
+ raise NotImplementedError ()
398
473
399
474
def image_name (self , * , tag = None , repo_base = None ):
400
475
if not tag :
@@ -406,22 +481,32 @@ class TargetImage:
406
481
image_name = f"{ repo_base } /{ image_name } "
407
482
return image_name
408
483
409
- def flat_name (self ):
410
- return f"{ self .name } .{ self .tag_name ()} "
411
-
412
484
def __str__ (self ):
413
485
return self .image_name ()
414
486
415
- def all_names (self , baseless = False ):
416
- yield self .image_name ()
417
- for tag , _ in self .additional_tags :
418
- yield self .image_name (tag = tag )
419
- if self .repo_base and baseless :
420
- yield self .image_name (repo_base = "" )
421
- for tag , qual in self .additional_tags :
422
- if qual == QUAL_NONE :
423
- continue
424
- yield self .image_name (tag = tag , repo_base = "" )
487
+
488
+ class TargetImage (Target ):
489
+ def __init__ (
490
+ self , name , pkg_source , distro , arch , extra_tag = "" , * , repo_base = ""
491
+ ):
492
+ super ().__init__ (
493
+ name ,
494
+ pkg_source = pkg_source ,
495
+ distro = distro ,
496
+ extra_tag = extra_tag ,
497
+ repo_base = repo_base ,
498
+ )
499
+ self .arch = arch
500
+
501
+ def tag_name (self ):
502
+ tag_parts = [self .pkg_source , self .distro , self .arch ]
503
+ if self .extra_tag :
504
+ tag_parts .append (self .extra_tag )
505
+ tag = "-" .join (tag_parts )
506
+ return tag
507
+
508
+ def supports_arch (self , arch ):
509
+ return arch == self .arch
425
510
426
511
@classmethod
427
512
def parse (cls , image_name ):
@@ -438,9 +523,98 @@ class TargetImage:
438
523
)
439
524
440
525
441
- def generate_images (cli ):
442
- """Given full image names or a matrix of kind/pkg_source/distro_base/arch
443
- values generate a list of target images to build/process.
526
+ class TargetIndex (Target ):
527
+ def __init__ (
528
+ self ,
529
+ name ,
530
+ * ,
531
+ pkg_source ,
532
+ distro ,
533
+ contents = None ,
534
+ extra_tag = "" ,
535
+ repo_base = "" ,
536
+ ):
537
+ super ().__init__ (
538
+ name ,
539
+ pkg_source = pkg_source ,
540
+ distro = distro ,
541
+ extra_tag = extra_tag ,
542
+ repo_base = repo_base ,
543
+ )
544
+ self .contents = contents or []
545
+
546
+ def key (self ):
547
+ return (self .name , self .pkg_source , self .distro )
548
+
549
+ def tag_name (self ):
550
+ tag_parts = [self .pkg_source , self .distro ]
551
+ if self .extra_tag :
552
+ tag_parts .append (self .extra_tag )
553
+ tag = "-" .join (tag_parts )
554
+ return tag
555
+
556
+ def merge (self , other ):
557
+ assert self .name == other .name
558
+ assert self .pkg_source == other .pkg_source
559
+ assert self .distro == other .distro
560
+ self .contents .extend (other .contents )
561
+
562
+ def supports_arch (self , arch ):
563
+ return True
564
+
565
+ @classmethod
566
+ def from_image (cls , img ):
567
+ return cls (
568
+ img .name ,
569
+ pkg_source = img .pkg_source ,
570
+ distro = img .distro ,
571
+ contents = [img ],
572
+ repo_base = img .repo_base or "" ,
573
+ )
574
+
575
+
576
+ class BuildRequest :
577
+ def __init__ (self , images = None , indexes = None ):
578
+ self .images = list (images or [])
579
+ self .indexes = list (indexes or [])
580
+
581
+ def __bool__ (self ):
582
+ return bool (self .images or self .indexes )
583
+
584
+ def expanded (self , indexes = False , distro_qualified = True ):
585
+ new_req = self .__class__ (self .images , self .indexes )
586
+ if indexes :
587
+ new_req ._build_indexes ()
588
+ new_req ._expand_special_tags (distro_qualified = distro_qualified )
589
+ return new_req
590
+
591
+ def _expand_special_tags (self , distro_qualified = True ):
592
+ if self .indexes :
593
+ for image in self .indexes :
594
+ # distro qualified is redundant with the default tag of an
595
+ # index/manifest as well as mainly needed for backwards
596
+ # compatibility something we don't want for indexes.
597
+ add_special_tags (image , distro_qualified = False )
598
+ else :
599
+ for image in self .images :
600
+ add_special_tags (image , distro_qualified = distro_qualified )
601
+
602
+ def _build_indexes (self ):
603
+ _indexes = {}
604
+ for image in self .images :
605
+ image_index = TargetIndex .from_image (image )
606
+ key = image_index .key ()
607
+ if key in _indexes :
608
+ _indexes [key ].merge (image_index )
609
+ else :
610
+ _indexes [key ] = image_index
611
+ self .indexes = list (_indexes .values ())
612
+
613
+
614
+ def generate_request (cli ):
615
+ """Given command line parameters with full image names or a matrix of
616
+ kind/pkg_source/distro_base/arch values generate request object containing
617
+ the target images or indexes to build and/or otherwise process.
444
618
"""
445
619
images = {}
446
620
for img in cli .image or []:
@@ -459,7 +633,9 @@ def generate_images(cli):
459
633
repo_base = rc .find_base (distro_base ),
460
634
)
461
635
images [str (timg )] = timg
462
- return list (images .values ())
636
+ return BuildRequest (images = images .values ()).expanded (
637
+ indexes = cli .combined , distro_qualified = cli .distro_qualified
638
+ )
463
639
464
640
465
641
def add_special_tags (img , distro_qualified = True ):
@@ -471,28 +647,32 @@ def add_special_tags(img, distro_qualified=True):
471
647
# to keep us compatible with older tagging schemes from earlier versions of
472
648
# the project.
473
649
_host_arch = host_arch ()
650
+ arch_ok = img .supports_arch (_host_arch )
474
651
if img .distro in [FEDORA , OPENSUSE ]:
475
- if img . arch == _host_arch and img .pkg_source == DEFAULT :
652
+ if arch_ok and img .pkg_source == DEFAULT :
476
653
img .additional_tags .append ((LATEST , QUAL_NONE ))
477
- if img . arch == _host_arch and img .pkg_source == NIGHTLY :
654
+ if arch_ok and img .pkg_source == NIGHTLY :
478
655
img .additional_tags .append ((NIGHTLY , QUAL_NONE ))
479
656
if not distro_qualified :
480
657
return # skip creating "distro qualified" tags
481
- if img . arch == _host_arch and img .pkg_source == DEFAULT :
658
+ if arch_ok and img .pkg_source == DEFAULT :
482
659
img .additional_tags .append ((f"{ img .distro } -{ LATEST } " , QUAL_DISTRO ))
483
- if img . arch == _host_arch and img .pkg_source == NIGHTLY :
660
+ if arch_ok and img .pkg_source == NIGHTLY :
484
661
img .additional_tags .append ((f"{ img .distro } -{ NIGHTLY } " , QUAL_DISTRO ))
485
662
486
663
487
- def build (cli , target ):
664
+ def build (cli , target , rebuild = False ):
488
665
"""Command to build images."""
489
666
build_file = pathlib .Path (f"{ cli .buildfile_prefix } { target .flat_name ()} " )
490
667
common_src = "./images/common"
491
668
common_dst = str (kind_source_dir (target .name ) / ".common" )
492
- logger .debug ("Copying common tree: %r -> %r" , common_src , common_dst )
493
- shutil .copytree (common_src , common_dst , dirs_exist_ok = True )
494
- container_build (cli , target )
495
- cid = container_id (cli , target )
669
+ cid = maybe_container_id (cli , target )
670
+ logger .debug ("target: %s, cid=%s, rebuild=%s" , target , cid , rebuild )
671
+ if not cid or rebuild :
672
+ logger .debug ("Copying common tree: %r -> %r" , common_src , common_dst )
673
+ shutil .copytree (common_src , common_dst , dirs_exist_ok = True )
674
+ image_build (cli , target )
675
+ cid = container_id (cli , target )
496
676
with open (build_file , "w" ) as fh :
497
677
fh .write (f"{ cid } { target .image_name ()} \n " )
498
678
@@ -768,6 +948,13 @@ def main():
768
948
" will be created."
769
949
),
770
950
)
951
+ parser .add_argument (
952
+ "--combined" ,
953
+ action = argparse .BooleanOptionalAction ,
954
+ default = False ,
955
+ help = ("Specify if manifests/image indexes should be created." ),
956
+ )
957
+
771
958
behaviors = parser .add_mutually_exclusive_group ()
772
959
behaviors .add_argument (
773
960
"--push" ,
@@ -828,17 +1015,23 @@ def main():
828
1015
logging .basicConfig (level = cli .log_level )
829
1016
830
1017
_action = cli .main_action if cli .main_action else build
831
- imgs = []
1018
+ req = None
832
1019
try :
833
- imgs = generate_images (cli )
834
- for img in imgs :
835
- add_special_tags (img , cli .distro_qualified )
1020
+ req = generate_request (cli )
1021
+ for img in req .images :
836
1022
logger .info ("Image %s, extra tags: %s" , img , img .additional_tags )
837
1023
_action (cli , img )
1024
+ for index in req .indexes :
1025
+ logger .info (
1026
+ "Index (Manifest) %s, extra tags: %s" ,
1027
+ index ,
1028
+ index .additional_tags ,
1029
+ )
1030
+ _action (cli , index )
838
1031
except subprocess .CalledProcessError as err :
839
1032
logger .error ("Failed command: %s" , _cmd_to_str (err .cmd ))
840
1033
sys .exit (err .returncode )
841
- if not imgs :
1034
+ if not req :
842
1035
logger .error ("No images or image kinds supplied" )
843
1036
sys .exit (2 )
844
1037
0 commit comments