Files
peardock/server/handlers/swarm.js
T
Raven Scott 42a69c6dc7 Ship Track G UX, multi-shell terminals, Swarm default-on, install.peardock.boats
Probe bash/sh/ash and related shells until a container terminal works, enable Swarm APIs by default, add keyboard go-chords and appearance prefs, and point install docs at https://install.peardock.boats.
2026-07-11 18:06:46 -04:00

268 lines
9.0 KiB
JavaScript

/**
* Swarm / Services / Nodes / Tasks / Secrets / Configs handlers.
* On by default. Opt out with ENABLE_SWARM=0 / false / off / no.
*/
import { docker } from '../services/docker.js'
import * as validation from '../utils/validation.js'
import logger from '../utils/logger.js'
/**
* Swarm APIs are on by default.
* Opt out with ENABLE_SWARM=0 / false / off / no.
*/
export function isSwarmEnabled() {
const v = String(process.env.ENABLE_SWARM ?? '1').trim().toLowerCase()
if (v === '0' || v === 'false' || v === 'off' || v === 'no') return false
return true
}
function assertSwarm() {
if (!isSwarmEnabled()) {
const err = new Error(
'Swarm APIs disabled. Remove ENABLE_SWARM=0 to re-enable (on by default).'
)
err.code = 'FEATURE_DISABLED'
throw err
}
}
export function registerSwarmHandlers(session) {
// Always register so clients get a clear FEATURE_DISABLED error when off.
session.respond('swarmInit', async (args) => {
assertSwarm()
const opts = {
ListenAddr: args.ListenAddr || args.listenAddr || '0.0.0.0:2377',
AdvertiseAddr: args.AdvertiseAddr || args.advertiseAddr || undefined,
ForceNewCluster: Boolean(args.ForceNewCluster || args.forceNewCluster),
}
if (args.Spec) opts.Spec = args.Spec
const result = await docker.swarmInit(opts)
return { success: true, type: 'swarmInit', data: result }
})
session.respond('swarmJoin', async (args) => {
assertSwarm()
if (!args.JoinToken && !args.joinToken) throw new Error('JoinToken required')
if (!args.RemoteAddrs && !args.remoteAddrs) throw new Error('RemoteAddrs required')
const opts = {
ListenAddr: args.ListenAddr || args.listenAddr || '0.0.0.0:2377',
AdvertiseAddr: args.AdvertiseAddr || args.advertiseAddr,
RemoteAddrs: args.RemoteAddrs || args.remoteAddrs,
JoinToken: args.JoinToken || args.joinToken,
}
await docker.swarmJoin(opts)
return { success: true, message: 'Joined swarm' }
})
session.respond('swarmLeave', async (args) => {
assertSwarm()
await docker.swarmLeave({ force: Boolean(args.force) })
return { success: true, message: 'Left swarm' }
})
session.respond('swarmInspect', async () => {
assertSwarm()
const data = await docker.swarmInspect()
return { success: true, type: 'swarmInspect', data }
})
session.respond('swarmUpdate', async (args) => {
assertSwarm()
const version = args.version ?? args.Version
if (version == null) throw new Error('version required')
const spec = args.spec || args.Spec
if (!spec) throw new Error('spec required')
await docker.swarmUpdate(version, spec)
return { success: true, message: 'Swarm updated' }
})
// —— Services ——
session.respond('listServices', async (args) => {
assertSwarm()
const opts = {}
if (args.filters) opts.filters = args.filters
const data = await docker.listServices(opts)
return { success: true, type: 'services', data }
})
session.respond('createService', async (args) => {
assertSwarm()
const spec = args.spec || args
if (!spec?.Name && !spec?.TaskTemplate) {
throw new Error('Service spec with Name and TaskTemplate required')
}
const service = await docker.createService(spec)
return { success: true, message: 'Service created', data: service }
})
session.respond('removeService', async (args) => {
assertSwarm()
await docker.getService(args.id).remove()
return { success: true, message: `Service ${args.id} removed` }
})
session.respond('inspectService', async (args) => {
assertSwarm()
const data = await docker.getService(args.id).inspect()
return { success: true, type: 'service', data }
})
session.respond('updateService', async (args) => {
assertSwarm()
const version = args.version ?? args.Version
const spec = args.spec || args.Spec
if (version == null || !spec) throw new Error('version and spec required')
await docker.getService(args.id).update({ ...spec, version })
return { success: true, message: `Service ${args.id} updated` }
})
session.respond('scaleService', async (args) => {
assertSwarm()
const replicas = Number(args.replicas)
if (!Number.isFinite(replicas) || replicas < 0) throw new Error('replicas must be >= 0')
const service = docker.getService(args.id)
const inspected = await service.inspect()
const version = inspected.Version.Index
const spec = inspected.Spec
if (!spec.Mode) spec.Mode = {}
if (!spec.Mode.Replicated) spec.Mode.Replicated = {}
spec.Mode.Replicated.Replicas = replicas
await service.update({ ...spec, version })
return { success: true, message: `Service ${args.id} scaled to ${replicas}`, replicas }
})
session.respond('serviceLogs', async (args) => {
assertSwarm()
const opts = {
stdout: true,
stderr: true,
tail: Math.min(Number(args.tail) || 100, 2000),
timestamps: args.timestamps !== false,
}
if (args.since) opts.since = args.since
const buf = await docker.getService(args.id).logs(opts)
const text = Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf)
return { success: true, type: 'serviceLogs', id: args.id, data: text }
})
// —— Nodes ——
session.respond('listNodes', async (args) => {
assertSwarm()
const opts = {}
if (args.filters) opts.filters = args.filters
const data = await docker.listNodes(opts)
return { success: true, type: 'nodes', data }
})
session.respond('inspectNode', async (args) => {
assertSwarm()
const data = await docker.getNode(args.id).inspect()
return { success: true, type: 'node', data }
})
session.respond('updateNode', async (args) => {
assertSwarm()
const version = args.version ?? args.Version
const spec = args.spec || args.Spec
if (version == null || !spec) throw new Error('version and spec required')
await docker.getNode(args.id).update({ ...spec, version })
return { success: true, message: `Node ${args.id} updated` }
})
session.respond('removeNode', async (args) => {
assertSwarm()
await docker.getNode(args.id).remove({ force: Boolean(args.force) })
return { success: true, message: `Node ${args.id} removed` }
})
// —— Tasks ——
session.respond('listTasks', async (args) => {
assertSwarm()
const opts = {}
if (args.filters) opts.filters = args.filters
const data = await docker.listTasks(opts)
return { success: true, type: 'tasks', data }
})
session.respond('inspectTask', async (args) => {
assertSwarm()
const data = await docker.getTask(args.id).inspect()
return { success: true, type: 'task', data }
})
// —— Secrets ——
session.respond('listSecrets', async () => {
assertSwarm()
const data = await docker.listSecrets()
return { success: true, type: 'secrets', data }
})
session.respond('createSecret', async (args) => {
assertSwarm()
const name = validation.sanitizeString(args.Name || args.name, 128)
if (!name) throw new Error('Secret name required')
let data = args.Data ?? args.data
if (data == null) throw new Error('Secret data required')
if (typeof data === 'string' && args.encoding !== 'base64') {
data = Buffer.from(data, 'utf8').toString('base64')
}
const secret = await docker.createSecret({
Name: name,
Data: data,
Labels: args.Labels || args.labels || {},
})
return { success: true, message: `Secret ${name} created`, data: secret }
})
session.respond('removeSecret', async (args) => {
assertSwarm()
await docker.getSecret(args.id).remove()
return { success: true, message: `Secret ${args.id} removed` }
})
session.respond('inspectSecret', async (args) => {
assertSwarm()
const data = await docker.getSecret(args.id).inspect()
return { success: true, type: 'secret', data }
})
// —— Configs ——
session.respond('listConfigs', async () => {
assertSwarm()
const data = await docker.listConfigs()
return { success: true, type: 'configs', data }
})
session.respond('createConfig', async (args) => {
assertSwarm()
const name = validation.sanitizeString(args.Name || args.name, 128)
if (!name) throw new Error('Config name required')
let data = args.Data ?? args.data
if (data == null) throw new Error('Config data required')
if (typeof data === 'string' && args.encoding !== 'base64') {
data = Buffer.from(data, 'utf8').toString('base64')
}
const config = await docker.createConfig({
Name: name,
Data: data,
Labels: args.Labels || args.labels || {},
})
return { success: true, message: `Config ${name} created`, data: config }
})
session.respond('removeConfig', async (args) => {
assertSwarm()
await docker.getConfig(args.id).remove()
return { success: true, message: `Config ${args.id} removed` }
})
session.respond('inspectConfig', async (args) => {
assertSwarm()
const data = await docker.getConfig(args.id).inspect()
return { success: true, type: 'config', data }
})
logger.debug('Swarm handlers registered', { enabled: isSwarmEnabled() })
}