Skip to content

Commit 4a822f9

Browse files
committed
Multi platform builds, with buildx
1 parent 16a9973 commit 4a822f9

File tree

5 files changed

+112
-76
lines changed

5 files changed

+112
-76
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pytest-mock==3.14.1
3333
wrapt==1.17.2
3434
botocore==1.38.23
3535
boto3==1.38.23
36+
python-on-whales
3637

3738
# from kubeobject
3839
freezegun==1.5.2

scripts/release/atomic_pipeline.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def pipeline_process_image(
123123
dockerfile_path: str,
124124
dockerfile_args: Dict[str, str] = None,
125125
base_registry: str = None,
126-
architecture=None,
126+
platforms: list[str] = None,
127127
sign: bool = False,
128128
build_path: str = ".",
129129
with_sbom: bool = True,
@@ -153,7 +153,7 @@ def pipeline_process_image(
153153
dockerfile_path=dockerfile_path,
154154
dockerfile_args=dockerfile_args,
155155
base_registry=base_registry,
156-
architecture=architecture,
156+
platforms=platforms,
157157
sign=sign,
158158
build_path=build_path,
159159
)
@@ -311,14 +311,14 @@ def build_CLI_SBOM(build_configuration: BuildConfiguration):
311311
logger.info("Skipping SBOM Generation (enabled only for EVG)")
312312
return
313313

314-
if build_configuration.architecture is None or len(build_configuration.architecture) == 0:
314+
if build_configuration.platforms is None or len(build_configuration.platforms) == 0:
315315
architectures = ["linux/amd64", "linux/arm64", "darwin/arm64", "darwin/amd64"]
316-
elif "arm64" in build_configuration.architecture:
316+
elif "arm64" in build_configuration.platforms:
317317
architectures = ["linux/arm64", "darwin/arm64"]
318-
elif "amd64" in build_configuration.architecture:
318+
elif "amd64" in build_configuration.platforms:
319319
architectures = ["linux/amd64", "darwin/amd64"]
320320
else:
321-
logger.error(f"Unrecognized architectures {build_configuration.architecture}. Skipping SBOM generation")
321+
logger.error(f"Unrecognized architectures {build_configuration.platforms}. Skipping SBOM generation")
322322
return
323323

324324
release = load_release_file()

scripts/release/build_configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class BuildConfiguration:
1212

1313
parallel: bool = False
1414
parallel_factor: int = 0
15-
architecture: Optional[List[str]] = None
15+
platforms: Optional[List[str]] = None
1616
sign: bool = False
1717
all_agents: bool = False
1818
debug: bool = True

scripts/release/build_images.py

Lines changed: 96 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import sys
44
from typing import Dict
55

6+
import python_on_whales
7+
from python_on_whales.exceptions import DockerException
8+
import time
9+
610
import boto3
711
from botocore.exceptions import BotoCoreError, ClientError
812

@@ -40,82 +44,102 @@ def ecr_login_boto3(region: str, account_id: str):
4044
raise RuntimeError(f"Docker login failed: {login_resp}")
4145
logger.debug(f"ECR login succeeded: {status}")
4246

43-
44-
def build_image(docker_client: docker.DockerClient, tag: str, dockerfile: str, path: str, args: Dict[str, str] = {}):
47+
# TODO: don't do it every time ?
48+
def ensure_buildx_builder(builder_name: str = "multiarch") -> str:
49+
"""
50+
Ensures a Docker Buildx builder exists for multi-platform builds.
51+
52+
:param builder_name: Name for the buildx builder
53+
:return: The builder name that was created or reused
54+
"""
55+
docker = python_on_whales.docker
56+
57+
try:
58+
docker.buildx.create(
59+
name=builder_name,
60+
driver="docker-container",
61+
use=True,
62+
bootstrap=True,
63+
)
64+
logger.info(f"Created new buildx builder: {builder_name}")
65+
except DockerException as e:
66+
if f'existing instance for "{builder_name}"' in str(e):
67+
logger.info(f"Builder '{builder_name}' already exists – reusing it.")
68+
# Make sure it's the current one:
69+
docker.buildx.use(builder_name)
70+
else:
71+
# Some other failure happened
72+
logger.error(f"Failed to create buildx builder: {e}")
73+
raise
74+
75+
return builder_name
76+
77+
78+
def build_image(tag: str, dockerfile: str, path: str, args: Dict[str, str] = {}, push: bool = True, platforms: list[str] = None):
4579
"""
46-
Build a Docker image.
80+
Build a Docker image using python_on_whales and Docker Buildx for multi-architecture support.
4781
48-
:param docker_client:
49-
:param path: Build context path (directory with your Dockerfile)
50-
:param dockerfile: Name or relative path of the Dockerfile within `path`
5182
:param tag: Image tag (name:tag)
52-
:param args:
83+
:param dockerfile: Name or relative path of the Dockerfile within `path`
84+
:param path: Build context path (directory with your Dockerfile)
85+
:param args: Build arguments dictionary
86+
:param push: Whether to push the image after building
87+
:param platforms: List of target platforms (e.g., ["linux/amd64", "linux/arm64"])
5388
"""
54-
89+
docker = python_on_whales.docker
90+
5591
try:
56-
if args:
57-
args = {k: str(v) for k, v in args.items()}
58-
image, logs = docker_client.images.build(
59-
path=path,
60-
dockerfile=dockerfile,
61-
tag=tag,
62-
pull=False, # set True to always attempt to pull a newer base image
63-
buildargs=args,
64-
)
65-
logger.info(f"Successfully built {tag} (id: {image.id})")
66-
# Print build output
67-
for chunk in logs:
68-
if "stream" in chunk:
69-
logger.debug(chunk["stream"])
70-
except docker.errors.BuildError as e:
71-
logger.error("Build failed:")
72-
for stage in e.build_log:
73-
if "stream" in stage:
74-
logger.debug(stage["stream"])
75-
elif "error" in stage:
76-
logger.error(stage["error"])
77-
logger.error(e)
78-
logger.error(
79-
"Note that the docker client only surfaces the general error message. For detailed troubleshooting of the build failure, run the equivalent build command locally or use the docker Python API client directly."
92+
# Convert build args to the format expected by python_on_whales
93+
build_args = {k: str(v) for k, v in args.items()} if args else {}
94+
95+
# Set default platforms if not specified
96+
if platforms is None:
97+
platforms = ["linux/amd64"]
98+
99+
logger.info(f"Building image: {tag}")
100+
logger.info(f"Platforms: {platforms}")
101+
logger.info(f"Dockerfile: {dockerfile}")
102+
logger.info(f"Build context: {path}")
103+
logger.debug(f"Build args: {build_args}")
104+
105+
# Use buildx for multi-platform builds
106+
if len(platforms) > 1:
107+
logger.info(f"Multi-platform build for {len(platforms)} architectures")
108+
109+
# We need a special driver to handle multi platform builds
110+
builder_name = ensure_buildx_builder("multiarch")
111+
112+
# Build the image using buildx
113+
docker.buildx.build(
114+
context_path=path,
115+
file=dockerfile,
116+
tags=[tag],
117+
platforms=platforms,
118+
builder=builder_name,
119+
build_args=build_args,
120+
push=push,
121+
pull=False, # Don't always pull base images
80122
)
81-
raise RuntimeError(f"Failed to build image {tag}")
123+
124+
logger.info(f"Successfully built {'and pushed' if push else ''} {tag}")
125+
82126
except Exception as e:
83-
logger.error(f"Unexpected error: {e}")
84-
raise RuntimeError(f"Failed to build image {tag}")
127+
logger.error(f"Failed to build image {tag}: {e}")
128+
raise RuntimeError(f"Failed to build image {tag}: {str(e)}")
85129

86130

87-
def push_image(docker_client: docker.DockerClient, image: str, tag: str):
88-
"""
89-
Push a Docker image to a registry.
90-
91-
:param docker_client:
92-
:param image: Image name (e.g., 'my-image')
93-
:param tag: Image tag (e.g., 'latest')
94-
"""
95-
logger.debug(f"push_image - image: {image}, tag: {tag}")
96-
image_full_uri = f"{image}:{tag}"
97-
try:
98-
output = docker_client.images.push(image, tag=tag)
99-
if "error" in output:
100-
raise RuntimeError(f"Failed to push image {image_full_uri} {output}")
101-
logger.info(f"Successfully pushed {image_full_uri}")
102-
except Exception as e:
103-
logger.error(f"Failed to push image {image_full_uri} - {e}")
104-
sys.exit(1)
105-
106131

107132
def process_image(
108133
image_name: str,
109134
image_tag: str,
110135
dockerfile_path: str,
111136
dockerfile_args: Dict[str, str],
112137
base_registry: str,
113-
architecture: str = None,
138+
platforms: list[str] = None,
114139
sign: bool = False,
115140
build_path: str = ".",
141+
push: bool = True,
116142
):
117-
docker_client = docker.from_env()
118-
logger.debug("Docker client initialized")
119143
# Login to ECR using boto3
120144
ecr_login_boto3(region="us-east-1", account_id="268558157000")
121145

@@ -127,22 +151,29 @@ def process_image(
127151
create_ecr_repository(repo_to_create)
128152
logger.info(f"Created repository {repo_to_create}")
129153

130-
# Build image
154+
# Set default platforms if none provided TODO: remove from here and do it at higher level later
155+
if platforms is None:
156+
platforms = ["linux/amd64"]
157+
158+
# Build image with multi-platform support
131159
docker_registry = f"{base_registry}/{image_name}"
132-
arch_tag = f"-{architecture}" if architecture else ""
133-
image_tag = f"{image_tag}{arch_tag}"
134160
image_full_uri = f"{docker_registry}:{image_tag}"
161+
135162
logger.info(f"Building image: {image_full_uri}")
163+
logger.info(f"Target platforms: {platforms}")
136164
logger.info(f"Using Dockerfile at: {dockerfile_path}, and build path: {build_path}")
137165
logger.debug(f"Build args: {dockerfile_args}")
166+
167+
# Use new build_image function with buildx multi-platform support
138168
build_image(
139-
docker_client, path=build_path, dockerfile=f"{dockerfile_path}", tag=image_full_uri, args=dockerfile_args
169+
tag=image_full_uri,
170+
dockerfile=dockerfile_path,
171+
path=build_path,
172+
args=dockerfile_args,
173+
push=push,
174+
platforms=platforms
140175
)
141176

142-
# Push to staging registry
143-
logger.info(f"Pushing image: {image_tag} to {docker_registry}")
144-
push_image(docker_client, docker_registry, image_tag)
145-
146177
if sign:
147178
logger.info("Signing image")
148179
sign_image(docker_registry, image_tag)

scripts/release/main.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def _setup_tracing():
120120

121121

122122
def main():
123+
123124
_setup_tracing()
124125
parser = argparse.ArgumentParser(description="Build container images.")
125126
parser.add_argument("--include", action="append", help="Image to include.")
@@ -134,9 +135,9 @@ def main():
134135
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
135136
parser.add_argument("--sign", action="store_true", help="Sign images.")
136137
parser.add_argument(
137-
"--architecture",
138-
action="append",
139-
help="Target architecture for the build. Can be specified multiple times. Defaults to amd64 and arm64.",
138+
"--platform",
139+
default="linux/amd64",
140+
help="Target platforms for multi-arch builds (comma-separated). Example: linux/amd64,linux/arm64. Defaults to linux/amd64.",
140141
)
141142
parser.add_argument(
142143
"--all-agents",
@@ -159,6 +160,9 @@ def main():
159160
if args.skip:
160161
images_to_build = set(images_to_build) - set(args.skip)
161162

163+
# Parse platform argument (comma-separated)
164+
platforms = [p.strip() for p in args.platform.split(",")]
165+
162166
# Centralized configuration management
163167
build_context = BuildContext.from_environment()
164168
version_resolver = VersionResolver(build_context)
@@ -170,7 +174,7 @@ def main():
170174
base_registry=registry_resolver.get_base_registry(),
171175
parallel=args.parallel,
172176
debug=args.debug,
173-
architecture=args.architecture,
177+
architecture=platforms,
174178
sign=args.sign or build_context.signing_enabled,
175179
all_agents=args.all_agents or bool(os.environ.get("all_agents", False)),
176180
parallel_factor=args.parallel_factor,

0 commit comments

Comments
 (0)