Keep one rollback tag derived from original release
This commit is contained in:
@@ -367,7 +367,7 @@ complete audit succeeded.
|
||||
### Production image retention
|
||||
|
||||
After a deployment passes verification and its success journal is saved, Frame Flow
|
||||
removes obsolete `release-<commit>-<owner>` and `rollback-<owner>` images from the
|
||||
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
|
||||
@@ -376,3 +376,7 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -37,6 +37,7 @@ class Deploy:
|
||||
self.lock_id = None
|
||||
self.state = None
|
||||
self.old_image = None
|
||||
self.old_image_tag = ""
|
||||
self.secret_values = [
|
||||
self.env[key]
|
||||
for key in (
|
||||
@@ -196,6 +197,7 @@ class Deploy:
|
||||
):
|
||||
raise ValueError("Production user/restart policy differs from repository")
|
||||
self.old_image = old["Image"]
|
||||
self.old_image_tag = old["Config"].get("Image", "")
|
||||
volume = self.run(
|
||||
"volume", "inspect", self.env["PROD_STATE_VOLUME"], check=False
|
||||
)
|
||||
@@ -727,10 +729,15 @@ class Deploy:
|
||||
"""Retain container references and the verified deployment's rollback baseline."""
|
||||
try:
|
||||
protected = {self.state["previous_image"]}
|
||||
referenced_tags = set()
|
||||
container_ids = self.run("ps", "-aq").stdout.decode().split()
|
||||
if container_ids:
|
||||
containers = json.loads(self.run("inspect", *container_ids).stdout)
|
||||
protected.update(container["Image"] for container in containers)
|
||||
referenced_tags.update(
|
||||
container.get("Config", {}).get("Image", "")
|
||||
for container in containers
|
||||
)
|
||||
repo = self.env["PROD_IMAGE_REPO"]
|
||||
image_ids = (
|
||||
self.run("image", "ls", "--no-trunc", "-q", repo)
|
||||
@@ -746,17 +753,29 @@ class Deploy:
|
||||
for image in images:
|
||||
tags = image.get("RepoTags") or []
|
||||
# Shared images and unrelated tags are never cleanup targets.
|
||||
if image["Id"] in protected or not tags:
|
||||
if not tags:
|
||||
continue
|
||||
if not all(
|
||||
re.fullmatch(
|
||||
re.escape(repo)
|
||||
+ r":(?:release-[0-9a-f]{40}-[0-9a-f]{8}|rollback-[0-9a-f]{32})",
|
||||
+ r":(?: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:
|
||||
continue
|
||||
tags = [
|
||||
tag for tag in tags if tag != pin and tag not in referenced_tags
|
||||
]
|
||||
elif image["Id"] in protected:
|
||||
continue
|
||||
if not tags:
|
||||
continue
|
||||
result = self.run("image", "rm", *tags, check=False)
|
||||
if result.returncode:
|
||||
print("IMAGE RETENTION: referenced image retained", flush=True)
|
||||
@@ -776,6 +795,14 @@ class Deploy:
|
||||
# Refresh baseline under the daemon-level lock.
|
||||
previous = self.preflight()
|
||||
version = self.verify(previous)
|
||||
release = self.old_image_tag.removesuffix("-rollback")
|
||||
if re.fullmatch(
|
||||
re.escape(self.env["PROD_IMAGE_REPO"])
|
||||
+ r":release-[0-9a-f]{40}-[0-9a-f]{8}",
|
||||
release,
|
||||
):
|
||||
pin = release + "-rollback"
|
||||
else:
|
||||
pin = self.env["PROD_IMAGE_REPO"] + ":rollback-" + self.owner
|
||||
self.run("image", "tag", self.old_image, pin)
|
||||
candidate = self.build(json.loads(json.dumps(previous)))
|
||||
|
||||
@@ -98,6 +98,7 @@ def test_preflight_rejects_wrong_host_resources(tool, monkeypatch, change):
|
||||
def test_deploy_rolls_back_exact_baseline_without_deleting_data(
|
||||
tool, monkeypatch, failure
|
||||
):
|
||||
tool.old_image_tag = "pigway-frame-flow:release-" + "a" * 40 + "-" + "b" * 8
|
||||
old = {"services": {"pigway-frame-flow": {"image": "sha256:previous"}}}
|
||||
new = {"services": {"pigway-frame-flow": {"image": "candidate"}}}
|
||||
activated, calls, saved = [], [], []
|
||||
@@ -138,6 +139,7 @@ def test_deploy_rolls_back_exact_baseline_without_deleting_data(
|
||||
assert cleanup == ["success"]
|
||||
assert activated == [new]
|
||||
assert saved[-1]["status"] == "success"
|
||||
assert saved[-1]["rollback_tag"] == tool.old_image_tag + "-rollback"
|
||||
assert not any(
|
||||
isinstance(c, tuple) and ("down" in c or "prune" in c) for c in calls
|
||||
)
|
||||
@@ -255,9 +257,10 @@ def test_image_retention_protects_containers_rollback_and_other_tags(tool, monke
|
||||
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": "previous", "RepoTags": [rollback]},
|
||||
{"Id": "previous", "RepoTags": [release + "-rollback", release, rollback]},
|
||||
{"Id": "stopped", "RepoTags": [release]},
|
||||
{"Id": "obsolete", "RepoTags": [release, rollback]},
|
||||
{"Id": "shared", "RepoTags": [release, "other:latest"]},
|
||||
@@ -282,7 +285,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, rollback),
|
||||
("image", "rm", release, rollback),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user