mirror of
https://github.com/mkuf/prind.git
synced 2025-10-26 15:56:18 +01:00
184 lines
6.7 KiB
Python
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)
|