Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 39 additions & 16 deletions pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,19 +305,19 @@ def copy_into_container(client, src, dst):
# Sonar as an interface to docker. We decided to keep this asymmetry for now, as Sonar will be removed soon.


def create_and_push_manifest(image: str, tag: str) -> None:
def create_and_push_manifest(image: str, tag: str, architectures: list[str]) -> None:
final_manifest = image + ":" + tag

args = [
"docker",
"manifest",
"create",
final_manifest,
"--amend",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we even need the amend part anymore - since those images are always multiarch cc. @lucian-tosa

final_manifest + "-amd64",
"--amend",
final_manifest + "-arm64",
]

for arch in architectures:
args.extend(["--amend", f"{final_manifest}-{arch}"])

args_str = " ".join(args)
logger.debug(f"creating new manifest: {args_str}")
cp = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Expand Down Expand Up @@ -752,6 +752,14 @@ def submit(self, fn, *args, **kwargs):
"""


def should_skip_arm64():
"""
Determines if arm64 builds should be skipped based on environment.
Returns True if running in Evergreen pipeline as a patch.
"""
return is_running_in_evg_pipeline() and is_running_in_patch()


def build_image_daily(
image_name: str, # corresponds to the image_name in the release.json
min_version: str = None,
Expand All @@ -766,6 +774,10 @@ def get_architectures_set(build_configuration, args):
if arch_set == {"arm64"}:
raise ValueError("Building for ARM64 only is not supported yet")

if should_skip_arm64():
logger.info("Skipping ARM64 builds as this is running in EVG pipeline as a patch")
return {"amd64"}

# Automatic architecture detection is the default behavior if 'arch' argument isn't specified
if arch_set == set():
if check_multi_arch(
Expand All @@ -779,13 +791,13 @@ def get_architectures_set(build_configuration, args):

return arch_set

def create_and_push_manifests(args: dict):
def create_and_push_manifests(args: dict, architectures: list[str]):
"""Create and push manifests for all registries."""
registries = [args["ecr_registry_ubi"], args["quay_registry"]]
tags = [args["release_version"], args["release_version"] + "-b" + args["build_id"]]
for registry in registries:
for tag in tags:
create_and_push_manifest(registry + args["ubi_suffix"], tag)
create_and_push_manifest(registry + args["ubi_suffix"], tag, architectures=architectures)

def sign_image_concurrently(executor, args, futures, arch=None):
v = args["release_version"]
Expand Down Expand Up @@ -838,7 +850,7 @@ def inner(build_configuration: BuildConfiguration):
)
if build_configuration.sign:
sign_image_concurrently(executor, copy.deepcopy(args), futures, arch)
create_and_push_manifests(args)
create_and_push_manifests(args, list(arch_set))
for arch in arch_set:
args["architecture_suffix"] = f"-{arch}"
args["platform"] = arch
Expand Down Expand Up @@ -985,12 +997,13 @@ def build_image_generic(
if is_multi_arch:
# we only push the manifests of the context images here,
# since daily rebuilds will push the manifests for the proper images later
create_and_push_manifest(registry_address, f"{version}-context")
architectures = [v["architecture"] for v in multi_arch_args_list]
create_and_push_manifest(registry_address, f"{version}-context", architectures=architectures)
if not config.is_release_step_executed():
# Normally daily rebuild would create and push the manifests for the non-context images.
# But since we don't run daily rebuilds on ecr image builds, we can do that step instead here.
# We only need to push manifests for multi-arch images.
create_and_push_manifest(registry_address, version)
create_and_push_manifest(registry_address, version, architectures=architectures)
if config.sign and config.is_release_step_executed():
sign_and_verify_context_image(registry, version)
if config.is_release_step_executed() and version and QUAY_REGISTRY_URL in registry:
Expand Down Expand Up @@ -1044,7 +1057,14 @@ def build_community_image(build_configuration: BuildConfiguration, image_type: s

version, is_release = get_git_release_tag()
golang_version = os.getenv("GOLANG_VERSION", "1.24")
architectures = build_configuration.architecture or ["amd64", "arm64"]

# Use only amd64 if we should skip arm64 builds
if should_skip_arm64():
architectures = ["amd64"]
logger.info("Skipping ARM64 builds for community image as this is running in EVG pipeline as a patch")
else:
architectures = build_configuration.architecture or ["amd64", "arm64"]

multi_arch_args_list = []

for arch in architectures:
Expand All @@ -1064,7 +1084,7 @@ def build_community_image(build_configuration: BuildConfiguration, image_type: s
multi_arch_args_list=multi_arch_args_list,
inventory_file=inventory_file,
registry_address=f"{base_repo}/{image_name}",
is_multi_arch=True,
is_multi_arch=True, # We for pushing manifest anyway, even if arm64 is skipped in patches
)


Expand Down Expand Up @@ -1151,16 +1171,19 @@ def build_multi_arch_agent_in_sonar(
ecr_registry = os.environ.get("REGISTRY", "268558157000.dkr.ecr.us-east-1.amazonaws.com/dev")
ecr_agent_registry = ecr_registry + f"/mongodb-agent-ubi"
quay_agent_registry = QUAY_REGISTRY_URL + f"/mongodb-agent-ubi"
joined_args = list()
for arch in [arch_arm, arch_amd]:
joined_args.append(args | arch)
joined_args = [arch_amd]

# Only include arm64 if we shouldn't skip it
if not should_skip_arm64():
joined_args.append(arch_arm)

build_image_generic(
config=build_configuration,
image_name="mongodb-agent",
inventory_file="inventories/agent_non_matrix.yaml",
multi_arch_args_list=joined_args,
registry_address=quay_agent_registry if is_release else ecr_agent_registry,
is_multi_arch=True,
is_multi_arch=True, # We for pushing manifest anyway, even if arm64 is skipped in patches
is_run_in_parallel=True,
)

Expand Down
99 changes: 99 additions & 0 deletions pipeline_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,102 @@ def test_all_retries_fail(self, mock_sleep, mock_run):

self.assertEqual(mock_run.call_count, 3)
self.assertEqual(mock_sleep.call_count, 2)


@patch("subprocess.run")
def test_create_and_push_manifest_success(mock_run):
"""Test successful creation and pushing of manifest with multiple architectures."""
# Setup mock to return success for both calls
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=b"", stderr=b"")

image = "test/image"
tag = "1.0.0"
architectures = ["amd64", "arm64"]

from pipeline import create_and_push_manifest

create_and_push_manifest(image, tag, architectures)

assert mock_run.call_count == 2

# Verify first call - create manifest
create_call_args = mock_run.call_args_list[0][0][0]
assert create_call_args == [
"docker",
"manifest",
"create",
"test/image:1.0.0",
"--amend",
"test/image:1.0.0-amd64",
"--amend",
"test/image:1.0.0-arm64",
]

# Verify second call - push manifest
push_call_args = mock_run.call_args_list[1][0][0]
assert push_call_args == ["docker", "manifest", "push", f"{image}:{tag}"]


@patch("subprocess.run")
def test_create_and_push_manifest_single_arch(mock_run):
"""Test manifest creation with a single architecture."""
# Setup mock to return success for both calls
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=b"", stderr=b"")

image = "test/image"
tag = "1.0.0"
architectures = ["amd64"]

from pipeline import create_and_push_manifest

create_and_push_manifest(image, tag, architectures)

# Verify first call - create manifest (should only include one architecture)
create_call_args = mock_run.call_args_list[0][0][0]
assert " ".join(create_call_args) == f"docker manifest create {image}:{tag} --amend {image}:{tag}-amd64"


@patch("subprocess.run")
def test_create_and_push_manifest_create_error(mock_run):
"""Test error handling when manifest creation fails."""
# Setup mock to return error for create call
mock_run.return_value = subprocess.CompletedProcess(
args=[], returncode=1, stdout=b"", stderr=b"Error creating manifest"
)

image = "test/image"
tag = "1.0.0"
architectures = ["amd64", "arm64"]

from pipeline import create_and_push_manifest

# Verify exception is raised with the stderr content
with pytest.raises(Exception) as exc_info:
create_and_push_manifest(image, tag, architectures)

assert "Error creating manifest" in str(exc_info.value)
assert mock_run.call_count == 1 # Only the create call, not the push call


@patch("subprocess.run")
def test_create_and_push_manifest_push_error(mock_run):
"""Test error handling when manifest push fails."""
# Setup mock to return success for create but error for push
mock_run.side_effect = [
subprocess.CompletedProcess(args=[], returncode=0, stdout=b"", stderr=b""), # create success
subprocess.CompletedProcess(args=[], returncode=1, stdout=b"", stderr=b"Error pushing manifest"), # push error
]

# Call function with test parameters
image = "test/image"
tag = "1.0.0"
architectures = ["amd64", "arm64"]

from pipeline import create_and_push_manifest

# Verify exception is raised with the stderr content
with pytest.raises(Exception) as exc_info:
create_and_push_manifest(image, tag, architectures)

assert "Error pushing manifest" in str(exc_info.value)
assert mock_run.call_count == 2 # Both create and push calls