Verify prepared runtime IDs at launch (Refs #93)

This commit is contained in:
2026-10-09 19:18:54 -04:00
parent 596b160be3
commit d1f581d6a8
5 changed files with 135 additions and 17 deletions
+87 -1
View File
@@ -24,6 +24,7 @@ class FakeDocker:
self.fail_restart = False
self.lose_reply_after = None
self.started = 0
self.image_id = "sha256:" + "1" * 64
def inspect(self, name):
return self.containers.get(name)
@@ -34,6 +35,8 @@ class FakeDocker:
return json.dumps({"ServerVersion": "27.5.1"})
if args[:2] == ("image", "ls"):
return "ghcr.io/ayymoss/plutainer:latest\n"
if args[:4] == ("image", "inspect", "--format", "{{.Id}}"):
return self.image_id
if args[:2] == ("container", "create"):
labels = {args[index + 1].split("=", 1)[0]: args[index + 1].split("=", 1)[1] for index, arg in enumerate(args) if arg == "--label"}
name = args[args.index("--name") + 1]
@@ -989,7 +992,8 @@ class AgentTests(unittest.TestCase):
payload = self.create()
creation = next(args for args in self.docker.calls if args[:2] == ("container", "create"))
self.assertIn("--pull=never", creation)
self.assertEqual(creation[-1], payload["image"])
self.assertEqual(creation[-1], self.docker.image_id)
self.assertEqual(self.agent.spec(INSTANCE, 1)["image"], payload["image"])
self.assertFalse(any(args[:2] == ("image", "pull") for args in self.docker.calls))
def test_image_disappearing_after_validation_cannot_be_pulled_or_started(self):
@@ -1008,6 +1012,88 @@ class AgentTests(unittest.TestCase):
self.assertEqual(self.docker.started, 0)
self.assertFalse(any(args[:2] == ("image", "pull") for args in self.docker.calls))
def test_retargeted_prepared_image_requires_a_new_admin_pull_before_creation(self):
image = "registry.example/custom-runtime:stable"
payload = {"game": "t5", "libraryId": self.library_id, "image": image, "port": 28960,
"rconPassword": "rcon-secret", "env": {"PLUTAINER_KEY": "server-secret"}}
self.assertEqual(self.agent.execute(self.operation("runtime.pull", {"image": image}, "prepare"))["state"], "completed")
prepared_id = self.docker.image_id
self.docker.image_id = "sha256:" + "2" * 64
original = self.docker.call
def call(*args, **kwargs):
if args[:2] == ("image", "ls"):
# The old image still exists, so cached-ID inventory alone
# cannot prove that this mutable tag is safe to launch.
return (f"registry.example/custom-runtime|stable|<none>|{self.docker.image_id}\n"
f"<none>|<none>|<none>|{prepared_id}\n")
return original(*args, **kwargs)
with patch.object(self.docker, "call", side_effect=call):
self.assertIn(image, self.agent.inventory()["preparedRuntimeImages"])
result = self.agent.execute(self.operation("instance.create", payload, "retargeted"))
self.assertEqual(result["state"], "failed")
self.assertIn("Prepared runtime image has changed", result["error"])
self.assertFalse(any(args[:2] == ("container", "create") for args in self.docker.calls))
self.assertEqual(self.docker.started, 0)
self.assertFalse((self.agent.instance_dir(INSTANCE, 1) / "environment").exists())
self.assertEqual(self.agent.execute(self.operation("runtime.pull", {"image": image}, "prepare-again"))["state"], "completed")
self.assertEqual(self.agent.execute(self.operation("instance.create", payload, "retry"))["state"], "completed")
creation = next(args for args in self.docker.calls if args[:2] == ("container", "create"))
self.assertEqual(creation[-1], self.docker.image_id)
def test_retag_after_validation_cannot_replace_the_image_used_for_creation(self):
image = "ghcr.io/ayymoss/plutainer:latest"
self.agent.dispatch(self.operation("runtime.pull", {"image": image}))
prepared_id = self.docker.image_id
original = self.agent.write_initial_files
def retag(*args):
self.docker.image_id = "sha256:" + "2" * 64
return original(*args)
with patch.object(self.agent, "write_initial_files", side_effect=retag):
payload = self.create()
creation = next(args for args in self.docker.calls if args[:2] == ("container", "create"))
self.assertIn("--pull=never", creation)
self.assertEqual(creation[-1], prepared_id)
self.assertNotEqual(creation[-1], self.docker.image_id)
self.assertEqual(self.agent.spec(INSTANCE, 1), payload)
# Retagging must not invalidate an already created immutable instance.
calls = list(self.docker.calls)
self.assertTrue(self.agent.create(INSTANCE, 1, payload)["exists"])
self.assertEqual(self.docker.calls, calls)
def test_rebuild_rejects_retargeted_image_before_stopping_existing_container(self):
image = "ghcr.io/ayymoss/plutainer:latest"
self.agent.dispatch(self.operation("runtime.pull", {"image": image}))
payload = self.create()
self.docker.image_id = "sha256:" + "2" * 64
self.docker.calls.clear()
result = self.agent.execute(self.operation("instance.rebuild", {"nextGeneration": 2, "spec": payload}, "rebuild"))
self.assertEqual(result["state"], "failed")
self.assertIn("Prepared runtime image has changed", result["error"])
self.assertFalse(any(args[:2] in (("container", "stop"), ("container", "rm"), ("container", "create")) for args in self.docker.calls))
self.assertTrue(self.docker.inspect(self.agent.container_name(INSTANCE, 1))["State"]["Running"])
def test_launch_rejects_invalid_image_ids_and_preparation_records(self):
payload = {"game": "t5", "libraryId": self.library_id, "image": "prepared:runtime", "port": 28960}
for image_id in ("", "prepared:runtime", "sha256:short"):
with self.subTest(image_id=image_id):
self.docker.image_id = image_id
with self.assertRaisesRegex(AgentError, "valid runtime image ID"):
self.agent.create(INSTANCE, 1, payload)
self.docker.image_id = "sha256:" + "1" * 64
aliases = self.agent.root / "agent/runtime-images.json"
for raw in ("invalid json", "[]", "null"):
with self.subTest(records=raw):
aliases.write_text(raw)
with self.assertRaisesRegex(AgentError, "Runtime preparation records"):
self.agent.create(INSTANCE, 1, payload)
self.assertFalse(any(args[:2] == ("container", "create") for args in self.docker.calls))
self.assertEqual(self.docker.started, 0)
def test_manifest_is_advertised_and_reconciles_as_a_read_operation(self):
self.create()
operation = self.operation("files.manifest", {"roots": ["configs", "scripts"], "ignoreNames": [], "paths": ["configs/server.cfg"]})