mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
feat(worktree): isolated per-branch checkouts sharing one cache stack
Parallel checkouts collided on the compose subnet, host ports and container names, so a quality run in a worktree died at compose-up. Slot each checkout via custom.env and layer two overrides on top: the shared cache network in place of a second cache stack, and the shared .git directory without which git aborts inside the container. Both overlays gate on the resource they need, not on the slot number, so a stray INFINITO_INSTANCE cannot make compose demand a value nobody set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cf82841be8
commit
8aba198aec
16 files changed
+480
-18
No files matched your search
@@ -767,6 +767,26 @@ test-unit: install
|
||||
INFINITO_COMPILE=0 \
|
||||
bash scripts/tests/code/wrapper.sh
|
||||
|
||||
.PHONY: worktree-down
|
||||
# Stop a branch worktree's stack, release the checkout and free its slot.
|
||||
# Usage: make worktree-down branch=<name> [base=<dir>] [force=true]
|
||||
# Param branch: branch the worktree was created for (required).
|
||||
# Param base: parent directory the worktree lives in (default /tmp).
|
||||
# Param force: true discards uncommitted changes in the worktree.
|
||||
worktree-down:
|
||||
@test -n '$(branch)' || { echo 'usage: make worktree-down branch=<name> [base=<dir>] [force=true]'; exit 2; }
|
||||
@bash scripts/system/worktree/down.sh '$(branch)' '$(or $(base),/tmp)' '$(or $(force),false)'
|
||||
|
||||
.PHONY: worktree-up
|
||||
# Check a branch out into an isolated worktree with its own subnet, ports and container names.
|
||||
# Usage: make worktree-up branch=<name> [base=<dir>]
|
||||
# Note: the worktree shares the primary checkout's cache stack instead of starting its own.
|
||||
# Param branch: branch to check out (required).
|
||||
# Param base: parent directory for the worktree (default /tmp).
|
||||
worktree-up:
|
||||
@test -n '$(branch)' || { echo 'usage: make worktree-up branch=<name> [base=<dir>]'; exit 2; }
|
||||
@bash scripts/system/worktree/up.sh '$(branch)' '$(or $(base),/tmp)'
|
||||
|
||||
.PHONY: wsl2-dns-setup
|
||||
# Set up DNS on WSL2.
|
||||
wsl2-dns-setup:
|
||||
|
||||
@@ -135,5 +135,7 @@ alias i8tenu='m test-unit'
|
||||
alias i8tesi='m test-signed'
|
||||
alias i8texe='m test-external'
|
||||
alias i8wo='m wsl2-dns-setup'
|
||||
alias i8wodo='m worktree-down'
|
||||
alias i8wose='m wsl2-systemd-check'
|
||||
alias i8wotu='m wsl2-trust-windows'
|
||||
alias i8woup='m worktree-up'
|
||||
@@ -190,7 +190,7 @@ class Compose:
|
||||
args: list[str] = ["up", "-d"]
|
||||
if no_build:
|
||||
args.append("--no-build")
|
||||
if self.profile.registry_cache_active():
|
||||
if self.profile.owns_cache_stack():
|
||||
self._generate_package_frontend_certs(env)
|
||||
args += ["registry-cache", "package-cache", "package-cache-frontend"]
|
||||
args += ["coredns", "infinito"]
|
||||
@@ -199,8 +199,9 @@ class Compose:
|
||||
|
||||
self.wait_for_healthy()
|
||||
|
||||
if self.profile.registry_cache_active():
|
||||
if self.profile.owns_cache_stack():
|
||||
self._bootstrap_package_cache(env)
|
||||
if self.profile.registry_cache_active():
|
||||
self._install_package_frontend_ca_in_runner()
|
||||
|
||||
if run_entry_init:
|
||||
|
||||
@@ -8,12 +8,23 @@ VALID_DISTROS: tuple[str, ...] = distro_names()
|
||||
|
||||
|
||||
def compose_file_args() -> list[str]:
|
||||
"""Compose `-f` flags shared by up and down flows."""
|
||||
"""Compose `-f` flags shared by up and down flows.
|
||||
|
||||
Each override is gated on the resource it needs being present, not on the
|
||||
instance slot: an override whose `:?` guard is unsatisfied would abort the
|
||||
stack outright, so a checkout that was never handed a shared git dir or
|
||||
cache network simply does not layer that file in.
|
||||
"""
|
||||
from .profile import Profile
|
||||
|
||||
profile = Profile()
|
||||
out = ["-f", "compose.yml"]
|
||||
if Profile().registry_cache_active():
|
||||
if profile.shared_git_dir():
|
||||
out += ["-f", "compose/worktree.override.yml"]
|
||||
if profile.registry_cache_active():
|
||||
out += ["-f", "compose/cache.override.yml"]
|
||||
if profile.shared_cache_network():
|
||||
out += ["-f", "compose/cache.shared.override.yml"]
|
||||
if (os.environ.get("INFINITO_PUBLISH_PORTS") or "").strip().lower() == "false":
|
||||
out += ["-f", "compose/noports.override.yml"]
|
||||
return out
|
||||
|
||||
@@ -19,3 +19,25 @@ class Profile:
|
||||
def registry_cache_active(self) -> bool:
|
||||
"""True iff the cache stack should be loaded (local dev only)."""
|
||||
return not self.is_ci()
|
||||
|
||||
def instance(self) -> int:
|
||||
"""Slot index of this checkout; 0 is the primary one.
|
||||
|
||||
Bookkeeping only: the compose layering below keys off the concrete
|
||||
resources a worktree was handed, never off this number, so a stray
|
||||
slot cannot make compose demand a value nobody set.
|
||||
"""
|
||||
raw = (os.environ.get("INFINITO_INSTANCE") or "0").strip()
|
||||
return int(raw) if raw.isdigit() else 0
|
||||
|
||||
def shared_git_dir(self) -> str:
|
||||
"""Path of the checkout's shared .git directory, empty when it owns one."""
|
||||
return (os.environ.get("INFINITO_GIT_COMMON_DIR") or "").strip()
|
||||
|
||||
def shared_cache_network(self) -> str:
|
||||
"""Docker network of a cache stack owned elsewhere, empty when unset."""
|
||||
return (os.environ.get("INFINITO_CACHE_NETWORK") or "").strip()
|
||||
|
||||
def owns_cache_stack(self) -> bool:
|
||||
"""True iff this instance runs the cache stack rather than sharing it."""
|
||||
return self.registry_cache_active() and not self.shared_cache_network()
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
# Layered on cache.override.yml for secondary instances, which reach the
|
||||
# primary instance's cache stack instead of running one of their own.
|
||||
# See docs/contributing/environment/cache.md.
|
||||
|
||||
services:
|
||||
infinito:
|
||||
# Exception: required:false still makes compose CREATE the cache services,
|
||||
# which would claim the primary instance's IPs outside this subnet.
|
||||
depends_on: !reset null
|
||||
networks:
|
||||
cache:
|
||||
|
||||
networks:
|
||||
cache:
|
||||
external: true
|
||||
name: ${INFINITO_CACHE_NETWORK:?Set by 'make worktree-up' from the running
|
||||
cache stack; empty on the primary instance}
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
# Layered on compose.yml for secondary instances. A git worktree's .git is a
|
||||
# file pointing at an absolute path inside the primary checkout, so git only
|
||||
# works in the container when that directory is mounted where the pointer
|
||||
# expects it.
|
||||
|
||||
services:
|
||||
infinito:
|
||||
volumes:
|
||||
- ${INFINITO_GIT_COMMON_DIR:?Set by 'make worktree-up' from git rev-parse
|
||||
--git-common-dir}:${INFINITO_GIT_COMMON_DIR:?Set by 'make worktree-up'
|
||||
from git rev-parse --git-common-dir}
|
||||
@@ -34,6 +34,9 @@ INFINITO_BUILD=1
|
||||
# `1` keeps BuildKit on (default); `0` forces the compose wrapper to use the legacy builder.
|
||||
INFINITO_BUILDKIT_ENABLED=1
|
||||
|
||||
# External docker network carrying the shared cache stack; empty on the primary instance, which owns the stack itself.
|
||||
INFINITO_CACHE_NETWORK=
|
||||
|
||||
# Bind-mounted apt sources.list fragment for the package-cache frontend.
|
||||
INFINITO_CACHE_PACKAGE_APT_LIST=./compose/package-cache/apt.list
|
||||
|
||||
@@ -139,12 +142,18 @@ INFINITO_GATEWAY=172.30.0.1
|
||||
# Prefix path under which GHCR mirror lookups search for cached images.
|
||||
INFINITO_GHCR_MIRROR_PREFIX=mirror
|
||||
|
||||
# Shared .git directory a worktree checkout points at; empty on the primary instance, which carries its own.
|
||||
INFINITO_GIT_COMMON_DIR=
|
||||
|
||||
# Run-context flag like INFINITO_DEPLOY_MODE: the single-node workflow sets 'true' so host discovery drops compose-covered roles.
|
||||
INFINITO_HOST_EXCLUDE_COMPOSE=
|
||||
|
||||
# Image tag used to address the published infinito image (CI publishes 'latest' on push).
|
||||
INFINITO_IMAGE_TAG=latest
|
||||
|
||||
# Slot index isolating a parallel checkout: 0 owns the cache stack, 1..255 shift subnet, bind IP and container names.
|
||||
INFINITO_INSTANCE=0
|
||||
|
||||
# Repo-relative path to the Ansible vars file `infinito administration inventory provision --vars-file <...>` consumes.
|
||||
INFINITO_INVENTORY_VARS_FILE=inventories/development/default.yml
|
||||
|
||||
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Tear down the worktree a branch was checked out into: stop its compose
|
||||
# stack, then release the checkout and free its slot. Refuses to drop a
|
||||
# worktree with uncommitted changes unless force=true is passed.
|
||||
#
|
||||
# Use `make worktree-down` rather than calling this script directly.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
# shellcheck source=scripts/system/worktree/lib.sh
|
||||
source "${SCRIPT_DIR}/lib.sh"
|
||||
|
||||
branch="${1:?branch name required}"
|
||||
base="${2:-/tmp}"
|
||||
force="${3:-false}"
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
slug="$(worktree_slug "${branch}")"
|
||||
path="$(worktree_path "${slug}" "${base}")"
|
||||
|
||||
if [ ! -d "${path}" ]; then
|
||||
echo "FAILURE: no worktree at ${path} for branch '${branch}'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${force}" != "true" ] && [ -n "$(git -C "${path}" status --porcelain)" ]; then
|
||||
echo "FAILURE: ${path} has uncommitted changes; commit them or pass force=true" >&2
|
||||
git -C "${path}" status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
slot="$(worktree_slot_of "${path}")"
|
||||
echo ">>> Stopping the compose stack in ${path} (slot ${slot})"
|
||||
if ! make -C "${path}" compose-down; then
|
||||
echo ">>> WARNING: compose-down failed; removing the worktree anyway"
|
||||
fi
|
||||
|
||||
echo ">>> Removing worktree ${path}"
|
||||
if [ "${force}" = "true" ]; then
|
||||
git worktree remove --force "${path}"
|
||||
else
|
||||
git worktree remove "${path}"
|
||||
fi
|
||||
|
||||
echo "Slot ${slot} released."
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Shared helpers for the worktree slot manager. A slot is an integer that
|
||||
# isolates one checkout from every other: it shifts the compose subnet, the
|
||||
# host bind IP and the swarm lab subnet, so several worktrees can run their
|
||||
# stacks side by side. Slot 0 is the primary checkout and owns the shared
|
||||
# cache stack; every worktree gets the next free slot and joins that cache
|
||||
# over an external docker network.
|
||||
#
|
||||
# Volumes are deliberately not slotted: compose already namespaces them by
|
||||
# project, and INFINITO_DOCKER_VOLUME names a top-level compose key, so
|
||||
# renaming it would point the mount at an undeclared volume.
|
||||
#
|
||||
# The pins are written to custom.env AND passed to the generator in a cleared
|
||||
# environment: custom.env keeps them reproducible across later `make dotenv`
|
||||
# runs, while the explicit env covers branches whose generator predates
|
||||
# custom.env support. Inheriting the caller's environment would leak the
|
||||
# invoking worktree's own slot into the new one.
|
||||
#
|
||||
# Slot discovery reads custom.env first: a branch whose default.env predates
|
||||
# INFINITO_INSTANCE never materialises the key into .env, which would make the
|
||||
# checkout look like slot 0 and hand its number out twice.
|
||||
#
|
||||
# Sourced by up.sh and down.sh; not meant to be executed directly.
|
||||
|
||||
WORKTREE_DIR_PREFIX="infinito-wt-"
|
||||
|
||||
worktree_slug() {
|
||||
printf '%s' "$1" | sed -e 's#[^a-zA-Z0-9._-]#-#g' -e 's#^[^a-zA-Z0-9]*##' -e 's#[-.]*$##'
|
||||
}
|
||||
|
||||
worktree_path() {
|
||||
printf '%s/%s%s' "$2" "${WORKTREE_DIR_PREFIX}" "$1"
|
||||
}
|
||||
|
||||
worktree_slot_of() {
|
||||
local slot=0 file
|
||||
for file in "$1/custom.env" "$1/.env"; do
|
||||
if [ -f "${file}" ]; then
|
||||
slot="$(awk -F= '/^INFINITO_INSTANCE=/{print $2}' "${file}" | tail -n1)"
|
||||
if [ -n "${slot}" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
case "${slot}" in
|
||||
'' | *[!0-9]*) slot=0 ;;
|
||||
esac
|
||||
printf '%s' "${slot}"
|
||||
}
|
||||
|
||||
worktree_next_slot() {
|
||||
local max=0 path slot
|
||||
while read -r path; do
|
||||
slot="$(worktree_slot_of "${path}")"
|
||||
if [ "${slot}" -gt "${max}" ]; then
|
||||
max="${slot}"
|
||||
fi
|
||||
done < <(git worktree list --porcelain | awk '/^worktree /{print $2}')
|
||||
printf '%s' "$((max + 1))"
|
||||
}
|
||||
|
||||
worktree_cache_network() {
|
||||
docker inspect infinito-registry-cache \
|
||||
--format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' 2>/dev/null |
|
||||
head -n1
|
||||
}
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Create an isolated git worktree for a branch and pin it to a free slot, so
|
||||
# its compose stack no longer collides with the primary checkout on subnet,
|
||||
# host ports, container names or volumes. The cache stack is not duplicated:
|
||||
# the worktree joins the running one over an external network.
|
||||
#
|
||||
# Use `make worktree-up` rather than calling this script directly.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||
|
||||
# shellcheck source=scripts/system/worktree/lib.sh
|
||||
source "${SCRIPT_DIR}/lib.sh"
|
||||
|
||||
branch="${1:?branch name required}"
|
||||
base="${2:-/tmp}"
|
||||
max_slot=255
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
slug="$(worktree_slug "${branch}")"
|
||||
if [ -z "${slug}" ]; then
|
||||
echo "FAILURE: branch '${branch}' slugs to an empty name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
path="$(worktree_path "${slug}" "${base}")"
|
||||
if [ -e "${path}" ]; then
|
||||
echo "FAILURE: ${path} already exists; run 'make worktree-down branch=${branch}' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
slot="$(worktree_next_slot)"
|
||||
if [ "${slot}" -gt "${max_slot}" ]; then
|
||||
echo "FAILURE: no free slot left (max ${max_slot}); tear down an existing worktree" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cache_network="$(worktree_cache_network)"
|
||||
if [ -z "${cache_network}" ]; then
|
||||
echo ">>> WARNING: no running cache stack found; the worktree will start without a shared cache"
|
||||
fi
|
||||
|
||||
echo ">>> Creating worktree for '${branch}' at ${path} (slot ${slot})"
|
||||
git worktree add "${path}" "${branch}"
|
||||
|
||||
git_common_dir="$(git rev-parse --path-format=absolute --git-common-dir)"
|
||||
|
||||
echo ">>> Pinning slot ${slot} in ${path}/custom.env"
|
||||
cat >"${path}/custom.env" <<PINS
|
||||
INFINITO_INSTANCE=${slot}
|
||||
INFINITO_GIT_COMMON_DIR=${git_common_dir}
|
||||
INFINITO_SUBNET=172.30.${slot}.0/24
|
||||
INFINITO_GATEWAY=172.30.${slot}.1
|
||||
INFINITO_DNS_IP=172.30.${slot}.53
|
||||
INFINITO_IP4=172.30.${slot}.10
|
||||
INFINITO_BIND_IP=127.0.0.$((slot + 1))
|
||||
INFINITO_SWARM_LAB_SUBNET=192.168.$((244 + slot)).0/24
|
||||
INFINITO_RUNNER_PREFIX=infinito-${slug}
|
||||
INFINITO_CONTAINER=infinito_nexus_${slug//[.-]/_}
|
||||
INFINITO_CACHE_NETWORK=${cache_network}
|
||||
PINS
|
||||
|
||||
echo ">>> Generating .env from the pins"
|
||||
env -i -C "${path}" \
|
||||
HOME="${HOME}" \
|
||||
PATH="${PATH}" \
|
||||
INFINITO_INSTANCE="${slot}" \
|
||||
INFINITO_GIT_COMMON_DIR="${git_common_dir}" \
|
||||
INFINITO_SUBNET="172.30.${slot}.0/24" \
|
||||
INFINITO_GATEWAY="172.30.${slot}.1" \
|
||||
INFINITO_DNS_IP="172.30.${slot}.53" \
|
||||
INFINITO_IP4="172.30.${slot}.10" \
|
||||
INFINITO_BIND_IP="127.0.0.$((slot + 1))" \
|
||||
INFINITO_SWARM_LAB_SUBNET="192.168.$((244 + slot)).0/24" \
|
||||
INFINITO_RUNNER_PREFIX="infinito-${slug}" \
|
||||
INFINITO_CACHE_NETWORK="${cache_network}" \
|
||||
python3 -m cli.meta.env
|
||||
|
||||
cat <<SUMMARY
|
||||
|
||||
Worktree ready.
|
||||
|
||||
branch ${branch}
|
||||
path ${path}
|
||||
slot ${slot}
|
||||
subnet 172.30.${slot}.0/24
|
||||
bind IP 127.0.0.$((slot + 1))
|
||||
container infinito_nexus_${slug//[.-]/_}
|
||||
cache net ${cache_network:-<none>}
|
||||
|
||||
Next:
|
||||
cd ${path}
|
||||
make compose-up
|
||||
|
||||
Release with:
|
||||
make worktree-down branch=${branch}
|
||||
SUMMARY
|
||||
@@ -188,6 +188,12 @@ class TestDotenvSmoke(unittest.TestCase):
|
||||
produced .env contains the expected baseline keys."""
|
||||
|
||||
def test_clean_run_produces_expected_keys(self) -> None:
|
||||
if (PROJECT_ROOT / "custom.env").is_file():
|
||||
self.skipTest(
|
||||
"custom.env overrides handler-computed values by design "
|
||||
"(see utils/env/builder.py:apply_custom_env), so this checkout "
|
||||
"cannot produce a clean run; 'make worktree-up' writes one."
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
env = {
|
||||
k: v
|
||||
|
||||
@@ -12,8 +12,6 @@ from cli.administration.deploy.development.compose import Compose
|
||||
|
||||
class TestComposeUpRetries(unittest.TestCase):
|
||||
def _compose(self) -> Compose:
|
||||
# repo_root is only used as a cwd in subprocess calls; for these unit tests
|
||||
# we do not execute real subprocesses.
|
||||
return Compose(repo_root=Path("/tmp/infinito-nexus"), distro="arch")
|
||||
|
||||
@patch("time.sleep", autospec=True)
|
||||
@@ -22,7 +20,6 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
) -> None:
|
||||
compose = self._compose()
|
||||
|
||||
# Fail 2 times, then succeed.
|
||||
compose.run = MagicMock(
|
||||
side_effect=[
|
||||
subprocess.CalledProcessError(1, ["docker", "compose"], "out1", "err1"),
|
||||
@@ -42,13 +39,10 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
]
|
||||
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
|
||||
|
||||
# 3 calls: fail, fail, succeed
|
||||
self.assertEqual(compose.run.call_count, 3)
|
||||
|
||||
# Sleep after each failure (2 times), not after success
|
||||
self.assertEqual(sleep_mock.call_count, 2)
|
||||
sleep_mock.assert_any_call(30)
|
||||
# Ensure all sleeps used exactly 30 seconds
|
||||
self.assertTrue(all(call.args == (30,) for call in sleep_mock.call_args_list))
|
||||
|
||||
@patch("time.sleep", autospec=True)
|
||||
@@ -57,7 +51,6 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
) -> None:
|
||||
compose = self._compose()
|
||||
|
||||
# Always fail.
|
||||
compose.run = MagicMock(
|
||||
side_effect=subprocess.CalledProcessError(
|
||||
1, ["docker", "compose"], "out", "err"
|
||||
@@ -69,10 +62,8 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
compose._compose_up_with_retries(args, attempts=6, delay_s=30)
|
||||
|
||||
# Called exactly 6 times
|
||||
self.assertEqual(compose.run.call_count, 6)
|
||||
|
||||
# Sleep between attempts: after attempts 1..5 => 5 sleeps
|
||||
self.assertEqual(sleep_mock.call_count, 5)
|
||||
self.assertTrue(all(call.args == (30,) for call in sleep_mock.call_args_list))
|
||||
|
||||
@@ -101,6 +92,8 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
"GITHUB_ACTIONS": "true",
|
||||
"INFINITO_RUNNING_ON_GITHUB": "true",
|
||||
"CI": "true",
|
||||
"INFINITO_GIT_COMMON_DIR": "",
|
||||
"INFINITO_CACHE_NETWORK": "",
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
@@ -125,7 +118,6 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
cmd = run_mock.call_args.args[0]
|
||||
env = run_mock.call_args.kwargs["env"]
|
||||
|
||||
# CI: cache override is not layered in.
|
||||
self.assertEqual(
|
||||
cmd,
|
||||
[
|
||||
@@ -148,6 +140,8 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
"GITHUB_ACTIONS": "",
|
||||
"INFINITO_RUNNING_ON_GITHUB": "",
|
||||
"CI": "",
|
||||
"INFINITO_GIT_COMMON_DIR": "",
|
||||
"INFINITO_CACHE_NETWORK": "",
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
@@ -164,7 +158,6 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
cmd = run_mock.call_args.args[0]
|
||||
env = run_mock.call_args.kwargs["env"]
|
||||
|
||||
# Local: cache override is layered onto compose.yml.
|
||||
self.assertEqual(
|
||||
cmd,
|
||||
[
|
||||
@@ -195,7 +188,6 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
"INFINITO_BUILD": "1",
|
||||
"INFINITO_IMAGE": "infinito-debian",
|
||||
"INFINITO_PULL_POLICY": "never",
|
||||
# CI-pinned so cache stays inactive; this test asserts build behaviour only.
|
||||
"CI": "true",
|
||||
"GITHUB_ACTIONS": "true",
|
||||
"INFINITO_RUNNING_ON_GITHUB": "true",
|
||||
@@ -258,6 +250,8 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
"CI": "",
|
||||
"GITHUB_ACTIONS": "",
|
||||
"INFINITO_RUNNING_ON_GITHUB": "",
|
||||
"INFINITO_GIT_COMMON_DIR": "",
|
||||
"INFINITO_CACHE_NETWORK": "",
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
@@ -272,7 +266,6 @@ class TestComposeUpRetries(unittest.TestCase):
|
||||
|
||||
compose.up(run_entry_init=False)
|
||||
|
||||
# Cache services precede coredns + infinito so depends_on health gates resolve.
|
||||
compose._compose_up_with_retries.assert_called_once_with(
|
||||
[
|
||||
"up",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Unit tests for compose_file_args.
|
||||
|
||||
Pins which override files each runtime context layers on compose.yml. Every
|
||||
override is gated on the resource it needs rather than on the instance slot,
|
||||
because an unsatisfied `:?` guard inside one of them aborts the whole stack.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from cli.administration.deploy.development.env import compose_file_args
|
||||
|
||||
_LOCAL_PRIMARY = {
|
||||
"GITHUB_ACTIONS": "",
|
||||
"INFINITO_RUNNING_ON_GITHUB": "",
|
||||
"CI": "",
|
||||
"INFINITO_INSTANCE": "0",
|
||||
"INFINITO_GIT_COMMON_DIR": "",
|
||||
"INFINITO_CACHE_NETWORK": "",
|
||||
"INFINITO_PUBLISH_PORTS": "",
|
||||
}
|
||||
|
||||
_WORKTREE = {
|
||||
**_LOCAL_PRIMARY,
|
||||
"INFINITO_INSTANCE": "2",
|
||||
"INFINITO_GIT_COMMON_DIR": "/repo/.git",
|
||||
"INFINITO_CACHE_NETWORK": "primary_default",
|
||||
}
|
||||
|
||||
|
||||
class TestComposeFileArgs(unittest.TestCase):
|
||||
@patch.dict(os.environ, _LOCAL_PRIMARY, clear=False)
|
||||
def test_primary_instance_loads_the_cache_stack(self) -> None:
|
||||
self.assertEqual(
|
||||
compose_file_args(),
|
||||
["-f", "compose.yml", "-f", "compose/cache.override.yml"],
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, _WORKTREE, clear=False)
|
||||
def test_worktree_adds_the_git_and_shared_cache_overrides(self) -> None:
|
||||
self.assertEqual(
|
||||
compose_file_args(),
|
||||
[
|
||||
"-f",
|
||||
"compose.yml",
|
||||
"-f",
|
||||
"compose/worktree.override.yml",
|
||||
"-f",
|
||||
"compose/cache.override.yml",
|
||||
"-f",
|
||||
"compose/cache.shared.override.yml",
|
||||
],
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {**_LOCAL_PRIMARY, "INFINITO_INSTANCE": "5"}, clear=False)
|
||||
def test_a_stray_slot_alone_changes_nothing(self) -> None:
|
||||
self.assertEqual(
|
||||
compose_file_args(),
|
||||
["-f", "compose.yml", "-f", "compose/cache.override.yml"],
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {**_LOCAL_PRIMARY, "INFINITO_GIT_COMMON_DIR": "/repo/.git"})
|
||||
def test_a_shared_git_dir_alone_adds_only_the_worktree_override(self) -> None:
|
||||
self.assertEqual(
|
||||
compose_file_args(),
|
||||
[
|
||||
"-f",
|
||||
"compose.yml",
|
||||
"-f",
|
||||
"compose/worktree.override.yml",
|
||||
"-f",
|
||||
"compose/cache.override.yml",
|
||||
],
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {**_WORKTREE, "CI": "true"}, clear=False)
|
||||
def test_ci_keeps_the_git_mount_but_drops_every_cache_override(self) -> None:
|
||||
self.assertEqual(
|
||||
compose_file_args(),
|
||||
["-f", "compose.yml", "-f", "compose/worktree.override.yml"],
|
||||
)
|
||||
|
||||
@patch.dict(os.environ, {**_LOCAL_PRIMARY, "CI": "true"}, clear=False)
|
||||
def test_ci_loads_no_override_at_all(self) -> None:
|
||||
self.assertEqual(compose_file_args(), ["-f", "compose.yml"])
|
||||
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{**_LOCAL_PRIMARY, "INFINITO_PUBLISH_PORTS": "false"},
|
||||
clear=False,
|
||||
)
|
||||
def test_unpublished_ports_append_the_noports_override(self) -> None:
|
||||
self.assertEqual(
|
||||
compose_file_args()[-2:], ["-f", "compose/noports.override.yml"]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
@@ -8,11 +8,13 @@ from unittest.mock import patch
|
||||
|
||||
from cli.administration.deploy.development.profile import Profile
|
||||
|
||||
# Blank explicit signals; clear=True would unset PATH.
|
||||
_BLANK_CI_ENV = {
|
||||
"GITHUB_ACTIONS": "",
|
||||
"INFINITO_RUNNING_ON_GITHUB": "",
|
||||
"CI": "",
|
||||
"INFINITO_INSTANCE": "0",
|
||||
"INFINITO_GIT_COMMON_DIR": "",
|
||||
"INFINITO_CACHE_NETWORK": "",
|
||||
}
|
||||
|
||||
|
||||
@@ -73,5 +75,45 @@ class TestProfileRegistryCacheActive(unittest.TestCase):
|
||||
self.assertEqual(p.registry_cache_active(), not p.is_ci())
|
||||
|
||||
|
||||
class TestProfileInstance(unittest.TestCase):
|
||||
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": ""}, clear=False)
|
||||
def test_unset_instance_is_primary(self) -> None:
|
||||
self.assertEqual(Profile().instance(), 0)
|
||||
|
||||
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": "7"}, clear=False)
|
||||
def test_numeric_instance_is_parsed(self) -> None:
|
||||
self.assertEqual(Profile().instance(), 7)
|
||||
|
||||
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": " 3 "}, clear=False)
|
||||
def test_surrounding_whitespace_is_tolerated(self) -> None:
|
||||
self.assertEqual(Profile().instance(), 3)
|
||||
|
||||
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": "abc"}, clear=False)
|
||||
def test_garbage_falls_back_to_primary(self) -> None:
|
||||
self.assertEqual(Profile().instance(), 0)
|
||||
|
||||
|
||||
class TestProfileOwnsCacheStack(unittest.TestCase):
|
||||
@patch.dict(os.environ, _BLANK_CI_ENV, clear=False)
|
||||
def test_owns_the_stack_when_no_network_was_handed_over(self) -> None:
|
||||
self.assertTrue(Profile().owns_cache_stack())
|
||||
|
||||
@patch.dict(
|
||||
os.environ, {**_BLANK_CI_ENV, "INFINITO_CACHE_NETWORK": "primary_default"}
|
||||
)
|
||||
def test_shares_instead_of_owning_when_a_network_is_named(self) -> None:
|
||||
p = Profile()
|
||||
self.assertFalse(p.owns_cache_stack())
|
||||
self.assertTrue(p.registry_cache_active())
|
||||
|
||||
@patch.dict(os.environ, {**_BLANK_CI_ENV, "INFINITO_INSTANCE": "7"}, clear=False)
|
||||
def test_a_stray_slot_alone_does_not_give_the_stack_away(self) -> None:
|
||||
self.assertTrue(Profile().owns_cache_stack())
|
||||
|
||||
@patch.dict(os.environ, {**_BLANK_CI_ENV, "CI": "true"}, clear=False)
|
||||
def test_ci_never_owns_the_stack(self) -> None:
|
||||
self.assertFalse(Profile().owns_cache_stack())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
Vendored
+3
@@ -88,6 +88,9 @@ STATIC_KEYS = (
|
||||
"INFINITO_RESCUE_DIAGNOSTICS_BASE",
|
||||
"INFINITO_RUNNER_PREFIX",
|
||||
"INFINITO_TIMEOUT_MULTIPLIER",
|
||||
"INFINITO_INSTANCE",
|
||||
"INFINITO_GIT_COMMON_DIR",
|
||||
"INFINITO_CACHE_NETWORK",
|
||||
"INFINITO_CACHE_REGISTRY_HOST_PATH",
|
||||
"INFINITO_CACHE_REGISTRY_CA_HOST_PATH",
|
||||
"INFINITO_CACHE_REGISTRY_IP",
|
||||
|
||||
Reference in new issue
Block a user