Files
prind/scripts/build/build.py
2024-11-12 08:36:17 +01:00

184 lines
6.7 KiB
Python

#!/usr/bin/env python3
## Prerequisites
## * user has read access to upstream repo
## * docker login for each repo was executed
## * qemu has been set up
## * docker buildx instance has been created and bootstrapped
import os
import re
import git
import sys
import getpass
import logging
import argparse
import tempfile
from datetime import datetime, timezone
from python_on_whales import docker
# Parse arguments
parser = argparse.ArgumentParser(
prog="Build",
description="Build container images for prind"
)
parser.add_argument("app",help="App to build. Directory must be located at ./docker/<app>")
parser.add_argument("--backfill",type=int,default=3,help="Number of latest upstream git tags to build images for [default: 3]")
parser.add_argument("--registry",help="Where to push images to, /<app> will be appended")
parser.add_argument("--platform",action="append",default=["linux/amd64"],help="Platform to build for. Repeat to build a multi-platform image [default: linux/amd64]")
parser.add_argument("--push",action="store_true",default=False,help="Push image to registry [default: False]")
parser.add_argument("--dry-run",action="store_true",default=False,help="Do not actually build images [default: False]")
parser.add_argument("--force",action="store_true",default=False,help="Build images even though they exist in the registry [default: False]")
parser.add_argument("--version",help="Which upstream Ref to build. Will overwrite automatic Version extraction from upstream")
parser.add_argument("--upstream",help="Overwrite upstream Repo Url. Will skip Url extraction from Dockerfile")
parser.add_argument("--suffix",help="Suffix to add after the image tag. Skips the creation of the 'latest' tag")
args = parser.parse_args()
#---
# Set up logging
logger = logging.getLogger('prind')
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger.addHandler(ch)
#---
# static definitions
context = "docker/" + args.app
dockerfile = context + "/Dockerfile"
build = {
"upstream": None,
"targets": [],
"versions": {},
"summary": {
"success": [],
"failure": [],
"skipped": []
},
"labels": {
"org.prind.version": os.environ.get("GITHUB_SHA",(git.Repo(search_parent_directories=True)).head.object.hexsha),
"org.prind.image.created": datetime.now(timezone.utc).astimezone().isoformat(),
"org.prind.image.authors": os.environ.get("GITHUB_REPOSITORY_OWNER",getpass.getuser()),
"org.prind.image.url": "{GITHUB_SERVER_URL}/{GITHUB_REPOSITORY}".format(**os.environ) if "GITHUB_REPOSITORY" in os.environ else "local",
"org.prind.image.documentation": ("{GITHUB_SERVER_URL}/{GITHUB_REPOSITORY}/blob/{GITHUB_SHA}/docker/" + args.app + "/README.md").format(**os.environ) if "GITHUB_REPOSITORY" in os.environ else "local",
"org.prind.image.source": ("{GITHUB_SERVER_URL}/{GITHUB_REPOSITORY}/blob/{GITHUB_SHA}/docker/" + args.app).format(**os.environ) if "GITHUB_REPOSITORY" in os.environ else "local",
}
}
#---
# extract info from dockerfile
logger.info("Reading " + dockerfile)
with open(dockerfile) as file:
for line in file:
# upstream repository url
repo = re.findall(r'ARG REPO.*', line)
if repo:
build["upstream"] = repo[0].split('=')[1]
# build targets
target = re.findall(r'FROM .* AS .*', line)
if target:
if not "build" in target[0]:
build["targets"].append(target[0].split(' AS ')[-1])
if args.upstream:
logger.warning("Upstream Repo has been overwritten to: " + args.upstream )
build["upstream"] = args.upstream
else:
logger.info("Found upstream repository: " + build["upstream"])
if len(build["targets"]) < 1:
logger.error("No targets found. Nothing to build")
sys.exit(1)
else:
logger.info("Found docker targets: " + str(build["targets"]))
#---
# populate version dict
if args.version:
# version from args
logger.warning("Version '" + args.version + "' specified, skipping upstream lookup")
build["versions"][args.version] = { "latest": True }
else:
# extract info from upstream
logger.info("Cloning Upstream repository")
tmp = tempfile.TemporaryDirectory()
upstream_repo = git.Repo.clone_from(build["upstream"], tmp.name)
logger.info("Generating Versions from Upstream repository")
## latest
latest_version = upstream_repo.git.describe("--tags")
build["versions"][latest_version] = { "latest": True }
## tags
upstream_repo_sorted_tags = upstream_repo.git.tag("-l", "--sort=v:refname").split('\n')
for i in range(1,args.backfill+1):
tag = upstream_repo_sorted_tags[-abs(i)]
if tag not in build["versions"].keys():
build["versions"][tag] = { "latest": False }
tmp.cleanup()
logger.info("Found versions: " + str(build["versions"]))
#---
# Build all targets for all versions
for version in build["versions"].keys():
for target in build["targets"]:
# Create list of docker tags
docker_image = "/".join(filter(None, (args.registry, args.app)))
tags = [
docker_image + ":" + (version if target == "run" else '-'.join([version, target])) + (f"_{args.suffix}" if args.suffix else ""),
*(docker_image + (":latest" if target == "run" else '-'.join([":latest", target])) for _i in range(1) if build["versions"][version]["latest"] and not args.suffix),
]
try:
if args.force:
logger.warning("Build is forced")
raise
else:
# Check if the image already exists
docker.buildx.imagetools.inspect(tags[0])
logger.info("Image " + tags[0] + " exists, nothing to to.")
build["summary"]["skipped"].append(tags[0])
except:
if args.dry_run:
logger.debug("[dry-run] Would build " + tags[0])
else:
try:
# Build if image does not exist
logger.info("Building " + tags[0])
stream = (
docker.buildx.build(
# Build specific
context_path = context,
build_args = {"REPO": build["upstream"], "VERSION": version},
platforms = args.platform,
target = target,
push = args.push,
tags = tags,
labels = {
**build["labels"],
"org.prind.image.version": version
},
stream_logs = True
)
)
for line in stream:
logger.info("BUILD: " + line.strip())
logger.info("Successfully built " + tags[0])
build["summary"]["success"].append(tags[0])
except:
logger.critical("Failed to build " + tags[0])
build["summary"]["failure"].append(tags[0])
logger.info("Build Summary: " + str(build["summary"]))
if len(build["summary"]["failure"]) > 0:
sys.exit(1)