Publish curated Unraid-AppOps-Gateway source from b6d6a3e1657000c6e0a482c8056a3fbebbafd711
This commit is contained in:
1 parent
2d50064a8f
commit
d825f5045d
11 files changed
+438
-43
No files matched your search
@@ -1,3 +1,3 @@
|
||||
# Public source export
|
||||
|
||||
This source snapshot was generated from the private canonical repository at revision `28831bd43867347a132f1c2b0f388632b9cb561e`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||
This source snapshot was generated from the private canonical repository at revision `b6d6a3e1657000c6e0a482c8056a3fbebbafd711`. It contains no private Git history or operational evidence. Changes are published from the canonical repository.
|
||||
+23
-13
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"sourceRevision": "28831bd43867347a132f1c2b0f388632b9cb561e",
|
||||
"sourceRevision": "b6d6a3e1657000c6e0a482c8056a3fbebbafd711",
|
||||
"files": [
|
||||
{
|
||||
"path": ".dockerignore",
|
||||
@@ -524,7 +524,7 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/app/services/controller_artifact_helper_spec.py",
|
||||
"sha256": "dc456d0005b7181098d99028fa8ea110fb53b037aac1c031c98a9101a61183bd",
|
||||
"sha256": "b6340734fcba86a341b2c29e7930ed30016d19352acd4a015d1e194c415655ac",
|
||||
"bytes": 6029
|
||||
},
|
||||
{
|
||||
@@ -872,6 +872,11 @@
|
||||
"sha256": "fa4f78d15225f1d0e3dad8f1cd7283dfbe17b411d5158ade8effb0111ff7efa7",
|
||||
"bytes": 1072
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/fixtures/controller_artifact_helper_spec_2_1_14.txt",
|
||||
"sha256": "1da47a3f061ef0b89158c9f646bb7018ce88b81c0687d8b27abe65b74df67fea",
|
||||
"bytes": 4948
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_ai_remediation_planner.py",
|
||||
"sha256": "784b83960cdff238e0ee25caaf83cfd1cb7c3cfc40976036b28bd256f6626e44",
|
||||
@@ -922,6 +927,11 @@
|
||||
"sha256": "447788187bb37bcaac7d99df8fdbe8caa76c0e71acea3c5c8f86faf3972d572d",
|
||||
"bytes": 16611
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_appops_staged_source_recovery.py",
|
||||
"sha256": "9c420f0d2355edee8232b420f322b60f69b701b68da22213e9207d98ca030611",
|
||||
"bytes": 13283
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_authentik_connector.py",
|
||||
"sha256": "54b24a90e7238aaed22d0dfbae70d9772006d20cdb9dbd43d781af48892dcd94",
|
||||
@@ -939,22 +949,22 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_artifact_bootstrap.py",
|
||||
"sha256": "3389f7cc8225ff2f03b46aa79a3a70dae7a6198d1ab52fd0879638bcf4c92f83",
|
||||
"bytes": 80172
|
||||
"sha256": "d9acae805eedcf50e90222ce52f3a43ad101e558d7c1b49d4f55968b208e957f",
|
||||
"bytes": 82161
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_artifact_bootstrap_created_child_recovery.py",
|
||||
"sha256": "a9613e4bad2d66cdf51a840b96674b857b6b82ef125a11345ab9fa2a61735ffd",
|
||||
"bytes": 21205
|
||||
"sha256": "8be4fabde7f2092e6b8f98fb35d96055acab207716466a85baf8f844ae1a7869",
|
||||
"bytes": 21192
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_artifact_bootstrap_failed_child_recovery.py",
|
||||
"sha256": "5da80eed7ef420bee44cec4e20fe45bd4918245c6236a4481aeffe71c939d82f",
|
||||
"bytes": 18314
|
||||
"sha256": "7bc8baa74edffb086b4f3dccf9f69184aaf62761a43b57e7912966d786ad32dd",
|
||||
"bytes": 18295
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_artifact_promoter.py",
|
||||
"sha256": "8eee2bdaf60422824f605637b1820c431fdacea305e1b68b3618fffa0dc000ee",
|
||||
"sha256": "e1f987217790868228daba95ab564b4fb9e44798a80070e6c1d76d0b1ab6fe0a",
|
||||
"bytes": 7570
|
||||
},
|
||||
{
|
||||
@@ -994,8 +1004,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_controller_state_reconciliation.py",
|
||||
"sha256": "b6b5e82f7818fbfa7cc2faf44dad0aaa23e812fbfa9f09f3e738c80cd74237e1",
|
||||
"bytes": 11300
|
||||
"sha256": "bbe33d1faf84f4a230be6d2e2cb65a8d6d81dde4abb91663aac37ef3ef7fe24a",
|
||||
"bytes": 11387
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_deluge_connector.py",
|
||||
@@ -1084,8 +1094,8 @@
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_forge_database_credentials.py",
|
||||
"sha256": "fdab22c57d546a7a1bd912abc63777ca4a1e03c3826ddcebb9301553130c42a3",
|
||||
"bytes": 17599
|
||||
"sha256": "b28732574a2d53891208b6f192f5ce5da51fb3975ae642834d37d3eb489550da",
|
||||
"bytes": 17826
|
||||
},
|
||||
{
|
||||
"path": "backend/tests/test_forge_first_install_controller.py",
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any
|
||||
|
||||
SOURCE_REPOSITORY = "jens/unraid-appops-gateway"
|
||||
HELPER_NAME = "itworx-controller-artifact-promoter-modern"
|
||||
CONTROLLER_VERSION = "2.1.14"
|
||||
CONTROLLER_VERSION = "2.1.17"
|
||||
HELPER_LABEL = "itworx.controller.promoter"
|
||||
_REVISION = re.compile(r"^[0-9a-f]{40}$")
|
||||
_IMAGE_ID = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
SOURCE_REPOSITORY = "jens/unraid-appops-gateway"
|
||||
HELPER_NAME = "itworx-controller-artifact-promoter-modern"
|
||||
CONTROLLER_VERSION = "2.1.14"
|
||||
HELPER_LABEL = "itworx.controller.promoter"
|
||||
_REVISION = re.compile(r"^[0-9a-f]{40}$")
|
||||
_IMAGE_ID = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
_CONTAINER_ID = re.compile(r"^[0-9a-f]{64}$")
|
||||
_VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$")
|
||||
|
||||
|
||||
def helper_container_spec(
|
||||
*,
|
||||
revision: str,
|
||||
image_id: str,
|
||||
current_controller_id: str | None = None,
|
||||
current_controller_revision: str | None = None,
|
||||
current_controller_tree: str | None = None,
|
||||
current_controller_version: str | None = None,
|
||||
) -> 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")
|
||||
|
||||
bootstrap_values = (
|
||||
current_controller_id,
|
||||
current_controller_revision,
|
||||
current_controller_tree,
|
||||
current_controller_version,
|
||||
)
|
||||
if any(value is not None for value in bootstrap_values):
|
||||
if (
|
||||
not _CONTAINER_ID.fullmatch(current_controller_id or "")
|
||||
or not _REVISION.fullmatch(current_controller_revision or "")
|
||||
or not _REVISION.fullmatch(current_controller_tree or "")
|
||||
or not _VERSION.fullmatch(current_controller_version or "")
|
||||
):
|
||||
raise ValueError("controller_promoter_bootstrap_binding_invalid")
|
||||
|
||||
command = f"""set -Eeuo pipefail
|
||||
umask 077
|
||||
revision='{revision}'
|
||||
version='{CONTROLLER_VERSION}'
|
||||
image_id='{image_id}'
|
||||
repo='/gitea-repositories/jens/unraid-appops-gateway.git'
|
||||
[[ \"$(stat -c '%a:%u:%g' /tmp)\" == '1777:0:0' ]]
|
||||
work=\"$(mktemp -d -p /tmp itworx-controller-artifact-promoter-{revision[:12]}.XXXXXX)\"
|
||||
[[ -d \"$work\" && ! -L \"$work\" && \"$(stat -c '%u:%a' \"$work\")\" == '0:700' ]]
|
||||
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/prepare_controller_hub_release_volume.py \\
|
||||
scripts/vendor \\
|
||||
scripts/run_repopulse_adoption.py \
|
||||
scripts/upgrade_deployment_controller.sh > \"$work/controller.tar\"
|
||||
chmod 0600 \"$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\" \\
|
||||
--image-id \"$image_id\" \\
|
||||
--confirm UPGRADE_DEPLOYMENT_CONTROLLER_V2
|
||||
"""
|
||||
|
||||
labels = {
|
||||
HELPER_LABEL: "true",
|
||||
"itworx.source.repository": SOURCE_REPOSITORY,
|
||||
"itworx.source.revision": revision,
|
||||
"itworx.controller.version": CONTROLLER_VERSION,
|
||||
}
|
||||
if current_controller_id is not None:
|
||||
labels.update(
|
||||
{
|
||||
"itworx.controller.bootstrap.target.id": current_controller_id,
|
||||
"itworx.controller.bootstrap.target.image": image_id,
|
||||
"itworx.controller.bootstrap.target.revision": current_controller_revision or "",
|
||||
"itworx.controller.bootstrap.target.tree": current_controller_tree or "",
|
||||
"itworx.controller.bootstrap.target.version": current_controller_version or "",
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"Image": image_id,
|
||||
"User": "0:0",
|
||||
"Cmd": ["/bin/bash", "-lc", command],
|
||||
"Labels": labels,
|
||||
"HostConfig": {
|
||||
"Binds": [
|
||||
"/mnt/cache:/mnt/cache",
|
||||
"/mnt/user:/mnt/user",
|
||||
"/var/lib/docker:/var/lib/docker:ro",
|
||||
"/srv/example/appdata/gitea/git/repositories:/gitea-repositories:ro",
|
||||
"/var/run/docker.sock:/var/run/docker.sock",
|
||||
],
|
||||
"NetworkMode": "host",
|
||||
"Privileged": False,
|
||||
"ReadonlyRootfs": True,
|
||||
"CpuPeriod": 100000,
|
||||
"CpuQuota": 100000,
|
||||
"Memory": 1024 * 1024 * 1024,
|
||||
"PidsLimit": 128,
|
||||
"Tmpfs": {"/tmp": "rw,nosuid,nodev,size=268435456,mode=1777"},
|
||||
"SecurityOpt": ["no-new-privileges:true"],
|
||||
"RestartPolicy": {"Name": "no"},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BROKER = ROOT / "ops/gitea-deploy-broker"
|
||||
REPOSITORY = "jens/unraid-appops-gateway"
|
||||
REVISION = "c" * 40
|
||||
SOURCE = "/srv/example/appdata/unraid-appops-gateway/source"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def recovery(monkeypatch):
|
||||
monkeypatch.syspath_prepend(str(BROKER))
|
||||
if "fcntl" not in sys.modules:
|
||||
monkeypatch.setitem(sys.modules, "fcntl", SimpleNamespace(LOCK_EX=2, LOCK_UN=8, flock=lambda *_: None))
|
||||
spec = importlib.util.spec_from_file_location("staged_source_deploy_test", BROKER / "deploy.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
pointer = {"revision": module.APPOPS_LEGACY_OLD_REVISION, "liveRecordSha256": "f" * 64}
|
||||
active = {"pointer": pointer, "live": {"containerId": "1" * 64,
|
||||
"runtimeFingerprint": "2" * 64, "sourceFingerprint": "3" * 64}}
|
||||
snapshot = {"revision": module.APPOPS_LEGACY_OLD_REVISION, "legacy_provenance": True,
|
||||
"receipt": {}, "appops_runtime_model": pointer, "appops_data_backup": {"safe": True}}
|
||||
metadata = {"builtAt": "2026-10-09T00:00:00.000Z"}
|
||||
payload = {"kind": "followup", "repository": REPOSITORY, "revision": REVISION,
|
||||
"phase": "source_staged", "expectedStateSha256": "4" * 64,
|
||||
"priorModelPointer": pointer, "priorSnapshot": snapshot, "registryFingerprint": "5" * 64,
|
||||
"preMutationRuntimeFingerprint": "2" * 64, "preMutationRuntimeSha256": "6" * 64,
|
||||
"sourceFingerprint": "3" * 64, "metadata": metadata}
|
||||
pending = {"sha256": "7" * 64, "payload": payload}
|
||||
identity = {"revision": module.APPOPS_LEGACY_OLD_REVISION, "containerId": "1" * 64,
|
||||
"runtimeFingerprint": "2" * 64, "runtimeCasSha256": "6" * 64}
|
||||
state = {"live_status": "healthy"}
|
||||
facts = {"stateSha": "4" * 64, "registry": "5" * 64, "source": "8" * 64,
|
||||
"postSource": "3" * 64, "active": active, "identity": identity}
|
||||
calls = []
|
||||
reader = SimpleNamespace(
|
||||
PROJECTS_PATH=Path("registry"),
|
||||
_self_upgrade_pending_model_transaction=lambda: pending,
|
||||
_self_upgrade_active_compose_model=lambda *_args, **_kwargs: facts["active"],
|
||||
_self_upgrade_verify_rollback_model=lambda *_: True,
|
||||
_self_upgrade_read_fixed_file=lambda *_args, **_kwargs: b"registry",
|
||||
_self_upgrade_secret=lambda: b"private-key",
|
||||
_self_upgrade_mac=lambda *_: facts["registry"],
|
||||
_self_upgrade_model_runtime_identity=lambda *_: facts["identity"],
|
||||
_self_upgrade_source_fingerprint=lambda *_: facts["source"],
|
||||
_self_upgrade_source_is_exact_legacy=lambda *_: facts["postSource"],
|
||||
)
|
||||
monkeypatch.setattr(module, "require_host_root", lambda: None)
|
||||
monkeypatch.setattr(module, "_appops_status_reader", lambda: reader)
|
||||
monkeypatch.setattr(module, "read_appops_state_preimage", lambda _: (state, b"state", facts["stateSha"]))
|
||||
monkeypatch.setattr(module, "validate_commit", lambda *_: REVISION)
|
||||
monkeypatch.setattr(module, "deployment_metadata", lambda *_args, **_kwargs: metadata)
|
||||
monkeypatch.setattr(module, "verify_appops_data_backup", lambda *_: calls.append("backup-verified"))
|
||||
monkeypatch.setattr(module, "_verify_appops_staged_source", lambda *_: calls.append("candidate-verified"))
|
||||
monkeypatch.setattr(module, "_appops_source_untracked_fingerprint", lambda *_: "9" * 64)
|
||||
monkeypatch.setattr(module, "tracked_files", lambda *_: [])
|
||||
monkeypatch.setattr(module, "write_source_metadata", lambda *_: None)
|
||||
monkeypatch.setattr(module, "sync_source", lambda _repo, _revision, _root, _slug, paths, **_kwargs: calls.append(("restore", copy.deepcopy(paths))))
|
||||
monkeypatch.setattr(module, "inspect_container", lambda _: {"Id": "1" * 64})
|
||||
monkeypatch.setattr(module, "_appops_update_model_transaction", lambda _project, phase, bindings: calls.append((phase, bindings)))
|
||||
project = {"slug": "unraid-appops-gateway", "source_dir": SOURCE, "clean_untracked_paths": ["backend"]}
|
||||
return module, project, pending, facts, calls, reader
|
||||
|
||||
|
||||
def test_signed_staged_source_restores_without_runtime_or_state_writes(recovery):
|
||||
module, project, pending, _facts, calls, _reader = recovery
|
||||
_, fingerprint = module.inspect_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION)
|
||||
result = module.resume_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION, fingerprint)
|
||||
assert result["status"] == "source-restored"
|
||||
phases = [item[0] for item in calls if isinstance(item, tuple)]
|
||||
assert phases == ["rolling_back", "restore", "rolled_back"]
|
||||
restored = next(item[1] for item in calls if isinstance(item, tuple) and item[0] == "restore")
|
||||
assert restored == []
|
||||
assert project["clean_untracked_paths"] == ["backend"]
|
||||
assert calls[-1][1] == {"restoredLiveReference": pending["payload"]["priorModelPointer"]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("drift", ["target", "kind", "revision", "phase", "imageSeal", "state", "registry",
|
||||
"container", "runtime", "runtimeCas", "priorModel", "sourceAuthority", "sourceUnsafe"])
|
||||
def test_staged_source_recovery_rejects_drift_before_any_write(recovery, drift):
|
||||
module, project, pending, facts, calls, _reader = recovery
|
||||
payload = pending["payload"]
|
||||
if drift == "target":
|
||||
project["source_dir"] = "/other"
|
||||
elif drift in {"kind", "revision", "phase"}:
|
||||
payload[drift] = "foreign"
|
||||
elif drift == "imageSeal":
|
||||
payload["candidateImageId"] = "sha256:" + "a" * 64
|
||||
elif drift in {"state", "registry"}:
|
||||
facts["stateSha" if drift == "state" else "registry"] = "a" * 64
|
||||
elif drift in {"container", "runtime", "runtimeCas"}:
|
||||
facts["identity"][{"container": "containerId", "runtime": "runtimeFingerprint", "runtimeCas": "runtimeCasSha256"}[drift]] = "a" * 64
|
||||
elif drift == "priorModel":
|
||||
payload["priorModelPointer"] = {"foreign": True}
|
||||
elif drift == "sourceAuthority":
|
||||
payload["sourceFingerprint"] = "a" * 64
|
||||
else:
|
||||
facts["source"] = None
|
||||
with pytest.raises(module.DeployError):
|
||||
module.inspect_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION)
|
||||
assert not any(isinstance(item, tuple) for item in calls)
|
||||
|
||||
|
||||
def test_source_rollback_requires_fresh_preflight(recovery):
|
||||
module, project, _pending, _facts, calls, _reader = recovery
|
||||
with pytest.raises(module.DeployError, match="preflight changed"):
|
||||
module.resume_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION, "0" * 64)
|
||||
assert not any(isinstance(item, tuple) for item in calls)
|
||||
|
||||
|
||||
def test_source_rollback_never_seals_failed_source_restoration(recovery):
|
||||
module, project, _pending, facts, calls, _reader = recovery
|
||||
_, fingerprint = module.inspect_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION)
|
||||
facts["postSource"] = "0" * 64
|
||||
with pytest.raises(module.DeployError, match="signed unchanged preimage"):
|
||||
module.resume_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION, fingerprint)
|
||||
assert not any(isinstance(item, tuple) and item[0] == "rolled_back" for item in calls)
|
||||
|
||||
|
||||
def test_source_rollback_reconciles_crash_after_exact_source_restore(recovery):
|
||||
module, project, pending, facts, calls, _reader = recovery
|
||||
pending["payload"].update(phase="rolling_back", failureCode="staged_source_restore", sourceRestoreIntent={"safe": True})
|
||||
facts["source"] = "3" * 64
|
||||
_, fingerprint = module.inspect_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION)
|
||||
module.resume_appops_staged_source_rollback(project, REPOSITORY, Path("git"), REVISION, fingerprint)
|
||||
assert "candidate-verified" not in calls
|
||||
assert [item[0] for item in calls if isinstance(item, tuple)] == ["restore", "rolled_back"]
|
||||
|
||||
|
||||
def test_staged_source_verifier_rejects_edited_tracked_file_and_unknown_files(recovery, monkeypatch, tmp_path):
|
||||
_module, _project, _pending, _facts, _calls, _reader = recovery
|
||||
# Restore the real verifier instead of the orchestration fixture adapter.
|
||||
spec = importlib.util.spec_from_file_location("staged_source_real_verifier", BROKER / "deploy.py")
|
||||
real = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(real)
|
||||
source = tmp_path / "source"
|
||||
(source / ".gitea-deploy").mkdir(parents=True)
|
||||
(source / ".gitea-deploy/revision").write_text(REVISION + "\n")
|
||||
(source / ".gitea-deploy/tracked-files.txt").write_text("tracked.py\n")
|
||||
(source / "tracked.py").write_bytes(b"committed")
|
||||
def extract(_repo, _revision, target):
|
||||
(target / "tracked.py").write_bytes(b"committed")
|
||||
monkeypatch.setattr(real, "tracked_files", lambda *_: ["tracked.py"])
|
||||
monkeypatch.setattr(real, "extract_revision", extract)
|
||||
project = {"source_dir": str(source)}
|
||||
real._verify_appops_staged_source(Path("git"), project, REVISION)
|
||||
(source / "tracked.py").write_bytes(b"altered!!")
|
||||
with pytest.raises(real.DeployError, match="committed source changed"):
|
||||
real._verify_appops_staged_source(Path("git"), project, REVISION)
|
||||
(source / "tracked.py").write_bytes(b"committed")
|
||||
(source / "unknown.txt").write_bytes(b"foreign")
|
||||
with pytest.raises(real.DeployError, match="unknown file"):
|
||||
real._verify_appops_staged_source(Path("git"), project, REVISION)
|
||||
|
||||
|
||||
def test_controller_promotion_holds_nonblocking_production_lock_before_stop():
|
||||
script = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text()
|
||||
assert script.index('flock -n -x "$deployment_lock_fd"') < script.index('"${work}/${PREPARER_NAME}" prepare')
|
||||
assert script.index('flock -n -x "$deployment_lock_fd"') < script.index('"${work}/${PREPARER_NAME}" ensure')
|
||||
assert script.index('flock -n -x "$promotion_lock_fd"') < script.index('"${work}/${PREPARER_NAME}" ensure')
|
||||
assert script.index('flock -n -x "$deployment_lock_fd"') < script.index('stop --backup "$backup"')
|
||||
assert "production deployment is active; controller promotion refused" in script
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt" or getattr(os, "geteuid", lambda: -1)() != 0,
|
||||
reason="real controller manifest ownership requires root POSIX validation")
|
||||
@pytest.mark.parametrize("interruption", ["after_delete", "during_extract", "between_manifests", "foreign_bytes", "foreign_file"])
|
||||
def test_partial_own_source_restore_accepts_only_known_git_states(recovery, monkeypatch, tmp_path, interruption):
|
||||
module, _project, _pending, _facts, _calls, reader = recovery
|
||||
root = tmp_path / "source"
|
||||
root.mkdir()
|
||||
(root / ".gitea-deploy").mkdir()
|
||||
candidate = {"same.py": b"candidate contents", "candidate-only.py": b"candidate only"}
|
||||
old = {"same.py": b"old contents", "old-only.py": b"old only"}
|
||||
candidate_files, old_files = sorted(candidate), sorted(old)
|
||||
def files(_repo, revision):
|
||||
return candidate_files if revision == REVISION else old_files
|
||||
def extract(_repo, revision, target):
|
||||
for name, raw in (candidate if revision == REVISION else old).items():
|
||||
(target / name).write_bytes(raw)
|
||||
(target / name).chmod(0o644)
|
||||
monkeypatch.setattr(module, "tracked_files", files)
|
||||
monkeypatch.setattr(module, "extract_revision", extract)
|
||||
extract(Path("git"), REVISION, root)
|
||||
manifests = root / ".gitea-deploy"
|
||||
(manifests / "revision").write_text(REVISION + "\n")
|
||||
(manifests / "tracked-files.txt").write_text("\n".join(candidate_files) + "\n")
|
||||
for path in manifests.iterdir():
|
||||
path.chmod(0o644)
|
||||
os.chown(path, 100, 101)
|
||||
(root / "candidate-only.py").unlink()
|
||||
if interruption == "during_extract":
|
||||
(root / "same.py").write_bytes(b"old con")
|
||||
elif interruption == "between_manifests":
|
||||
(root / "same.py").write_bytes(old["same.py"])
|
||||
(root / "old-only.py").write_bytes(old["old-only.py"])
|
||||
(manifests / "tracked-files.txt").write_text("old-on")
|
||||
elif interruption == "foreign_bytes":
|
||||
(root / "same.py").write_bytes(b"foreign mutation")
|
||||
elif interruption == "foreign_file":
|
||||
(root / "extra.py").write_bytes(b"unknown")
|
||||
intent = {"trackedOwners": {name: [0, 0] for name in candidate_files}, "untrackedFingerprint": "9" * 64}
|
||||
args = (Path("git"), {"source_dir": str(root)}, REVISION, intent, reader, b"private-key")
|
||||
if interruption.startswith("foreign"):
|
||||
with pytest.raises(module.DeployError):
|
||||
module._verify_appops_partial_source_restore(*args)
|
||||
else:
|
||||
module._verify_appops_partial_source_restore(*args)
|
||||
|
||||
|
||||
def test_partial_source_restore_rejects_untracked_secret_drift(recovery, monkeypatch):
|
||||
module, project, _pending, _facts, _calls, reader = recovery
|
||||
monkeypatch.setattr(module, "_appops_source_untracked_fingerprint", lambda *_: "a" * 64)
|
||||
with pytest.raises(module.DeployError, match="untracked files changed"):
|
||||
module._verify_appops_partial_source_restore(Path("git"), project, REVISION,
|
||||
{"untrackedFingerprint": "9" * 64}, reader, b"private-key")
|
||||
@@ -37,6 +37,7 @@ CALLER_ID = "c" * 64
|
||||
MODERN_HELPER_ID = "f" * 64
|
||||
RETIRED_HELPER_ID = bootstrap.RETIRED_FAILED_MODERN_HELPER_ID
|
||||
HOST_HOSTNAME = "Tower"
|
||||
RETIRED_MODERN_HELPER_SOURCE = (ROOT / "backend/tests/fixtures/controller_artifact_helper_spec_2_1_14.txt").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
LEGACY_HELPER_SOURCE_FIXTURE = '''
|
||||
@@ -570,6 +571,8 @@ class FakeProtectedGit:
|
||||
self.blobs_read.append((revision, path))
|
||||
if revision not in {self.revision, bootstrap.RETIRED_FAILED_MODERN_REVISION} or path != "backend/app/services/controller_artifact_helper_spec.py":
|
||||
raise bootstrap.BootstrapError("controller_promoter_source_blob_invalid")
|
||||
if revision == bootstrap.RETIRED_FAILED_MODERN_REVISION:
|
||||
return RETIRED_MODERN_HELPER_SOURCE.encode("utf-8")
|
||||
return self.helper_source.encode("utf-8")
|
||||
|
||||
def read_legacy_blob(self, revision: str, path: str) -> bytes:
|
||||
@@ -586,7 +589,7 @@ def _health(
|
||||
*,
|
||||
revision: str = REVISION,
|
||||
tree: str = TREE,
|
||||
version: str = "2.1.14",
|
||||
version: str = "2.1.17",
|
||||
artifact_sha256: str = "1" * 64,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -829,15 +832,16 @@ def test_controller_source_provenance_failure_precedes_health_and_side_effects()
|
||||
assert docker.started == []
|
||||
|
||||
|
||||
def _retired_failed_child(helper_source: str) -> dict[str, Any]:
|
||||
def _retired_failed_child() -> dict[str, Any]:
|
||||
spec = bootstrap.render_modern_helper_spec(
|
||||
helper_source,
|
||||
RETIRED_MODERN_HELPER_SOURCE,
|
||||
revision=bootstrap.RETIRED_FAILED_MODERN_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",
|
||||
helper_version="2.1.14",
|
||||
)
|
||||
child = _inspect_from_spec(
|
||||
"itworx-controller-artifact-promoter-modern",
|
||||
@@ -1295,7 +1299,7 @@ def test_exact_failed_historical_child_is_removed_before_current_helper_launch()
|
||||
version="2.1.12",
|
||||
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
||||
)
|
||||
failed_child = _retired_failed_child(helper_source)
|
||||
failed_child = _retired_failed_child()
|
||||
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
|
||||
git = FakeProtectedGit(helper_source)
|
||||
|
||||
@@ -1343,7 +1347,7 @@ def test_current_214_baseline_is_verified_and_bound_to_new_helper():
|
||||
"itworx.controller.promoter": "true",
|
||||
"itworx.source.repository": "jens/unraid-appops-gateway",
|
||||
"itworx.source.revision": REVISION,
|
||||
"itworx.controller.version": "2.1.14",
|
||||
"itworx.controller.version": "2.1.17",
|
||||
"itworx.controller.bootstrap.target.id": MODERN_CONTROLLER_CONTAINER_ID,
|
||||
"itworx.controller.bootstrap.target.image": CONTROLLER_IMAGE_ID,
|
||||
"itworx.controller.bootstrap.target.revision": MODERN_CONTROLLER_REVISION,
|
||||
@@ -1356,7 +1360,7 @@ def test_retired_failed_child_is_removed_only_after_current_214_profile_rechecks
|
||||
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
failed_child = _retired_failed_child(helper_source)
|
||||
failed_child = _retired_failed_child()
|
||||
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
|
||||
docker.containers["gitea-deploy-control"] = _modern_controller_source_inspect(
|
||||
MODERN_CONTROLLER_REVISION
|
||||
@@ -1417,7 +1421,7 @@ def test_retired_cleanup_refuses_any_current_214_identity_or_topology_drift(muta
|
||||
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
failed_child = _retired_failed_child(helper_source)
|
||||
failed_child = _retired_failed_child()
|
||||
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
|
||||
controller = _modern_controller_source_inspect(MODERN_CONTROLLER_REVISION)
|
||||
health = _modern_baseline_health()
|
||||
@@ -1515,7 +1519,7 @@ def test_retired_helper_cleanup_fails_closed_on_any_identity_or_state_drift(muta
|
||||
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
failed_child = _retired_failed_child(helper_source)
|
||||
failed_child = _retired_failed_child()
|
||||
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
|
||||
git = FakeProtectedGit(helper_source)
|
||||
if mutation == "running":
|
||||
@@ -1614,7 +1618,7 @@ def test_same_failed_source_revision_is_never_retried_or_removed():
|
||||
)
|
||||
docker = FakeDocker(
|
||||
_legacy_helper_spec(bootstrap.RETIRED_FAILED_MODERN_REVISION),
|
||||
existing_child=_retired_failed_child(helper_source),
|
||||
existing_child=_retired_failed_child(),
|
||||
)
|
||||
git = FakeProtectedGit(helper_source)
|
||||
git.revision = bootstrap.RETIRED_FAILED_MODERN_REVISION
|
||||
@@ -1626,16 +1630,14 @@ def test_same_failed_source_revision_is_never_retried_or_removed():
|
||||
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
||||
)
|
||||
|
||||
result = _run_bootstrap(
|
||||
docker,
|
||||
git,
|
||||
health_sequence=[legacy_health],
|
||||
source_revision=git.revision,
|
||||
source_tree=git.tree,
|
||||
)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["exitCode"] == 1
|
||||
with pytest.raises(bootstrap.BootstrapError, match="controller_promoter_retired_helper_retry_refused"):
|
||||
_run_bootstrap(
|
||||
docker,
|
||||
git,
|
||||
health_sequence=[legacy_health],
|
||||
source_revision=git.revision,
|
||||
source_tree=git.tree,
|
||||
)
|
||||
assert docker.removed == []
|
||||
assert docker.created == []
|
||||
|
||||
@@ -1824,7 +1826,7 @@ def test_successful_child_requires_exact_source_tree_version_and_health():
|
||||
|
||||
assert result["status"] == "promoted"
|
||||
assert result["revision"] == REVISION
|
||||
assert result["controllerVersion"] == "2.1.14"
|
||||
assert result["controllerVersion"] == "2.1.17"
|
||||
assert result["artifactSha256"] == "1" * 64
|
||||
assert docker.started == [MODERN_HELPER_ID]
|
||||
assert docker.created[0]["Image"] == CONTROLLER_IMAGE_ID
|
||||
@@ -2079,3 +2081,29 @@ def test_modern_upgrade_path_still_requires_private_archive_and_image_id(
|
||||
assert "archive is outside the helper private temporary directory" in result.stderr
|
||||
if modern_call == "missing-image":
|
||||
assert "image ID" in result.stderr
|
||||
|
||||
|
||||
def test_current_helper_version_is_distinct_from_fixed_controller_baselines():
|
||||
source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_bytes()
|
||||
namespace = bootstrap._safe_modern_spec_namespace(source)
|
||||
assert namespace["CONTROLLER_VERSION"] == "2.1.17"
|
||||
assert bootstrap._verified_historical_helper_version(source) == "2.1.17"
|
||||
assert [baseline.version for baseline in bootstrap.CONTROLLER_BASELINES] == ["2.1.12", "2.1.14"]
|
||||
with pytest.raises(bootstrap.BootstrapError, match="controller_promoter_modern_spec_identity_invalid"):
|
||||
bootstrap._safe_modern_spec_namespace(RETIRED_MODERN_HELPER_SOURCE.encode("utf-8"))
|
||||
assert bootstrap._verified_historical_helper_version(RETIRED_MODERN_HELPER_SOURCE.encode("utf-8")) == "2.1.14"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.1.16", "2.1.18", "9.9.9"])
|
||||
def test_current_and_historical_helper_reject_unreviewed_target_versions(version):
|
||||
source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_bytes()
|
||||
foreign = source.replace(b'CONTROLLER_VERSION = "2.1.17"', f'CONTROLLER_VERSION = "{version}"'.encode())
|
||||
with pytest.raises(bootstrap.BootstrapError, match="controller_promoter_modern_spec_identity_invalid"):
|
||||
bootstrap._safe_modern_spec_namespace(foreign)
|
||||
with pytest.raises(bootstrap.BootstrapError, match="controller_promoter_modern_spec_identity_invalid"):
|
||||
bootstrap._verified_historical_helper_version(foreign)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.1.14", "2.1.16", "2.1.18"])
|
||||
def test_promoted_health_cannot_substitute_a_baseline_or_unknown_version(version):
|
||||
assert not bootstrap._health_matches(_health(version=version), REVISION, TREE)
|
||||
@@ -173,7 +173,7 @@ def _inputs(*, current_child: bool) -> tuple[Docker, Git, dict[str, Any], str]:
|
||||
current_controller_tree=baseline.tree,
|
||||
current_controller_version=baseline.version,
|
||||
)
|
||||
child = original._retired_failed_child(helper_source)
|
||||
child = original._retired_failed_child()
|
||||
child["Id"] = child_id
|
||||
child["Image"] = baseline.image_id
|
||||
child["State"] = _created_state()
|
||||
|
||||
@@ -48,7 +48,7 @@ class Docker(original.FakeDocker):
|
||||
|
||||
def inputs():
|
||||
source = (original.ROOT / bootstrap.MODERN_HELPER_SOURCE_PATH).read_text()
|
||||
child = original._retired_failed_child(source)
|
||||
child = original._retired_failed_child()
|
||||
baseline = bootstrap.MODERN_CONTROLLER_BASELINE
|
||||
spec = bootstrap.render_modern_helper_spec(
|
||||
source,
|
||||
@@ -212,7 +212,7 @@ def historical_inputs():
|
||||
helper_source = (
|
||||
original.ROOT / "backend/app/services/controller_artifact_helper_spec.py"
|
||||
).read_text(encoding="utf-8")
|
||||
child = original._retired_failed_child(helper_source)
|
||||
child = original._retired_failed_child()
|
||||
docker = original.FakeDocker(
|
||||
original._legacy_helper_spec(), existing_child=child
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ def test_controller_promoter_helper_spec_is_fixed_and_bounded():
|
||||
spec = helper_container_spec(revision=revision, image_id=image_id)
|
||||
|
||||
assert HELPER_NAME == "itworx-controller-artifact-promoter-modern"
|
||||
assert CONTROLLER_VERSION == "2.1.14"
|
||||
assert CONTROLLER_VERSION == "2.1.17"
|
||||
assert re.fullmatch(r"2\.1\.\d+", CONTROLLER_VERSION)
|
||||
assert spec["Image"] == image_id
|
||||
assert spec["HostConfig"]["Privileged"] is False
|
||||
@@ -79,7 +79,7 @@ def test_controller_promoter_helper_spec_is_fixed_and_bounded():
|
||||
assert revision in command
|
||||
assert "--image-id" in command
|
||||
assert CONTROLLER_VERSION in command
|
||||
assert "version='2.1.14'" in command
|
||||
assert "version='2.1.17'" in command
|
||||
assert "/gitea-repositories/jens/unraid-appops-gateway.git" in command
|
||||
archive_start = command.index('git --git-dir="$repo" archive')
|
||||
archive_end = command.index('> "$work/controller.tar"', archive_start)
|
||||
@@ -110,7 +110,7 @@ def test_controller_promoter_bootstrap_child_spec_binds_legacy_target_identity()
|
||||
"itworx.controller.promoter": "true",
|
||||
"itworx.source.repository": "jens/unraid-appops-gateway",
|
||||
"itworx.source.revision": revision,
|
||||
"itworx.controller.version": "2.1.14",
|
||||
"itworx.controller.version": "2.1.17",
|
||||
"itworx.controller.bootstrap.target.id": target_id,
|
||||
"itworx.controller.bootstrap.target.image": image_id,
|
||||
"itworx.controller.bootstrap.target.revision": target_revision,
|
||||
|
||||
@@ -197,7 +197,8 @@ def test_upgrade_prepares_before_stop_and_reconciles_between_stop_and_activation
|
||||
assert script.index(prepare) < script.index(stop)
|
||||
assert script.index(stop) < script.index(reconcile)
|
||||
assert script.index(reconcile) < script.index(activate)
|
||||
assert 'flock -x "$promotion_lock_fd"' in script
|
||||
assert 'flock -n -x "$promotion_lock_fd"' in script
|
||||
assert script.index('flock -n -x "$promotion_lock_fd"') < script.index(prepare)
|
||||
assert 'mv -- "$CONTROLLER_DIR"' not in script
|
||||
assert 'cp -a -- "$CONTROLLER_DIR"' not in script
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ def _install_fake_docker(
|
||||
"if behavior == 'echo-received':\n"
|
||||
" os.write(2, first.encode() + b'\\n')\n"
|
||||
"if behavior == 'exit-after-first':\n"
|
||||
" open(marker, 'a').write('first-input-validated\\n')\n"
|
||||
" raise SystemExit(8)\n"
|
||||
"os.write(2, b'Enter it again: ')\n"
|
||||
"second = sys.stdin.readline().rstrip('\\r\\n')\n"
|
||||
@@ -288,7 +289,8 @@ def test_any_password_input_without_success_becomes_unknown(
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
monkeypatch.setattr(module, "PROMPT_TIMEOUT_SECONDS", 0.15)
|
||||
# Prove the post-input branch, rather than timing out Python startup under load.
|
||||
monkeypatch.setattr(module, "PROMPT_TIMEOUT_SECONDS", 2.0)
|
||||
monkeypatch.setattr(module, "REMOTE_TIMEOUT_SECONDS", 1)
|
||||
monkeypatch.setattr(module, "REMOTE_KILL_AFTER_SECONDS", 1)
|
||||
monkeypatch.setattr(module, "REMOTE_TERMINATION_MARGIN_SECONDS", 0.1)
|
||||
@@ -296,6 +298,7 @@ def test_any_password_input_without_success_becomes_unknown(
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert marker.read_text(encoding="utf-8") == "first-input-validated\n"
|
||||
assert result["status"] == "outcome_unknown"
|
||||
assert result["reason"] == "psql_result_uncertain"
|
||||
assert states[module.RUNTIME_ROLE] == "unknown"
|
||||
|
||||
Reference in new issue
Block a user