dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,170 @@
|
||||
import { createServer } from 'node:http'
|
||||
import { chmod, mkdir, rm } from 'node:fs/promises'
|
||||
import { dirname } from 'node:path'
|
||||
import { PlatformGarbageCollector } from './gc.mjs'
|
||||
|
||||
async function jsonBody(request) {
|
||||
const chunks = []
|
||||
let size = 0
|
||||
for await (const chunk of request) {
|
||||
size += chunk.byteLength
|
||||
if (size > 256 * 1024) throw new Error('Bootstrap request body is too large')
|
||||
chunks.push(chunk)
|
||||
}
|
||||
return size === 0 ? {} : JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
||||
}
|
||||
|
||||
function send(response, status, value) {
|
||||
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
||||
response.end(`${JSON.stringify(value)}\n`)
|
||||
}
|
||||
|
||||
export function createBootstrapControl(runner, { deployments, trust, systemPlugins, systemSkills, snapshots } = {}) {
|
||||
return createServer(async (request, response) => {
|
||||
let pathname = 'invalid-url'
|
||||
try {
|
||||
pathname = new URL(request.url ?? '/', 'http://bootstrap.internal').pathname
|
||||
if (request.method === 'GET' && pathname === '/v1/status') send(response, 200, {
|
||||
...runner.status(),
|
||||
bootstrapVersion: process.env.DSH_BOOTSTRAP_VERSION ?? '1.0.0',
|
||||
})
|
||||
else if (request.method === 'POST' && pathname === '/v1/reload') send(response, 200, await runner.reload())
|
||||
else if (request.method === 'GET' && pathname === '/v1/health') send(response, 200, await runner.health())
|
||||
else if (request.method === 'GET' && pathname === '/v1/system-plugins' && systemPlugins !== undefined) {
|
||||
send(response, 200, { plugins: await systemPlugins.list() })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/system-plugins/action' && systemPlugins !== undefined) {
|
||||
const body = await jsonBody(request)
|
||||
send(response, 200, { plugins: await systemPlugins.configure(body.id, body.action) })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/system-plugins/recovery-action' && systemPlugins !== undefined) {
|
||||
const body = await jsonBody(request)
|
||||
send(response, 200, { plugins: await systemPlugins.recover(body.id, body.action) })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/system-plugins/discard' && systemPlugins !== undefined) {
|
||||
send(response, 200, { plugins: await systemPlugins.discard() })
|
||||
} else if (request.method === 'GET' && pathname === '/v1/system-skills' && systemSkills !== undefined) {
|
||||
send(response, 200, { skills: await systemSkills.list() })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/system-skills/action' && systemSkills !== undefined) {
|
||||
const body = await jsonBody(request)
|
||||
send(response, 200, { skills: await systemSkills.configure(body.skillId, body.action) })
|
||||
}
|
||||
else if (request.method === 'GET' && pathname === '/v1/deployments/current' && deployments !== undefined) {
|
||||
const state = await deployments.state()
|
||||
send(response, 200, { record: state.current === null ? null : await deployments.record(state.current) })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/deployments/activate' && deployments !== undefined && trust !== undefined) {
|
||||
const body = await jsonBody(request)
|
||||
const slots = await deployments.activateManaged(body.record, {
|
||||
healthCheck: () => runner.reload(),
|
||||
activateReceipts: tokens => trust.activate(tokens),
|
||||
})
|
||||
send(response, 200, { slots })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/deployments/rollback' && deployments !== undefined && trust !== undefined) {
|
||||
const body = await jsonBody(request)
|
||||
const state = await deployments.state()
|
||||
if (state.previous === null) throw new Error('no previous Deployment exists')
|
||||
if (body.recordId !== undefined && body.recordId !== state.previous) throw new Error('requested Deployment is not previous')
|
||||
const slots = await deployments.rollback({
|
||||
healthCheck: () => runner.reload(),
|
||||
activateReceipts: tokens => trust.activate(tokens),
|
||||
})
|
||||
send(response, 200, { slots })
|
||||
} else if (request.method === 'GET' && pathname === '/v1/deployments/rollback-plan' && deployments !== undefined) {
|
||||
send(response, 200, await deployments.rollbackPlan())
|
||||
} else if (request.method === 'POST' && pathname === '/v1/deployments/candidate' && deployments !== undefined) {
|
||||
const body = await jsonBody(request)
|
||||
const record = await deployments.stageCandidate(body.record, () => runner.reload())
|
||||
send(response, 200, { recordId: record.id })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/deployments/candidate/commit' && deployments !== undefined && trust !== undefined) {
|
||||
const body = await jsonBody(request)
|
||||
const slots = await deployments.commitCandidate(body.recordId, tokens => trust.activate(tokens))
|
||||
send(response, 200, { slots })
|
||||
} else if (request.method === 'POST' && pathname === '/v1/deployments/candidate/cancel' && deployments !== undefined) {
|
||||
send(response, 200, await deployments.cancelCandidate())
|
||||
} else if (request.method === 'POST' && pathname === '/v1/platform/gc' && deployments !== undefined) {
|
||||
send(response, 200, await new PlatformGarbageCollector({ paths: deployments.paths, deployments, snapshots }).collect())
|
||||
} else if (request.method === 'POST' && pathname === '/v1/deployments/runtime/reset' && deployments !== undefined) {
|
||||
send(response, 200, await deployments.resetCurrentRuntime({
|
||||
pauseDsh: () => runner.pause('dsh-runtime'),
|
||||
restartDsh: () => runner.restart('dsh-runtime'),
|
||||
}))
|
||||
}
|
||||
else {
|
||||
const operation = /^\/v1\/components\/([a-z0-9][a-z0-9._-]{0,127})\/(pause|suspend|resume|restart)$/.exec(pathname)
|
||||
if (request.method === 'POST' && operation !== null) {
|
||||
const [componentId, action] = operation.slice(1)
|
||||
if (componentId === 'dsh-runtime' && action === 'resume' && deployments !== undefined) {
|
||||
const status = await deployments.exclusive(async () => {
|
||||
await deployments.setOperation('starting')
|
||||
try {
|
||||
const value = await runner.resume(componentId)
|
||||
await deployments.publishStatus()
|
||||
return value
|
||||
} catch (error) {
|
||||
await deployments.publishStatus({
|
||||
recoveryMode: runner.status().recoveryMode
|
||||
?? (error instanceof Error ? error.message : String(error)),
|
||||
})
|
||||
throw error
|
||||
}
|
||||
})
|
||||
send(response, 200, status)
|
||||
} else if (componentId === 'dsh-runtime' && action === 'restart' && deployments !== undefined) {
|
||||
const status = await deployments.exclusive(async () => {
|
||||
await deployments.setOperation('restarting')
|
||||
let transaction
|
||||
try {
|
||||
transaction = await systemPlugins?.prepare?.()
|
||||
const value = await runner.restart(componentId, {
|
||||
skipPreparation: true,
|
||||
recoverStart: true,
|
||||
beforeStart: () => transaction?.activate?.(),
|
||||
onStartFailure: () => transaction?.rollback?.(),
|
||||
})
|
||||
if (transaction?.commit !== undefined) {
|
||||
try {
|
||||
await transaction.commit()
|
||||
} catch (error) {
|
||||
await runner.record?.('system-plugin.view.commit.failed', { error, level: 'warning' })
|
||||
}
|
||||
}
|
||||
await deployments.publishStatus()
|
||||
return value
|
||||
} catch (error) {
|
||||
try {
|
||||
await deployments.publishStatus({
|
||||
operation: 'restart-failed',
|
||||
recoveryMode: runner.status().recoveryMode
|
||||
?? (error instanceof Error ? error.message : String(error)),
|
||||
})
|
||||
} catch (statusError) {
|
||||
await runner.record?.('deployment.status.failed', {
|
||||
error: statusError,
|
||||
level: 'warning',
|
||||
originalError: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
send(response, 200, status)
|
||||
} else send(response, 200, await runner[action](componentId))
|
||||
} else send(response, 404, { error: 'not found' })
|
||||
}
|
||||
} catch (error) {
|
||||
await runner.record?.('bootstrap.request.failed', {
|
||||
error,
|
||||
method: request.method ?? null,
|
||||
pathname,
|
||||
})
|
||||
send(response, 500, { error: error instanceof Error ? error.message : 'Bootstrap operation failed' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function listenBootstrapControl(server, socketPath) {
|
||||
await mkdir(dirname(socketPath), { recursive: true })
|
||||
await rm(socketPath, { force: true })
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject)
|
||||
server.listen(socketPath, resolve)
|
||||
})
|
||||
await chmod(socketPath, 0o600)
|
||||
}
|
||||
Reference in new issue
Block a user