From 8a67e6bcc742a935f33ef7e5eddf66e0145c58dc Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:21:40 +0200 Subject: [PATCH 1/3] feat: the hub as a compose project, held disabled for cutover, and every rollout reported to the hub --- deploy/graphite-hub/compose.yaml | 34 +++++++++++ deploy/graphite-hub/values | 8 +++ host/bootstrap.sh | 3 +- host/graphite-report | 97 ++++++++++++++++++++++++++++++++ host/graphite-up | 2 + projects/graphite-hub.yml | 6 +- 6 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 deploy/graphite-hub/compose.yaml create mode 100755 host/graphite-report diff --git a/deploy/graphite-hub/compose.yaml b/deploy/graphite-hub/compose.yaml new file mode 100644 index 0000000..ef4b336 --- /dev/null +++ b/deploy/graphite-hub/compose.yaml @@ -0,0 +1,34 @@ +services: + app: + extends: {file: ../../shared/app.yaml, service: app} + image: ghcr.io/project-graphite/graphite-hub/site:${IMAGE_TAG:?} + environment: + REDIS_URL: redis://:${REDIS_PASSWORD:?}@redis:6379 + SESSION_SECRET: ${SESSION_SECRET:?} + GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:?} + GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:?} + GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:?} + DEPLOY_REPORT_SECRET: ${DEPLOY_REPORT_SECRET:?} + PLATFORM_APP_CLIENT_ID: ${PLATFORM_APP_CLIENT_ID:?} + PLATFORM_APP_PRIVATE_KEY: ${PLATFORM_APP_PRIVATE_KEY:?} + SMTP_URL: ${SMTP_URL:-} + ALERTS_EMAIL: ${ALERTS_EMAIL:-} + ALERTS_FROM: ${ALERTS_FROM:-} + volumes: [data:/data] + labels: {graphite.backup.volumes: data} + healthcheck: + test: [CMD, node, -e, "fetch('http://127.0.0.1:3000/health/ready').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"] + start_period: 20s + stop_grace_period: 10s + depends_on: + redis: {condition: service_healthy} + + redis: + extends: {file: ../../shared/redis-password.yaml, service: redis} + +volumes: + data: {} + redis-data: {} + +networks: + edge: {external: true} diff --git a/deploy/graphite-hub/values b/deploy/graphite-hub/values index 7464517..94f000d 100644 --- a/deploy/graphite-hub/values +++ b/deploy/graphite-hub/values @@ -1 +1,9 @@ IMAGE_TAG=sha-004b86c +ENABLED=false +PROXIED=true +APP_MEMORY=1024M +APP_RESERVE=512M +APP_CPUS=1.5 +REDIS_MEMORY=128M +REDIS_MAXMEMORY=96mb +REDIS_POLICY=noeviction diff --git a/host/bootstrap.sh b/host/bootstrap.sh index b38d302..3e7980a 100755 --- a/host/bootstrap.sh +++ b/host/bootstrap.sh @@ -14,13 +14,14 @@ id deploy >/dev/null 2>&1 || useradd --system --create-home --shell /usr/sbin/no install -d -o deploy -g deploy /srv/graphite /srv/graphite/bin /srv/graphite/edge /srv/graphite/edge/routes curl -fsSL -o /srv/graphite/bin/graphite-up "$source/host/graphite-up" +curl -fsSL -o /srv/graphite/bin/graphite-report "$source/host/graphite-report" curl -fsSL -o /srv/graphite/bin/agent.py "$source/host/agent.py" curl -fsSL -o /srv/graphite/edge/compose.yaml "$source/edge/compose.yaml" curl -fsSL -o /srv/graphite/edge/traefik.yml "$source/edge/traefik.yml" curl -fsSL -o /srv/graphite/edge/routes/edge.yml "$source/edge/agent.yml" curl -fsSL -o /etc/systemd/system/graphite-agent.service "$source/host/graphite-agent.service" printf 'CF_DNS_API_TOKEN=%s\n' "$CF_DNS_API_TOKEN" > /srv/graphite/edge/secrets.env -chmod 755 /srv/graphite/bin/graphite-up +chmod 755 /srv/graphite/bin/graphite-up /srv/graphite/bin/graphite-report chown -R deploy:deploy /srv/graphite chmod 600 /srv/graphite/edge/secrets.env diff --git a/host/graphite-report b/host/graphite-report new file mode 100755 index 0000000..ca05b4a --- /dev/null +++ b/host/graphite-report @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +import datetime +import hashlib +import hmac +import json +import pathlib +import re +import subprocess +import sys +import time +import urllib.error +import urllib.request + +BASE = pathlib.Path("/srv/graphite") +AGENT_ENV = pathlib.Path("/etc/graphite/agent.env") +HUB = "graphite-hub" +LINE = re.compile(r"""^([A-Z_][A-Z0-9_]*)=(?:'([^']*)'|"((?:[^"\\]|\\.)*)"|(.*))$""") +ESCAPES = {"n": "\n", "r": "\r", "t": "\t"} + + +def settings(path): + found = {} + for line in path.read_text(encoding="utf-8").splitlines() if path.exists() else []: + match = LINE.match(line) + if match: + single, double, plain = match.group(2, 3, 4) + found[match[1]] = single if single is not None else re.sub( + r"\\(.)", lambda escape: ESCAPES.get(escape[1], escape[1]), double + ) if double is not None else plain.strip() + return found + + +def docker(*args): + return subprocess.run(["docker", *args], capture_output=True, text=True, timeout=30).stdout.strip() + + +def hub_address(): + container = docker( + "ps", "-q", + "--filter", f"label=com.docker.compose.project={HUB}", + "--filter", "label=com.docker.compose.service=app", + ).split("\n")[0] + if not container: + return None + return docker("inspect", "-f", '{{with index .NetworkSettings.Networks "edge"}}{{.IPAddress}}{{end}}', container) or None + + +def health(host): + started = time.monotonic() + try: + with urllib.request.urlopen(f"https://{host}/", timeout=10) as response: + status = response.status + except urllib.error.HTTPError as error: + status = error.code + except (urllib.error.URLError, OSError): + status = None + return status, round((time.monotonic() - started) * 1000) + + +def main(name, started, exit_status): + project = settings(BASE / name / "values") + if "HOST" not in project: + print(f"graphite-report: {name} has no host to report") + return 0 + secret = settings(AGENT_ENV).get("DEPLOY_REPORT_SECRET") + address = hub_address() + if not secret or not address: + print(f"graphite-report: nothing reported for {name}") + return 0 + status, ms = health(project["HOST"]) + body = json.dumps({ + "project": name, + "tag": project.get("IMAGE_TAG", "unknown"), + "startedAt": started, + "finishedAt": datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), + "ok": exit_status == "0", + **({"healthStatus": status} if status is not None else {}), + "healthMs": ms, + }).encode() + stamp = str(int(time.time())) + request = urllib.request.Request( + f"http://{address}:3000/internal/deploys", + data=body, + method="POST", + headers={ + "Content-Type": "application/json", + "X-Graphite-Timestamp": stamp, + "X-Graphite-Signature": hmac.new(secret.encode(), stamp.encode() + b"." + body, hashlib.sha256).hexdigest(), + }, + ) + with urllib.request.urlopen(request, timeout=10) as response: + print(f"graphite-report: {name} reported to the hub ({response.status})") + return 0 + + +if __name__ == "__main__": + sys.exit(main(*sys.argv[1:4])) diff --git a/host/graphite-up b/host/graphite-up index 74753af..dc9b0d5 100755 --- a/host/graphite-up +++ b/host/graphite-up @@ -2,6 +2,8 @@ set -eu name=$1 +started=$(date -u +%Y-%m-%dT%H:%M:%SZ) +trap 'status=$?; /srv/graphite/bin/graphite-report "$name" "$started" "$status" || echo "graphite-up: rollout report failed" >&2; exit $status' EXIT cd "/srv/graphite/$name" env=/etc/graphite/$name.env if [ ! -f "$env" ]; then diff --git a/projects/graphite-hub.yml b/projects/graphite-hub.yml index 7d1b577..8e863ac 100644 --- a/projects/graphite-hub.yml +++ b/projects/graphite-hub.yml @@ -12,9 +12,5 @@ status: live listed: false deploy: - kind: pages + kind: compose host: project-graphite.com - source: - directory: site - output: dist - command: npm run build From 875fd08331019f1ad8f46461657620a57be782fc Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:26:22 +0200 Subject: [PATCH 2/3] feat: the console gets DEPLOY_REPORT_SECRET to read the hub's metrics --- deploy/console/compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/console/compose.yaml b/deploy/console/compose.yaml index 381df37..99008ea 100644 --- a/deploy/console/compose.yaml +++ b/deploy/console/compose.yaml @@ -17,6 +17,7 @@ services: ALERTS_FROM: ${ALERTS_FROM:-} PLATFORM_APP_CLIENT_ID: ${PLATFORM_APP_CLIENT_ID:-} PLATFORM_APP_PRIVATE_KEY: ${PLATFORM_APP_PRIVATE_KEY:-} + DEPLOY_REPORT_SECRET: ${DEPLOY_REPORT_SECRET:-} privileged: true pid: host volumes: [/var/run/docker.sock:/var/run/docker.sock, data:/data, logs:/logs] From 57679e22e4559ed7c3d4bb6b85a7bf7ec29b19df Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:28:33 +0200 Subject: [PATCH 3/3] chore: keep ENABLED=false on the last line of the hub's values --- deploy/graphite-hub/values | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/graphite-hub/values b/deploy/graphite-hub/values index 94f000d..121f76f 100644 --- a/deploy/graphite-hub/values +++ b/deploy/graphite-hub/values @@ -1,5 +1,4 @@ IMAGE_TAG=sha-004b86c -ENABLED=false PROXIED=true APP_MEMORY=1024M APP_RESERVE=512M @@ -7,3 +6,4 @@ APP_CPUS=1.5 REDIS_MEMORY=128M REDIS_MAXMEMORY=96mb REDIS_POLICY=noeviction +ENABLED=false