From cdc230d6eb4552795ecec336530cff397fa3bec7 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Wed, 16 Sep 2026 12:58:31 +0200 Subject: [PATCH 1/3] ROX-36983: version containers used in tests (#1789) After a change in the Vi/Vim versioned used by Fedora broke our integration tests, we have decided to tag and version all images used in our tests. This patch adds a CI job for building and pushing the images when `tests/containers/QA_TAG` is updated, much like we do in stackrox/collector. It also modifies the existing tests from always building the images to: * Try to find the image locally. * Attempt to pull it if it is not found. * If unable to pulled and allowed to build locally, do that. --- .github/workflows/ci.yml | 12 ++- .github/workflows/integration-tests.yml | 5 ++ .github/workflows/konflux-tests.yml | 6 ++ .github/workflows/test-containers-build.yml | 84 +++++++++++++++++++++ ansible/run-tests.yml | 6 +- tests/conftest.py | 5 ++ tests/containers.py | 54 +++++++++++++ tests/containers/Makefile | 10 +++ tests/containers/QA_TAG | 1 + tests/containers/constants.mk | 13 ++++ tests/containers/editors/Containerfile | 2 +- tests/containers/editors/Makefile | 19 +++++ tests/containers/fedora/Containerfile | 1 + tests/containers/fedora/Makefile | 19 +++++ tests/containers/self-deleter/Makefile | 19 +++++ tests/test_editors/commons.py | 38 ++++++++-- tests/test_editors/test_sed.py | 20 ++--- tests/test_editors/test_vi.py | 36 ++++----- tests/test_misc.py | 16 ++-- 19 files changed, 320 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/test-containers-build.yml create mode 100644 tests/containers.py create mode 100644 tests/containers/Makefile create mode 100644 tests/containers/QA_TAG create mode 100644 tests/containers/constants.mk create mode 100644 tests/containers/editors/Makefile create mode 100644 tests/containers/fedora/Containerfile create mode 100644 tests/containers/fedora/Makefile create mode 100644 tests/containers/self-deleter/Makefile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed6cf466..be6b2aed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,6 +128,10 @@ jobs: pip install ruff make format-check + unit-tests: + uses: ./.github/workflows/unit-tests.yml + secrets: inherit + container-build: uses: ./.github/workflows/container-build.yml secrets: inherit @@ -139,25 +143,29 @@ jobs: tag-suffix: '-otel' make-target: 'image-otel' - unit-tests: - uses: ./.github/workflows/unit-tests.yml + test-containers-build: + uses: ./.github/workflows/test-containers-build.yml secrets: inherit integration-tests: needs: - container-build + - test-containers-build uses: ./.github/workflows/integration-tests.yml with: tag: ${{ needs.container-build.outputs.tag }} + qa-tag: ${{ needs.test-containers-build.outputs.qa-tag }} vms: '["fedora-coreos", "fcarm", "rhel", "rhel-arm64", "rhcos", "rhcos-arm64"]' secrets: inherit integration-tests-otel: needs: - container-build-otel + - test-containers-build uses: ./.github/workflows/integration-tests.yml with: tag: ${{ needs.container-build-otel.outputs.tag }} + qa-tag: ${{ needs.test-containers-build.outputs.qa-tag }} job-tag: otel vms: '["fedora-coreos"]' output-type: otlp diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 56de2a0c..091e0556 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -13,6 +13,9 @@ on: tag: description: The container image tag to test type: string + qa-tag: + description: The test containers tag to use + type: string job-tag: description: Additional tag to prevent collision on GCP VM naming type: string @@ -75,6 +78,7 @@ jobs: env: FACT_VERSION: ${{ inputs.version }} FACT_REGISTRY: ${{ inputs.registry }} + FACT_QA_TAG: ${{ inputs.qa-tag }} FACT_TAG: ${{ inputs.tag }} OUTPUT_TYPE: ${{ inputs.output-type }} run: | @@ -85,6 +89,7 @@ jobs: fact: image: ${FACT_IMAGE_NAME} version: ${FACT_VERSION} + qa_tag: ${FACT_QA_TAG} output_type: ${OUTPUT_TYPE} quay: username: ${{ secrets.QUAY_RHACS_ENG_RO_USERNAME }} diff --git a/.github/workflows/konflux-tests.yml b/.github/workflows/konflux-tests.yml index bed02ef0..549b249e 100644 --- a/.github/workflows/konflux-tests.yml +++ b/.github/workflows/konflux-tests.yml @@ -40,15 +40,21 @@ jobs: image: rhacs-eng/release-fact:${{ needs.init.outputs.fact-tag }} limit: 2400 # 40m + test-containers-build: + uses: ./.github/workflows/test-containers-build.yml + secrets: inherit + run-konflux-tests: uses: ./.github/workflows/integration-tests.yml needs: - init - wait-for-images + - test-containers-build with: version: ${{ github.head_ref || github.ref_name }} registry: quay.io/rhacs-eng/release-fact tag: ${{ needs.init.outputs.fact-tag }} + qa-tag: ${{ needs.test-containers-build.outputs.qa-tag }} job-tag: konf vms: '["fedora-coreos", "fcarm", "rhel", "rhel-arm64", "rhcos", "rhcos-arm64"]' secrets: inherit diff --git a/.github/workflows/test-containers-build.yml b/.github/workflows/test-containers-build.yml new file mode 100644 index 00000000..1ae27a3a --- /dev/null +++ b/.github/workflows/test-containers-build.yml @@ -0,0 +1,84 @@ +name: Build integration test containers + +on: + workflow_call: + outputs: + qa-tag: + value: ${{ jobs.determine-qa-tag.outputs.qa-tag }} + +jobs: + should-build-test-containers: + name: Determine if tests image needs to be built + runs-on: ubuntu-24.04 + outputs: + build-image: ${{ (steps.changed.outputs.qa-tag == 'true' || + contains(github.event.pull_request.labels.*.name, 'rebuild-test-container')) }} + + steps: + - uses: actions/checkout@v7 + + - uses: dorny/paths-filter@v4 + id: changed + with: + list-files: shell + base: ${{ github.ref }} + + # Only trigger a rebuild when the QA tag has changed + filters: | + qa-tag: + - tests/containers/QA_TAG + + determine-qa-tag: + name: Determine QA tag + runs-on: ubuntu-24.04 + needs: + - should-build-test-containers + outputs: + qa-tag: ${{ steps.qa-tag.outputs.qa-tag }} + steps: + - uses: actions/checkout@v7 + with: + submodules: true + fetch-depth: 0 + persist-credentials: false + - name: Determine QA tag + id: qa-tag + run: | + QA_TAG="$(cat "${GITHUB_WORKSPACE}/tests/containers/QA_TAG")" + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" && "${{ needs.should-build-test-containers.outputs.build-image }}" == "true" ]]; then + QA_TAG="${QA_TAG}-$(make tag)" + fi + echo "::notice::Using QA tag: ${QA_TAG}" + echo "qa-tag=${QA_TAG}" >> "$GITHUB_OUTPUT" + + build-test-containers: + name: Build and push test containers + runs-on: ubuntu-24.04 + needs: + - determine-qa-tag + - should-build-test-containers + if: needs.should-build-test-containers.outputs.build-image == 'true' || + contains(github.event.pull_request.labels.*.name, 'rebuild-test-container') + + steps: + - uses: actions/checkout@v7 + + - name: Setup QEMU + uses: docker/setup-qemu-action@v4 + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to quay.io/rhacs-eng + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ secrets.QUAY_RHACS_ENG_RW_USERNAME }} + password: ${{ secrets.QUAY_RHACS_ENG_RW_PASSWORD }} + + - name: Build and Push containers + env: + FACT_QA_TAG: ${{ needs.determine-qa-tag.outputs.qa-tag }} + PLATFORM: linux/amd64,linux/arm64 + run: | + make -C "${GITHUB_WORKSPACE}/tests/containers" build-and-push diff --git a/ansible/run-tests.yml b/ansible/run-tests.yml index 9d5ff91f..4129efb9 100644 --- a/ansible/run-tests.yml +++ b/ansible/run-tests.yml @@ -43,6 +43,7 @@ environment: DOCKER_HOST: "{{ runtime_host }}" FACT_IMAGE_NAME: "{{ fact.image | default(None) }}" + FACT_QA_TAG: "{{ qa_tag | default(None) }}" OUTPUT_TYPE: "{{ output_type | default('grpc') }}" ansible.builtin.shell: cmd: | @@ -65,7 +66,10 @@ ../third_party/stackrox/proto/internalapi/sensor/sfa_iservice.proto # Run the tests - pytest --image="${FACT_IMAGE_NAME}" --junit-xml=results.xml --output="${OUTPUT_TYPE}" + pytest --image="${FACT_IMAGE_NAME}" \ + --no-local-builds \ + --junit-xml=results.xml \ + --output="${OUTPUT_TYPE}" always: - name: Make logs directories diff --git a/tests/conftest.py b/tests/conftest.py index dc5b733d..7bcfa45e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -338,3 +338,8 @@ def pytest_addoption(parser: pytest.Parser): choices=['grpc', 'otlp', 'all'], help='Output mode to test: grpc, otlp, or all (default: grpc)', ) + parser.addoption( + '--no-local-builds', + action='store_true', + help='Do not build test containers locally', + ) diff --git a/tests/containers.py b/tests/containers.py new file mode 100644 index 00000000..af515dc2 --- /dev/null +++ b/tests/containers.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import os + +import docker +import docker.errors +from docker.models.images import Image + + +def read_qa_tag() -> str: + tag = os.environ.get('FACT_QA_TAG') + if tag is not None: + return tag + + with open('containers/QA_TAG') as f: + return f.read().strip() + + +QA_TAG = read_qa_tag() +QA_REPOSITORY = os.environ.get( + 'FACT_QA_REPOSITORY', 'quay.io/rhacs-eng/qa-multi-arch' +) + + +def pull_or_build( + docker_client: docker.DockerClient, + tag: str, + path: str | None, + no_local_builds: bool, + containerfile: str = 'Containerfile', +) -> Image: + tag = f'{tag}-{QA_TAG}' + image = f'{QA_REPOSITORY}:{tag}' + + try: + return docker_client.images.get(image) + except docker.errors.ImageNotFound: + print(f'{image} not found locally, attempting to pull') + + try: + return docker_client.images.pull(QA_REPOSITORY, tag=tag) + except docker.errors.APIError as e: + if no_local_builds: + raise e + + if e.status_code != 401 and e.status_code != 404: + raise e + print(f'Failed to pull image: {e}') + print('Attempting to build image from source') + + image, _ = docker_client.images.build( + path=path, tag=image, dockerfile=containerfile + ) + return image diff --git a/tests/containers/Makefile b/tests/containers/Makefile new file mode 100644 index 00000000..4eb28d59 --- /dev/null +++ b/tests/containers/Makefile @@ -0,0 +1,10 @@ +include constants.mk + +TARGETS := build build-and-push +DIRS := $(wildcard ./*/) + +$(TARGETS): $(DIRS) +$(DIRS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +.PHONY: $(TARGETS) $(DIRS) diff --git a/tests/containers/QA_TAG b/tests/containers/QA_TAG new file mode 100644 index 00000000..6e8bf73a --- /dev/null +++ b/tests/containers/QA_TAG @@ -0,0 +1 @@ +0.1.0 diff --git a/tests/containers/constants.mk b/tests/containers/constants.mk new file mode 100644 index 00000000..01b5cb2a --- /dev/null +++ b/tests/containers/constants.mk @@ -0,0 +1,13 @@ +REPO_ROOT := $(shell git rev-parse --show-toplevel) + +include $(REPO_ROOT)/constants.mk + +# PLATFORM can be a comma separated list of platforms of the form linux/ +# it is passed to buildx, which will build for each one (this is important +# for pushing multi-arch images because `buildx --push` will push a manifest list +# containing all the relevant platforms) +PLATFORM ?= linux/amd64 + +# Path needs to be absolute so subdirectories don't fail when they include this +# file. +FACT_QA_TAG ?= $(shell cat "$(REPO_ROOT)/tests/containers/QA_TAG") diff --git a/tests/containers/editors/Containerfile b/tests/containers/editors/Containerfile index 47c91b43..3357f249 100644 --- a/tests/containers/editors/Containerfile +++ b/tests/containers/editors/Containerfile @@ -1,4 +1,4 @@ -FROM quay.io/fedora/fedora:43 +FROM quay.io/fedora/fedora:44 RUN dnf install -y \ neovim \ diff --git a/tests/containers/editors/Makefile b/tests/containers/editors/Makefile new file mode 100644 index 00000000..df1f655e --- /dev/null +++ b/tests/containers/editors/Makefile @@ -0,0 +1,19 @@ +include ../constants.mk + +FACT_QA_EDITORS_TAG := fact-editors-$(FACT_QA_TAG) + +all: build + +run: + $(DOCKER) buildx build $(BUILD_ARGS) --platform $(PLATFORM) \ + -t quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_EDITORS_TAG) \ + -f Containerfile \ + $(CURDIR) + +build: BUILD_ARGS = --load +build: run + +build-and-push: BUILD_ARGS = --push +build-and-push: run + +.PHONY: build build-and-push all run diff --git a/tests/containers/fedora/Containerfile b/tests/containers/fedora/Containerfile new file mode 100644 index 00000000..3ebbf5b2 --- /dev/null +++ b/tests/containers/fedora/Containerfile @@ -0,0 +1 @@ +FROM quay.io/fedora/fedora:44 diff --git a/tests/containers/fedora/Makefile b/tests/containers/fedora/Makefile new file mode 100644 index 00000000..c3eabd41 --- /dev/null +++ b/tests/containers/fedora/Makefile @@ -0,0 +1,19 @@ +include ../constants.mk + +FACT_QA_FEDORA_TAG := fact-fedora-$(FACT_QA_TAG) + +all: build + +run: + $(DOCKER) buildx build $(BUILD_ARGS) --platform $(PLATFORM) \ + -t quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_FEDORA_TAG) \ + -f Containerfile \ + $(CURDIR) + +build: BUILD_ARGS = --load +build: run + +build-and-push: BUILD_ARGS = --push +build-and-push: run + +.PHONY: build build-and-push all run diff --git a/tests/containers/self-deleter/Makefile b/tests/containers/self-deleter/Makefile new file mode 100644 index 00000000..74ed5009 --- /dev/null +++ b/tests/containers/self-deleter/Makefile @@ -0,0 +1,19 @@ +include ../constants.mk + +FACT_QA_SELF_DELETER_TAG := fact-self-deleter-$(FACT_QA_TAG) + +all: build + +run: + $(DOCKER) buildx build $(BUILD_ARGS) --platform $(PLATFORM) \ + -t quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_SELF_DELETER_TAG) \ + -f Containerfile \ + $(CURDIR) + +build: BUILD_ARGS = --load +build: run + +build-and-push: BUILD_ARGS = --push +build-and-push: run + +.PHONY: build build-and-push all run diff --git a/tests/test_editors/commons.py b/tests/test_editors/commons.py index 441cebf2..036d4aeb 100644 --- a/tests/test_editors/commons.py +++ b/tests/test_editors/commons.py @@ -3,23 +3,27 @@ import os import docker -import docker.models.containers import docker.models.images import pytest +from containers import pull_or_build + def get_vi_test_file(dir: str): return os.path.join(dir, '4913') @pytest.fixture(scope='session') -def build_editor_image(docker_client: docker.DockerClient): - image, _ = docker_client.images.build( +def build_editor_image( + request: pytest.FixtureRequest, docker_client: docker.DockerClient +): + no_local_builds = bool(request.config.getoption('--no-local-builds')) + return pull_or_build( + docker_client, + tag='fact-editors', path='containers/editors', - tag='editors:latest', - dockerfile='Containerfile', + no_local_builds=no_local_builds, ) - return image def run_editor_container( @@ -47,10 +51,28 @@ def run_editor_container( container.remove() +@pytest.fixture(scope='session') +def build_fedora_image( + request: pytest.FixtureRequest, docker_client: docker.DockerClient +) -> docker.models.images.Image: + no_local_builds = bool(request.config.getoption('--no-local-builds')) + return pull_or_build( + docker_client, + tag='fact-fedora', + path='containers/fedora', + no_local_builds=no_local_builds, + ) + + @pytest.fixture -def vi_container(docker_client: docker.DockerClient, ignored_dir: str): +def fedora_container( + docker_client: docker.DockerClient, + build_fedora_image: docker.models.images.Image, + ignored_dir: str, +): + image = build_fedora_image.tags[0] yield from run_editor_container( - 'quay.io/fedora/fedora:43', + image, docker_client, ignored_dir, ) diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index 7f823974..1af10ca0 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -9,18 +9,18 @@ def test_sed( - vi_container: docker.models.containers.Container, + fedora_container: docker.models.containers.Container, server: EventServer, ): - assert vi_container.id is not None + assert fedora_container.id is not None # File Under Test fut = '/mounted/test.txt' create_cmd = f'sh -c "echo \'This is a test\' > {fut}"' sed_cmd = rf'sed -i -e "s/a test/not \\0/" {fut}' - container_id = vi_container.id[:12] + container_id = fedora_container.id[:12] - vi_container.exec_run(create_cmd) - vi_container.exec_run(sed_cmd) + fedora_container.exec_run(create_cmd) + fedora_container.exec_run(sed_cmd) shell = Process.in_container( exe_path='/usr/bin/bash', @@ -72,18 +72,18 @@ def test_sed( def test_sed_ovfs( - vi_container: docker.models.containers.Container, + fedora_container: docker.models.containers.Container, server: EventServer, ): - assert vi_container.id is not None + assert fedora_container.id is not None # File Under Test fut = '/container-dir/test.txt' create_cmd = f'sh -c "echo \'This is a test\' > {fut}"' sed_cmd = rf'sed -i -e "s/a test/not \\0/" {fut}' - container_id = vi_container.id[:12] + container_id = fedora_container.id[:12] - vi_container.exec_run(create_cmd) - vi_container.exec_run(sed_cmd) + fedora_container.exec_run(create_cmd) + fedora_container.exec_run(sed_cmd) shell = Process.in_container( exe_path='/usr/bin/bash', diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index f834841f..c8aa55f0 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -8,10 +8,10 @@ def test_new_file( - vi_container: docker.models.containers.Container, + fedora_container: docker.models.containers.Container, server: EventServer, ): - assert vi_container.id is not None + assert fedora_container.id is not None fut = '/mounted/test.txt' swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' @@ -19,13 +19,13 @@ def test_new_file( cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" - vi_container.exec_run(cmd) + fedora_container.exec_run(cmd) process = Process.in_container( exe_path=exe, args=cmd, name='vi', - container_id=vi_container.id[:12], + container_id=fedora_container.id[:12], ) events = [ @@ -77,10 +77,10 @@ def test_new_file( def test_new_file_ovfs( - vi_container: docker.models.containers.Container, + fedora_container: docker.models.containers.Container, server: EventServer, ): - assert vi_container.id is not None + assert fedora_container.id is not None fut = '/container-dir/test.txt' swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' @@ -88,13 +88,13 @@ def test_new_file_ovfs( cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" - vi_container.exec_run(cmd) + fedora_container.exec_run(cmd) process = Process.in_container( exe_path=exe, args=cmd, name='vi', - container_id=vi_container.id[:12], + container_id=fedora_container.id[:12], ) events = [ @@ -146,23 +146,23 @@ def test_new_file_ovfs( def test_open_file( - vi_container: docker.models.containers.Container, + fedora_container: docker.models.containers.Container, server: EventServer, ): - assert vi_container.id is not None + assert fedora_container.id is not None fut = '/mounted/test.txt' fut_backup = f'{fut}~' swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') exe = '/usr/bin/vi' - container_id = vi_container.id[:12] + container_id = fedora_container.id[:12] cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" # We ensure the file exists before editing. - vi_container.exec_run(f'touch {fut}') - vi_container.exec_run(cmd) + fedora_container.exec_run(f'touch {fut}') + fedora_container.exec_run(cmd) touch_process = Process.in_container( exe_path='/usr/bin/touch', @@ -287,23 +287,23 @@ def test_open_file( def test_open_file_ovfs( - vi_container: docker.models.containers.Container, + fedora_container: docker.models.containers.Container, server: EventServer, ): - assert vi_container.id is not None + assert fedora_container.id is not None fut = '/container-dir/test.txt' fut_backup = f'{fut}~' swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' vi_test_file = get_vi_test_file('/container-dir') exe = '/usr/bin/vi' - container_id = vi_container.id[:12] + container_id = fedora_container.id[:12] cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" # We ensure the file exists before editing. - vi_container.exec_run(f'touch {fut}') - vi_container.exec_run(cmd) + fedora_container.exec_run(f'touch {fut}') + fedora_container.exec_run(cmd) touch_process = Process.in_container( exe_path='/usr/bin/touch', diff --git a/tests/test_misc.py b/tests/test_misc.py index ce275239..54b970c5 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -8,18 +8,22 @@ import pytest from conftest import dump_logs +from containers import pull_or_build from event import Event, EventType, Process from server import EventServer -@pytest.fixture -def build_self_deleter(docker_client: docker.DockerClient): - image, _ = docker_client.images.build( +@pytest.fixture(scope='session') +def build_self_deleter( + request: pytest.FixtureRequest, docker_client: docker.DockerClient +): + no_local_builds = bool(request.config.getoption('--no-local-builds')) + return pull_or_build( + docker_client, + tag='fact-self-deleter', path='containers/self-deleter', - tag='self-deleter:latest', - dockerfile='Containerfile', + no_local_builds=no_local_builds, ) - return image @pytest.fixture From 92fefc4d7f43ac3a573cd821ae89fb793a0cad5a Mon Sep 17 00:00:00 2001 From: ovalenti Date: Fri, 23 Oct 2026 10:45:12 +0200 Subject: [PATCH 2/3] ROX-37670: test container rebuilds happen during nightlies (#2209) * Never rebuild test containers on scheduled builds * Bump test container version * Use debian:trixie as the base distro for 'editors' * Use centos images + pin image versions and packages used This leads to a closer and (hopefully) more stable version of the existing fedora images we were previously using. --------- Co-authored-by: Mauro Ezequiel Moltrasio --- .github/workflows/test-containers-build.yml | 5 ++- tests/containers/QA_TAG | 2 +- tests/containers/base/Containerfile | 2 + tests/containers/{fedora => base}/Makefile | 4 +- tests/containers/editors/Containerfile | 12 ++++-- tests/containers/fedora/Containerfile | 1 - tests/test_editors/commons.py | 12 +++--- tests/test_editors/test_nvim.py | 10 +++-- tests/test_editors/test_sed.py | 20 ++++----- tests/test_editors/test_vi.py | 46 +++++++++++---------- tests/test_editors/test_vim.py | 10 +++-- 11 files changed, 68 insertions(+), 56 deletions(-) create mode 100644 tests/containers/base/Containerfile rename tests/containers/{fedora => base}/Makefile (71%) delete mode 100644 tests/containers/fedora/Containerfile diff --git a/.github/workflows/test-containers-build.yml b/.github/workflows/test-containers-build.yml index 1ae27a3a..87aa5a4b 100644 --- a/.github/workflows/test-containers-build.yml +++ b/.github/workflows/test-containers-build.yml @@ -11,8 +11,9 @@ jobs: name: Determine if tests image needs to be built runs-on: ubuntu-24.04 outputs: - build-image: ${{ (steps.changed.outputs.qa-tag == 'true' || - contains(github.event.pull_request.labels.*.name, 'rebuild-test-container')) }} + build-image: ${{ (github.event_name != 'schedule' && + (steps.changed.outputs.qa-tag == 'true' || + contains(github.event.pull_request.labels.*.name, 'rebuild-test-container'))) }} steps: - uses: actions/checkout@v7 diff --git a/tests/containers/QA_TAG b/tests/containers/QA_TAG index 6e8bf73a..0ea3a944 100644 --- a/tests/containers/QA_TAG +++ b/tests/containers/QA_TAG @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/tests/containers/base/Containerfile b/tests/containers/base/Containerfile new file mode 100644 index 00000000..102da78d --- /dev/null +++ b/tests/containers/base/Containerfile @@ -0,0 +1,2 @@ +# CentOS Stream 10 +FROM quay.io/centos/centos@sha256:6f8d618844ed5af4365bad24bea19fb0435fd5ec808201af9aeb19d7a28eb515 diff --git a/tests/containers/fedora/Makefile b/tests/containers/base/Makefile similarity index 71% rename from tests/containers/fedora/Makefile rename to tests/containers/base/Makefile index c3eabd41..f29828ed 100644 --- a/tests/containers/fedora/Makefile +++ b/tests/containers/base/Makefile @@ -1,12 +1,12 @@ include ../constants.mk -FACT_QA_FEDORA_TAG := fact-fedora-$(FACT_QA_TAG) +FACT_QA_BASE_TAG := fact-base-$(FACT_QA_TAG) all: build run: $(DOCKER) buildx build $(BUILD_ARGS) --platform $(PLATFORM) \ - -t quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_FEDORA_TAG) \ + -t quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_BASE_TAG) \ -f Containerfile \ $(CURDIR) diff --git a/tests/containers/editors/Containerfile b/tests/containers/editors/Containerfile index 3357f249..0c8529e1 100644 --- a/tests/containers/editors/Containerfile +++ b/tests/containers/editors/Containerfile @@ -1,5 +1,9 @@ -FROM quay.io/fedora/fedora:44 +# CentOS Stream 10 +FROM quay.io/centos/centos@sha256:6f8d618844ed5af4365bad24bea19fb0435fd5ec808201af9aeb19d7a28eb515 -RUN dnf install -y \ - neovim \ - vim +RUN dnf config-manager --set-enabled crb && \ + dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm && \ + dnf install -y \ + neovim-0.10.1-4.el10_0 \ + vim-enhanced-2:9.1.083-25.el10 && \ + dnf clean all diff --git a/tests/containers/fedora/Containerfile b/tests/containers/fedora/Containerfile deleted file mode 100644 index 3ebbf5b2..00000000 --- a/tests/containers/fedora/Containerfile +++ /dev/null @@ -1 +0,0 @@ -FROM quay.io/fedora/fedora:44 diff --git a/tests/test_editors/commons.py b/tests/test_editors/commons.py index 036d4aeb..622891cc 100644 --- a/tests/test_editors/commons.py +++ b/tests/test_editors/commons.py @@ -52,25 +52,25 @@ def run_editor_container( @pytest.fixture(scope='session') -def build_fedora_image( +def build_base_image( request: pytest.FixtureRequest, docker_client: docker.DockerClient ) -> docker.models.images.Image: no_local_builds = bool(request.config.getoption('--no-local-builds')) return pull_or_build( docker_client, - tag='fact-fedora', - path='containers/fedora', + tag='fact-base', + path='containers/base', no_local_builds=no_local_builds, ) @pytest.fixture -def fedora_container( +def base_container( docker_client: docker.DockerClient, - build_fedora_image: docker.models.images.Image, + build_base_image: docker.models.images.Image, ignored_dir: str, ): - image = build_fedora_image.tags[0] + image = build_base_image.tags[0] yield from run_editor_container( image, docker_client, diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index d2217440..69812b1d 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -50,8 +50,9 @@ def test_open_file( editor_container.exec_run(cmd) touch = Process.in_container( - exe_path='/usr/bin/touch', - args=f'touch {fut}', + exe_path='/usr/bin/coreutils', + args="/usr/bin/coreutils '--coreutils-prog-shebang=touch' " + + f'/usr/bin/touch {fut}', name='touch', container_id=container_id, ) @@ -166,8 +167,9 @@ def test_open_file_ovfs( editor_container.exec_run(cmd) touch = Process.in_container( - exe_path='/usr/bin/touch', - args=f'touch {fut}', + exe_path='/usr/bin/coreutils', + args="/usr/bin/coreutils '--coreutils-prog-shebang=touch' " + + f'/usr/bin/touch {fut}', name='touch', container_id=container_id, ) diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index 1af10ca0..9ce033db 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -9,18 +9,18 @@ def test_sed( - fedora_container: docker.models.containers.Container, + base_container: docker.models.containers.Container, server: EventServer, ): - assert fedora_container.id is not None + assert base_container.id is not None # File Under Test fut = '/mounted/test.txt' create_cmd = f'sh -c "echo \'This is a test\' > {fut}"' sed_cmd = rf'sed -i -e "s/a test/not \\0/" {fut}' - container_id = fedora_container.id[:12] + container_id = base_container.id[:12] - fedora_container.exec_run(create_cmd) - fedora_container.exec_run(sed_cmd) + base_container.exec_run(create_cmd) + base_container.exec_run(sed_cmd) shell = Process.in_container( exe_path='/usr/bin/bash', @@ -72,18 +72,18 @@ def test_sed( def test_sed_ovfs( - fedora_container: docker.models.containers.Container, + base_container: docker.models.containers.Container, server: EventServer, ): - assert fedora_container.id is not None + assert base_container.id is not None # File Under Test fut = '/container-dir/test.txt' create_cmd = f'sh -c "echo \'This is a test\' > {fut}"' sed_cmd = rf'sed -i -e "s/a test/not \\0/" {fut}' - container_id = fedora_container.id[:12] + container_id = base_container.id[:12] - fedora_container.exec_run(create_cmd) - fedora_container.exec_run(sed_cmd) + base_container.exec_run(create_cmd) + base_container.exec_run(sed_cmd) shell = Process.in_container( exe_path='/usr/bin/bash', diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index c8aa55f0..cf6615d2 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -8,10 +8,10 @@ def test_new_file( - fedora_container: docker.models.containers.Container, + base_container: docker.models.containers.Container, server: EventServer, ): - assert fedora_container.id is not None + assert base_container.id is not None fut = '/mounted/test.txt' swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' @@ -19,13 +19,13 @@ def test_new_file( cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" - fedora_container.exec_run(cmd) + base_container.exec_run(cmd) process = Process.in_container( exe_path=exe, args=cmd, name='vi', - container_id=fedora_container.id[:12], + container_id=base_container.id[:12], ) events = [ @@ -77,10 +77,10 @@ def test_new_file( def test_new_file_ovfs( - fedora_container: docker.models.containers.Container, + base_container: docker.models.containers.Container, server: EventServer, ): - assert fedora_container.id is not None + assert base_container.id is not None fut = '/container-dir/test.txt' swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' @@ -88,13 +88,13 @@ def test_new_file_ovfs( cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" - fedora_container.exec_run(cmd) + base_container.exec_run(cmd) process = Process.in_container( exe_path=exe, args=cmd, name='vi', - container_id=fedora_container.id[:12], + container_id=base_container.id[:12], ) events = [ @@ -146,27 +146,28 @@ def test_new_file_ovfs( def test_open_file( - fedora_container: docker.models.containers.Container, + base_container: docker.models.containers.Container, server: EventServer, ): - assert fedora_container.id is not None + assert base_container.id is not None fut = '/mounted/test.txt' fut_backup = f'{fut}~' swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') exe = '/usr/bin/vi' - container_id = fedora_container.id[:12] + container_id = base_container.id[:12] cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" # We ensure the file exists before editing. - fedora_container.exec_run(f'touch {fut}') - fedora_container.exec_run(cmd) + base_container.exec_run(f'touch {fut}') + base_container.exec_run(cmd) touch_process = Process.in_container( - exe_path='/usr/bin/touch', - args=f'touch {fut}', + exe_path='/usr/bin/coreutils', + args="/usr/bin/coreutils '--coreutils-prog-shebang=touch' " + + f'/usr/bin/touch {fut}', name='touch', container_id=container_id, ) @@ -287,27 +288,28 @@ def test_open_file( def test_open_file_ovfs( - fedora_container: docker.models.containers.Container, + base_container: docker.models.containers.Container, server: EventServer, ): - assert fedora_container.id is not None + assert base_container.id is not None fut = '/container-dir/test.txt' fut_backup = f'{fut}~' swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' vi_test_file = get_vi_test_file('/container-dir') exe = '/usr/bin/vi' - container_id = fedora_container.id[:12] + container_id = base_container.id[:12] cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" # We ensure the file exists before editing. - fedora_container.exec_run(f'touch {fut}') - fedora_container.exec_run(cmd) + base_container.exec_run(f'touch {fut}') + base_container.exec_run(cmd) touch_process = Process.in_container( - exe_path='/usr/bin/touch', - args=f'touch {fut}', + exe_path='/usr/bin/coreutils', + args="/usr/bin/coreutils '--coreutils-prog-shebang=touch' " + + f'/usr/bin/touch {fut}', name='touch', container_id=container_id, ) diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index 57408256..5a6cd01c 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -160,8 +160,9 @@ def test_open_file( editor_container.exec_run(cmd) touch_process = Process.in_container( - exe_path='/usr/bin/touch', - args=f'touch {fut}', + exe_path='/usr/bin/coreutils', + args="/usr/bin/coreutils '--coreutils-prog-shebang=touch' " + + f'/usr/bin/touch {fut}', name='touch', container_id=container_id, ) @@ -300,8 +301,9 @@ def test_open_file_ovfs( editor_container.exec_run(cmd) touch_process = Process.in_container( - exe_path='/usr/bin/touch', - args=f'touch {fut}', + exe_path='/usr/bin/coreutils', + args="/usr/bin/coreutils '--coreutils-prog-shebang=touch' " + + f'/usr/bin/touch {fut}', name='touch', container_id=container_id, ) From 8fb6ca88ebfd815926df64025c7bc57041028899 Mon Sep 17 00:00:00 2001 From: Mauro Ezequiel Moltrasio Date: Mon, 26 Oct 2026 11:43:57 +0100 Subject: [PATCH 3/3] chore: validate test images don't exist on remote before building (#2214) * chore: validate test images don't exist on remote before building This is a follow up to #2209, we need to backport those changes and we need to ensure release branches will not accidentally overwrite images built in the main branch. A new `exists` make target is added to all test containers and a global check ensures at least one of the images is missing before triggering the rebuild. On normal operation, new versions of the images will be built by the main branch and be available once the QA_TAG bump is backported to release branches. In this case the paths-filter action will determine a rebuild is needed, but the new check will skip building the images. If the images are in the process of being built on the main branch and some of them are not yet pushed, the backported commit will push the images again but they will have been built in such a small span of time the resulting images will pretty much be the same. On PRs, if the containers need to be rebuilt a unique tag is generated and the step will always return the images don't exist. * Global FACT_QA_TAG and portable redirections * Don't conflate network/registry errors with manifest does not exist errors --- .github/workflows/test-containers-build.yml | 43 ++++++++++++++++++++- tests/containers/Makefile | 2 +- tests/containers/base/Makefile | 5 ++- tests/containers/editors/Makefile | 5 ++- tests/containers/self-deleter/Makefile | 5 ++- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-containers-build.yml b/.github/workflows/test-containers-build.yml index 87aa5a4b..e5ebb128 100644 --- a/.github/workflows/test-containers-build.yml +++ b/.github/workflows/test-containers-build.yml @@ -60,6 +60,8 @@ jobs: - should-build-test-containers if: needs.should-build-test-containers.outputs.build-image == 'true' || contains(github.event.pull_request.labels.*.name, 'rebuild-test-container') + env: + FACT_QA_TAG: ${{ needs.determine-qa-tag.outputs.qa-tag }} steps: - uses: actions/checkout@v7 @@ -77,9 +79,48 @@ jobs: username: ${{ secrets.QUAY_RHACS_ENG_RW_USERNAME }} password: ${{ secrets.QUAY_RHACS_ENG_RW_PASSWORD }} + - name: Check images already exist + id: images-exist + run: | + manifest_exists() { + local image=$1 output + if output=$(docker manifest inspect "${image}" 2>&1); then + return 0 + elif echo "${output}" | grep -Eiq 'no such manifest:|manifest unknown'; then + return 1 + else + printf '%s: %s\n' "${image}" "${output}" >&2 + return 2 + fi + } + + image_names=$(make --no-print-directory -s \ + -C "${GITHUB_WORKSPACE}/tests/containers" image-names) || exit $? + + EXIST=true + while IFS= read -r image_name; do + [[ -z "${image_name}" ]] && continue + if manifest_exists "${image_name}"; then + continue + else + status=$? + fi + + if [[ "${status}" -eq 1 ]]; then + EXIST=false + else + exit "${status}" + fi + done <<< "${image_names}" + + if [[ "${EXIST}" == false ]]; then + echo "::notice::At least one test container manifest is absent" + fi + echo "exist=${EXIST}" >> "${GITHUB_OUTPUT}" + - name: Build and Push containers + if: steps.images-exist.outputs.exist == 'false' || contains(github.event.pull_request.labels.*.name, 'rebuild-test-container') env: - FACT_QA_TAG: ${{ needs.determine-qa-tag.outputs.qa-tag }} PLATFORM: linux/amd64,linux/arm64 run: | make -C "${GITHUB_WORKSPACE}/tests/containers" build-and-push diff --git a/tests/containers/Makefile b/tests/containers/Makefile index 4eb28d59..3998bc20 100644 --- a/tests/containers/Makefile +++ b/tests/containers/Makefile @@ -1,6 +1,6 @@ include constants.mk -TARGETS := build build-and-push +TARGETS := build build-and-push image-names DIRS := $(wildcard ./*/) $(TARGETS): $(DIRS) diff --git a/tests/containers/base/Makefile b/tests/containers/base/Makefile index f29828ed..9065f26a 100644 --- a/tests/containers/base/Makefile +++ b/tests/containers/base/Makefile @@ -16,4 +16,7 @@ build: run build-and-push: BUILD_ARGS = --push build-and-push: run -.PHONY: build build-and-push all run +image-names: + @echo quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_BASE_TAG) + +.PHONY: build build-and-push all run image-names diff --git a/tests/containers/editors/Makefile b/tests/containers/editors/Makefile index df1f655e..384d30d6 100644 --- a/tests/containers/editors/Makefile +++ b/tests/containers/editors/Makefile @@ -16,4 +16,7 @@ build: run build-and-push: BUILD_ARGS = --push build-and-push: run -.PHONY: build build-and-push all run +image-names: + @echo quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_EDITORS_TAG) + +.PHONY: build build-and-push all run image-names diff --git a/tests/containers/self-deleter/Makefile b/tests/containers/self-deleter/Makefile index 74ed5009..0a0fd6c7 100644 --- a/tests/containers/self-deleter/Makefile +++ b/tests/containers/self-deleter/Makefile @@ -16,4 +16,7 @@ build: run build-and-push: BUILD_ARGS = --push build-and-push: run -.PHONY: build build-and-push all run +image-names: + @echo quay.io/rhacs-eng/qa-multi-arch:$(FACT_QA_SELF_DELETER_TAG) + +.PHONY: build build-and-push all run image-names