1144 lines
43 KiB
Python
1144 lines
43 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from scripts import bootstrap_controller_artifact_promoter as bootstrap
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
LEGACY_REVISION = "5553c04ce497c08a35ed301052dc5e0b02e5d9b4"
|
|
LEGACY_TREE = "1624e1748053f998d8427c8c8a4203277d22f13c"
|
|
APPOPS_CONTAINER_ID = "72daa9f638defc4f8897d8aad23c4181054b2a1bc04c993078a5ac9597eeb0ec"
|
|
APPOPS_IMAGE_ID = "sha256:9b5eddf32c8eb5361106fc35cc0e056c49d287a20d1fb26490662cc8447f09d4"
|
|
CONTROLLER_CONTAINER_ID = "e4e7550405aa2a9a91901faf1177d46a35b0fc43c304976ec8971865d43e30eb"
|
|
CONTROLLER_IMAGE_ID = "sha256:c474d435a4f55ac5205570da0caa47e103f89a6c2ef96ea45861a23eb7b005dc"
|
|
CONTROLLER_IMAGE_REF = "docker.gitea.com/runner-images:ubuntu-latest"
|
|
CONTROLLER_REVISION = "034c9f844d2455850ba37b31379b7edaa786583e"
|
|
CONTROLLER_TREE = "6b8e92942e6e5eca8a9aa9da0a1509b2019d7b7b"
|
|
REVISION = "a" * 40
|
|
TREE = "b" * 40
|
|
CALLER_ID = "c" * 64
|
|
MODERN_HELPER_ID = "f" * 64
|
|
|
|
|
|
LEGACY_HELPER_SOURCE_FIXTURE = '''
|
|
def helper_container_spec(*, revision: str, image_id: str) -> dict[str, Any]:
|
|
if not _REVISION.fullmatch(revision):
|
|
raise ValueError("controller_promoter_revision_invalid")
|
|
if not _IMAGE_ID.fullmatch(image_id):
|
|
raise ValueError("controller_promoter_image_invalid")
|
|
|
|
command = f"""set -Eeuo pipefail
|
|
revision='{revision}'
|
|
version='{CONTROLLER_VERSION}'
|
|
repo='/gitea-repositories/jens/unraid-appops-gateway.git'
|
|
work='/mnt/cache/appdata/gitea-deploy-runners/.controller-promoter-{revision[:12]}'
|
|
rm -rf -- "$work"
|
|
install -d -m 0700 "$work"
|
|
cleanup() {{ rm -rf -- "$work"; }}
|
|
trap cleanup EXIT
|
|
command -v git >/dev/null
|
|
command -v docker >/dev/null
|
|
docker compose version >/dev/null
|
|
command -v python3 >/dev/null
|
|
command -v tar >/dev/null
|
|
command -v curl >/dev/null
|
|
git --git-dir="$repo" cat-file -e "$revision^{{commit}}"
|
|
tree="$(git --git-dir="$repo" rev-parse "$revision^{{tree}}")"
|
|
git --git-dir="$repo" archive --format=tar "$revision" \\
|
|
ops/gitea-deploy-broker \\
|
|
scripts/prepare_controller_artifact.py \\
|
|
scripts/reconcile_controller_state.py \\
|
|
scripts/run_repopulse_adoption.py > "$work/controller.tar"
|
|
archive_sha="$(sha256sum "$work/controller.tar" | cut -d ' ' -f 1)"
|
|
git --git-dir="$repo" show "$revision:scripts/upgrade_deployment_controller.sh" > "$work/upgrade.sh"
|
|
chmod 0700 "$work/upgrade.sh"
|
|
bash "$work/upgrade.sh" \\
|
|
--archive "$work/controller.tar" \\
|
|
--archive-sha256 "$archive_sha" \\
|
|
--source-repository '{SOURCE_REPOSITORY}' \\
|
|
--source-revision "$revision" \\
|
|
--source-tree "$tree" \\
|
|
--controller-version "$version" \\
|
|
--confirm UPGRADE_DEPLOYMENT_CONTROLLER_V2
|
|
"""
|
|
|
|
return {
|
|
"Image": image_id,
|
|
"User": "0:0",
|
|
"Cmd": ["/bin/bash", "-lc", command],
|
|
"Labels": {
|
|
HELPER_LABEL: "true",
|
|
"itworx.source.repository": SOURCE_REPOSITORY,
|
|
"itworx.source.revision": revision,
|
|
"itworx.controller.version": CONTROLLER_VERSION,
|
|
},
|
|
"HostConfig": {
|
|
"Binds": [
|
|
"/mnt/cache:/mnt/cache",
|
|
"/mnt/user:/mnt/user",
|
|
"/srv/example/appdata/gitea/git/repositories:/gitea-repositories:ro",
|
|
"/var/run/docker.sock:/var/run/docker.sock",
|
|
],
|
|
"NetworkMode": "host",
|
|
"Privileged": False,
|
|
"SecurityOpt": ["no-new-privileges:true"],
|
|
"RestartPolicy": {"Name": "no"},
|
|
},
|
|
}
|
|
'''
|
|
|
|
|
|
def _legacy_helper_spec() -> dict[str, Any]:
|
|
tree = ast.parse(LEGACY_HELPER_SOURCE_FIXTURE)
|
|
function = next(
|
|
node
|
|
for node in tree.body
|
|
if isinstance(node, ast.FunctionDef)
|
|
and node.name == "helper_container_spec"
|
|
)
|
|
namespace: dict[str, Any] = {
|
|
"re": re,
|
|
"Any": Any,
|
|
"_REVISION": re.compile(r"^[0-9a-f]{40}$"),
|
|
"_IMAGE_ID": re.compile(r"^sha256:[0-9a-f]{64}$"),
|
|
"SOURCE_REPOSITORY": "jens/unraid-appops-gateway",
|
|
"HELPER_NAME": "itworx-controller-artifact-promoter",
|
|
"CONTROLLER_VERSION": "2.1.5",
|
|
"HELPER_LABEL": "itworx.controller.promoter",
|
|
}
|
|
exec(
|
|
compile(ast.Module(body=[function], type_ignores=[]), "legacy-promoter", "exec"),
|
|
namespace,
|
|
)
|
|
return namespace["helper_container_spec"](revision=REVISION, image_id=CONTROLLER_IMAGE_ID)
|
|
|
|
|
|
def _image(labels: dict[str, str]) -> dict[str, Any]:
|
|
return {"Id": APPOPS_IMAGE_ID, "Config": {"Labels": dict(labels)}}
|
|
|
|
|
|
def _appops_labels() -> dict[str, str]:
|
|
return {
|
|
"org.opencontainers.image.source": "https://gitea.example.com/Jens/Unraid-AppOps-Gateway",
|
|
"org.opencontainers.image.revision": LEGACY_REVISION,
|
|
"org.opencontainers.image.source-tree": LEGACY_TREE,
|
|
"org.opencontainers.image.version": "1.8.0-platform-5553c04ce497",
|
|
"org.opencontainers.image.created": "2026-10-01T16:32:01Z",
|
|
"com.docker.compose.project": "unraid-appops-gateway",
|
|
"com.docker.compose.service": "appops-gateway",
|
|
"com.docker.compose.project.working_dir": "/srv/example/appdata/unraid-appops-gateway/source",
|
|
"com.docker.compose.project.config_files": "/srv/example/appdata/unraid-appops-gateway/source/docker-compose.yml",
|
|
}
|
|
|
|
|
|
def _controller_labels() -> dict[str, str]:
|
|
return {
|
|
"io.buildah.version": "1.35.0",
|
|
"org.opencontainers.image.authors": "Gitea",
|
|
"org.opencontainers.image.created": "2026-10-01T00:01:57Z",
|
|
"org.opencontainers.image.description": "Official docker images used by Gitea runner to run workflows.",
|
|
"org.opencontainers.image.documentation": "https://gitea.com/gitea/runner-images/src/branch/main/README.md",
|
|
"org.opencontainers.image.licenses": "MIT",
|
|
"org.opencontainers.image.revision": "317f8e3c510e5ec773a103f5d9a9e5769105c59b",
|
|
"org.opencontainers.image.source": "https://gitea.com/gitea/runner-images",
|
|
"org.opencontainers.image.title": "Official Gitea Actions runner images",
|
|
"org.opencontainers.image.url": "https://gitea.com/gitea/runner-images",
|
|
"org.opencontainers.image.vendor": "Gitea",
|
|
"org.opencontainers.image.version": "v26.10.01",
|
|
}
|
|
|
|
|
|
def _controller_container_labels() -> dict[str, str]:
|
|
return {
|
|
**_controller_labels(),
|
|
"com.docker.compose.config-hash": "577d77247471213db8a5c465ee2450765ba52cb4048f983c42083ac45a16ddb7",
|
|
"com.docker.compose.container-number": "1",
|
|
"com.docker.compose.depends_on": "",
|
|
"com.docker.compose.image": "sha256:adbb51b2873bf05546019c341caf6fd4368159546c36a5aea81b4d8bf088804e",
|
|
"com.docker.compose.oneoff": "False",
|
|
"com.docker.compose.project": "gitea-deploy-runners",
|
|
"com.docker.compose.project.config_files": "/mnt/cache/appdata/gitea-deploy-runners/docker-compose.yml",
|
|
"com.docker.compose.project.working_dir": "/mnt/cache/appdata/gitea-deploy-runners",
|
|
"com.docker.compose.replace": "gitea-deploy-control",
|
|
"com.docker.compose.service": "deploy-control",
|
|
"com.docker.compose.version": "2.40.3",
|
|
"tech.itworx.deployment.target": "unraid",
|
|
"tech.itworx.role": "gitea-deploy-controller",
|
|
"tech.itworx.gitea.scope": "Jens",
|
|
}
|
|
|
|
|
|
def _modern_controller_labels() -> dict[str, str]:
|
|
return {
|
|
**_controller_labels(),
|
|
"com.docker.compose.config-hash": "a" * 64,
|
|
"com.docker.compose.container-number": "1",
|
|
"com.docker.compose.depends_on": "",
|
|
"com.docker.compose.image": CONTROLLER_IMAGE_ID,
|
|
"com.docker.compose.oneoff": "False",
|
|
"com.docker.compose.project": "gitea-deploy-runners",
|
|
"com.docker.compose.project.config_files": "-",
|
|
"com.docker.compose.project.environment_file": "/dev/null",
|
|
"com.docker.compose.project.working_dir": "/var/lib/docker/volumes/itworx-controller-hub-releases/_data",
|
|
"com.docker.compose.replace": "gitea-deploy-control",
|
|
"com.docker.compose.service": "deploy-control",
|
|
"com.docker.compose.version": "2.40.3",
|
|
"tech.itworx.deployment.target": "unraid",
|
|
"tech.itworx.gitea.scope": "Jens",
|
|
"tech.itworx.role": "gitea-deploy-controller",
|
|
}
|
|
|
|
|
|
def _modern_controller_mounts(source_revision: str) -> list[dict[str, Any]]:
|
|
artifact_source = (
|
|
"/var/lib/docker/volumes/itworx-controller-hub-releases/_data/"
|
|
f"controller-artifacts/{source_revision}"
|
|
)
|
|
return [
|
|
{"Type": "bind", "Source": "/boot", "Destination": "/boot", "RW": True, "Propagation": "rprivate"},
|
|
{"Type": "bind", "Source": "/var/lib/docker", "Destination": "/var/lib/docker", "RW": False, "Propagation": "rslave"},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/srv/example/appdata/gitea/git/repositories",
|
|
"Destination": "/gitea-repositories",
|
|
"RW": False,
|
|
"Propagation": "rprivate",
|
|
},
|
|
{"Type": "bind", "Source": artifact_source, "Destination": "/opt/gitea-deploy", "RW": False, "Propagation": "rprivate"},
|
|
{"Type": "bind", "Source": "/var/run/docker.sock", "Destination": "/var/run/docker.sock", "RW": True, "Propagation": "rprivate"},
|
|
{"Type": "bind", "Source": "/mnt/user", "Destination": "/mnt/user", "RW": True, "Propagation": "rprivate"},
|
|
{"Type": "bind", "Source": "/mnt/cache", "Destination": "/mnt/cache", "RW": True, "Propagation": "rprivate"},
|
|
{"Type": "bind", "Source": "/mnt/disks", "Destination": "/mnt/disks", "RW": False, "Propagation": "rprivate"},
|
|
{
|
|
"Type": "volume",
|
|
"Name": "itworx-controller-hub-releases",
|
|
"Source": "/var/lib/docker/volumes/itworx-controller-hub-releases/_data",
|
|
"Destination": "/controller-hub-releases",
|
|
"RW": True,
|
|
"Propagation": "",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/var/lib/docker/volumes/itworx-controller-hub-releases/_data/controller-config",
|
|
"Destination": "/controller-config",
|
|
"RW": False,
|
|
"Propagation": "rprivate",
|
|
},
|
|
]
|
|
|
|
|
|
def _modern_controller_inspect(source_revision: str) -> dict[str, Any]:
|
|
controller = _controller_inspect()
|
|
controller.update({"Id": "e" * 64, "Image": CONTROLLER_IMAGE_ID})
|
|
controller["Config"]["Image"] = CONTROLLER_IMAGE_ID
|
|
controller["Config"]["Labels"] = _modern_controller_labels()
|
|
controller["Mounts"] = _modern_controller_mounts(source_revision)
|
|
return controller
|
|
|
|
|
|
def _appops_inspect() -> dict[str, Any]:
|
|
labels = _appops_labels()
|
|
return {
|
|
"Id": APPOPS_CONTAINER_ID,
|
|
"Image": APPOPS_IMAGE_ID,
|
|
"Name": "/Unraid-AppOps-Gateway",
|
|
"State": {"Running": True, "Health": {"Status": "healthy"}},
|
|
"Config": {
|
|
"Hostname": "unraid-appops-gateway",
|
|
"Image": "unraid-appops-gateway:platform-5553c04ce497",
|
|
"Labels": labels,
|
|
},
|
|
"HostConfig": {"RestartPolicy": {"Name": "unless-stopped"}},
|
|
"Mounts": [
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/srv/example/appdata/unraid-appops-gateway-1216",
|
|
"Destination": "/data",
|
|
"RW": True,
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/srv/example/appdata/comfyui-nvidia/basedir/models",
|
|
"Destination": "/mnt/privatemuse-comfyui-models",
|
|
"RW": True,
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _controller_inspect() -> dict[str, Any]:
|
|
return {
|
|
"Id": CONTROLLER_CONTAINER_ID,
|
|
"Image": CONTROLLER_IMAGE_ID,
|
|
"Name": "/gitea-deploy-control",
|
|
"State": {"Running": True},
|
|
"Config": {"Image": CONTROLLER_IMAGE_REF, "Labels": _controller_container_labels()},
|
|
"HostConfig": {
|
|
"Binds": [
|
|
"/mnt/user:/mnt/user:rw",
|
|
"/mnt/cache:/mnt/cache:rw",
|
|
"/mnt/disks:/mnt/disks:ro",
|
|
"/boot:/boot:rw",
|
|
"/var/lib/docker:/var/lib/docker:ro",
|
|
"/srv/example/appdata/gitea/git/repositories:/gitea-repositories:ro",
|
|
"/mnt/cache/appdata/gitea-deploy-runners/controller:/opt/gitea-deploy:ro",
|
|
"/var/run/docker.sock:/var/run/docker.sock:rw",
|
|
],
|
|
"Mounts": None,
|
|
},
|
|
"Mounts": [
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/boot",
|
|
"Destination": "/boot",
|
|
"RW": True,
|
|
"Propagation": "rprivate",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/var/lib/docker",
|
|
"Destination": "/var/lib/docker",
|
|
"RW": False,
|
|
"Propagation": "rslave",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/srv/example/appdata/gitea/git/repositories",
|
|
"Destination": "/gitea-repositories",
|
|
"RW": False,
|
|
"Propagation": "rprivate",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/mnt/cache/appdata/gitea-deploy-runners/controller",
|
|
"Destination": "/opt/gitea-deploy",
|
|
"RW": False,
|
|
"Propagation": "rprivate",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/var/run/docker.sock",
|
|
"Destination": "/var/run/docker.sock",
|
|
"RW": True,
|
|
"Propagation": "rprivate",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/mnt/user",
|
|
"Destination": "/mnt/user",
|
|
"RW": True,
|
|
"Propagation": "rprivate",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/mnt/cache",
|
|
"Destination": "/mnt/cache",
|
|
"RW": True,
|
|
"Propagation": "rprivate",
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/mnt/disks",
|
|
"Destination": "/mnt/disks",
|
|
"RW": False,
|
|
"Propagation": "rprivate",
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _inspect_from_spec(
|
|
name: str,
|
|
identifier: str,
|
|
image_id: str,
|
|
spec: dict[str, Any],
|
|
*,
|
|
state: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"Id": identifier,
|
|
"Image": image_id,
|
|
"Name": f"/{name}",
|
|
"State": dict(state),
|
|
"Config": {
|
|
"Image": spec["Image"],
|
|
"User": spec["User"],
|
|
"Cmd": spec["Cmd"],
|
|
"Labels": {**_controller_labels(), **spec["Labels"]},
|
|
"Hostname": identifier[:12],
|
|
},
|
|
"HostConfig": {"ReadonlyRootfs": False, **spec["HostConfig"]},
|
|
}
|
|
|
|
|
|
class FakeDocker:
|
|
def __init__(self, caller_spec: dict[str, Any], *, existing_child: dict[str, Any] | None = None):
|
|
self.containers = {
|
|
APPOPS_CONTAINER_ID: _appops_inspect(),
|
|
"gitea-deploy-control": _controller_inspect(),
|
|
CALLER_ID[:12]: _inspect_from_spec(
|
|
"itworx-controller-artifact-promoter",
|
|
CALLER_ID,
|
|
CONTROLLER_IMAGE_ID,
|
|
caller_spec,
|
|
state={"Running": True},
|
|
),
|
|
}
|
|
self.images = {
|
|
APPOPS_IMAGE_ID: _image(_appops_labels()),
|
|
CONTROLLER_IMAGE_ID: {
|
|
"Id": CONTROLLER_IMAGE_ID,
|
|
"Config": {"Labels": _controller_labels()},
|
|
},
|
|
}
|
|
self.existing_child = existing_child
|
|
self.created: list[dict[str, Any]] = []
|
|
self.started: list[str] = []
|
|
self.log_bytes = b"controller_v2_ready revision=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"
|
|
|
|
def inspect_container(self, reference: str) -> dict[str, Any] | None:
|
|
if reference == "itworx-controller-artifact-promoter-modern":
|
|
return self.existing_child
|
|
return self.containers.get(reference)
|
|
|
|
def inspect_image(self, image_id: str) -> dict[str, Any] | None:
|
|
return self.images.get(image_id)
|
|
|
|
def create_container(self, name: str, spec: dict[str, Any]) -> str:
|
|
assert name == "itworx-controller-artifact-promoter-modern"
|
|
self.created.append(spec)
|
|
container = _inspect_from_spec(
|
|
name,
|
|
MODERN_HELPER_ID,
|
|
spec["Image"],
|
|
spec,
|
|
state={"Running": False, "ExitCode": 0, "Status": "exited"},
|
|
)
|
|
self.containers[name] = container
|
|
self.containers[MODERN_HELPER_ID] = container
|
|
return MODERN_HELPER_ID
|
|
|
|
def start_container(self, identifier: str) -> None:
|
|
self.started.append(identifier)
|
|
source_revision = self.created[-1]["Labels"]["itworx.source.revision"]
|
|
self.containers["gitea-deploy-control"] = _modern_controller_inspect(source_revision)
|
|
|
|
def container_logs(self, identifier: str, *, tail: int, max_bytes: int) -> bytes:
|
|
assert identifier == MODERN_HELPER_ID
|
|
assert tail <= 40
|
|
assert max_bytes <= 8192
|
|
return self.log_bytes[:max_bytes]
|
|
|
|
|
|
class FakeProtectedGit:
|
|
def __init__(self, helper_source: str):
|
|
self.revision = REVISION
|
|
self.tree = TREE
|
|
self.helper_source = helper_source
|
|
self.blobs_read: list[tuple[str, str]] = []
|
|
self.main_checks = 0
|
|
|
|
def verify_current_main(self, revision: str, tree: str) -> None:
|
|
self.main_checks += 1
|
|
if (revision, tree) != (self.revision, self.tree):
|
|
raise bootstrap.BootstrapError("controller_promoter_source_not_current_main")
|
|
|
|
def verify_legacy_revision(self, revision: str, tree: str) -> None:
|
|
if (revision, tree) != (LEGACY_REVISION, LEGACY_TREE):
|
|
raise bootstrap.BootstrapError("controller_promoter_legacy_provenance_invalid")
|
|
|
|
def read_blob(self, revision: str, path: str) -> bytes:
|
|
self.blobs_read.append((revision, path))
|
|
if revision != self.revision or path != "backend/app/services/controller_artifact_helper_spec.py":
|
|
raise bootstrap.BootstrapError("controller_promoter_source_blob_invalid")
|
|
return self.helper_source.encode("utf-8")
|
|
|
|
def read_legacy_blob(self, revision: str, path: str) -> bytes:
|
|
self.blobs_read.append((revision, path))
|
|
if (revision, path) != (
|
|
LEGACY_REVISION,
|
|
"backend/app/services/controller_artifact_promoter.py",
|
|
):
|
|
raise bootstrap.BootstrapError("controller_promoter_legacy_blob_invalid")
|
|
return LEGACY_HELPER_SOURCE_FIXTURE.encode("utf-8")
|
|
|
|
|
|
def _health(
|
|
*,
|
|
revision: str = REVISION,
|
|
tree: str = TREE,
|
|
version: str = "2.1.14",
|
|
artifact_sha256: str = "1" * 64,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"status": "ok",
|
|
"protocolVersion": "itworx-controller-request/v2",
|
|
"artifact": {
|
|
"apiVersion": "itworx.controller-artifact/v1",
|
|
"sourceRepository": "jens/unraid-appops-gateway",
|
|
"sourceRevision": revision,
|
|
"sourceTree": tree,
|
|
"controllerVersion": version,
|
|
"artifactSha256": artifact_sha256,
|
|
},
|
|
"delivery": {
|
|
"health": {
|
|
"configuration": "healthy",
|
|
"inbox": "healthy",
|
|
"artifact": "healthy",
|
|
}
|
|
},
|
|
}
|
|
|
|
|
|
def _run_bootstrap(
|
|
docker: FakeDocker,
|
|
git: FakeProtectedGit,
|
|
*,
|
|
health: dict[str, Any] | None = None,
|
|
initial_health: dict[str, Any] | None = None,
|
|
hostname: str = CALLER_ID[:12],
|
|
):
|
|
source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
git.helper_source = source
|
|
initial = initial_health or _health(
|
|
revision=CONTROLLER_REVISION,
|
|
tree=CONTROLLER_TREE,
|
|
version="2.1.12",
|
|
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
|
)
|
|
health_reads = [initial, health or _health()]
|
|
|
|
def read_health() -> dict[str, Any]:
|
|
return health_reads.pop(0) if health_reads else health or _health()
|
|
|
|
return bootstrap.run_bootstrap(
|
|
"jens/unraid-appops-gateway",
|
|
REVISION,
|
|
TREE,
|
|
docker=docker,
|
|
protected_git=git,
|
|
hostname=hostname,
|
|
health_reader=read_health,
|
|
monotonic=lambda: 0.0,
|
|
sleeper=lambda _seconds: None,
|
|
child_timeout_seconds=0.0,
|
|
)
|
|
|
|
|
|
def test_initial_controller_runtime_accepts_exact_current_compose_container():
|
|
docker = FakeDocker(_legacy_helper_spec())
|
|
base_image_labels = bootstrap._inspect_controller_base_image(docker)
|
|
health = _health(
|
|
revision=CONTROLLER_REVISION,
|
|
tree=CONTROLLER_TREE,
|
|
version="2.1.12",
|
|
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
|
)
|
|
|
|
assert (
|
|
bootstrap._verify_controller_runtime(
|
|
docker,
|
|
health_payload=health,
|
|
base_image_labels=base_image_labels,
|
|
)
|
|
== base_image_labels
|
|
)
|
|
|
|
|
|
def test_immutable_legacy_helper_source_fixture_passes_bootstrap_identity_guard():
|
|
legacy_spec = _legacy_helper_spec()
|
|
docker = FakeDocker(legacy_spec)
|
|
git = FakeProtectedGit("")
|
|
|
|
result = _run_bootstrap(docker, git)
|
|
|
|
assert result["status"] == "promoted"
|
|
assert docker.created
|
|
assert git.blobs_read == [
|
|
(REVISION, "backend/app/services/controller_artifact_helper_spec.py"),
|
|
(LEGACY_REVISION, "backend/app/services/controller_artifact_promoter.py"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
[
|
|
"caller-image",
|
|
"caller-label",
|
|
"caller-revision",
|
|
"caller-command",
|
|
"caller-bind",
|
|
"appops-image",
|
|
"appops-provenance",
|
|
"target-id",
|
|
"target-source",
|
|
"target-tree",
|
|
"target-version",
|
|
"old-target-id",
|
|
"old-target-image",
|
|
"old-target-version",
|
|
"target-config-image",
|
|
"target-compose-hash",
|
|
"target-compose-missing",
|
|
"target-compose-extra",
|
|
"target-compose-image",
|
|
"target-tech-label",
|
|
"target-tech-extra",
|
|
"target-path",
|
|
"target-extra-mount",
|
|
"target-bind-mode",
|
|
"base-image-installed-provenance",
|
|
],
|
|
)
|
|
def test_bootstrap_rejects_unknown_runtime_identity_without_creating_child(mutation: str):
|
|
docker = FakeDocker(_legacy_helper_spec())
|
|
if mutation == "caller-image":
|
|
docker.containers[CALLER_ID[:12]]["Image"] = "sha256:" + "9" * 64
|
|
elif mutation == "caller-label":
|
|
docker.containers[CALLER_ID[:12]]["Config"]["Labels"]["itworx.controller.version"] = "2.1.4"
|
|
elif mutation == "caller-revision":
|
|
docker.containers[CALLER_ID[:12]]["Config"]["Labels"]["itworx.source.revision"] = "d" * 40
|
|
elif mutation == "caller-command":
|
|
docker.containers[CALLER_ID[:12]]["Config"]["Cmd"].append("--image-id")
|
|
elif mutation == "caller-bind":
|
|
docker.containers[CALLER_ID[:12]]["HostConfig"]["Binds"].append("/tmp:/payload")
|
|
elif mutation == "appops-image":
|
|
docker.containers[APPOPS_CONTAINER_ID]["Image"] = "sha256:" + "8" * 64
|
|
elif mutation == "appops-provenance":
|
|
docker.images[APPOPS_IMAGE_ID]["Config"]["Labels"]["org.opencontainers.image.revision"] = "f" * 40
|
|
elif mutation == "target-id":
|
|
docker.containers["gitea-deploy-control"]["Id"] = "0" * 64
|
|
elif mutation == "old-target-id":
|
|
docker.containers["gitea-deploy-control"]["Id"] = "5539b672e66890e657d0f3b0e6e94f9c9ca5b47c245e120d330f6f1d5be73fae"
|
|
elif mutation == "old-target-image":
|
|
docker.containers["gitea-deploy-control"]["Image"] = "sha256:adbb51b2873bf05546019c341caf6fd4368159546c36a5aea81b4d8bf088804e"
|
|
elif mutation == "target-source":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["org.opencontainers.image.revision"] = "f" * 40
|
|
elif mutation == "target-tree":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["org.opencontainers.image.source-tree"] = "f" * 40
|
|
elif mutation == "target-version":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["org.opencontainers.image.version"] = "2.1.11"
|
|
elif mutation == "old-target-version":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["org.opencontainers.image.version"] = "v26.09.29"
|
|
elif mutation == "target-config-image":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Image"] = "docker.gitea.com/runner-images:ubuntu-22.04"
|
|
elif mutation == "target-compose-hash":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["com.docker.compose.config-hash"] = "0" * 64
|
|
elif mutation == "target-compose-missing":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"].pop("com.docker.compose.service")
|
|
elif mutation == "target-compose-extra":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["com.docker.compose.container_name"] = "gitea-deploy-control"
|
|
elif mutation == "target-compose-image":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["com.docker.compose.image"] = CONTROLLER_IMAGE_ID
|
|
elif mutation == "target-tech-label":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["tech.itworx.role"] = "other"
|
|
elif mutation == "target-tech-extra":
|
|
docker.containers["gitea-deploy-control"]["Config"]["Labels"]["tech.itworx.extra"] = "unexpected"
|
|
elif mutation == "target-path":
|
|
docker.containers["gitea-deploy-control"]["Mounts"][3]["Destination"] = "/controller-config"
|
|
elif mutation == "target-extra-mount":
|
|
docker.containers["gitea-deploy-control"]["Mounts"].append(
|
|
{
|
|
"Type": "bind",
|
|
"Source": "/tmp",
|
|
"Destination": "/tmp",
|
|
"RW": True,
|
|
"Propagation": "rprivate",
|
|
}
|
|
)
|
|
docker.containers["gitea-deploy-control"]["HostConfig"]["Binds"].append(
|
|
"/tmp:/tmp:rw"
|
|
)
|
|
elif mutation == "target-bind-mode":
|
|
docker.containers["gitea-deploy-control"]["HostConfig"]["Binds"][6] = (
|
|
"/mnt/cache/appdata/gitea-deploy-runners/controller:/opt/gitea-deploy:rw"
|
|
)
|
|
docker.containers["gitea-deploy-control"]["Mounts"][3]["RW"] = True
|
|
elif mutation == "base-image-installed-provenance":
|
|
labels = docker.images[CONTROLLER_IMAGE_ID]["Config"]["Labels"]
|
|
labels["org.opencontainers.image.revision"] = CONTROLLER_REVISION
|
|
labels["org.opencontainers.image.source-tree"] = CONTROLLER_TREE
|
|
git = FakeProtectedGit("")
|
|
|
|
with pytest.raises(bootstrap.BootstrapError):
|
|
_run_bootstrap(docker, git)
|
|
|
|
assert docker.created == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
["revision", "tree", "version", "artifact-sha", "delivery-health"],
|
|
)
|
|
def test_bootstrap_requires_exact_mounted_controller_artifact_health(mutation: str):
|
|
initial = _health(
|
|
revision=CONTROLLER_REVISION,
|
|
tree=CONTROLLER_TREE,
|
|
version="2.1.12",
|
|
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
|
)
|
|
artifact = initial["artifact"]
|
|
if mutation == "revision":
|
|
artifact["sourceRevision"] = "9" * 40
|
|
elif mutation == "tree":
|
|
artifact["sourceTree"] = "9" * 40
|
|
elif mutation == "version":
|
|
artifact["controllerVersion"] = "2.1.11"
|
|
elif mutation == "artifact-sha":
|
|
artifact["artifactSha256"] = "9" * 64
|
|
elif mutation == "delivery-health":
|
|
initial["delivery"]["health"]["artifact"] = "degraded"
|
|
|
|
docker = FakeDocker(_legacy_helper_spec())
|
|
git = FakeProtectedGit("")
|
|
with pytest.raises(bootstrap.BootstrapError):
|
|
_run_bootstrap(docker, git, initial_health=initial)
|
|
assert docker.created == []
|
|
|
|
|
|
def test_running_modern_child_returns_unknown_without_duplicate_or_stop():
|
|
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
helper_spec = bootstrap.render_modern_helper_spec(
|
|
helper_source,
|
|
revision=REVISION,
|
|
image_id=CONTROLLER_IMAGE_ID,
|
|
current_controller_id=CONTROLLER_CONTAINER_ID,
|
|
current_controller_revision=CONTROLLER_REVISION,
|
|
current_controller_tree=CONTROLLER_TREE,
|
|
current_controller_version="2.1.12",
|
|
)
|
|
existing = _inspect_from_spec(
|
|
"itworx-controller-artifact-promoter-modern",
|
|
MODERN_HELPER_ID,
|
|
CONTROLLER_IMAGE_ID,
|
|
helper_spec,
|
|
state={"Running": True, "Status": "running"},
|
|
)
|
|
docker = FakeDocker(_legacy_helper_spec(), existing_child=existing)
|
|
git = FakeProtectedGit(helper_source)
|
|
|
|
result = _run_bootstrap(docker, git, health=_health())
|
|
|
|
assert result["status"] == "outcome_unknown"
|
|
assert result["reason"] == "controller_promoter_modern_helper_running"
|
|
assert docker.created == []
|
|
assert docker.started == []
|
|
|
|
|
|
def test_completed_modern_child_with_wrong_artifact_is_not_relaunched():
|
|
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
helper_spec = bootstrap.render_modern_helper_spec(
|
|
helper_source,
|
|
revision=REVISION,
|
|
image_id=CONTROLLER_IMAGE_ID,
|
|
current_controller_id=CONTROLLER_CONTAINER_ID,
|
|
current_controller_revision=CONTROLLER_REVISION,
|
|
current_controller_tree=CONTROLLER_TREE,
|
|
current_controller_version="2.1.12",
|
|
)
|
|
existing = _inspect_from_spec(
|
|
"itworx-controller-artifact-promoter-modern",
|
|
MODERN_HELPER_ID,
|
|
CONTROLLER_IMAGE_ID,
|
|
helper_spec,
|
|
state={"Running": False, "ExitCode": 0, "Status": "exited"},
|
|
)
|
|
docker = FakeDocker(_legacy_helper_spec(), existing_child=existing)
|
|
git = FakeProtectedGit(helper_source)
|
|
|
|
result = _run_bootstrap(
|
|
docker,
|
|
git,
|
|
initial_health=_health(tree="9" * 40),
|
|
)
|
|
|
|
assert result["status"] == "outcome_unknown"
|
|
assert result["reason"] == "controller_promoter_postcheck_failed"
|
|
assert docker.created == []
|
|
|
|
|
|
def test_completed_child_uses_new_health_after_controller_recreation():
|
|
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
helper_spec = bootstrap.render_modern_helper_spec(
|
|
helper_source,
|
|
revision=REVISION,
|
|
image_id=CONTROLLER_IMAGE_ID,
|
|
current_controller_id=CONTROLLER_CONTAINER_ID,
|
|
current_controller_revision=CONTROLLER_REVISION,
|
|
current_controller_tree=CONTROLLER_TREE,
|
|
current_controller_version="2.1.12",
|
|
)
|
|
existing = _inspect_from_spec(
|
|
"itworx-controller-artifact-promoter-modern",
|
|
MODERN_HELPER_ID,
|
|
CONTROLLER_IMAGE_ID,
|
|
helper_spec,
|
|
state={"Running": False, "ExitCode": 0, "Status": "exited"},
|
|
)
|
|
docker = FakeDocker(_legacy_helper_spec(), existing_child=existing)
|
|
docker.containers["gitea-deploy-control"] = _modern_controller_inspect(REVISION)
|
|
git = FakeProtectedGit(helper_source)
|
|
|
|
result = _run_bootstrap(
|
|
docker,
|
|
git,
|
|
initial_health=_health(),
|
|
)
|
|
|
|
assert result["status"] == "already_applied"
|
|
assert result["revision"] == REVISION
|
|
assert docker.created == []
|
|
assert docker.started == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
[
|
|
"post-image-config",
|
|
"post-compose-hash",
|
|
"post-compose-image",
|
|
"post-compose-extra",
|
|
"post-compose-environment-file",
|
|
"post-tech-label",
|
|
"post-tech-extra",
|
|
"post-artifact-revision",
|
|
"post-volume-name",
|
|
"post-volume-source",
|
|
"post-volume-mode",
|
|
"post-config-bind-source",
|
|
"post-config-bind-mode",
|
|
"post-extra-mount",
|
|
],
|
|
)
|
|
def test_postactivation_controller_requires_exact_compose_runtime(mutation: str):
|
|
docker = FakeDocker(_legacy_helper_spec())
|
|
controller = _modern_controller_inspect(REVISION)
|
|
docker.containers["gitea-deploy-control"] = controller
|
|
|
|
if mutation == "post-image-config":
|
|
controller["Config"]["Image"] = CONTROLLER_IMAGE_REF
|
|
elif mutation == "post-compose-hash":
|
|
controller["Config"]["Labels"]["com.docker.compose.config-hash"] = "bad"
|
|
elif mutation == "post-compose-image":
|
|
controller["Config"]["Labels"]["com.docker.compose.image"] = "sha256:" + "d" * 64
|
|
elif mutation == "post-compose-extra":
|
|
controller["Config"]["Labels"]["com.docker.compose.extra"] = "unexpected"
|
|
elif mutation == "post-compose-environment-file":
|
|
controller["Config"]["Labels"]["com.docker.compose.project.environment_file"] = "/tmp/.env"
|
|
elif mutation == "post-tech-label":
|
|
controller["Config"]["Labels"]["tech.itworx.gitea.scope"] = "Other"
|
|
elif mutation == "post-tech-extra":
|
|
controller["Config"]["Labels"]["tech.itworx.extra"] = "unexpected"
|
|
elif mutation == "post-artifact-revision":
|
|
controller["Mounts"][3]["Source"] = (
|
|
"/var/lib/docker/volumes/itworx-controller-hub-releases/_data/"
|
|
f"controller-artifacts/{'d' * 40}"
|
|
)
|
|
elif mutation == "post-volume-name":
|
|
controller["Mounts"][8]["Name"] = "foreign-volume"
|
|
elif mutation == "post-volume-source":
|
|
controller["Mounts"][8]["Source"] = "/tmp/foreign-volume"
|
|
elif mutation == "post-volume-mode":
|
|
controller["Mounts"][8]["RW"] = False
|
|
elif mutation == "post-config-bind-source":
|
|
controller["Mounts"][9]["Source"] = "/tmp/config"
|
|
elif mutation == "post-config-bind-mode":
|
|
controller["Mounts"][9]["RW"] = True
|
|
elif mutation == "post-extra-mount":
|
|
controller["Mounts"].append(dict(controller["Mounts"][0]))
|
|
|
|
assert not bootstrap._verified_live_controller(docker, source_revision=REVISION)
|
|
|
|
|
|
def test_postactivation_accepts_well_formed_compose_hash():
|
|
docker = FakeDocker(_legacy_helper_spec())
|
|
controller = _modern_controller_inspect(REVISION)
|
|
controller["Config"]["Labels"]["com.docker.compose.config-hash"] = "b" * 64
|
|
docker.containers["gitea-deploy-control"] = controller
|
|
|
|
assert bootstrap._verified_live_controller(docker, source_revision=REVISION)
|
|
|
|
|
|
def test_successful_child_requires_exact_source_tree_version_and_health():
|
|
docker = FakeDocker(_legacy_helper_spec())
|
|
git = FakeProtectedGit("")
|
|
|
|
result = _run_bootstrap(docker, git)
|
|
|
|
assert result["status"] == "promoted"
|
|
assert result["revision"] == REVISION
|
|
assert result["controllerVersion"] == "2.1.14"
|
|
assert result["artifactSha256"] == "1" * 64
|
|
assert docker.started == [MODERN_HELPER_ID]
|
|
assert docker.created[0]["Image"] == CONTROLLER_IMAGE_ID
|
|
assert docker.created[0]["HostConfig"]["ReadonlyRootfs"] is True
|
|
|
|
|
|
def test_child_logs_are_bounded_and_redacted_before_result_output():
|
|
docker = FakeDocker(_legacy_helper_spec())
|
|
docker.log_bytes = (
|
|
b"controller_v2_ready revision=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"
|
|
b"GITEA_ADMIN_TOKEN=very-secret-value\n"
|
|
)
|
|
git = FakeProtectedGit("")
|
|
|
|
result = _run_bootstrap(docker, git)
|
|
|
|
assert result["status"] == "promoted"
|
|
assert "very-secret-value" not in result["logTail"]
|
|
assert "[redacted]" in result["logTail"]
|
|
assert len(result["logTail"].encode("utf-8")) <= 4096
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"temporary_root_info,accepted",
|
|
[
|
|
("1777:0:0", True),
|
|
("700:0:0", True),
|
|
("777:0:0", False),
|
|
("755:0:0", False),
|
|
("770:0:0", False),
|
|
("1700:0:0", False),
|
|
("1777:1000:0", False),
|
|
("700:0:1000", False),
|
|
],
|
|
)
|
|
def test_legacy_upgrade_signature_fetches_bootstrap_only_from_protected_git(
|
|
tmp_path: Path, temporary_root_info: str, accepted: bool
|
|
):
|
|
bash = shutil.which("bash")
|
|
if bash is None:
|
|
pytest.skip("bash is required to exercise the upgrade script")
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
git_trace = tmp_path / "git-args.jsonl"
|
|
python_trace = tmp_path / "python-args.json"
|
|
protected_source = "# trusted protected Git bootstrap source\n"
|
|
git_shim = bin_dir / "git"
|
|
git_shim.write_text(
|
|
f"#!{sys.executable}\n"
|
|
"import json, os, sys\n"
|
|
f"trace = {str(git_trace)!r}\n"
|
|
f"with open(trace, 'a', encoding='utf-8') as stream: stream.write(json.dumps(sys.argv[1:]) + '\\n')\n"
|
|
"args = [arg for arg in sys.argv[1:] if not arg.startswith('--git-dir=')]\n"
|
|
f"revision = {REVISION!r}\n"
|
|
f"tree = {TREE!r}\n"
|
|
"if args[-3:] == ['rev-parse', 'refs/heads/main', '--'] or args[-2:] == ['rev-parse', 'refs/heads/main']: print(revision)\n"
|
|
"elif any(arg.endswith('^{tree}') for arg in args): print(tree)\n"
|
|
f"elif len(args) >= 3 and args[:2] == ['cat-file', '-s']: print({len(protected_source)})\n"
|
|
"elif len(args) >= 3 and args[0] == 'cat-file' and args[1] == 'blob' and args[-1].endswith(':scripts/bootstrap_controller_artifact_promoter.py'):\n"
|
|
f" sys.stdout.write({protected_source!r})\n"
|
|
"else: raise SystemExit(91)\n",
|
|
encoding="utf-8",
|
|
)
|
|
stat_shim = bin_dir / "stat"
|
|
stat_shim.write_text(
|
|
f"#!{sys.executable}\n"
|
|
"import pathlib, sys\n"
|
|
"args = sys.argv[1:]\n"
|
|
"target = args[-1]\n"
|
|
f"if target == '/tmp': print({temporary_root_info!r})\n"
|
|
"elif args[1] == '%u:%a': print('0:700')\n"
|
|
"elif args[1] == '%u:%a:%s': print('0:600:' + str(pathlib.Path(target).stat().st_size))\n"
|
|
"else: raise SystemExit(1)\n",
|
|
encoding="utf-8",
|
|
)
|
|
python_shim = bin_dir / "python3"
|
|
python_shim.write_text(
|
|
f"#!{sys.executable}\n"
|
|
"import json, pathlib, sys\n"
|
|
f"path = pathlib.Path({str(python_trace)!r})\n"
|
|
"path.write_text(json.dumps({'script': pathlib.Path(sys.argv[1]).read_text(), 'args': sys.argv[2:]}), encoding='utf-8')\n",
|
|
encoding="utf-8",
|
|
)
|
|
git_shim.chmod(0o755)
|
|
stat_shim.chmod(0o755)
|
|
python_shim.chmod(0o755)
|
|
archive = f"/mnt/cache/appdata/gitea-deploy-runners/.controller-promoter-{REVISION[:12]}/controller.tar"
|
|
result = subprocess.run(
|
|
[
|
|
bash,
|
|
str(ROOT / "scripts/upgrade_deployment_controller.sh"),
|
|
"--archive",
|
|
archive,
|
|
"--archive-sha256",
|
|
"a" * 64,
|
|
"--source-repository",
|
|
"jens/unraid-appops-gateway",
|
|
"--source-revision",
|
|
REVISION,
|
|
"--source-tree",
|
|
TREE,
|
|
"--controller-version",
|
|
"2.1.5",
|
|
"--confirm",
|
|
"UPGRADE_DEPLOYMENT_CONTROLLER_V2",
|
|
],
|
|
cwd=ROOT,
|
|
env={**os.environ, "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}"},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
if not accepted:
|
|
assert result.returncode == 66, result.stderr
|
|
assert "temporary root is unsafe" in result.stderr
|
|
assert not python_trace.exists()
|
|
calls = [json.loads(line) for line in git_trace.read_text(encoding="utf-8").splitlines()]
|
|
assert not any("cat-file" in args for args in calls)
|
|
return
|
|
assert result.returncode == 0, result.stderr
|
|
bootstrap_call = json.loads(python_trace.read_text(encoding="utf-8"))
|
|
assert bootstrap_call["script"] == protected_source
|
|
assert bootstrap_call["args"] == [
|
|
"--source-repository",
|
|
"jens/unraid-appops-gateway",
|
|
"--source-revision",
|
|
REVISION,
|
|
"--source-tree",
|
|
TREE,
|
|
]
|
|
calls = [json.loads(line) for line in git_trace.read_text(encoding="utf-8").splitlines()]
|
|
assert any(args[-1:] == [f"{REVISION}:scripts/bootstrap_controller_artifact_promoter.py"] for args in calls)
|
|
assert all(archive not in args for args in calls)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutation",
|
|
["source", "revision", "tree", "version", "extra", "image-id"],
|
|
)
|
|
def test_upgrade_script_rejects_mutated_legacy_call_without_bootstrap(
|
|
tmp_path: Path, mutation: str
|
|
):
|
|
bash = shutil.which("bash")
|
|
if bash is None:
|
|
pytest.skip("bash is required to exercise the upgrade script")
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
marker = tmp_path / "bootstrap-ran"
|
|
python_shim = bin_dir / "python3"
|
|
python_shim.write_text(
|
|
f"#!/usr/bin/env python3\nfrom pathlib import Path\nPath({str(marker)!r}).write_text('ran')\n",
|
|
encoding="utf-8",
|
|
)
|
|
python_shim.chmod(0o755)
|
|
args = [
|
|
"--archive",
|
|
f"/mnt/cache/appdata/gitea-deploy-runners/.controller-promoter-{REVISION[:12]}/controller.tar",
|
|
"--archive-sha256",
|
|
"a" * 64,
|
|
"--source-repository",
|
|
"jens/unraid-appops-gateway",
|
|
"--source-revision",
|
|
REVISION,
|
|
"--source-tree",
|
|
TREE,
|
|
"--controller-version",
|
|
"2.1.5",
|
|
"--confirm",
|
|
"UPGRADE_DEPLOYMENT_CONTROLLER_V2",
|
|
]
|
|
if mutation == "source":
|
|
args[args.index("jens/unraid-appops-gateway")] = "attacker/repo"
|
|
elif mutation == "revision":
|
|
args[args.index(REVISION)] = "A" * 40
|
|
elif mutation == "tree":
|
|
args[args.index(TREE)] = "f" * 40
|
|
elif mutation == "version":
|
|
args[args.index("2.1.5")] = "2.1.4"
|
|
elif mutation == "extra":
|
|
args.extend(["--unexpected", "value"])
|
|
elif mutation == "image-id":
|
|
args.extend(["--image-id", CONTROLLER_IMAGE_ID])
|
|
result = subprocess.run(
|
|
[bash, str(ROOT / "scripts/upgrade_deployment_controller.sh"), *args],
|
|
cwd=ROOT,
|
|
env={**os.environ, "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}"},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode != 0
|
|
assert not marker.exists()
|
|
|
|
|
|
@pytest.mark.parametrize("modern_call", ["missing-image", "shared-archive"])
|
|
@pytest.mark.parametrize("temporary_root_info", ["1777:0:0", "700:0:0", "777:0:0"])
|
|
def test_modern_upgrade_path_still_requires_private_archive_and_image_id(
|
|
tmp_path: Path, modern_call: str, temporary_root_info: str
|
|
):
|
|
bash = shutil.which("bash")
|
|
if bash is None:
|
|
pytest.skip("bash is required to exercise the upgrade script")
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
stat_shim = bin_dir / "stat"
|
|
stat_shim.write_text(
|
|
f"#!{sys.executable}\nimport sys\n"
|
|
f"if sys.argv[-1] == '/tmp': print({temporary_root_info!r})\n"
|
|
"else: raise SystemExit(91)\n",
|
|
encoding="utf-8",
|
|
)
|
|
stat_shim.chmod(0o755)
|
|
archive = (
|
|
f"/tmp/itworx-controller-artifact-promoter-{REVISION[:12]}.AAAAAA/controller.tar"
|
|
if modern_call == "missing-image"
|
|
else f"/mnt/cache/appdata/gitea-deploy-runners/.controller-promoter-{REVISION[:12]}/controller.tar"
|
|
)
|
|
args = [
|
|
"--archive",
|
|
archive,
|
|
"--archive-sha256",
|
|
"a" * 64,
|
|
"--source-repository",
|
|
"jens/unraid-appops-gateway",
|
|
"--source-revision",
|
|
REVISION,
|
|
"--source-tree",
|
|
TREE,
|
|
"--controller-version",
|
|
"2.1.14",
|
|
"--confirm",
|
|
"UPGRADE_DEPLOYMENT_CONTROLLER_V2",
|
|
]
|
|
if modern_call == "shared-archive":
|
|
args.extend(["--image-id", CONTROLLER_IMAGE_ID])
|
|
result = subprocess.run(
|
|
[bash, str(ROOT / "scripts/upgrade_deployment_controller.sh"), *args],
|
|
cwd=ROOT,
|
|
env={**os.environ, "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}"},
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
assert result.returncode != 0
|
|
if modern_call == "shared-archive":
|
|
if temporary_root_info == "777:0:0":
|
|
assert "helper temporary root is unsafe" in result.stderr
|
|
else:
|
|
assert "archive is outside the helper private temporary directory" in result.stderr
|
|
if modern_call == "missing-image":
|
|
assert "image ID" in result.stderr
|