Skip to content

Commit e21702b

Browse files
hack/build-image: add --combined option for building indexes
Indexes (aka manifests) allow for images to be built for different architectures and the associated with one tag (or set of shared tags). The build script enables indexes/manifests when supplied with the --combined flag. The `build` function is changed a bit and no longer always builds an image. If the image is already present locally, the build will be skipped - this is done because (re)building an image of a different architecture can be very slow. This new behavior tries to avoid costly unintentional rebuilds. A future change will add a new command to force a rebuild. Signed-off-by: John Mulligan <[email protected]>
1 parent 554ecd1 commit e21702b

File tree

1 file changed

+232
-39
lines changed

1 file changed

+232
-39
lines changed

hack/build-image

Lines changed: 232 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Usage:
3737

3838
import argparse
3939
import functools
40+
import json
4041
import logging
4142
import os
4243
import pathlib
@@ -265,6 +266,59 @@ def container_build(cli, target):
265266
run(cli, **task)
266267

267268

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+
268322
def create_common_container_engine_args(cli, target):
269323
args = []
270324
pkgs_from = PACKAGES_FROM[target.pkg_source]
@@ -294,6 +348,15 @@ def container_id(cli, target):
294348
"""Construct and run a command to fetch a hexidecimal id for a container
295349
image.
296350
"""
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()
297360
args = [
298361
container_engine(cli),
299362
"inspect",
@@ -377,24 +440,36 @@ def _split_img(image_name, max_tag_split=3):
377440
return base, iname, tparts
378441

379442

380-
class TargetImage:
443+
class Target:
381444
def __init__(
382-
self, name, pkg_source, distro, arch, extra_tag="", *, repo_base=""
445+
self, name, *, pkg_source, distro, extra_tag="", repo_base=""
383446
):
384447
self.name = name
385448
self.pkg_source = pkg_source
386449
self.distro = distro
387-
self.arch = arch
388450
self.extra_tag = extra_tag
389451
self.repo_base = repo_base
390452
self.additional_tags = []
391453

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+
392471
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()
398473

399474
def image_name(self, *, tag=None, repo_base=None):
400475
if not tag:
@@ -406,22 +481,32 @@ class TargetImage:
406481
image_name = f"{repo_base}/{image_name}"
407482
return image_name
408483

409-
def flat_name(self):
410-
return f"{self.name}.{self.tag_name()}"
411-
412484
def __str__(self):
413485
return self.image_name()
414486

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
425510

426511
@classmethod
427512
def parse(cls, image_name):
@@ -438,9 +523,98 @@ class TargetImage:
438523
)
439524

440525

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.
444618
"""
445619
images = {}
446620
for img in cli.image or []:
@@ -459,7 +633,9 @@ def generate_images(cli):
459633
repo_base=rc.find_base(distro_base),
460634
)
461635
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+
)
463639

464640

465641
def add_special_tags(img, distro_qualified=True):
@@ -471,28 +647,32 @@ def add_special_tags(img, distro_qualified=True):
471647
# to keep us compatible with older tagging schemes from earlier versions of
472648
# the project.
473649
_host_arch = host_arch()
650+
arch_ok = img.supports_arch(_host_arch)
474651
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:
476653
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:
478655
img.additional_tags.append((NIGHTLY, QUAL_NONE))
479656
if not distro_qualified:
480657
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:
482659
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:
484661
img.additional_tags.append((f"{img.distro}-{NIGHTLY}", QUAL_DISTRO))
485662

486663

487-
def build(cli, target):
664+
def build(cli, target, rebuild=False):
488665
"""Command to build images."""
489666
build_file = pathlib.Path(f"{cli.buildfile_prefix}{target.flat_name()}")
490667
common_src = "./images/common"
491668
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)
496676
with open(build_file, "w") as fh:
497677
fh.write(f"{cid} {target.image_name()}\n")
498678

@@ -768,6 +948,13 @@ def main():
768948
" will be created."
769949
),
770950
)
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+
771958
behaviors = parser.add_mutually_exclusive_group()
772959
behaviors.add_argument(
773960
"--push",
@@ -828,17 +1015,23 @@ def main():
8281015
logging.basicConfig(level=cli.log_level)
8291016

8301017
_action = cli.main_action if cli.main_action else build
831-
imgs = []
1018+
req = None
8321019
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:
8361022
logger.info("Image %s, extra tags: %s", img, img.additional_tags)
8371023
_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)
8381031
except subprocess.CalledProcessError as err:
8391032
logger.error("Failed command: %s", _cmd_to_str(err.cmd))
8401033
sys.exit(err.returncode)
841-
if not imgs:
1034+
if not req:
8421035
logger.error("No images or image kinds supplied")
8431036
sys.exit(2)
8441037

0 commit comments

Comments
 (0)