diff --git a/README.rst b/README.rst index 7b17a0999..ae4901d40 100644 --- a/README.rst +++ b/README.rst @@ -76,10 +76,10 @@ By default, an exception will be raised if any requests to unmocked addresses ar .. _httpx: https://pypi.org/project/httpx/ .. _HTTPX2: https://httpx2.pydantic.dev/ -Using Docker to mock calls to Vuforia from any language -------------------------------------------------------- +Using containers to mock calls to Vuforia from any language +----------------------------------------------------------- -It is possible run a Mock VWS instance using Docker containers. +You can run a Mock VWS instance using Docker or Apple's ``container`` CLI. This allows you to run tests against a mock VWS instance regardless of the language or tooling you are using. diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 24342e1df..90266b1c2 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -130,6 +130,52 @@ Give an option once per backend or marker to skip. .. _pytest-multi-backend: https://adamtheturtle.github.io/pytest-multi-backend/ +Container deployment tests +-------------------------- + +The deployment tests build and run three separate services through the Docker Python SDK. +They use the Docker engine selected by the current Docker configuration by default: + +.. code-block:: console + + $ uv run --group=dev pytest tests/mock_vws/test_docker.py + +.. _socktainer-setup: + +To run the same tests with Apple's container runtime, complete the runtime installation steps in :ref:`apple-container-setup`, then install `Socktainer`_ 1.5.1 or later. +It provides the Docker-compatible API used by the Python deployment tests: + +.. code-block:: console + + $ brew install socktainer + $ socktainer --no-auto-start --no-docker-context + +Keep Socktainer running in that terminal. +In another terminal, select its socket and API version for the test command: + +.. code-block:: console + + $ DOCKER_HOST="unix://$HOME/.socktainer/container.sock" \ + DOCKER_API_VERSION=1.51 \ + uv run --group=dev pytest tests/mock_vws/test_docker.py + +Socktainer 1.5.1 requires the explicit API version because its automatic version negotiation is incompatible with the Docker Python SDK. +This is tracked in `Socktainer issue 433`_. +The deployment tests automatically negotiate the API version when ``DOCKER_API_VERSION`` is unset. + +Both configurations exercise native health checks, published HTTP ports, application behavior and restarts. +Running the mock directly with Apple's CLI uses the same images and is documented in :ref:`apple-container-setup`. +The tests bind their published ports to loopback explicitly because Socktainer 1.5.1 ignores automatic port publishing. +See `Socktainer issue 434`_. +Test containers, image tags and networks are removed after the run, including when setup fails. +Hosted CI continues to use Docker. +Apple deployment validation runs on a local Mac. + +.. _Socktainer: https://github.com/socktainer/socktainer +.. _Socktainer issue 433: https://github.com/socktainer/socktainer/issues/433 +.. _Socktainer issue 434: https://github.com/socktainer/socktainer/issues/434 + + Verifying signed Model Target requests -------------------------------------- diff --git a/docs/source/docker.rst b/docs/source/docker.rst index 9bcece73e..4ca54910c 100644 --- a/docs/source/docker.rst +++ b/docs/source/docker.rst @@ -1,7 +1,8 @@ -Running a server with Docker -============================ +Running a server in containers +============================== -It is possible run a Mock VWS instance using Docker containers. +You can run a Mock VWS instance using Docker or Apple's `container`_ CLI. +Both use the same published images. This allows you to run tests against a mock VWS instance regardless of the language or tooling you are using. @@ -17,8 +18,11 @@ The VWS and VWQ containers must point to the target manager container using the .. _creating-containers: -Creating containers -^^^^^^^^^^^^^^^^^^^ +Creating containers with Docker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Docker users can run the commands below directly. +For a deployment using Apple's CLI directly, follow :ref:`apple-container-setup`. .. code-block:: console @@ -48,6 +52,78 @@ Set it to the URL clients use to reach the published VWS port. For clients on another machine, replace ``http://127.0.0.1:5006`` with an address those clients can reach. +.. _apple-container-setup: + +Creating containers with Apple container +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This setup requires a Mac with Apple silicon and macOS 26 or later, Apple's ``container`` CLI 1.5.0 or later, and ``jq`` to read the container's address. +Install the runtime, start its services, and install the recommended Linux kernel: + +.. code-block:: console + + $ brew install container jq + $ container system start --disable-kernel-install + $ container system kernel set --recommended + +Allow Local Network access for ``container-runtime-linux`` when macOS prompts. +If requests to a published port reset, check this permission in System Settings → Privacy & Security → Local Network, then restart the container services. + +Create a network and start the target manager first. +Read its IP address on that network for the VWS and VWQ services to use: + +.. code-block:: console + + $ container network create vws-bridge-network + $ container run \ + --detach \ + --publish 127.0.0.1:5005:5000 \ + --name vuforia-target-manager-mock \ + --network vws-bridge-network \ + ghcr.io/vws-python/vuforia-target-manager-mock + $ TARGET_MANAGER_IP="$(container inspect vuforia-target-manager-mock | jq -er '.[0].status.networks[0].ipv4Address | split("/")[0]')" + $ export TARGET_MANAGER_BASE_URL="http://$TARGET_MANAGER_IP:5000" + $ container run \ + --detach \ + --publish 127.0.0.1:5006:5000 \ + --name vuforia-vws-mock \ + --env "TARGET_MANAGER_BASE_URL=$TARGET_MANAGER_BASE_URL" \ + --env VWS_BASE_URL=http://127.0.0.1:5006 \ + --network vws-bridge-network \ + ghcr.io/vws-python/vuforia-vws-mock + $ container run \ + --detach \ + --publish 127.0.0.1:5007:5000 \ + --name vuforia-vwq-mock \ + --env "TARGET_MANAGER_BASE_URL=$TARGET_MANAGER_BASE_URL" \ + --network vws-bridge-network \ + ghcr.io/vws-python/vuforia-vwq-mock + +Using the target manager's IP avoids requiring DNS configuration for container names on this network. +The IP lookup removes the subnet suffix returned by ``container inspect``. +The scheme and container port are fixed, so the URL is constructed separately. +If you recreate the target manager, read its new address and recreate VWS and VWQ with the updated ``TARGET_MANAGER_BASE_URL``. +The host ports match the Docker example, so the HTTP examples below work with either deployment. +``VWS_BASE_URL`` is the URL clients use to reach VWS and download recognition reports. + +Run each readiness probe until it exits successfully: + +.. code-block:: console + + $ container exec vuforia-target-manager-mock python /app/src/mock_vws/_flask_server/healthcheck.py + $ container exec vuforia-vws-mock python /app/src/mock_vws/_flask_server/healthcheck.py + $ container exec vuforia-vwq-mock python /app/src/mock_vws/_flask_server/healthcheck.py + +To stop and remove this deployment: + +.. code-block:: console + + $ container rm --force vuforia-vwq-mock vuforia-vws-mock vuforia-target-manager-mock + $ container network rm vws-bridge-network + +.. _container: https://github.com/apple/container + + Adding a database to the mock target manager -------------------------------------------- @@ -61,7 +137,7 @@ To add a database, make a request to the following endpoint against the target m .. autoflask:: mock_vws._flask_server.target_manager:TARGET_MANAGER_FLASK_APP :endpoints: create_cloud_database -For example, with the containers set up as in :ref:`creating-containers`, use ``curl``: +For example, with either deployment above, use ``curl``: .. code-block:: console @@ -200,6 +276,8 @@ VWS container Building images from source ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +These commands build the images with ``docker buildx``. + .. code-block:: console $ export REPOSITORY_ROOT="$PWD" @@ -212,3 +290,11 @@ Building images from source $ docker buildx build "$REPOSITORY_ROOT" --file "$DOCKERFILE" --target target-manager --tag "$TARGET_MANAGER_TAG" $ docker buildx build "$REPOSITORY_ROOT" --file "$DOCKERFILE" --target vws --tag "$VWS_TAG" $ docker buildx build "$REPOSITORY_ROOT" --file "$DOCKERFILE" --target vwq --tag "$VWQ_TAG" + +To build the same ``Dockerfile`` stages with Apple's CLI, set the variables above and run: + +.. code-block:: console + + $ container build "$REPOSITORY_ROOT" --file "$DOCKERFILE" --target target-manager --tag "$TARGET_MANAGER_TAG" + $ container build "$REPOSITORY_ROOT" --file "$DOCKERFILE" --target vws --tag "$VWS_TAG" + $ container build "$REPOSITORY_ROOT" --file "$DOCKERFILE" --target vwq --tag "$VWQ_TAG" diff --git a/docs/source/index.rst b/docs/source/index.rst index bee921f05..450be21de 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,10 +16,10 @@ This requires Python |minimum-python-version|\+. .. include:: httpx2-example.rst -Using Docker to mock calls to Vuforia from any language -------------------------------------------------------- +Using containers to mock calls to Vuforia from any language +----------------------------------------------------------- -It is possible run a Mock VWS instance using Docker containers. +You can run a Mock VWS instance using Docker or Apple's ``container`` CLI. This allows you to run tests against a mock VWS instance regardless of the language or tooling you are using. diff --git a/newsfragments/+apple-container.change.md b/newsfragments/+apple-container.change.md new file mode 100644 index 000000000..964820c74 --- /dev/null +++ b/newsfragments/+apple-container.change.md @@ -0,0 +1,2 @@ +- Document running and building the mock directly with Apple's container CLI using the same images as Docker. + Support the existing deployment tests through Socktainer's optional Docker API, allow an explicit API version, and bind all test service ports to loopback. diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index d8ce4feb8..547d6d53a 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -7,6 +7,7 @@ MyST OAuth Pydantic Reco +Socktainer Towncrier Ubuntu VuMark @@ -73,6 +74,7 @@ learnings linters linting login +loopback macOS matcher matcher's @@ -126,6 +128,7 @@ rfc rgb str stringify +subnet subprocess timestamp todo diff --git a/tests/mock_vws/test_docker.py b/tests/mock_vws/test_docker.py index 367f1a363..b0bea3318 100644 --- a/tests/mock_vws/test_docker.py +++ b/tests/mock_vws/test_docker.py @@ -11,6 +11,7 @@ import datetime import io import json +import os import socket import uuid import zipfile @@ -281,10 +282,25 @@ def _vws_client( ) +@beartype +def _docker_client() -> docker.DockerClient: + """Return a client for the Docker API engine selected by the + environment. + + Returns: + A client using the requested API version, or automatic negotiation. + """ + # Socktainer needs an explicit API version until negotiation is fixed: + # https://github.com/socktainer/socktainer/issues/433 + return docker.from_env( + version=os.environ.get(key="DOCKER_API_VERSION", default="auto"), + ) + + @beartype def _create_bridge_network(*, resources: ExitStack) -> Network: """Create a test network and register its removal immediately.""" - client = docker.from_env() + client = _docker_client() name = "test-vws-bridge-" + uuid.uuid4().hex try: network = client.networks.create(name=name, driver="bridge") @@ -309,7 +325,7 @@ def _build_image( Returns: The built image. """ - client = docker.from_env() + client = _docker_client() dockerfile = f"{repository_root}/src/mock_vws/_flask_server/Dockerfile" try: image, _ = client.images.build( @@ -371,7 +387,7 @@ def fixture_mock_deployment() -> Iterator[_MockDeployment]: start=Path(__file__).resolve(), ) ) - client = docker.from_env() + client = _docker_client() random = uuid.uuid4().hex with ExitStack() as resources: @@ -404,10 +420,13 @@ def fixture_mock_deployment() -> Iterator[_MockDeployment]: vws_host_port = _free_port() base_vws_url = f"http://127.0.0.1:{vws_host_port}" + # Target-manager and VWQ need explicit ports while Socktainer ignores + # publish_all_ports=True: + # https://github.com/socktainer/socktainer/issues/434 target_manager_container = client.containers.create( image=target_manager_image, name=target_manager_container_name, - publish_all_ports=True, + ports={"5000/tcp": ("127.0.0.1", _free_port())}, network=custom_bridge_network.name, ) _start_container( @@ -429,7 +448,7 @@ def fixture_mock_deployment() -> Iterator[_MockDeployment]: vwq_container = client.containers.create( image=vwq_image, name="vws-mock-vwq-" + random, - publish_all_ports=True, + ports={"5000/tcp": ("127.0.0.1", _free_port())}, network=custom_bridge_network.name, environment={ "TARGET_MANAGER_BASE_URL": target_manager_internal_base_url,