Files
peardock/server/handlers/stacks.js
T
snxraven 02f25e0981 Complete roadmap optionals: Holesail, Swarm UI, GitOps, virtualization.
Add tunnel persistence and container one-click tunnels with local
Holesail client bind, Swarm services/nodes/tasks view, GitOps stack
sync from git, deploy wizard step chrome, virtualized container lists,
and structure regression tests. Mark Tracks A–D and optional future done.
2026-07-10 22:53:30 -04:00

117 lines
3.9 KiB
JavaScript

/**
* Docker Compose stack RPC handlers.
*/
import * as composeManager from '../utils/composeManager.js'
import * as validation from '../utils/validation.js'
import { docker } from '../services/docker.js'
import { broadcastContainers } from './containers.js'
import { fetchComposeFromGit } from '../utils/gitops.js'
import logger from '../utils/logger.js'
export function registerStackHandlers(session) {
session.respond('deployStack', async (args) => {
const { composeContent, stackName } = args
if (!composeContent || !stackName) {
throw new Error('Compose content and stack name required')
}
const sanitizedStackName = validation.sanitizeString(stackName, 63)
if (!validation.isValidContainerName(sanitizedStackName)) {
throw new Error('Invalid stack name')
}
// Validate YAML early for clear errors
composeManager.validateComposeFile(composeContent)
const profiles = Array.isArray(args.profiles)
? args.profiles.map(String)
: typeof args.profiles === 'string'
? args.profiles.split(',').map((s) => s.trim()).filter(Boolean)
: []
const result = await composeManager.deployComposeStack(
docker,
composeContent,
sanitizedStackName,
{
overrideContent: args.overrideContent || args.override || null,
envFileContent: args.envFileContent || args.envFile || null,
profiles,
env: args.env && typeof args.env === 'object' ? args.env : {},
build: Boolean(args.build),
// Default true: tear down containers created in a failed partial deploy
rollback: args.rollback !== false,
}
)
await broadcastContainers()
return { success: true, ...result }
})
session.respond('listStacks', async () => {
const stacks = await composeManager.listStacks(docker)
return { type: 'stacks', data: stacks }
})
session.respond('removeStack', async (args) => {
const result = await composeManager.removeComposeStack(docker, args.stackName)
await broadcastContainers()
return { success: true, ...result }
})
session.respond('stackPs', async (args) => {
if (!args.stackName) throw new Error('stackName required')
return composeManager.stackPs(docker, args.stackName)
})
session.respond('stackLogs', async (args) => {
if (!args.stackName) throw new Error('stackName required')
return composeManager.stackLogs(docker, args.stackName, {
tail: args.tail,
})
})
session.respond('stackPull', async (args) => {
if (!args.stackName) throw new Error('stackName required')
return composeManager.stackPull(docker, args.stackName, {
composeContent: args.composeContent,
})
})
/**
* GitOps: fetch compose from git and deploy.
* args: { stackName, repoUrl, ref?, composePath?, build?, rollback? }
*/
session.respond('syncStackFromGit', async (args = {}) => {
const stackName = validation.sanitizeString(args.stackName, 63)
if (!stackName || !validation.isValidContainerName(stackName)) {
throw new Error('Valid stackName required')
}
if (!args.repoUrl) throw new Error('repoUrl required')
logger.info('GitOps sync starting', {
stackName,
repo: String(args.repoUrl).slice(0, 80),
ref: args.ref || 'main',
})
const fetched = await fetchComposeFromGit({
repoUrl: args.repoUrl,
ref: args.ref,
composePath: args.composePath,
})
composeManager.validateComposeFile(fetched.composeContent)
const result = await composeManager.deployComposeStack(
docker,
fetched.composeContent,
stackName,
{
build: Boolean(args.build),
rollback: args.rollback !== false,
}
)
await broadcastContainers()
return {
success: true,
message: `Stack "${stackName}" deployed from git${fetched.commit ? ` @ ${fetched.commit.slice(0, 8)}` : ''}`,
commit: fetched.commit || null,
composePath: fetched.path,
...result,
}
})
}