108 lines
3.3 KiB
Python
108 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the dashboard image through Portainer, then trigger GitOps redeploy."""
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import json
|
|
import os
|
|
import tarfile
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
PORTAINER_URL = os.environ["PORTAINER_URL"].rstrip("/")
|
|
PORTAINER_KEY = os.environ["PORTAINER_API_KEY"]
|
|
ENDPOINT_ID = 17
|
|
STACK_NAME = "arena-dashboard"
|
|
IMAGE = "arena-dashboard:latest"
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def request(path: str, *, method: str = "GET", data: bytes | None = None,
|
|
content_type: str | None = None, timeout: int = 60):
|
|
headers = {"X-API-Key": PORTAINER_KEY}
|
|
if content_type:
|
|
headers["Content-Type"] = content_type
|
|
req = urllib.request.Request(
|
|
f"{PORTAINER_URL}/api{path}", data=data, headers=headers, method=method
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
body = response.read()
|
|
return response.status, body
|
|
except urllib.error.HTTPError as exc:
|
|
detail = exc.read().decode("utf-8", "replace")
|
|
raise RuntimeError(f"Portainer HTTP {exc.code}: {detail}") from exc
|
|
|
|
|
|
def build_context() -> bytes:
|
|
buf = io.BytesIO()
|
|
with tarfile.open(fileobj=buf, mode="w") as archive:
|
|
for path in ROOT.rglob("*"):
|
|
if not path.is_file() or ".git" in path.parts:
|
|
continue
|
|
archive.add(path, arcname=path.relative_to(ROOT).as_posix())
|
|
return buf.getvalue()
|
|
|
|
|
|
def build_image() -> None:
|
|
_, raw = request(
|
|
f"/endpoints/{ENDPOINT_ID}/docker/build?t={IMAGE}&dockerfile=Dockerfile&rm=true",
|
|
method="POST",
|
|
data=build_context(),
|
|
content_type="application/x-tar",
|
|
timeout=600,
|
|
)
|
|
errors = []
|
|
for line in raw.decode("utf-8", "replace").splitlines():
|
|
try:
|
|
event = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
if event.get("error"):
|
|
errors.append(event["error"])
|
|
if event.get("stream"):
|
|
print(event["stream"], end="")
|
|
if errors:
|
|
raise RuntimeError("image build failed: " + "; ".join(errors))
|
|
|
|
|
|
def redeploy_stack() -> None:
|
|
_, raw = request("/stacks")
|
|
stacks = json.loads(raw)
|
|
stack = next(
|
|
(item for item in stacks
|
|
if item.get("Name") == STACK_NAME and item.get("EndpointId") == ENDPOINT_ID),
|
|
None,
|
|
)
|
|
if stack is None:
|
|
raise RuntimeError(f"Portainer stack {STACK_NAME!r} was not found")
|
|
|
|
# GitOps polling sees the pushed commit. ForceUpdate also makes later polls
|
|
# recreate the service from the local image built by this workflow.
|
|
payload = {
|
|
"RepositoryReferenceName": "refs/heads/main",
|
|
"RepositoryAuthentication": False,
|
|
"TLSSkipVerify": False,
|
|
"Env": stack.get("Env") or [],
|
|
"AutoUpdate": {
|
|
"Interval": "5m",
|
|
"Webhook": "",
|
|
"ForceUpdate": True,
|
|
"ForcePullImage": False,
|
|
},
|
|
}
|
|
request(
|
|
f"/stacks/{stack['Id']}/git?endpointId={ENDPOINT_ID}",
|
|
method="POST",
|
|
data=json.dumps(payload).encode(),
|
|
content_type="application/json",
|
|
timeout=300,
|
|
)
|
|
print(f"Redeploy requested for stack {stack['Id']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
build_image()
|
|
redeploy_stack()
|