Skip to content

Commit c72ba03

Browse files
committed
Support migrations from a local checkout
1 parent 586f0aa commit c72ba03

3 files changed

Lines changed: 105 additions & 9 deletions

File tree

robotics_integration_tests/custom_containers/migrations_runner.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,57 @@
11
from pathlib import Path
22

33
from docker.models.networks import Network
4+
from loguru import logger
45

56
from robotics_integration_tests.custom_containers.image_builder import build_image_once
67
from robotics_integration_tests.custom_containers.stream_logging_docker_container import (
78
StreamLoggingDockerContainer,
89
)
910
from robotics_integration_tests.settings.settings import settings
1011

12+
# Where a local checkout is mounted inside the migrations runner.
13+
_LOCAL_REPO_MOUNT = "/src"
14+
15+
16+
def _with_migrations_source(
17+
container: StreamLoggingDockerContainer,
18+
source_dir: str,
19+
project_folder: str,
20+
setting_name: str,
21+
) -> StreamLoggingDockerContainer:
22+
"""Take migrations from a local checkout instead of cloning from GitHub.
23+
24+
Mounted read-only; the entrypoint copies it into the container so that nothing
25+
can be written back into the working tree. The copy means uncommitted and
26+
untracked migrations are picked up, which is the reason to use this at all.
27+
28+
Validation is deliberately strict and happens here, before the container
29+
starts: silently falling back to the GitHub clone would leave you believing
30+
you had tested a local migration when you had not.
31+
"""
32+
if not source_dir:
33+
return container
34+
35+
resolved: Path = Path(source_dir).expanduser().resolve()
36+
if not resolved.is_dir():
37+
raise ValueError(
38+
f"{setting_name} is set to '{source_dir}' (resolved to '{resolved}'), "
39+
"which is not a directory."
40+
)
41+
42+
project_dir: Path = resolved / project_folder
43+
if not list(project_dir.glob("*.csproj")):
44+
raise ValueError(
45+
f"{setting_name} is set to '{resolved}', but no .csproj was found in "
46+
f"'{project_dir}'. Point it at the repository root of the service, not "
47+
"at the project folder."
48+
)
49+
50+
logger.info(f"Using migrations from local checkout: {resolved}")
51+
return container.with_volume_mapping(
52+
str(resolved), _LOCAL_REPO_MOUNT, "ro"
53+
).with_env("LOCAL_REPO_PATH", _LOCAL_REPO_MOUNT)
54+
1155

1256
def create_migrations_runner_container(
1357
network: Network, postgres_connection_string: str, name: str = "flotilla_migrations", test_id: str = ""
@@ -30,7 +74,12 @@ def create_migrations_runner_container(
3074
.with_env("EF_PROJECT_PATH", settings.BACKEND_PROJECT_FILE_FOLDER)
3175
.with_env("EF_STARTUP_PATH", settings.BACKEND_PROJECT_FILE_FOLDER)
3276
)
33-
return container
77+
return _with_migrations_source(
78+
container,
79+
source_dir=settings.FLOTILLA_MIGRATIONS_SOURCE_DIR,
80+
project_folder=settings.BACKEND_PROJECT_FILE_FOLDER,
81+
setting_name="FLOTILLA_MIGRATIONS_SOURCE_DIR",
82+
)
3483

3584

3685
def create_sara_migrations_runner_container(
@@ -54,4 +103,9 @@ def create_sara_migrations_runner_container(
54103
.with_env("EF_PROJECT_PATH", settings.SARA_BACKEND_PROJECT_FILE_FOLDER)
55104
.with_env("EF_STARTUP_PATH", settings.SARA_BACKEND_PROJECT_FILE_FOLDER)
56105
)
57-
return container
106+
return _with_migrations_source(
107+
container,
108+
source_dir=settings.SARA_MIGRATIONS_SOURCE_DIR,
109+
project_folder=settings.SARA_BACKEND_PROJECT_FILE_FOLDER,
110+
setting_name="SARA_MIGRATIONS_SOURCE_DIR",
111+
)

robotics_integration_tests/custom_images/migrations_runner/entrypoint.sh

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,50 @@ WAIT_FOR_DB_TIMEOUT="${WAIT_FOR_DB_TIMEOUT:-60}"
1717
: "${AZURE_CLIENT_ID:?AZURE_CLIENT_ID must be set at runtime}"
1818
: "${AZURE_TENANT_ID:?AZURE_TENANT_ID must be set at runtime}"
1919

20-
echo "Cloning $GIT_REPO @ $GIT_REF ..."
2120
rm -rf /work/repo
22-
if [ "$GIT_REF" = "latest" ]; then
23-
BRANCH=$(curl -s ${GITHUB_TOKEN:+-H "Authorization: token $GITHUB_TOKEN"} \
24-
"https://api.github.com/repos/$GIT_REPO/releases/latest" | jq -r .tag_name)
25-
echo "Resolved latest to $BRANCH"
21+
mkdir -p /work/repo
22+
23+
if [ -n "${LOCAL_REPO_PATH:-}" ]; then
24+
# Migrations come from a local checkout mounted read-only, so that locally
25+
# built service images and the database schema come from the same source.
26+
# Copied rather than used in place: the build writes bin/ and obj/ into the
27+
# project, and the mount is read-only precisely so the caller's working tree
28+
# cannot be modified.
29+
#
30+
# bin and obj are excluded not to save space but for correctness: they are
31+
# host-architecture build output, and obj/project.assets.json embeds absolute
32+
# host paths, both of which break restore inside this container.
33+
echo "Copying migrations source from $LOCAL_REPO_PATH ..."
34+
[ -d "$LOCAL_REPO_PATH" ] || { echo "LOCAL_REPO_PATH '$LOCAL_REPO_PATH' is not a directory."; exit 1; }
35+
tar -C "$LOCAL_REPO_PATH" \
36+
--exclude=bin \
37+
--exclude=obj \
38+
--exclude=node_modules \
39+
--exclude=.git \
40+
--exclude=TestResults \
41+
-cf - . | tar -C /work/repo -xf -
2642
else
27-
BRANCH="main"
43+
echo "Cloning $GIT_REPO @ $GIT_REF ..."
44+
if [ "$GIT_REF" = "latest" ]; then
45+
BRANCH=$(curl -s ${GITHUB_TOKEN:+-H "Authorization: token $GITHUB_TOKEN"} \
46+
"https://api.github.com/repos/$GIT_REPO/releases/latest" | jq -r .tag_name)
47+
echo "Resolved latest to $BRANCH"
48+
else
49+
BRANCH="main"
50+
fi
51+
rm -rf /work/repo
52+
git clone --depth 1 --branch "$BRANCH" "https://github.com/$GIT_REPO" /work/repo
2853
fi
29-
git clone --depth 1 --branch "$BRANCH" "https://github.com/$GIT_REPO" /work/repo
3054

3155
cd /work/repo
3256

57+
# Guard against a source that does not contain what we expect, rather than
58+
# letting it surface later as an opaque dotnet-ef failure.
59+
if ! ls "$EF_PROJECT_PATH"/*.csproj >/dev/null 2>&1; then
60+
echo "No .csproj found at '$EF_PROJECT_PATH' in the migrations source."
61+
exit 1
62+
fi
63+
3364
echo "Restoring projects for EF design-time..."
3465
dotnet restore "$EF_STARTUP_PATH" || dotnet restore "$EF_PROJECT_PATH" || true
3566

robotics_integration_tests/settings/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ def KEYVAULT_URI(self) -> str:
5858
GIT_REPOSITORY_FOR_MIGRATIONS_REF: str = Field(default="latest")
5959
BACKEND_PROJECT_FILE_FOLDER: str = Field(default="backend/api")
6060

61+
# Path to a local flotilla checkout to take migrations from. When set, it
62+
# takes precedence over cloning GIT_REPOSITORY_FOR_MIGRATIONS from GitHub, and
63+
# GIT_REPOSITORY_FOR_MIGRATIONS_REF is ignored. Use this together with locally
64+
# built images (see scripts/build_local_images.sh) so that the schema and the
65+
# application code come from the same source. Uncommitted and untracked
66+
# migrations are included, since the directory is copied rather than cloned.
67+
FLOTILLA_MIGRATIONS_SOURCE_DIR: str = Field(default="")
68+
6169
# PostgreSQL Sara Database environment
6270
POSTGRESQL_IMAGE: str = Field(default="postgres:16")
6371
SARA_DB_USER: str = Field(default="sara")
@@ -69,6 +77,9 @@ def KEYVAULT_URI(self) -> str:
6977

7078
SARA_BACKEND_PROJECT_FILE_FOLDER: str = Field(default="api")
7179

80+
# See FLOTILLA_MIGRATIONS_SOURCE_DIR.
81+
SARA_MIGRATIONS_SOURCE_DIR: str = Field(default="")
82+
7283
# Migrations runner environment
7384
RELATIVE_PATH_TO_DOCKERFILE: str = Field(
7485
default="./robotics_integration_tests/custom_images/migrations_runner/"

0 commit comments

Comments
 (0)