diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index 03ce9ca..f117eb3 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -12,7 +12,7 @@ on: workflow_dispatch: {} env: - CONTAINER_CMD: docker + CONTAINER_CMD: podman jobs: checks: runs-on: ubuntu-latest @@ -45,28 +45,28 @@ jobs: matrix: package_source: [default, nightly] os: [centos, fedora, opensuse] - arch: [amd64, arm64] + arch: + - {name: amd64, host: ubuntu-24.04} + - {name: arm64, host: ubuntu-24.04-arm} exclude: # there are no nightly packages for opensuse - package_source: nightly os: opensuse - - os: centos - arch: arm64 include: - package_source: devbuilds os: centos - arch: amd64 + arch: {name: amd64, host: ubuntu-24.04} - package_source: ceph20 os: centos - arch: amd64 - runs-on: ubuntu-latest + arch: {name: amd64, host: ubuntu-24.04} + runs-on: ${{ matrix.arch.host }} env: BUILDAH_FORMAT: oci - IMG_TAG: ${{ matrix.package_source }}-${{ matrix.os }}-${{ matrix.arch }} + IMG_TAG: ${{ matrix.package_source }}-${{ matrix.os }}-${{ matrix.arch.name }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Build the server image - run: make KIND=server PACKAGE_SOURCE=${{ matrix.package_source }} OS_NAME=${{ matrix.os}} BUILD_ARCH=${{ matrix.arch}} build-image + run: make KIND=server PACKAGE_SOURCE=${{ matrix.package_source }} OS_NAME=${{ matrix.os }} BUILD_ARCH=${{ matrix.arch.name }} build-image - name: Save image run: > ${{ env.CONTAINER_CMD }} save @@ -84,7 +84,9 @@ jobs: matrix: package_source: [default, nightly] os: [centos, fedora, opensuse] - arch: [amd64, arm64] + arch: + - {name: amd64, host: ubuntu-24.04} + - {name: arm64, host: ubuntu-24.04-arm} exclude: # there are no nightly packages for opensuse - package_source: nightly @@ -92,16 +94,14 @@ jobs: # the distro packages for centos do not include an ad-dc - package_source: default os: centos - - os: centos - arch: arm64 - runs-on: ubuntu-latest + runs-on: ${{ matrix.arch.host }} env: BUILDAH_FORMAT: oci - IMG_TAG: ${{ matrix.package_source }}-${{ matrix.os }}-${{ matrix.arch }} + IMG_TAG: ${{ matrix.package_source }}-${{ matrix.os }}-${{ matrix.arch.name }} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Build the ad server image - run: make KIND=ad-server PACKAGE_SOURCE=${{ matrix.package_source }} OS_NAME=${{ matrix.os }} BUILD_ARCH=${{ matrix.arch }} build-image + run: make KIND=ad-server PACKAGE_SOURCE=${{ matrix.package_source }} OS_NAME=${{ matrix.os }} BUILD_ARCH=${{ matrix.arch.name }} build-image - name: Save image run: > ${{ env.CONTAINER_CMD }} save @@ -118,18 +118,17 @@ jobs: strategy: matrix: os: [centos, fedora, opensuse] - arch: [amd64, arm64] - exclude: - - os: centos - arch: arm64 - runs-on: ubuntu-latest + arch: + - {name: amd64, host: ubuntu-24.04} + - {name: arm64, host: ubuntu-24.04-arm} + runs-on: ${{ matrix.arch.host }} env: BUILDAH_FORMAT: oci - IMG_TAG: default-${{ matrix.os }}-${{ matrix.arch }} + IMG_TAG: default-${{ matrix.os }}-${{ matrix.arch.name}} steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: build the client image - run: make KIND=client OS_NAME=${{ matrix.os }} BUILD_ARCH=${{ matrix.arch }} build-image + run: make KIND=client OS_NAME=${{ matrix.os }} BUILD_ARCH=${{ matrix.arch.name }} build-image # The client image is used as a base for the samba-toolbox build process. - name: Save image run: > @@ -308,23 +307,54 @@ jobs: --container-engine=${CONTAINER_CMD} --repo-base=${REPO_BASE} --no-distro-qualified + -i samba-server:ceph20-centos-amd64 + -i samba-server:default-centos-amd64 + -i samba-server:default-centos-arm64 -i samba-server:default-fedora-amd64 -i samba-server:default-fedora-arm64 - -i samba-server:default-opensuse-arm64 + -i samba-server:devbuilds-centos-amd64 + -i samba-server:nightly-centos-amd64 + -i samba-server:nightly-centos-arm64 -i samba-server:nightly-fedora-amd64 -i samba-server:nightly-fedora-arm64 - -i samba-server:nightly-centos-amd64 - -i samba-server:devbuilds-centos-amd64 - -i samba-server:ceph20-centos-amd64 -i samba-ad-server:default-fedora-amd64 -i samba-ad-server:default-fedora-arm64 - -i samba-ad-server:default-opensuse-arm64 + -i samba-ad-server:nightly-centos-amd64 + -i samba-ad-server:nightly-centos-arm64 -i samba-ad-server:nightly-fedora-amd64 -i samba-ad-server:nightly-fedora-arm64 + -i samba-client:default-centos-amd64 + -i samba-client:default-centos-arm64 -i samba-client:default-fedora-amd64 -i samba-client:default-fedora-arm64 - -i samba-client:default-opensuse-arm64 -i samba-toolbox:default-fedora-amd64 + - name: Create manifests/indexes + run: > + ./hack/build-image + --index + --container-engine=${CONTAINER_CMD} + -i ${REPO_BASE}/samba-server:ceph20-centos-amd64 + -i ${REPO_BASE}/samba-server:default-centos-amd64 + -i ${REPO_BASE}/samba-server:default-centos-arm64 + -i ${REPO_BASE}/samba-server:default-fedora-amd64 + -i ${REPO_BASE}/samba-server:default-fedora-arm64 + -i ${REPO_BASE}/samba-server:devbuilds-centos-amd64 + -i ${REPO_BASE}/samba-server:nightly-centos-amd64 + -i ${REPO_BASE}/samba-server:nightly-centos-arm64 + -i ${REPO_BASE}/samba-server:nightly-fedora-amd64 + -i ${REPO_BASE}/samba-server:nightly-fedora-arm64 + -i ${REPO_BASE}/samba-ad-server:default-fedora-amd64 + -i ${REPO_BASE}/samba-ad-server:default-fedora-arm64 + -i ${REPO_BASE}/samba-ad-server:nightly-centos-amd64 + -i ${REPO_BASE}/samba-ad-server:nightly-centos-arm64 + -i ${REPO_BASE}/samba-ad-server:nightly-fedora-amd64 + -i ${REPO_BASE}/samba-ad-server:nightly-fedora-arm64 + -i ${REPO_BASE}/samba-client:default-centos-amd64 + -i ${REPO_BASE}/samba-client:default-centos-arm64 + -i ${REPO_BASE}/samba-client:default-fedora-amd64 + -i ${REPO_BASE}/samba-client:default-fedora-arm64 + -i ${REPO_BASE}/samba-toolbox:default-fedora-amd64 + # Please keep the above list of arguments SORTED. - name: List images prior to pushing (debug) run: | ${{ env.CONTAINER_CMD }} images @@ -335,19 +365,27 @@ jobs: --container-engine=${CONTAINER_CMD} --verbose --push-state=exists - --push-selected-tags=mixed + --push-kinds=mixed,index-multiarch + --push-format=oci + -i ${REPO_BASE}/samba-server:ceph20-centos-amd64 + -i ${REPO_BASE}/samba-server:default-centos-amd64 + -i ${REPO_BASE}/samba-server:default-centos-arm64 -i ${REPO_BASE}/samba-server:default-fedora-amd64 -i ${REPO_BASE}/samba-server:default-fedora-arm64 - -i ${REPO_BASE}/samba-server:default-opensuse-arm64 - -i ${REPO_BASE}/samba-server:nightly-fedora-amd64 - -i ${REPO_BASE}/samba-server:nightly-centos-amd64 -i ${REPO_BASE}/samba-server:devbuilds-centos-amd64 - -i ${REPO_BASE}/samba-server:ceph20-centos-amd64 + -i ${REPO_BASE}/samba-server:nightly-centos-amd64 + -i ${REPO_BASE}/samba-server:nightly-centos-arm64 + -i ${REPO_BASE}/samba-server:nightly-fedora-amd64 + -i ${REPO_BASE}/samba-server:nightly-fedora-arm64 -i ${REPO_BASE}/samba-ad-server:default-fedora-amd64 -i ${REPO_BASE}/samba-ad-server:default-fedora-arm64 - -i ${REPO_BASE}/samba-ad-server:default-opensuse-arm64 + -i ${REPO_BASE}/samba-ad-server:nightly-centos-amd64 + -i ${REPO_BASE}/samba-ad-server:nightly-centos-arm64 -i ${REPO_BASE}/samba-ad-server:nightly-fedora-amd64 + -i ${REPO_BASE}/samba-ad-server:nightly-fedora-arm64 + -i ${REPO_BASE}/samba-client:default-centos-amd64 + -i ${REPO_BASE}/samba-client:default-centos-arm64 -i ${REPO_BASE}/samba-client:default-fedora-amd64 -i ${REPO_BASE}/samba-client:default-fedora-arm64 - -i ${REPO_BASE}/samba-client:default-opensuse-arm64 -i ${REPO_BASE}/samba-toolbox:default-fedora-amd64 + # Please keep the above list of arguments SORTED. diff --git a/Makefile b/Makefile index ee071e6..385a123 100644 --- a/Makefile +++ b/Makefile @@ -160,7 +160,7 @@ test-nightly-server: $(BUILDFILE_NIGHTLY_SERVER) ### Check Rules: static checks, quality tools ### -check: check-shell-scripts check-yaml +check: check-shell-scripts check-yaml check-python .PHONY: check # rule requires shellcheck and find to run check-shell-scripts: $(filter $(ALT_BIN)%,$(SHELLCHECK)) @@ -177,6 +177,15 @@ check-gitlint: $(filter $(ALT_BIN)%,$(GITLINT)) $(GITLINT) -C .gitlint --commits origin/master.. lint .PHONY: check-gitlint +check-python: _py + _py/bin/flake8 hack/build-image + _py/bin/black --check -l78 hack/build-image +.PHONY: check-python + +_py: + python3 -m venv _py + _py/bin/pip install flake8 black + ### Misc. Rules ### diff --git a/hack/build-image b/hack/build-image index 515f929..6f03ad4 100755 --- a/hack/build-image +++ b/hack/build-image @@ -38,6 +38,7 @@ Usage: import argparse import configparser import contextlib +import json import logging import os import pathlib @@ -86,6 +87,7 @@ ARCHITECTURES = { "amd64": AMD64, "arm64": ARM64, } +ANY_ARCH = "any" # DISTROS - list of supported distro bases FEDORA = "fedora" @@ -128,6 +130,7 @@ LATEST = "latest" QUAL_NONE = "unqualified" QUAL_DISTRO = "distro-qualified" QUAL_FQIN = "fqin" +QUAL_INDEX = "index" _DISCOVERED_CONTAINER_ENGINES = [] @@ -358,7 +361,8 @@ def _sambacc_prefix(cli): yield ( "RUN dnf install -y /usr/bin/createrepo_c" f" && createrepo_c {distpath}" - f" && echo -e '[sambacc]\\nbaseurl=file://{distpath}\\nenabled=1\\ngpgcheck=0\\n'" + " && echo -e '[sambacc]\\n" + f"baseurl=file://{distpath}\\nenabled=1\\ngpgcheck=0\\n'" f" > {distpath}/sambacc.repo" ) yield "# --- sambacc prefix ---" @@ -372,7 +376,8 @@ def _sambacc_install(cli): yield "# --- begin modified sambacc install ---" yield ( "RUN" - f" --mount=type=bind,from=sccbuilder,source={distpath},destination={distpath}" + f" --mount=type=bind,from=sccbuilder,source={distpath}," + f"destination={distpath}" f" bash -x /usr/local/bin/install-sambacc.sh {distpath}" " ${SAMBACC_VERSION_SUFFIX}" ) @@ -400,10 +405,63 @@ def _scan_containerfile(path, tags="sambacc"): def container_push(cli, push_name): """Construct and execute a command to push a container image.""" - args = [container_engine(cli), "push", push_name] + args = [container_engine(cli), "push"] + if cli.push_format: + args.append(f"-f{cli.push_format}") + args.append(push_name) run(cli, args, check=True) +def manifest_push(cli, push_name): + """Construct and execute a command to push a manifest/index.""" + args = [ + container_engine(cli), + "manifest", + "push", + ] + if cli.push_format: + args.append(f"-f{cli.push_format}") + args.append("--all") + args.append(push_name) + run(cli, args, check=True) + + +def manifest_inspect(cli, name): + """Return manifest information.""" + eng = container_engine(cli) + args = [eng, "manifest", "inspect", name] + res = run(cli, args, check=True, capture_output=True) + index_info = json.loads(res.stdout) + return index_info + + +def manifest_create(cli, name): + """Create a manifest.""" + eng = container_engine(cli) + args = [eng, "manifest", "create", name] + return run(cli, args, check=True) + + +def manifest_add( + cli, manifest_name, name, *, annotations=None, containers_storage=True +): + """Add an item to a manifest.""" + eng = container_engine(cli) + args = [eng, "manifest", "add"] + # annotations are not used for much but it could become interesting or + # useful in the future + for key, value in (annotations or {}).items(): + args.append(f"--annotation={key}={value}") + + args.append(manifest_name) + # this works around a very weird issue I saw where the image ids + # were changing after push. + if containers_storage: + name = f"containers-storage:{name}" + args.append(name) + return run(cli, args, check=True) + + def container_id(cli, target): """Construct and run a command to fetch a hexidecimal id for a container image. @@ -514,6 +572,17 @@ class TargetImage: continue yield self.image_name(tag=tag, repo_base="") + def as_index(self): + """Return a new target index object based on this image.""" + return TargetIndex( + name=self.name, + pkg_source=self.pkg_source, + distro=self.distro, + extra_tag=self.extra_tag, + repo_base=self.repo_base, + include=self, + ) + @classmethod def parse(cls, image_name): if "/" in image_name: @@ -535,6 +604,48 @@ class TargetImage: ) +class TargetIndex(TargetImage): + def __init__( + self, + name, + pkg_source, + distro, + *, + extra_tag="", + repo_base=None, + include=None, + ): + self.name = name + self.pkg_source = pkg_source + self.distro = distro + self.arch = ANY_ARCH + self.extra_tag = extra_tag + self.repo_base = repo_base + self._images = [] + if isinstance(include, list): + self._images.extend(include) + elif include: + self._images.append(include) + + def images(self): + return self._images + + def merge(self, other): + if other is None: + return self + assert self.name == other.name + assert self.pkg_source == other.pkg_source + assert self.distro == other.distro + return self.__class__( + name=self.name, + pkg_source=self.pkg_source, + distro=self.distro, + extra_tag=self.extra_tag, + repo_base=self.repo_base, + include=self._images + other.images(), + ) + + def generate_images(cli): """Given full image names or a matrix of kind/pkg_source/distro_base/arch values generate a list of target images to build/process. @@ -596,41 +707,65 @@ def build(cli, target): class QMatcher: """Push only tags that meet the specified criteria: - all - all tags; - unqualified - only unqualified tags (eg. latest); - distro - only distribution base qualifed tags (eg. fedora-latest); - fqin - only fully qualified tags (eg. default-centos-amd64); - mixed - only fqin and unqualified tags; - least-qualified (default) - exactly one tag, with the least - number of qualifications + all: + all tags; + unqualified: + only unqualified tags (eg. latest); + distro/distro-qualified: + only distribution base qualifed tags (eg. fedora-latest); + fqin: + only fully qualified tags (eg. default-centos-amd64); + mixed: + only fqin and unqualified tags; + least-qualified (default): + exactly one tag, with the least number of qualifications; + index: + Index/manifest "images"; + index-multiarch: + Index/manifest "images" that represent more than one architecture; """ def __init__(self, key): - self.qualifications = [] + self.qualifications = set() self.count = 0 self.max_matches = 0 + self.minimum_index_images = 1 + if not key: + self.enable("") + return + kinds = key if isinstance(key, list) else key.split(",") + for kind in kinds: + self.enable(kind) + + def enable(self, key): + if not key: + key = "" + _multiarch = f"{QUAL_INDEX}-multiarch" + aliases = { + "least-qualified": [QUAL_NONE, QUAL_DISTRO, QUAL_FQIN], + "": [QUAL_NONE, QUAL_DISTRO, QUAL_FQIN], + "all": [QUAL_NONE, QUAL_DISTRO, QUAL_FQIN], + "mixed": [QUAL_NONE, QUAL_FQIN], + QUAL_NONE: [QUAL_NONE], + QUAL_DISTRO: [QUAL_DISTRO], + "distro": [QUAL_DISTRO], + QUAL_FQIN: [QUAL_FQIN], + QUAL_INDEX: [QUAL_INDEX], + _multiarch: [QUAL_INDEX], + } + try: + quals = aliases[key] + except KeyError: + vals = ", ".join(aliases.keys()) + raise argparse.ArgumentTypeError( + f"value must be one of: {vals} - not {key!r}" + ) + self.qualifications.update(quals) if not key or key == "least-qualified": - self.qualifications = [QUAL_NONE, QUAL_DISTRO, QUAL_FQIN] self.max_matches = 1 - elif key == "all": - pass - elif key == "mixed": - self.qualifications = [QUAL_NONE, QUAL_FQIN] - else: - try: - mq = { - "unqualified": QUAL_NONE, - "distro": QUAL_DISTRO, - "fqin": QUAL_FQIN, - }[key] - except KeyError: - raise argparse.ArgumentTypeError( - "value must be one of:" - " all, least-qualified, unqualified, distro, fqin;" - f" not {key}" - ) - self.qualifications = [mq] + if key == _multiarch: + self.minimum_index_images = 2 def __call__(self, qv): if self.max_matches and self.count >= self.max_matches: @@ -641,30 +776,6 @@ class QMatcher: return False -def push(cli, target): - """Command to push images.""" - if cli.push_state == "rebuild": - build(cli, target) - if cli.push_state == "exists": - try: - container_id(cli, target) - except subprocess.CalledProcessError: - build(cli, target) - - to_push = [] - push_name = target.image_name() - for tag, qual in target.additional_tags: - if tag in ("latest", "nightly"): - to_push.append((target.image_name(tag=tag), qual)) - if tag.endswith(("-latest", "-nightly")): - to_push.append((target.image_name(tag=tag), qual)) - to_push.append((push_name, QUAL_FQIN)) - qmatcher = cli.push_selected_tags or QMatcher("") - for push_name, tag_qual in to_push: - if qmatcher(tag_qual): - container_push(cli, push_name) - - def retag(cli, target): """Command to regenerate any missing unqualified tags.""" cid = container_id(cli, target) @@ -703,6 +814,162 @@ def print_tags(cli, target): print(f"{prefix}{name}") +class Builder: + def generate(self, cli): + raise NotImplementedError() + + def for_target(self, cli, target): + raise NotImplementedError() + + def for_all(self, cli): + raise NotImplementedError() + + +class SimpleBuilder(Builder): + def __init__(self, target_fn, generate_fn=None): + self.target_fn = target_fn + self.generate_fn = generate_fn or generate_images + + def generate(self, cli): + return self.generate_fn(cli) + + def for_target(self, cli, target): + return self.target_fn(cli, target) + + def for_all(self, cli): + pass + + +class BasicBuilder(Builder): + def generate(self, cli): + return generate_images(cli) + + def for_all(self, cli): + pass + + +class IndexBuilder(BasicBuilder): + def __init__(self): + self.indexes = {} + + def for_target(self, cli, target): + idxtarget = target.as_index() + logger.info("Target index: %s", idxtarget) + key = str(idxtarget) + self.indexes[key] = idxtarget.merge(self.indexes.get(key)) + + def for_all(self, cli): + for idxtarget in self.indexes.values(): + logger.info("Creating index: %s", idxtarget) + self._clear(cli, idxtarget) + self._build(cli, idxtarget) + count = self._verify(cli, idxtarget) + logger.info("Created %s with %d manifests", idxtarget, count) + + def _clear(self, cli, idxtarget): + eng = container_engine(cli) + args = [eng, "manifest", "rm", idxtarget.image_name()] + res = run(cli, args, capture_output=True, check=False) + if res.returncode == 0: + logger.info("Removed previous manifest %s", idxtarget) + else: + logger.debug("Unable to remove previous manifest %s", idxtarget) + + def _build(self, cli, idxtarget): + manifest_create(cli, idxtarget.image_name()) + pfx = "org.samba.samba-container" + for img in idxtarget.images(): + manifest_add( + cli, + idxtarget.image_name(), + img.image_name(), + annotations={ + f"{pfx}.pkg-source": img.pkg_source, + f"{pfx}.distro": img.distro, + }, + ) + + def _verify(self, cli, idxtarget): + index_info = manifest_inspect(cli, idxtarget.image_name()) + img_count = len(index_info["manifests"]) + expecting = len(idxtarget.images()) + # I saw some weird stuff while developing this and so this + # check tries to alert on some of those conditions. + if img_count != expecting: + logger.error( + "confused manifest: saw %d entries, expected %d (%r)", + img_count, + expecting, + idxtarget.images(), + ) + raise ValueError("unexpected number of manifest entries") + return img_count + + +def _push_weight(value): + push_name, tag_qual = value + # do all "real" fqin images first + if tag_qual == QUAL_FQIN: + return 0, push_name + # do indexes last + if tag_qual == QUAL_INDEX: + return 100, push_name + # do non-fqins after fqins + return 10, push_name + + +class Pusher(BasicBuilder): + def __init__(self): + self._to_push = set() + + def for_target(self, cli, target): + if cli.push_state == "rebuild": + build(cli, target) + if cli.push_state == "exists": + try: + container_id(cli, target) + except subprocess.CalledProcessError: + build(cli, target) + push_name = target.image_name() + for tag, qual in target.additional_tags: + if tag in ("latest", "nightly"): + self._to_push.add((target.image_name(tag=tag), qual)) + if tag.endswith(("-latest", "-nightly")): + self._to_push.add((target.image_name(tag=tag), qual)) + self._to_push.add((push_name, QUAL_FQIN)) + qmatcher = cli.push_kinds or QMatcher("") + if QUAL_INDEX in qmatcher.qualifications: + self._to_push.add((target.as_index().image_name(), QUAL_INDEX)) + + def for_all(self, cli): + qmatcher = cli.push_kinds or QMatcher("") + for push_name, tag_qual in sorted(self._to_push, key=_push_weight): + if not qmatcher(tag_qual): + logger.debug("Excluding %s from push", push_name) + continue + if tag_qual == QUAL_INDEX: + self._push_index(cli, qmatcher, push_name) + else: + container_push(cli, push_name) + + def _push_index(self, cli, qmatcher, push_name): + try: + index_info = manifest_inspect(cli, push_name) + except subprocess.CalledProcessError: + logger.warning("No manifest present for %s, skipping", push_name) + return + img_count = len(index_info["manifests"]) + logger.debug("Manifest/index %s has %d images", push_name, img_count) + if img_count >= qmatcher.minimum_index_images: + manifest_push(cli, push_name) + else: + logger.warning( + "Skipping manifest push for %s, too few images (%d)", + push_name, + img_count, + ) + + class CLIContext: def __init__(self, cli): self._cli = cli @@ -852,10 +1119,17 @@ def main(): ), ) parser.add_argument( + "--push-kinds", + "--push-what", "--push-selected-tags", type=QMatcher, help=QMatcher.__doc__, ) + parser.add_argument( + "--push-format", + choices=("oci", "v2s2"), + help="Manifest type to use when pushing", + ) parser.add_argument( "--buildfile-prefix", default=".build.", @@ -896,7 +1170,7 @@ def main(): "--push", action="store_const", dest="main_action", - const=push, + const=Pusher, help="Push images", ) behaviors.add_argument( @@ -930,20 +1204,36 @@ def main(): " for a given FQIN. Requires FQIN to already exist locally." ), ) + behaviors.add_argument( + "--index", + action="store_const", + dest="main_action", + const=IndexBuilder, + help="Create indexes (aka manifests) for named images", + ) cli = CLIContext(parser.parse_args()) if os.environ.get("BUILD_IMAGE_DEBUG") in ("1", "yes"): cli.log_level = logging.DEBUG logging.basicConfig(level=cli.log_level) - _action = cli.main_action if cli.main_action else build + if not cli.main_action: + logger.debug("Using default action") + builder = SimpleBuilder(build) + elif hasattr(cli.main_action, "generate"): + logger.debug("Using builder object: %r", cli.main_action) + builder = cli.main_action() + else: + logger.debug("Legacy mode: using simple builder wrapper") + builder = SimpleBuilder(cli.main_action) imgs = [] try: - imgs = generate_images(cli) + imgs = builder.generate(cli) for img in imgs: add_special_tags(img, cli.distro_qualified) logger.info("Image %s, extra tags: %s", img, img.additional_tags) - _action(cli, img) + builder.for_target(cli, img) + builder.for_all(cli) except subprocess.CalledProcessError as err: logger.error("Failed command: %s", _cmd_to_str(err.cmd)) sys.exit(err.returncode)