Retain only the verified production latest image
This commit is contained in:
+13
-15
@@ -107,7 +107,7 @@ been performed during implementation; those are first-run acceptance steps.
|
||||
an immutable rollback tag so normal dangling-image cleanup cannot remove it.
|
||||
4. Refuse a stale checked-out `main` revision that no longer equals remote main.
|
||||
Build an immutable `release-<commit>-<owner>` tag locally, without a registry
|
||||
and without changing `latest` or CI image tags.
|
||||
then point `latest` at the candidate; CI image tags are never changed.
|
||||
5. Persist a `prepared` journal with old/new runtime configs. Create an additive
|
||||
SQLite online backup under `/data/deploy-backups/<owner>.db`. No data/config
|
||||
directory or file is removed, replaced or recursively chowned.
|
||||
@@ -266,7 +266,7 @@ as an Actions artifact. First bootstrap has no prior healthy version to roll bac
|
||||
`192.0.2.10:18787` binding (not loopback).
|
||||
4. Open `http://192.0.2.10:18787/api/v1/version`; its `commit` must equal the full
|
||||
workflow checkout commit. Check `/ready` returns HTTP 200. The immutable image tag
|
||||
is `pigway-frame-flow:release-<commit>-<transaction>`; compare its image ID in
|
||||
is `pigway-frame-flow:latest`; compare its image ID in
|
||||
Container Manager with the running container.
|
||||
5. Keep `PROD_DEPLOY_ENABLED=true`. Subsequent main pushes Verify then deploy using
|
||||
this same runtime and journal mechanism. The first update records bootstrap as
|
||||
@@ -366,17 +366,15 @@ complete audit succeeded.
|
||||
|
||||
### Production image retention
|
||||
|
||||
After a deployment passes verification and its success journal is saved, Frame Flow
|
||||
removes obsolete release and rollback images from the
|
||||
configured production image repository. It retains the running version and the
|
||||
previous deployment baseline for one-step rollback. Images referenced by any
|
||||
container (including stopped containers), images with unrelated tags, and CI
|
||||
images are protected. Retention does not prune build cache, volumes, or data.
|
||||
Failed deployments do not run retention; cleanup failure produces a warning and
|
||||
never rolls back a successfully verified deployment. Historical deployment
|
||||
journals remain available for audit, but only the latest rollback image is
|
||||
retained locally; older releases require rebuilding before use.
|
||||
Production uses `pigway-frame-flow:latest`. Builds first create a unique release
|
||||
image, then tag it as latest before activation. The old image is pinned during
|
||||
an update so a failed deployment can restore it. Only after the new version
|
||||
passes verification are old release/rollback images and duplicate tags removed.
|
||||
After successful deployment, only the current production image remains; there
|
||||
is no retained historical image for manual rollback. Rebuild a historical
|
||||
revision before manually reverting a successful deployment.
|
||||
|
||||
The rollback image keeps one tag: its original `release-<commit>-<owner>`
|
||||
tag with `-rollback` appended. Duplicate release/legacy rollback tags are removed
|
||||
only after the rollback alias exists, and container-referenced tags remain protected.
|
||||
Images referenced by other containers (including stopped containers), images
|
||||
with unrelated tags, and CI resources remain protected. Cleanup never prunes
|
||||
volumes, data, or shared build cache. Cleanup failure is a warning and does not
|
||||
roll back a verified deployment. Failed deployments do not run cleanup.
|
||||
|
||||
@@ -337,6 +337,12 @@ class Deploy:
|
||||
def rollback(self, previous):
|
||||
self.activate(previous)
|
||||
self.verify(previous)
|
||||
self.run(
|
||||
"image",
|
||||
"tag",
|
||||
previous["services"]["pigway-frame-flow"]["image"],
|
||||
self.env["PROD_IMAGE_REPO"] + ":latest",
|
||||
)
|
||||
print("ROLLBACK VERIFIED", flush=True)
|
||||
|
||||
def secret_config(self):
|
||||
@@ -392,7 +398,9 @@ class Deploy:
|
||||
command += ["--build-arg", key + "=" + value]
|
||||
self.run(*command, str(ROOT))
|
||||
service = config["services"]["pigway-frame-flow"]
|
||||
service["image"] = image
|
||||
latest = self.env["PROD_IMAGE_REPO"] + ":latest"
|
||||
self.run("image", "tag", image, latest)
|
||||
service["image"] = latest
|
||||
proposed = self.secret_config()
|
||||
service["environment"].update(proposed)
|
||||
for key in ("SYNOLOGY_PHOTOS_URL", "SYNOLOGY_PHOTOS_CERT_SHA256"):
|
||||
@@ -666,6 +674,7 @@ class Deploy:
|
||||
self.state["status"] = "success"
|
||||
self.state_io(self.state)
|
||||
print("BOOTSTRAP VERIFIED", flush=True)
|
||||
self.cleanup_images()
|
||||
except BaseException:
|
||||
print("BOOTSTRAP FAILED", flush=True)
|
||||
self.diagnostics()
|
||||
@@ -726,9 +735,9 @@ class Deploy:
|
||||
print("Scoped container logs (redacted):", text, flush=True)
|
||||
|
||||
def cleanup_images(self):
|
||||
"""Retain container references and the verified deployment's rollback baseline."""
|
||||
"""Keep only latest after verification; protect unrelated container images."""
|
||||
try:
|
||||
protected = {self.state["previous_image"]}
|
||||
protected = set()
|
||||
referenced_tags = set()
|
||||
container_ids = self.run("ps", "-aq").stdout.decode().split()
|
||||
if container_ids:
|
||||
@@ -758,22 +767,23 @@ class Deploy:
|
||||
if not all(
|
||||
re.fullmatch(
|
||||
re.escape(repo)
|
||||
+ r":(?:release-[0-9a-f]{40}-[0-9a-f]{8}(?:-rollback)?|rollback-[0-9a-f]{32})",
|
||||
+ r":(?:latest|release-[0-9a-f]{40}-[0-9a-f]{8}(?:-rollback)?|rollback-[0-9a-f]{32})",
|
||||
tag,
|
||||
)
|
||||
for tag in tags
|
||||
):
|
||||
continue
|
||||
if image["Id"] == self.state["previous_image"]:
|
||||
pin = self.state.get("rollback_tag")
|
||||
# Untag duplicates only after the rollback alias is present.
|
||||
if pin not in tags:
|
||||
latest = repo + ":latest"
|
||||
if image["Id"] in protected:
|
||||
# Current production uses latest. Other containers retain
|
||||
# their referenced tags and images, even when stopped.
|
||||
if latest not in tags:
|
||||
continue
|
||||
tags = [
|
||||
tag for tag in tags if tag != pin and tag not in referenced_tags
|
||||
tag
|
||||
for tag in tags
|
||||
if tag != latest and tag not in referenced_tags
|
||||
]
|
||||
elif image["Id"] in protected:
|
||||
continue
|
||||
if not tags:
|
||||
continue
|
||||
result = self.run("image", "rm", *tags, check=False)
|
||||
|
||||
@@ -253,13 +253,13 @@ def test_lock_conflict_never_activates_production(tool, monkeypatch):
|
||||
tool.deploy()
|
||||
|
||||
|
||||
def test_image_retention_protects_containers_rollback_and_other_tags(tool, monkeypatch):
|
||||
def test_image_retention_keeps_latest_and_protects_other_containers(tool, monkeypatch):
|
||||
tool.state = {"previous_image": "previous"}
|
||||
release = "pigway-frame-flow:release-" + "a" * 40 + "-" + "b" * 8
|
||||
rollback = "pigway-frame-flow:rollback-" + "c" * 32
|
||||
tool.state["rollback_tag"] = release + "-rollback"
|
||||
images = [
|
||||
{"Id": "current", "RepoTags": [release]},
|
||||
{"Id": "current", "RepoTags": ["pigway-frame-flow:latest", release]},
|
||||
{"Id": "previous", "RepoTags": [release + "-rollback", release, rollback]},
|
||||
{"Id": "stopped", "RepoTags": [release]},
|
||||
{"Id": "obsolete", "RepoTags": [release, rollback]},
|
||||
@@ -273,7 +273,15 @@ def test_image_retention_protects_containers_rollback_and_other_tags(tool, monke
|
||||
if args == ("ps", "-aq"):
|
||||
data = "container1 container2"
|
||||
elif args[0] == "inspect":
|
||||
data = json.dumps([{"Image": "current"}, {"Image": "stopped"}])
|
||||
data = json.dumps(
|
||||
[
|
||||
{
|
||||
"Image": "current",
|
||||
"Config": {"Image": "pigway-frame-flow:latest"},
|
||||
},
|
||||
{"Image": "stopped"},
|
||||
]
|
||||
)
|
||||
elif args[:2] == ("image", "ls"):
|
||||
data = "current previous stopped obsolete shared ci"
|
||||
elif args[:2] == ("image", "inspect"):
|
||||
@@ -285,7 +293,8 @@ def test_image_retention_protects_containers_rollback_and_other_tags(tool, monke
|
||||
monkeypatch.setattr(tool, "run", run)
|
||||
tool.cleanup_images()
|
||||
assert [c for c in calls if c[:2] == ("image", "rm")] == [
|
||||
("image", "rm", release, rollback),
|
||||
("image", "rm", release),
|
||||
("image", "rm", release + "-rollback", release, rollback),
|
||||
("image", "rm", release, rollback),
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user