98 lines
4.5 KiB
Python
98 lines
4.5 KiB
Python
"""Queue a digest-pinned, uniquely identified run on the gitops branch."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import uuid
|
|
|
|
|
|
def command(*args, cwd=None, env=None):
|
|
return subprocess.run(args, cwd=cwd, env=env, check=True, text=True,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip()
|
|
|
|
|
|
def resolve_release(release):
|
|
if re.fullmatch(r"[0-9a-f]{40}", release):
|
|
ref = release
|
|
else:
|
|
command("git", "check-ref-format", f"refs/tags/{release}")
|
|
ref = f"refs/tags/{release}"
|
|
sha = command("git", "rev-parse", "--verify", f"{ref}^{{commit}}")
|
|
if not re.fullmatch(r"[0-9a-f]{40}", sha):
|
|
raise ValueError("Invalid revision")
|
|
# Only revisions in the trusted default branch's history may run.
|
|
command("git", "merge-base", "--is-ancestor", sha, "origin/master")
|
|
return sha
|
|
|
|
|
|
def image_digest(reference, sha):
|
|
command("docker", "pull", reference)
|
|
image = json.loads(command("docker", "image", "inspect", reference))[0]
|
|
if (image["Config"].get("Labels") or {}).get("org.opencontainers.image.revision") != sha:
|
|
raise ValueError("Image revision label does not match the selected release")
|
|
repository = reference.rsplit(":", 1)[0]
|
|
matches = [item for item in image.get("RepoDigests", [])
|
|
if item.startswith(repository + "@sha256:")]
|
|
if len(matches) != 1 or not re.fullmatch(re.escape(repository) + r"@sha256:[0-9a-f]{64}", matches[0]):
|
|
raise ValueError("No unambiguous registry digest")
|
|
return matches[0]
|
|
|
|
|
|
def queue(request):
|
|
user, token = os.environ["GITOPS_USER"], os.environ["GITOPS_TOKEN"]
|
|
if not user or not token:
|
|
raise ValueError("GitOps credentials are required")
|
|
url = os.environ["GITOPS_URL"]
|
|
if url != "https://gitea.vandeplanque.com/tonio/car-tracker.git":
|
|
raise ValueError("Unexpected GitOps repository")
|
|
branch = os.environ.get("GITOPS_BRANCH", "gitops")
|
|
if branch != "gitops":
|
|
raise ValueError("Unexpected state branch")
|
|
with tempfile.TemporaryDirectory(prefix="car-tracker-request-") as temporary:
|
|
root = Path(temporary)
|
|
askpass = root / "askpass.sh"
|
|
askpass.write_text('#!/bin/sh\ncase "$1" in\n*Username*) printf "%s\\n" "$GITOPS_USER" ;;\n*) printf "%s\\n" "$GITOPS_TOKEN" ;;\nesac\n')
|
|
askpass.chmod(0o700)
|
|
env = {**os.environ, "GIT_ASKPASS": str(askpass), "GIT_TERMINAL_PROMPT": "0"}
|
|
repo = root / "state"
|
|
repo.mkdir()
|
|
command("git", "init", cwd=repo, env=env)
|
|
command("git", "remote", "add", "origin", url, cwd=repo, env=env)
|
|
exists = command("git", "ls-remote", "--heads", "origin", f"refs/heads/{branch}", cwd=repo, env=env)
|
|
source = branch if exists else "master"
|
|
command("git", "fetch", "origin", source, cwd=repo, env=env)
|
|
command("git", "checkout", "-b", branch, "FETCH_HEAD", cwd=repo, env=env)
|
|
path = repo / "deploy/request.json"
|
|
path.parent.mkdir(exist_ok=True)
|
|
path.write_text(json.dumps(request, indent=2) + "\n")
|
|
command("git", "add", "deploy/request.json", cwd=repo, env=env)
|
|
command("git", "-c", "user.name=Car tracker Actions", "-c", "user.email=actions@localhost",
|
|
"commit", "-m", f"Queue app run {request['request_id']} ({request['revision'][:12]})", cwd=repo, env=env)
|
|
# Normal push is compare-and-swap: concurrent requests fail rather than
|
|
# silently overwriting each other. Never force-push the request history.
|
|
command("git", "push", "origin", f"HEAD:refs/heads/{branch}", cwd=repo, env=env)
|
|
|
|
|
|
def main():
|
|
try:
|
|
sha = resolve_release(os.environ["RELEASE"])
|
|
reference = f"{os.environ['REGISTRY']}/{os.environ['IMAGE_REPOSITORY']}:{sha}"
|
|
digest = image_digest(reference, sha)
|
|
request = {"schema_version": 1, "request_id": str(uuid.uuid4()), "revision": sha, "image": digest}
|
|
queue(request)
|
|
print(f"Queued request: {request['request_id']}")
|
|
print(f"Image: {digest}")
|
|
print("This confirms Git publication only. Inspect the production agent's outcome for collection/import status.")
|
|
return 0
|
|
except (KeyError, ValueError, OSError, subprocess.SubprocessError):
|
|
# Git/registry errors may contain authentication details: don't echo them.
|
|
print("Request failed: check the release, published image, credentials and state branch. No run was confirmed.")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|