This commit is contained in:
+10
-4
@@ -35,7 +35,7 @@ import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
VERSION = "1.8.5"
|
||||
VERSION = "1.8.6"
|
||||
LABEL = "io.electric-cherry"
|
||||
MAX_FILE = 32 * 1024 * 1024
|
||||
MANIFEST_MAX_ENTRIES = 10000
|
||||
@@ -1462,7 +1462,7 @@ class Agent:
|
||||
progress = list(progress.values())
|
||||
libraries.extend({"id": item["libraryVersionId"], "state": item["status"]} for item in progress)
|
||||
caches.extend(progress)
|
||||
images = []
|
||||
images, prepared_images = [], []
|
||||
if docker_running:
|
||||
with contextlib.suppress(AgentError):
|
||||
lines = self.docker.call("image", "ls", "--no-trunc", "--digests", "--format", "{{.Repository}}|{{.Tag}}|{{.Digest}}|{{.ID}}", timeout=15).splitlines()
|
||||
@@ -1480,7 +1480,13 @@ class Agent:
|
||||
aliases = self.root / "agent" / "runtime-images.json"
|
||||
if aliases.exists():
|
||||
with contextlib.suppress(OSError, ValueError):
|
||||
images.extend(alias for alias, image_id in json.loads(aliases.read_text()).items() if image_id in image_ids)
|
||||
saved = json.loads(aliases.read_text())
|
||||
if isinstance(saved, dict):
|
||||
# Only runtime.pull records administrator-prepared
|
||||
# references. Other Docker cache entries are useful
|
||||
# diagnostics, but do not grant launch permission.
|
||||
prepared_images = [alias for alias, image_id in saved.items() if isinstance(image_id, str) and image_id in image_ids]
|
||||
images.extend(prepared_images)
|
||||
resources = {}
|
||||
if docker_running:
|
||||
observations = []
|
||||
@@ -1499,7 +1505,7 @@ class Agent:
|
||||
update = self.update_inventory()
|
||||
capabilities = ["library.remove", "plutonium.cache", "instance.cancel-create", "instance.safety-override", "instance.unhealthy-recovery", "instance.autoscaling-remove", "operations.read-lane", "operations.health-lane", "rcon.status-batch", "instance.heartbeat-state", "files.manifest", "files.blobs", "logs.read-offset"] + (["host.decommission"] if self.can_uninstall() else []) + (["agent.update"] if update["enabled"] else [])
|
||||
plutonium_cache = self.plutonium_cache_inventory()
|
||||
return {"hostname": socket.gethostname(), "agentVersion": VERSION, "protocolVersion": 1, "capabilities": capabilities, "agentUpdate": update, "agentLanes": self.lane_inventory(), "os": os_name, "osVersion": platform.release(), "architecture": platform.machine(), "cpu": platform.processor() or platform.machine(), "cpuCount": os.cpu_count() or 0, **resources, **({"cpuUsagePercent": cpu_usage} if cpu_usage is not None else {}), "memoryTotal": memory.get("MemTotal", 0), "memoryAvailable": memory.get("MemAvailable", 0), "storageTotal": usage.total, "storageAvailable": usage.free, "dockerVersion": docker_version, "dockerRunning": docker_running, "directoriesWritable": os.access(self.root, os.W_OK), "runtimeImages": sorted(set(images)), **({"plutoniumCache": plutonium_cache} if plutonium_cache else {}), "libraries": libraries, "libraryCaches": caches, "currentOperation": self.current_operation, "portRange": self.config.get("portRange", {"min": 28960, "max": 29060})}
|
||||
return {"hostname": socket.gethostname(), "agentVersion": VERSION, "protocolVersion": 1, "capabilities": capabilities, "agentUpdate": update, "agentLanes": self.lane_inventory(), "os": os_name, "osVersion": platform.release(), "architecture": platform.machine(), "cpu": platform.processor() or platform.machine(), "cpuCount": os.cpu_count() or 0, **resources, **({"cpuUsagePercent": cpu_usage} if cpu_usage is not None else {}), "memoryTotal": memory.get("MemTotal", 0), "memoryAvailable": memory.get("MemAvailable", 0), "storageTotal": usage.total, "storageAvailable": usage.free, "dockerVersion": docker_version, "dockerRunning": docker_running, "directoriesWritable": os.access(self.root, os.W_OK), "runtimeImages": sorted(set(images)), "preparedRuntimeImages": sorted(set(prepared_images)), **({"plutoniumCache": plutonium_cache} if plutonium_cache else {}), "libraries": libraries, "libraryCaches": caches, "currentOperation": self.current_operation, "portRange": self.config.get("portRange", {"min": 28960, "max": 29060})}
|
||||
|
||||
def safe_to_disrupt(self, instance_id, generation, *, allow_unhealthy=False):
|
||||
health = self.inspect(instance_id, generation)
|
||||
|
||||
@@ -2502,12 +2502,62 @@ class AgentTests(unittest.TestCase):
|
||||
return f"ghcr.io/ayymoss/plutainer|latest|{image_digest}|{image_id}\n"
|
||||
return original(*args, **kwargs)
|
||||
self.docker.call = call
|
||||
module.atomic_json(self.agent.root / "agent/runtime-images.json", {"ghcr.io/ayymoss/plutainer": image_id, "removed-image": "missing-id"})
|
||||
images = self.agent.inventory()["runtimeImages"]
|
||||
prepared = ["ghcr.io/ayymoss/plutainer", "ghcr.io/ayymoss/plutainer@" + image_digest]
|
||||
module.atomic_json(self.agent.root / "agent/runtime-images.json", {**dict.fromkeys(prepared, image_id), "removed-image": "missing-id"})
|
||||
inventory = self.agent.inventory()
|
||||
images = inventory["runtimeImages"]
|
||||
self.assertIn("ghcr.io/ayymoss/plutainer@" + image_digest, images)
|
||||
self.assertIn("ghcr.io/ayymoss/plutainer:latest", images)
|
||||
self.assertIn("ghcr.io/ayymoss/plutainer", images)
|
||||
self.assertNotIn("removed-image", images)
|
||||
self.assertEqual(inventory["preparedRuntimeImages"], prepared)
|
||||
|
||||
def test_runtime_pull_prepares_only_the_requested_reference_from_the_docker_cache(self):
|
||||
image = "registry.example/custom-runtime:stable"
|
||||
image_id = "sha256:" + "1" * 64
|
||||
original = self.docker.call
|
||||
|
||||
def call(*args, **kwargs):
|
||||
if args[:2] == ("image", "ls"):
|
||||
return (f"registry.example/custom-runtime|stable|<none>|{image_id}\n"
|
||||
f"registry.example/custom-runtime|unprepared-alias|<none>|{image_id}\n"
|
||||
"unrelated-service|latest|<none>|sha256:unrelated\n")
|
||||
if args[:2] == ("image", "inspect"):
|
||||
self.assertEqual(args[-1], image)
|
||||
return image_id
|
||||
return original(*args, **kwargs)
|
||||
|
||||
self.docker.call = call
|
||||
before = self.agent.inventory()
|
||||
self.assertIn(image, before["runtimeImages"])
|
||||
self.assertIn("unrelated-service:latest", before["runtimeImages"])
|
||||
self.assertEqual(before["preparedRuntimeImages"], [])
|
||||
|
||||
result = self.agent.execute(self.operation("runtime.pull", {"image": image}))
|
||||
self.assertEqual(result["state"], "completed")
|
||||
inventory = self.agent.inventory()
|
||||
self.assertEqual(inventory["runtimeImages"], before["runtimeImages"])
|
||||
self.assertEqual(inventory["preparedRuntimeImages"], [image])
|
||||
|
||||
def test_runtime_preparation_inventory_fails_closed_when_records_or_docker_are_unavailable(self):
|
||||
aliases = self.agent.root / "agent/runtime-images.json"
|
||||
original = self.docker.call
|
||||
|
||||
def call(*args, **kwargs):
|
||||
if args[:2] == ("image", "ls"):
|
||||
return "custom-runtime|latest|<none>|sha256:prepared\n"
|
||||
return original(*args, **kwargs)
|
||||
|
||||
self.docker.call = call
|
||||
for content in ("invalid json", "[]", '{"custom-runtime:latest": []}', '{"custom-runtime:latest": "sha256:removed"}'):
|
||||
with self.subTest(content=content):
|
||||
aliases.write_text(content)
|
||||
inventory = self.agent.inventory()
|
||||
self.assertEqual(inventory["preparedRuntimeImages"], [])
|
||||
self.assertEqual(inventory["runtimeImages"], ["custom-runtime:latest"])
|
||||
module.atomic_json(aliases, {"custom-runtime:latest": "sha256:prepared"})
|
||||
with patch.object(self.docker, "call", side_effect=AgentError("Docker unavailable")):
|
||||
self.assertEqual(self.agent.inventory()["preparedRuntimeImages"], [])
|
||||
|
||||
def test_heartbeat_applies_configured_host_port_range_before_deployment(self):
|
||||
self.agent.request = lambda *_: {"host": {"portMin": 30000, "portMax": 30100}}
|
||||
|
||||
Reference in New Issue
Block a user