diff --git a/.env.sample b/.env.sample index 049a7b02a..67777f5f6 100644 --- a/.env.sample +++ b/.env.sample @@ -98,3 +98,8 @@ PIXL_MAX_MESSAGES_IN_FLIGHT=100 # Project configs directory PROJECT_CONFIGS_DIR=projects/configs + +# user and group IDs for pixl:pixl. +# Only needed in production at build time +PIXL_USER_UID=7001 +PIXL_USER_GID=7001 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2b765b841..2e3495b52 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -186,30 +186,6 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - # pre-build and cache the orthanc-anon image: installing python3 takes a while, doesn't push - - name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.ref }}-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-${{ github.ref }}- - ${{ runner.os }}-buildx- - save-always: true - - uses: docker/build-push-action@v5 - with: - context: . - file: docker/orthanc-anon/Dockerfile - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - - # Temp fix - # https://github.com/docker/build-push-action/issues/252 - # https://github.com/moby/buildkit/issues/1896 - name: Move cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache - name: Init Python uses: actions/setup-python@v5 @@ -228,15 +204,6 @@ jobs: - name: Create .secrets.env run: touch .secrets.env - - name: Build test services - working-directory: test - run: | - docker compose build - - - name: Build services - run: | - docker compose build - - name: Run tests working-directory: test env: @@ -245,7 +212,20 @@ jobs: EXPORT_AZ_TENANT_ID: ${{ secrets.EXPORT_AZ_TENANT_ID }} EXPORT_AZ_KEY_VAULT_NAME: ${{ secrets.EXPORT_AZ_KEY_VAULT_NAME }} run: | - ./run-system-test.sh + + # These two variables are needed for creating the pixl user and group on the GHA runner. + # Don't worry about passing them into pytest if you're running pytest manually or through your + # IDE - they're only used inside an "if GITHUB_ACTIONS" block anyway. + # We could do this in the setup function, but we don't at that point know the location of the + # exports dir (how the exports dir is determined is due to change anyway) + source .env + export PIXL_USER_UID PIXL_USER_GID + sudo --non-interactive groupadd --gid $PIXL_USER_GID pixl + sudo --non-interactive useradd --uid $PIXL_USER_UID --gid pixl --groups docker pixl + sudo --non-interactive chown -R pixl:docker host_export_root_dir + sudo --non-interactive chmod -R ug+rwx host_export_root_dir + + sudo -u pixl -g pixl ./run-system-test.sh echo FINISHED SYSTEM TEST SCRIPT - name: Dump ehr-api docker logs for debugging diff --git a/docker-compose.yml b/docker-compose.yml index 54c8493e1..138a34c62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,9 +26,12 @@ x-proxy-common: &proxy-common https_proxy: *https-proxy NO_PROXY: *no-proxy no_proxy: *no-proxy +x-pixl-uid-gid: &pixl-uid-gid + PIXL_USER_UID: ${PIXL_USER_UID} + PIXL_USER_GID: ${PIXL_USER_GID} x-build-args-common: &build-args-common - <<: [*proxy-common] + <<: [*proxy-common, *pixl-uid-gid] x-pixl-common-env: &pixl-common-env ENV: ${ENV} @@ -100,6 +103,7 @@ services: - *logs-volume networks: - pixl-net + user: pixl healthcheck: test: ["CMD", "curl", "-f", "http://hasher-api:8000/heart-beat"] interval: 10s @@ -278,6 +282,7 @@ services: retries: 5 networks: - pixl-net + user: pixl # needed for testing under GHA (linux), so this container # can reach the test FTP server running on the docker host extra_hosts: @@ -304,6 +309,7 @@ services: retries: 5 networks: - pixl-net + user: pixl environment: <<: [*pixl-rabbit-mq, *proxy-common] ORTHANC_RAW_USERNAME: ${ORTHANC_RAW_USERNAME} diff --git a/docker/ehr-api/Dockerfile b/docker/ehr-api/Dockerfile index b032e71ad..3cf70ebf7 100644 --- a/docker/ehr-api/Dockerfile +++ b/docker/ehr-api/Dockerfile @@ -44,6 +44,10 @@ RUN --mount=type=cache,target=/root/.cache \ pip install core/ . && \ if [ "$TEST" = "true" ]; then pip install core/[test] .[test]; fi +ARG PIXL_USER_UID +ARG PIXL_USER_GID +RUN groupadd --gid $PIXL_USER_GID pixl && useradd --uid $PIXL_USER_UID --gid $PIXL_USER_GID pixl + HEALTHCHECK CMD /usr/bin/curl -f http://0.0.0.0:8000/heart-beat || exit 1 ENTRYPOINT ["uvicorn", "pixl_ehr.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker/hasher-api/Dockerfile b/docker/hasher-api/Dockerfile index af1305c68..c958b5904 100644 --- a/docker/hasher-api/Dockerfile +++ b/docker/hasher-api/Dockerfile @@ -24,6 +24,9 @@ RUN sed -i '/en_GB.UTF-8/s/^# //g' /etc/locale.gen && locale-gen # clean up RUN apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* +ARG PIXL_USER_UID +ARG PIXL_USER_GID +RUN groupadd --gid $PIXL_USER_GID pixl && useradd --uid $PIXL_USER_UID --gid $PIXL_USER_GID pixl WORKDIR /app # Install requirements before copying modules COPY ./hasher/pyproject.toml . @@ -34,4 +37,5 @@ COPY ./hasher/ . RUN --mount=type=cache,target=/root/.cache \ pip install . +RUN mkdir /logs && chown pixl:pixl /logs ENTRYPOINT ["uvicorn", "hasher.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker/imaging-api/Dockerfile b/docker/imaging-api/Dockerfile index 412284cd2..ed8460194 100644 --- a/docker/imaging-api/Dockerfile +++ b/docker/imaging-api/Dockerfile @@ -44,4 +44,8 @@ RUN --mount=type=cache,target=/root/.cache \ pip install core/ . && \ if [ "$TEST" = "true" ]; then pip install core/ .[test]; fi +ARG PIXL_USER_UID +ARG PIXL_USER_GID +RUN groupadd --gid $PIXL_USER_GID pixl && useradd --uid $PIXL_USER_UID --gid $PIXL_USER_GID pixl + ENTRYPOINT ["/app/scripts/migrate_and_run.sh"] diff --git a/docker/orthanc-anon/Dockerfile b/docker/orthanc-anon/Dockerfile index d918337da..62d9308b7 100644 --- a/docker/orthanc-anon/Dockerfile +++ b/docker/orthanc-anon/Dockerfile @@ -24,6 +24,11 @@ RUN python3 -m venv /.venv # Install curl, used in system tests RUN apt-get --assume-yes install curl +ARG PIXL_USER_UID +ARG PIXL_USER_GID +RUN echo JES $PIXL_USER_GID $PIXL_USER_UID JES WTF +RUN groupadd --gid $PIXL_USER_GID pixl && useradd --uid $PIXL_USER_UID --gid $PIXL_USER_GID pixl + # Install requirements before copying modules COPY ./pixl_core/pyproject.toml /pixl_core/pyproject.toml COPY ./pixl_dcmd/pyproject.toml /pixl_dcmd/pyproject.toml diff --git a/docker/orthanc-raw/Dockerfile b/docker/orthanc-raw/Dockerfile index 801abb400..9c136c5df 100644 --- a/docker/orthanc-raw/Dockerfile +++ b/docker/orthanc-raw/Dockerfile @@ -21,6 +21,11 @@ ARG ORTHANC_RAW_MAXIMUM_STORAGE_SIZE RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get update && \ apt-get install --yes --no-install-recommends python3-venv + +ARG PIXL_USER_UID +ARG PIXL_USER_GID +RUN groupadd --gid $PIXL_USER_GID pixl && useradd --uid $PIXL_USER_UID --gid $PIXL_USER_GID pixl + RUN python3 -m venv /.venv # Install requirements before copying modules diff --git a/pixl_imaging/tests/docker-compose.yml b/pixl_imaging/tests/docker-compose.yml index e5de17927..dd8c7cf2e 100644 --- a/pixl_imaging/tests/docker-compose.yml +++ b/pixl_imaging/tests/docker-compose.yml @@ -13,6 +13,10 @@ # limitations under the License. version: "3.8" +x-pixl-uid-gid: &pixl-uid-gid + PIXL_USER_UID: 7001 + PIXL_USER_GID: 7001 + volumes: orthanc-raw-data: vna-qr-data: @@ -26,6 +30,7 @@ services: context: ../../ dockerfile: ./docker/imaging-api/Dockerfile args: + <<: *pixl-uid-gid TEST: true depends_on: queue: @@ -76,6 +81,8 @@ services: build: context: ../../ dockerfile: ./docker/orthanc-raw/Dockerfile + args: + <<: *pixl-uid-gid environment: ORTHANC_NAME: "PIXL: Raw" ORTHANC_USERNAME: "orthanc" diff --git a/test/.env b/test/.env index 12d8484b5..79cfe6b83 100644 --- a/test/.env +++ b/test/.env @@ -82,3 +82,8 @@ FTP_USER_PASSWORD=longpassword # Project configs directory PROJECT_CONFIGS_DIR=projects/configs + +# user and group IDs for pixl:pixl that will be created during image +# builds and on the GHA runner during the system test +PIXL_USER_UID=7001 +PIXL_USER_GID=7001 diff --git a/test/conftest.py b/test/conftest.py index a266d4055..169e66065 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. """System/E2E test setup""" +import os +import subprocess from pathlib import Path import pytest @@ -22,7 +24,7 @@ pytest_plugins = "pytest_pixl" -@pytest.fixture() +@pytest.fixture(scope="session") def host_export_root_dir(): """Intermediate export dir as seen from the host""" return Path(__file__).parents[1] / "projects" / "exports" @@ -32,10 +34,58 @@ def host_export_root_dir(): RESOURCES_DIR = TEST_DIR / "resources" RESOURCES_OMOP_DIR = RESOURCES_DIR / "omop" +PIXL_USER_UID = os.getenv("PIXL_USER_UID") +PIXL_USER_GID = os.getenv("PIXL_USER_GID") + @pytest.fixture(scope="session") -def _setup_pixl_cli(ftps_server) -> None: +def _setup_pixl_cli(ftps_server, host_export_root_dir) -> None: """Run pixl populate/start. Cleanup intermediate export dir on exit.""" + # see if it's already created + try: + print("JES: trying first ls -laR") + run_subprocess(["ls", "-laR", host_export_root_dir.parents[0]]) + except subprocess.CalledProcessError: + try: + print("JES: trying second ls -laR") + run_subprocess(["ls", "-laR", host_export_root_dir.parents[1]]) + except subprocess.CalledProcessError: + pass + + # In production, it will be documented that the exports directory must be writable + # by the "pixl" user. For the system test we must do it here. + if os.getenv("GITHUB_ACTIONS") == "true": + # run_subprocess( + # [ + # "sudo", + # "--non-interactive", + # "groupadd", + # "--gid", + # str(PIXL_USER_GID), + # "pixl", + # ] + # ) + # run_subprocess( + # [ + # "sudo", + # "--non-interactive", + # "useradd", + # "--uid", + # str(PIXL_USER_UID), + # "--gid", + # str(PIXL_USER_GID), + # "pixl", + # ] + # ) + # this directory is part of the git repo so will already exist + run_subprocess( + ["sudo", "--non-interactive", "chown", "-R", "pixl:pixl", host_export_root_dir] + ) + print("JES: after create user pixl") + run_subprocess(["cat", "/etc/passwd"]) + + print("JES: afterwards ls -laR") + run_subprocess(["ls", "-laR", host_export_root_dir.parents[0]]) # CLI calls need to have CWD = test dir so they can find the pixl_config.yml file run_subprocess(["pixl", "populate", str(RESOURCES_OMOP_DIR.absolute())], TEST_DIR) # poll here for two minutes to check for imaging to be processed, printing progress diff --git a/test/docker-compose.yml b/test/docker-compose.yml index 85b279280..b244e6318 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -13,6 +13,10 @@ # limitations under the License. version: "3.8" +x-pixl-uid-gid: &pixl-uid-gid + PIXL_USER_UID: ${PIXL_USER_UID} + PIXL_USER_GID: ${PIXL_USER_GID} + volumes: vna-qr-data: @@ -72,6 +76,8 @@ services: cogstack-api: container_name: system-test-cogstack build: + args: + <<: *pixl-uid-gid context: ./dummy-services/cogstack/ healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/heart-beat"] @@ -80,3 +86,4 @@ services: retries: 5 networks: pixl-net: + user: pixl diff --git a/test/dummy-services/cogstack/Dockerfile b/test/dummy-services/cogstack/Dockerfile index 2caf85e37..d785574ec 100644 --- a/test/dummy-services/cogstack/Dockerfile +++ b/test/dummy-services/cogstack/Dockerfile @@ -23,6 +23,9 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ gnupg \ locales +ARG PIXL_USER_UID +ARG PIXL_USER_GID +RUN groupadd --gid $PIXL_USER_GID pixl && useradd --uid $PIXL_USER_UID --gid $PIXL_USER_GID pixl WORKDIR /app COPY cogstack.py . diff --git a/test/run-system-test.sh b/test/run-system-test.sh index f623e0d74..33c18d630 100755 --- a/test/run-system-test.sh +++ b/test/run-system-test.sh @@ -48,7 +48,16 @@ elif [ "$subcmd" = "teardown" ]; then teardown else setup + + # These two variables are needed for creating the pixl user and group on the GHA runner. + # Don't worry about passing them into pytest if you're running pytest manually or through your + # IDE - they're only used inside an "if GITHUB_ACTIONS" block anyway. + # We could do this in the setup function, but we don't at that point know the location of the + # exports dir (how the exports dir is determined is due to change anyway) +# source .env +# export PIXL_USER_UID PIXL_USER_GID pytest --verbose --log-cli-level INFO + echo FINISHED PYTEST COMMAND teardown echo SYSTEM TEST SUCCESSFUL