/** * 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() }) }