phoenix-deploy-api: OpenAPI, server, systemd install, env example
Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-27 18:50:54 -07:00
parent d38581f04a
commit 92d854a31c
5 changed files with 87 additions and 3 deletions

View File

@@ -28,3 +28,8 @@ PHOENIX_ALERT_WEBHOOK_SECRET=
# Optional: comma-separated API keys for /api/v1/* (X-API-Key or Bearer)
PHOENIX_PARTNER_KEYS=
# Optional: override path for GET /api/v1/public-sector/programs (else bundled copy, repo config/, or ../config/)
PUBLIC_SECTOR_MANIFEST_PATH=
# Optional: proxmox repo root on host (manifest = $PHOENIX_REPO_ROOT/config/public-sector-program-manifest.json)
PHOENIX_REPO_ROOT=

View File

@@ -16,9 +16,10 @@ Gitea webhook receiver and deploy endpoint stub for Gitea → Phoenix deployment
| GET | /api/v1/health/metrics | Prometheus query proxy (`?query=<PromQL>`) |
| GET | /api/v1/health/alerts | Active alerts (optional PROMETHEUS_ALERTS_URL) |
| GET | /api/v1/health/summary | Aggregated health for Portal |
| GET | /api/v1/public-sector/programs | Public-sector / eIDAS program manifest (JSON; **no API key**) |
| GET | /health | Health check |
All `/api/v1/*` routes accept optional partner API key when `PHOENIX_PARTNER_KEYS` is set (`X-API-Key` or `Authorization: Bearer <key>`).
All `/api/v1/*` routes except **`GET /api/v1/public-sector/programs`** accept optional partner API key when `PHOENIX_PARTNER_KEYS` is set (`X-API-Key` or `Authorization: Bearer <key>`).
## Environment
@@ -42,6 +43,10 @@ Copy `.env.example` to `.env` and set `GITEA_TOKEN` (and optionally `PHOENIX_DEP
| PHOENIX_WEBHOOK_URL | | Outbound webhook URL; POST deploy events with X-Phoenix-Signature |
| PHOENIX_WEBHOOK_SECRET | | Secret to sign webhook payloads (HMAC-SHA256) |
| PHOENIX_PARTNER_KEYS | | Comma-separated API keys for /api/v1/* (optional) |
| PUBLIC_SECTOR_MANIFEST_PATH | | Override JSON path for `/api/v1/public-sector/programs` |
| PHOENIX_REPO_ROOT | | Proxmox repo root; loads `config/public-sector-program-manifest.json` if present |
**Program manifest:** From a full repo checkout, the file is `config/public-sector-program-manifest.json`. `scripts/install-systemd.sh` copies it next to `server.js` on `/opt/phoenix-deploy-api` so the endpoint works without a full tree.
## Gitea Webhook Configuration

View File

@@ -15,6 +15,7 @@ tags:
- name: Infra
- name: VE
- name: Health
- name: PublicSector
- name: System
paths:
@@ -66,6 +67,27 @@ paths:
'202': { description: Accepted }
'401': { description: Unauthorized }
/api/v1/public-sector/programs:
get:
tags: [PublicSector]
summary: Public-sector and eIDAS program manifest (JSON)
description: |
Serves `config/public-sector-program-manifest.json` from the proxmox repo (or `PUBLIC_SECTOR_MANIFEST_PATH`).
No API key required. Returns 503 if the file is missing on the host.
responses:
'200':
description: Manifest JSON
content:
application/json:
schema:
type: object
properties:
schemaVersion: { type: string }
updated: { type: string }
programs: { type: array, items: { type: object } }
'500': { description: Invalid JSON or read error }
'503': { description: Manifest file not found }
/api/v1/infra/nodes:
get:
tags: [Infra]

View File

@@ -18,6 +18,13 @@ fi
echo "Installing Phoenix Deploy API to $TARGET ..."
mkdir -p "$TARGET"
cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$APP_DIR/package-lock.json" "$TARGET/" 2>/dev/null || cp -a "$APP_DIR/server.js" "$APP_DIR/package.json" "$TARGET/"
# Program manifest for GET /api/v1/public-sector/programs (server loads from cwd-adjacent copy on /opt)
if [[ -f "$REPO_ROOT/config/public-sector-program-manifest.json" ]]; then
cp -a "$REPO_ROOT/config/public-sector-program-manifest.json" "$TARGET/public-sector-program-manifest.json"
echo "Installed public-sector-program-manifest.json"
else
echo "WARN: $REPO_ROOT/config/public-sector-program-manifest.json missing — set PUBLIC_SECTOR_MANIFEST_PATH in .env"
fi
[ -f "$APP_DIR/.env" ] && cp "$APP_DIR/.env" "$TARGET/.env" || [ -f "$APP_DIR/.env.example" ] && cp "$APP_DIR/.env.example" "$TARGET/.env" || true
chown -R root:root "$TARGET"
cd "$TARGET" && npm install --omit=dev

View File

@@ -9,6 +9,7 @@
* GET /api/v1/infra/storage — Storage pools (Proxmox or stub)
* GET /api/v1/ve/vms — List VMs/CTs (Proxmox or stub)
* GET /api/v1/ve/vms/:node/:vmid/status — VM/CT status
* GET /api/v1/public-sector/programs — Public-sector / eIDAS program manifest (JSON)
* GET /health — Health check
*
* Env: PORT, GITEA_URL, GITEA_TOKEN, PHOENIX_DEPLOY_SECRET
@@ -18,7 +19,7 @@
import crypto from 'crypto';
import https from 'https';
import path from 'path';
import { readFileSync } from 'fs';
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import express from 'express';
@@ -42,6 +43,26 @@ const PHOENIX_WEBHOOK_URL = process.env.PHOENIX_WEBHOOK_URL || '';
const PHOENIX_WEBHOOK_SECRET = process.env.PHOENIX_WEBHOOK_SECRET || '';
const PARTNER_KEYS = (process.env.PHOENIX_PARTNER_KEYS || '').split(',').map((k) => k.trim()).filter(Boolean);
/**
* Manifest resolution order:
* 1) PUBLIC_SECTOR_MANIFEST_PATH (explicit file)
* 2) public-sector-program-manifest.json next to server.js (systemd install copies this)
* 3) PHOENIX_REPO_ROOT or PROXMOX_REPO_PATH + config/public-sector-program-manifest.json
* 4) ../config/... (running from phoenix-deploy-api inside full proxmox clone)
*/
function resolvePublicSectorManifestPath() {
const override = (process.env.PUBLIC_SECTOR_MANIFEST_PATH || '').trim();
if (override && existsSync(override)) return override;
const bundled = path.join(__dirname, 'public-sector-program-manifest.json');
if (existsSync(bundled)) return bundled;
const repoRoot = (process.env.PHOENIX_REPO_ROOT || process.env.PROXMOX_REPO_PATH || '').trim().replace(/\/$/, '');
if (repoRoot) {
const fromRepo = path.join(repoRoot, 'config', 'public-sector-program-manifest.json');
if (existsSync(fromRepo)) return fromRepo;
}
return path.join(__dirname, '..', 'config', 'public-sector-program-manifest.json');
}
const httpsAgent = new https.Agent({ rejectUnauthorized: process.env.PROXMOX_TLS_VERIFY !== '0' });
async function proxmoxRequest(endpoint, method = 'GET', body = null) {
@@ -207,6 +228,28 @@ app.post('/api/deploy', async (req, res) => {
}
});
/**
* GET /api/v1/public-sector/programs — Program registry for operators / Phoenix UI (non-secret).
* Registered before partnerKeyMiddleware so callers do not need X-API-Key.
*/
app.get('/api/v1/public-sector/programs', (req, res) => {
const manifestPath = resolvePublicSectorManifestPath();
try {
if (!existsSync(manifestPath)) {
return res.status(503).json({
error: 'Manifest not found',
path: manifestPath,
hint: 'Set PUBLIC_SECTOR_MANIFEST_PATH or deploy repo with config/public-sector-program-manifest.json next to phoenix-deploy-api/',
});
}
const raw = readFileSync(manifestPath, 'utf8');
const data = JSON.parse(raw);
res.type('application/json').json(data);
} catch (err) {
res.status(500).json({ error: err.message, path: manifestPath });
}
});
app.use('/api/v1', partnerKeyMiddleware);
/**
@@ -431,5 +474,7 @@ app.listen(PORT, () => {
if (!GITEA_TOKEN) console.warn('GITEA_TOKEN not set — commit status updates disabled');
if (!hasProxmox) console.warn('PROXMOX_* not set — Infra/VE API returns stub data');
if (PHOENIX_WEBHOOK_URL) console.log('Outbound webhook enabled:', PHOENIX_WEBHOOK_URL);
if (PARTNER_KEYS.length > 0) console.log('Partner API key auth enabled for /api/v1/*');
if (PARTNER_KEYS.length > 0) console.log('Partner API key auth enabled for /api/v1/* (except GET /api/v1/public-sector/programs)');
const mpath = resolvePublicSectorManifestPath();
console.log(`Public-sector manifest: ${mpath} (${existsSync(mpath) ? 'ok' : 'missing'})`);
});