This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Build and deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-deploy:
|
||||
runs-on: arena-builder
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build image on OptiPlex
|
||||
env:
|
||||
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
|
||||
PORTAINER_API_KEY: ${{ secrets.PORTAINER_API_KEY }}
|
||||
run: |
|
||||
python3 .gitea/scripts/build_and_deploy.py
|
||||
Reference in New Issue
Block a user