Replace static deploy-map with dynamic auth registry
updater now fetches app registry from auth /api/auth/internal/registry and uses internal_host/internal_port for SSH status checks and deploys. Removes hardcoded host map; platform infra (auth, portal, settings) kept in INFRA_DEPLOY/INFRA_HEALTH constants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b120fbf44e
commit
c26f7e18b2
3 changed files with 83 additions and 46 deletions
|
|
@ -1,16 +1,3 @@
|
|||
// Maps Forgejo repo names → target LXC SSH details.
|
||||
// Keys are the plain repo names Forgejo sends in the webhook (repository.name).
|
||||
// The updater only pulls + rebuilds existing LXCs — provisioning is done on the
|
||||
// Proxmox host with proxmox-helpers/add-app.sh, which appends new apps here.
|
||||
export const deployMap = {
|
||||
'noticeboard': { host: '10.10.10.112', path: '/opt/noticeboard' },
|
||||
'kitchen': { host: '10.10.10.110', path: '/opt/kitchen' },
|
||||
'cashup': { host: '10.10.10.111', path: '/opt/cashup' },
|
||||
'housekeeping': { host: '10.10.10.114', path: '/opt/housekeeping' },
|
||||
'forecasting': { host: '10.10.10.113', path: '/opt/forecasting' },
|
||||
'rates': { host: '10.10.10.115', path: '/opt/rates' },
|
||||
'portal': { host: '10.10.10.102', path: '/opt/portal' },
|
||||
'auth': { host: '10.10.10.101', path: '/opt/auth' },
|
||||
'settings': { host: '10.10.10.116', path: '/opt/settings' },
|
||||
// 'management' intentionally omitted — can't redeploy the container managing deploys
|
||||
}
|
||||
// Deploy targets are now fetched dynamically from the auth registry.
|
||||
// See src/index.js — INFRA_DEPLOY for platform services, appRegistry for user apps.
|
||||
// This file is no longer imported.
|
||||
|
|
|
|||
|
|
@ -2,33 +2,77 @@ import Fastify from 'fastify'
|
|||
import crypto from 'crypto'
|
||||
import { exec, execSync } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { deployMap } from '../deploy-map.js'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
const app = Fastify({ logger: true })
|
||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || ''
|
||||
const SSH_KEY = process.env.SSH_KEY_PATH || '/root/.ssh/id_ed25519'
|
||||
const FORGEJO_URL = process.env.FORGEJO_URL || 'https://git.pterois.co.uk'
|
||||
const FORGEJO_ORG = process.env.FORGEJO_ORG || 'hotel-manage-stack'
|
||||
const FORGEJO_TOKEN = process.env.FORGEJO_TOKEN || ''
|
||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || ''
|
||||
const SSH_KEY = process.env.SSH_KEY_PATH || '/root/.ssh/id_ed25519'
|
||||
const FORGEJO_URL = process.env.FORGEJO_URL || 'https://git.pterois.co.uk'
|
||||
const FORGEJO_ORG = process.env.FORGEJO_ORG || 'hotel-manage-stack'
|
||||
const FORGEJO_TOKEN = process.env.FORGEJO_TOKEN || ''
|
||||
const AUTH_URL = process.env.AUTH_URL || 'http://10.10.10.101:3001'
|
||||
const CENTRAL_AUTH_SECRET = process.env.CENTRAL_AUTH_SECRET || ''
|
||||
|
||||
// Platform services: fixed IPs, never in the user-facing apps table.
|
||||
// 'management' excluded from deploy — can't redeploy the container managing deploys.
|
||||
const INFRA_HEALTH = {
|
||||
portal: { host: '10.10.10.102', port: 3000 },
|
||||
auth: { host: '10.10.10.101', port: 3001 },
|
||||
settings: { host: '10.10.10.116', port: 3080 },
|
||||
}
|
||||
const INFRA_DEPLOY = {
|
||||
portal: { host: '10.10.10.102', path: '/opt/portal' },
|
||||
auth: { host: '10.10.10.101', path: '/opt/auth' },
|
||||
settings: { host: '10.10.10.116', path: '/opt/settings' },
|
||||
}
|
||||
|
||||
// App registry: fetched dynamically from auth — slug → { host, port, path }
|
||||
let appRegistry = {}
|
||||
|
||||
async function refreshRegistry() {
|
||||
try {
|
||||
const res = await fetch(`${AUTH_URL}/api/auth/internal/registry`, {
|
||||
headers: { Authorization: `Bearer ${CENTRAL_AUTH_SECRET}` },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (!res.ok) { app.log.warn(`Registry fetch ${res.status}`); return }
|
||||
const apps = await res.json()
|
||||
const next = {}
|
||||
for (const a of apps) {
|
||||
if (a.internal_host && a.internal_port) {
|
||||
next[a.slug] = { host: a.internal_host, port: a.internal_port, path: `/opt/${a.slug}` }
|
||||
}
|
||||
}
|
||||
appRegistry = next
|
||||
app.log.info(`Registry refreshed: ${Object.keys(next).join(', ') || '(empty)'}`)
|
||||
} catch (e) {
|
||||
app.log.warn(`Registry refresh failed: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function getHealthMap() {
|
||||
const map = { ...INFRA_HEALTH }
|
||||
for (const [slug, { host, port }] of Object.entries(appRegistry)) map[slug] = { host, port }
|
||||
return map
|
||||
}
|
||||
|
||||
function getDeployTarget(repo) {
|
||||
if (INFRA_DEPLOY[repo]) return INFRA_DEPLOY[repo]
|
||||
const a = appRegistry[repo]
|
||||
return a ? { host: a.host, path: a.path } : null
|
||||
}
|
||||
|
||||
function allDeployRepos() {
|
||||
return [...Object.keys(INFRA_DEPLOY), ...Object.keys(appRegistry)]
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const deployLog = []
|
||||
const statusCache = {}
|
||||
const CACHE_TTL = 5 * 60 * 1000
|
||||
|
||||
const healthMap = {
|
||||
'portal': { host: '10.10.10.102', port: 3000 },
|
||||
'auth': { host: '10.10.10.101', port: 3001 },
|
||||
'noticeboard': { host: '10.10.10.112', port: 3080 },
|
||||
'kitchen': { host: '10.10.10.110', port: 3080 },
|
||||
'cashup': { host: '10.10.10.111', port: 3080 },
|
||||
'housekeeping': { host: '10.10.10.114', port: 3080 },
|
||||
'forecasting': { host: '10.10.10.113', port: 3080 },
|
||||
'rates': { host: '10.10.10.115', port: 3080 },
|
||||
'settings': { host: '10.10.10.116', port: 3080 },
|
||||
}
|
||||
|
||||
function verifySignature(body, signature) {
|
||||
if (!WEBHOOK_SECRET) return true
|
||||
const expected = `sha256=${crypto.createHmac('sha256', WEBHOOK_SECRET).update(body).digest('hex')}`
|
||||
|
|
@ -61,7 +105,7 @@ async function getForgejoCommit(repo) {
|
|||
}
|
||||
|
||||
async function refreshStatus(repo) {
|
||||
const target = deployMap[repo]
|
||||
const target = getDeployTarget(repo)
|
||||
if (!target) return null
|
||||
|
||||
const [current, latest] = await Promise.all([
|
||||
|
|
@ -94,7 +138,7 @@ async function deploy(target, repoName) {
|
|||
entry.status = 'success'
|
||||
entry.output = out.slice(-500)
|
||||
app.log.info(`Deploy ${repoName} → success`)
|
||||
delete statusCache[repoName] // force re-check after deploy
|
||||
delete statusCache[repoName]
|
||||
} catch (err) {
|
||||
entry.status = 'failed'
|
||||
entry.error = err.message.slice(-500)
|
||||
|
|
@ -108,20 +152,18 @@ async function deploy(target, repoName) {
|
|||
app.get('/health', async () => ({ status: 'healthy' }))
|
||||
|
||||
app.get('/health-status', async () => {
|
||||
const results = await Promise.all(
|
||||
const healthMap = getHealthMap()
|
||||
return Promise.all(
|
||||
Object.entries(healthMap).map(async ([name, { host, port }]) => {
|
||||
const t0 = Date.now()
|
||||
try {
|
||||
await fetch(`http://${host}:${port}/health`, {
|
||||
signal: AbortSignal.timeout(3000),
|
||||
})
|
||||
await fetch(`http://${host}:${port}/health`, { signal: AbortSignal.timeout(3000) })
|
||||
return { name, up: true, ms: Date.now() - t0 }
|
||||
} catch {
|
||||
return { name, up: false, ms: null }
|
||||
}
|
||||
})
|
||||
)
|
||||
return results
|
||||
})
|
||||
|
||||
app.get('/deploys', async () => deployLog)
|
||||
|
|
@ -129,7 +171,7 @@ app.get('/deploys', async () => deployLog)
|
|||
app.get('/status', async (request) => {
|
||||
const single = request.query?.repo
|
||||
const force = request.query?.force === 'true'
|
||||
const repos = single ? [single] : Object.keys(deployMap)
|
||||
const repos = single ? [single] : allDeployRepos()
|
||||
const now = Date.now()
|
||||
const results = await Promise.all(repos.map(repo => {
|
||||
const cached = statusCache[repo]
|
||||
|
|
@ -139,10 +181,9 @@ app.get('/status', async (request) => {
|
|||
return results.filter(Boolean)
|
||||
})
|
||||
|
||||
// Manual deploy — no webhook signature needed, protected by portal admin auth
|
||||
app.post('/deploy/:repo', async (request, reply) => {
|
||||
const { repo } = request.params
|
||||
const target = deployMap[repo]
|
||||
const target = getDeployTarget(repo)
|
||||
if (!target) return reply.status(404).send({ error: 'Unknown repo' })
|
||||
|
||||
const running = deployLog.find(d => d.repo === repo && d.status === 'running')
|
||||
|
|
@ -152,7 +193,6 @@ app.post('/deploy/:repo', async (request, reply) => {
|
|||
return reply.status(202).send({ ok: true, deploying: repo })
|
||||
})
|
||||
|
||||
// Forgejo webhook — auto-deploy on push to main
|
||||
app.post('/webhook', { config: { rawBody: true } }, async (request, reply) => {
|
||||
const sig = request.headers['x-hub-signature-256']
|
||||
const rawBody = request.rawBody || JSON.stringify(request.body)
|
||||
|
|
@ -167,11 +207,19 @@ app.post('/webhook', { config: { rawBody: true } }, async (request, reply) => {
|
|||
return reply.status(200).send({ ok: true, skipped: 'not main branch' })
|
||||
}
|
||||
|
||||
const target = deployMap[repoName]
|
||||
// Refresh registry on push so a newly registered app deploys immediately
|
||||
await refreshRegistry()
|
||||
|
||||
const target = getDeployTarget(repoName)
|
||||
if (!target) return reply.status(200).send({ ok: true, skipped: 'no deploy target' })
|
||||
|
||||
deploy(target, repoName).catch(() => {})
|
||||
return reply.status(202).send({ ok: true, deploying: repoName })
|
||||
})
|
||||
|
||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
await refreshRegistry()
|
||||
setInterval(refreshRegistry, 5 * 60 * 1000) // keep registry fresh
|
||||
|
||||
await app.listen({ port: 9000, host: '0.0.0.0' })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue