Skip to content

Commit f083142

Browse files
jiridanekgoogle-labs-jules[bot]coderabbitai[bot]
authored
RHOAISTRAT-169: add s390x testing to CI using QEMU and podman build --platform (#1167)
* Add s390x testing to CI using QEMU and --platform This commit introduces CI testing for the s390x architecture by leveraging QEMU user-mode emulation and Podman's --platform flag. This revised approach avoids running Podman-in-Docker. Key changes: - Added `docker/setup-qemu-action` to the main build job in `.github/workflows/build-notebooks-TEMPLATE.yaml` to enable the runner to execute s390x binaries. - Modified `ci/cached-builds/gen_gha_matrix_jobs.py` to generate a matrix including an `s390x` boolean flag. - Updated the `build` job in `.github/workflows/build-notebooks-TEMPLATE.yaml`: - It now conditionally sets `--platform=linux/s390x` for `podman build` commands via `CONTAINER_BUILD_CACHE_ARGS` when `matrix.s390x` is true. - PyTest execution is conditional: - Runs tests marked `s390x` for s390x builds. - Runs tests marked `not s390x` for amd64 builds. - OpenShift and Playwright tests are skipped for s390x builds. - The main workflow files (`build-notebooks-pr.yaml` and `build-notebooks-push.yaml`) pass the `--s390x-images` argument to the matrix generation script to control s390x builds (excluded for PRs, included for pushes). - s390x-specific tests for `pyzmq` and `mongocli` are included and marked with `@pytest.mark.s390x`. This approach provides a cleaner way to build and test for s390x by directly using Podman's multi-architecture capabilities, facilitated by QEMU on the runner. * RHOAIENG-26279: include s390x platform in build workflows and update matrix configuration * RHOAIENG-26279: streamline s390x platform configuration in build workflows * RHOAIENG-26279: remove s390x-specific conditions from workflows and streamline testing logic * RHOAIENG-26279: refine container build cache arguments to explicitly pass platform configuration * RHOAIENG-24348: remove s390x marker from mongocli binary test in JupyterLab * RHOAIENG-26279: update s390x platform conditions in matrix generation and fix JupyterLab test assertion formatting * RHOAIENG-26279: fix matrix generation by updating key from `targets` to `targets_with_platform` in `has_jobs` calculation * RHOAIENG-26279: fix matrix generation by updating key from `targets` to `targets_with_platform` in `has_jobs` calculation * RHOAIENG-26279: add explicit `choices` constraints for `--rhel-images` and `--s390x-images` arguments in matrix generation script * RHOAIENG-26279: remove unused `--s390x-images` validation in matrix generation script * RHOAIENG-26279: centralize s390x-compatible target list into `S390X_COMPATIBLE` set in matrix generation script * RHOAIENG-26279: rename JupyterLab test method for pyzmq to improve clarity * RHOAIENG-26279: refactor JupyterLab pyzmq test to use encoded function execution logic for cleaner implementation * RHOAIENG-26279: update JupyterLab pyzmq test to reorder imports and add comments for ruff and pyright noqa annotations * RHOAIENG-26279: add runtime image fixture and move pyzmq test to runtime-specific test module * RHOAIENG-26279: simplify runtime image validation logic, reorder imports, and improve socket variable naming * Update tests/containers/runtimes/runtime_test.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * RHOAIENG-26279: add workaround for s390x podman build issue by passing extra build args * RHOAIENG-26279: update runtime image fixture to use yield instead of return * RHOAIENG-26279: update runtime_image fixture scope from session to function * RHOAIENG-26279: update platform check from architecture to platform for s390x podman build workaround * RHOAIENG-26279: refactor runtime test to use context manager for container lifecycle management * RHOAIENG-26279: refactor runtime test to use context manager for container lifecycle management # Conflicts: # tests/containers/runtimes/runtime_test.py * RHOAIENG-26279: update s390x podman build workaround env variable to use undefined flag * RHOAIENG-26279: fix Python interpreter path in runtime test to use user venv * RHOAIENG-26279: remove unused WorkbenchContainer import and cleanup test logic * RHOAIENG-26279: update runtime test to handle exec output as bytes instead of string * RHOAIENG-26279: remove redundant failure case in runtime test cleanup logic * RHOAIENG-26279: skip image tests on s390x due to unresolvable dependency installation issue --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 61c8d5b commit f083142

File tree

7 files changed

+141
-8
lines changed

7 files changed

+141
-8
lines changed

.github/workflows/build-notebooks-TEMPLATE.yaml

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ name: Build & Publish Notebook Servers (TEMPLATE)
1515
required: true
1616
description: "top workflow's `github`"
1717
type: string
18+
platform:
19+
required: true
20+
description: "platform to build, podman build --platform="
21+
type: string
1822
subscription:
1923
required: false
2024
default: false
@@ -57,6 +61,12 @@ jobs:
5761
with:
5862
ref: "refs/pull/${{ fromJson(inputs.github).event.number }}/merge"
5963

64+
- name: Set up QEMU to build linux/s390x
65+
if: ${{ inputs.platform == 'linux/s390x' }}
66+
uses: docker/setup-qemu-action@v3
67+
with:
68+
platforms: s390x
69+
6070
- run: mkdir -p $TMPDIR
6171

6272
# do this early because it's fast and why not
@@ -289,6 +299,19 @@ jobs:
289299

290300
# region Image build
291301

302+
- name: Compute extra podman build args
303+
id: extra-podman-build-args
304+
run: |
305+
set -Eeuxo pipefail
306+
307+
EXTRA_PODMAN_BUILD_ARGS=""
308+
if [[ "${{ inputs.platform }}" == "linux/s390x" ]]; then
309+
# workaround for known issue https://github.com/zeromq/libzmq/pull/4486
310+
# In qemu-user, CACHELINE_SIZE probe is undefined
311+
EXTRA_PODMAN_BUILD_ARGS+='--env=CXXFLAGS=-Dundefined=64'
312+
fi
313+
echo "EXTRA_PODMAN_BUILD_ARGS=$EXTRA_PODMAN_BUILD_ARGS" >> $GITHUB_OUTPUT
314+
292315
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
293316
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
294317

@@ -303,7 +326,8 @@ jobs:
303326
fromJson(inputs.github).event_name == 'workflow_dispatch' }}
304327
env:
305328
IMAGE_TAG: "${{ steps.calculated_vars.outputs.IMAGE_TAG }}"
306-
CONTAINER_BUILD_CACHE_ARGS: "--cache-from ${{ env.CACHE }} --cache-to ${{ env.CACHE }}"
329+
# Configuring `podman build --platform=` through CONTAINER_BUILD_CACHE_ARGS is a venerable hack on this project
330+
CONTAINER_BUILD_CACHE_ARGS: "${{ steps.extra-podman-build-args.outputs.EXTRA_PODMAN_BUILD_ARGS }} --platform=${{ inputs.platform }} --cache-from ${{ env.CACHE }} --cache-to ${{ env.CACHE }}"
307331
- name: "pull_request: make ${{ inputs.target }}"
308332
run: |
309333
# print running stats on disk occupancy
@@ -314,7 +338,8 @@ jobs:
314338
fromJson(inputs.github).event_name == 'pull_request_target' }}"
315339
env:
316340
IMAGE_TAG: "${{ steps.calculated_vars.outputs.IMAGE_TAG }}"
317-
CONTAINER_BUILD_CACHE_ARGS: "--cache-from ${{ env.CACHE }}"
341+
# Configuring `podman build --platform=` through CONTAINER_BUILD_CACHE_ARGS is a venerable hack on this project
342+
CONTAINER_BUILD_CACHE_ARGS: "${{ steps.extra-podman-build-args.outputs.EXTRA_PODMAN_BUILD_ARGS }} --platform=${{ inputs.platform }} --cache-from ${{ env.CACHE }}"
318343
# We don't have access to image registry, so disable pushing
319344
PUSH_IMAGES: "no"
320345

@@ -504,7 +529,9 @@ jobs:
504529
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
505530
506531
- name: "Run image tests"
507-
if: ${{ steps.have-tests.outputs.tests == 'true' }}
532+
# skip on s390x because we are unable to install requirements-elyra.txt that's installed by runtime image tests
533+
# https://raw.githubusercontent.com/opendatahub-io/elyra/refs/heads/main/etc/generic/requirements-elyra.txt
534+
if: ${{ steps.have-tests.outputs.tests == 'true' && !contains(fromJSON('["linux/s390x"]'), inputs.platform) }}
508535
run: python3 ci/cached-builds/make_test.py --target ${{ inputs.target }}
509536
env:
510537
IMAGE_TAG: "${{ steps.calculated_vars.outputs.IMAGE_TAG }}"

.github/workflows/build-notebooks-pr-rhel.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,6 @@ jobs:
7171
with:
7272
target: "${{ matrix.target }}"
7373
github: "${{ toJSON(github) }}"
74+
platform: "${{ matrix.platform }}"
7475
subscription: "${{ matrix.subscription }}"
7576
secrets: inherit

.github/workflows/build-notebooks-pr.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ jobs:
4040
python3 ci/cached-builds/gen_gha_matrix_jobs.py \
4141
--from-ref 'origin/${{ github.event.pull_request.base.ref }}' \
4242
--to-ref '${{ github.event.pull_request.head.ref }}' \
43-
--rhel-images exclude
43+
--rhel-images exclude \
44+
--s390x-images include
4445
id: gen
4546
env:
4647
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -56,5 +57,6 @@ jobs:
5657
with:
5758
target: "${{ matrix.target }}"
5859
github: "${{ toJSON(github) }}"
60+
platform: "${{ matrix.platform }}"
5961
subscription: "${{ matrix.subscription }}"
6062
secrets: inherit

.github/workflows/build-notebooks-push.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ jobs:
2929
- name: Determine targets to build (we want to build everything on push)
3030
run: |
3131
set -x
32-
python3 ci/cached-builds/gen_gha_matrix_jobs.py
32+
python3 ci/cached-builds/gen_gha_matrix_jobs.py \
33+
--s390x-images include
3334
id: gen
3435
env:
3536
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -45,5 +46,6 @@ jobs:
4546
with:
4647
target: "${{ matrix.target }}"
4748
github: "${{ toJSON(github) }}"
49+
platform: "${{ matrix.platform }}"
4850
subscription: "${{ matrix.subscription }}"
4951
secrets: inherit

ci/cached-builds/gen_gha_matrix_jobs.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121

2222
project_dir = pathlib.Path(__file__).parent.parent.parent.absolute()
2323

24+
S390X_COMPATIBLE = {
25+
"runtime-minimal-ubi9-python-3.11",
26+
# add more here
27+
}
28+
2429

2530
def extract_image_targets(makefile_dir: pathlib.Path | str | None = None) -> list[str]:
2631
if makefile_dir is None:
@@ -48,6 +53,12 @@ class RhelImages(enum.Enum):
4853
INCLUDE_ONLY = "include-only"
4954

5055

56+
class S390xImages(enum.Enum):
57+
EXCLUDE = "exclude"
58+
INCLUDE = "include"
59+
ONLY = "only"
60+
61+
5162
def main() -> None:
5263
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
5364

@@ -61,11 +72,21 @@ def main() -> None:
6172
argparser.add_argument(
6273
"--rhel-images",
6374
type=RhelImages,
75+
choices=list(RhelImages),
6476
required=False,
6577
default=RhelImages.INCLUDE,
6678
nargs="?",
6779
help="Whether to `include` rhel images or `exclude` them or `include-only` them",
6880
)
81+
argparser.add_argument(
82+
"--s390x-images",
83+
type=S390xImages,
84+
choices=list(S390xImages),
85+
required=False,
86+
default=S390xImages.INCLUDE,
87+
nargs="?",
88+
help="Whether to include, exclude, or only include s390x images",
89+
)
6990
args = argparser.parse_args()
7091

7192
targets = extract_image_targets()
@@ -84,19 +105,35 @@ def main() -> None:
84105
else:
85106
raise Exception(f"Unknown value for --rhel-images: {args.rhel_images}")
86107

108+
targets_with_platform: list[tuple[str, str]] = []
109+
for target in targets:
110+
if args.s390x_images != S390xImages.ONLY:
111+
targets_with_platform.append((target, "linux/amd64"))
112+
if args.s390x_images != S390xImages.EXCLUDE:
113+
# NOTE: hardcode the list of s390x-compatible Makefile targets in S390X_COMPATIBLE
114+
if target in S390X_COMPATIBLE:
115+
targets_with_platform.append((target, "linux/s390x"))
116+
87117
# https://stackoverflow.com/questions/66025220/paired-values-in-github-actions-matrix
88118
output = [
89119
"matrix="
90120
+ json.dumps(
91121
{
92-
"include": [{"target": target, "subscription": "rhel" in target} for target in targets],
122+
"include": [
123+
{
124+
"target": target,
125+
"platform": platform,
126+
"subscription": "rhel" in target,
127+
}
128+
for (target, platform) in targets_with_platform
129+
],
93130
},
94131
separators=(",", ":"),
95132
),
96-
"has_jobs=" + json.dumps(len(targets) > 0, separators=(",", ":")),
133+
"has_jobs=" + json.dumps(len(targets_with_platform) > 0, separators=(",", ":")),
97134
]
98135

99-
print("targets", targets)
136+
print("targets", targets_with_platform)
100137
print(*output, sep="\n")
101138

102139
if "GITHUB_ACTIONS" in os.environ:

tests/containers/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ def image(request):
115115
yield request.param
116116

117117

118+
@pytest.fixture(scope="function")
119+
def runtime_image(image: str):
120+
image_metadata = get_image_metadata(image)
121+
122+
if "-runtime-" not in image_metadata.labels["name"]:
123+
pytest.skip(f"Image {image} does not have any of '-runtime-' in {image_metadata.labels['name']=}'")
124+
125+
yield image_metadata
126+
127+
118128
@pytest.fixture(scope="function")
119129
def workbench_image(image: str):
120130
skip_if_not_workbench_image(image)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
import contextlib
4+
5+
import allure
6+
import pytest
7+
import testcontainers.core.container
8+
9+
from tests.containers import base_image_test, conftest, docker_utils
10+
11+
12+
class TestRuntimeImage:
13+
"""Tests for runtime images in this repository."""
14+
15+
@allure.description("Check that pyzmq library works correctly, important to check especially on s390x.")
16+
def test_pyzmq_import(self, runtime_image: conftest.Image) -> None:
17+
def check_zmq():
18+
# ruff: noqa: PLC0415 `import` should be at the top-level of a file
19+
import zmq # pyright: ignore reportMissingImports
20+
21+
context = zmq.Context()
22+
socket = None
23+
try:
24+
socket = context.socket(zmq.PAIR)
25+
print("pyzmq imported and socket created successfully")
26+
finally:
27+
if socket is not None:
28+
socket.close(0) # linger=0
29+
context.term()
30+
31+
with running_image(runtime_image.name) as container:
32+
exit_code, output_bytes = container.exec(
33+
# NOTE: /usr/bin/python3 would not find zmq, we need python3 in user's venv
34+
base_image_test.encode_python_function_execution_command_interpreter("python3", check_zmq)
35+
)
36+
37+
assert exit_code == 0, f"Python script execution failed. Output: {output_bytes}"
38+
assert b"pyzmq imported and socket created successfully" in output_bytes, (
39+
f"Expected success message not found in output. Output: {output_bytes}"
40+
)
41+
42+
43+
@contextlib.contextmanager
44+
def running_image(image: str):
45+
"""Usage: with running_image("quay.io/...") as container:"""
46+
container = testcontainers.core.container.DockerContainer(image=image, user=23456, group_add=[0])
47+
container.with_command("/bin/sh -c 'sleep infinity'")
48+
try:
49+
container.start()
50+
yield container
51+
except Exception as e:
52+
pytest.fail(f"Unexpected exception in test: {e}")
53+
finally:
54+
docker_utils.NotebookContainer(container).stop(timeout=0)

0 commit comments

Comments
 (0)