forked from snxraven/peardock
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.
268 lines
9.0 KiB
JavaScript
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() })
|
|
}
|