156 lines
7.8 KiB
JavaScript
156 lines
7.8 KiB
JavaScript
import { randomUUID } from 'node:crypto'
|
|
import { chmod, chown, lstat, mkdir, rename, rm, symlink } from 'node:fs/promises'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
|
|
const LEGACY_NAMES = Object.freeze([
|
|
'bootstrap', 'downloads', 'dsh', 'environments', 'run', 'runtime', 'snapshots', 'system-plugins', 'trust',
|
|
])
|
|
|
|
async function exists(path) {
|
|
return lstat(path).then(() => true, error => error?.code === 'ENOENT' ? false : Promise.reject(error))
|
|
}
|
|
|
|
export class PlatformPaths {
|
|
constructor(dataRoot = '/data/platform', runRoot = '/run/dsh-platform') {
|
|
this.dataRoot = resolve(dataRoot)
|
|
this.runRoot = resolve(runRoot)
|
|
this.stateRoot = join(this.dataRoot, 'state')
|
|
this.storeRoot = join(this.dataRoot, 'store')
|
|
this.cacheRoot = join(this.dataRoot, 'cache')
|
|
this.logsRoot = join(this.dataRoot, 'logs')
|
|
this.trustStateRoot = join(this.stateRoot, 'trust')
|
|
this.developmentTrustStateRoot = join(this.trustStateRoot, 'development')
|
|
this.bootstrapStateRoot = join(this.stateRoot, 'bootstrap')
|
|
this.deploymentStateRoot = join(this.stateRoot, 'deployments')
|
|
this.deploymentStatusPath = join(this.deploymentStateRoot, 'status.json')
|
|
this.systemSkillStatePath = join(this.deploymentStateRoot, 'skills.json')
|
|
this.updaterStateRoot = join(this.stateRoot, 'updater')
|
|
this.proxyStateRoot = join(this.stateRoot, 'proxy')
|
|
this.accessStateRoot = join(this.stateRoot, 'access')
|
|
this.proxyTestStatePath = join(this.proxyStateRoot, 'test-tasks.json')
|
|
this.managementStateRoot = join(this.stateRoot, 'management')
|
|
this.userPluginStatePath = join(this.managementStateRoot, 'user-plugins.json')
|
|
this.userPluginJournalPath = join(this.managementStateRoot, 'user-plugin-transaction.json')
|
|
this.proxyProviderInventoryPath = join(this.managementStateRoot, 'proxy-provider-inventory.json')
|
|
this.fileTasksRoot = join(this.managementStateRoot, 'file-tasks')
|
|
this.maintenanceLeasePath = join(this.managementStateRoot, 'maintenance-mutation.lock')
|
|
this.objectsRoot = join(this.storeRoot, 'objects')
|
|
this.bootstrapStoreRoot = join(this.storeRoot, 'bootstrap')
|
|
this.environmentsRoot = join(this.storeRoot, 'environments')
|
|
this.systemSkillCatalogsRoot = join(this.environmentsRoot, 'versions', '.system-skill-catalogs')
|
|
this.pristineRoot = join(this.storeRoot, 'pristine')
|
|
this.runtimesRoot = join(this.storeRoot, 'runtimes')
|
|
this.systemPluginsRoot = join(this.storeRoot, 'system-plugins')
|
|
this.snapshotsRoot = join(this.storeRoot, 'snapshots')
|
|
this.userPluginSnapshotsRoot = join(this.snapshotsRoot, 'user-plugins')
|
|
this.downloadsRoot = join(this.cacheRoot, 'downloads')
|
|
this.npmCacheRoot = join(this.cacheRoot, 'npm')
|
|
this.viewsRoot = join(this.runRoot, 'views')
|
|
this.deploymentViewsRoot = join(this.runRoot, 'deployments')
|
|
this.systemPluginViewsRoot = join(this.runRoot, 'system-plugin-views')
|
|
this.systemSkillsView = join(this.viewsRoot, 'skills')
|
|
this.deploymentView = join(this.runRoot, 'deployment')
|
|
this.trustSocket = join(this.runRoot, 'stage0-trust.sock')
|
|
this.bootstrapSocket = join(this.runRoot, 'bootstrap.sock')
|
|
this.dshLifecycleSocket = join(this.runRoot, 'dsh-lifecycle.sock')
|
|
this.managementSocket = join(this.runRoot, 'management.sock')
|
|
this.managementCliSocket = join(this.runRoot, 'management-cli.sock')
|
|
this.maintenanceSocket = join(this.runRoot, 'maintenance.sock')
|
|
this.userSkillSocket = join(this.runRoot, 'user-skills.sock')
|
|
this.snapshotSocket = join(this.runRoot, 'snapshot.sock')
|
|
this.gatewayAccessSocket = join(this.runRoot, 'gateway-access.sock')
|
|
this.accessRunRoot = join(this.runRoot, 'access')
|
|
this.accessSocket = join(this.accessRunRoot, 'access.sock')
|
|
this.accessRecoverySocket = join(this.accessRunRoot, 'recovery.sock')
|
|
this.accessLaunchSocket = join(this.runRoot, 'access-launch.sock')
|
|
this.proxyControlSocket = join(this.runRoot, 'outbound-proxy.sock')
|
|
this.proxyRoutingStatePath = join(this.runRoot, 'outbound-proxy-routing.json')
|
|
this.proxyLaunchSocket = join(this.runRoot, 'proxy-launch.sock')
|
|
this.recoverySocket = join(this.runRoot, 'recovery.sock')
|
|
}
|
|
}
|
|
|
|
export function trustStateRootForAuthority(paths, authority) {
|
|
return authority === 'development' ? paths.developmentTrustStateRoot : paths.trustStateRoot
|
|
}
|
|
|
|
export async function rejectLegacyLayout(paths) {
|
|
const found = []
|
|
for (const name of LEGACY_NAMES) {
|
|
if (await exists(join(paths.dataRoot, name))) found.push(name)
|
|
}
|
|
if (found.length > 0) {
|
|
throw new Error(
|
|
`legacy DSH platform volume layout detected (${found.join(', ')}); stop the container and clear only ${paths.dataRoot}, then restart. Do not delete /data/dsh.`,
|
|
)
|
|
}
|
|
}
|
|
|
|
export async function preparePersistentLayout(paths) {
|
|
await mkdir(paths.dataRoot, { recursive: true })
|
|
await rejectLegacyLayout(paths)
|
|
const directories = [
|
|
paths.trustStateRoot, paths.bootstrapStateRoot, paths.deploymentStateRoot, paths.updaterStateRoot,
|
|
paths.managementStateRoot, paths.fileTasksRoot,
|
|
paths.objectsRoot, paths.bootstrapStoreRoot, paths.environmentsRoot, paths.pristineRoot,
|
|
paths.runtimesRoot, paths.systemPluginsRoot, paths.snapshotsRoot, paths.userPluginSnapshotsRoot,
|
|
paths.downloadsRoot, paths.npmCacheRoot, paths.logsRoot,
|
|
]
|
|
await Promise.all(directories.map(path => mkdir(path, { recursive: true })))
|
|
return Object.freeze([...directories])
|
|
}
|
|
|
|
export async function resetRuntimeLayout(paths) {
|
|
if (paths.runRoot === '/' || paths.runRoot === paths.dataRoot) throw new Error('platform run root is unsafe')
|
|
await rm(paths.runRoot, { recursive: true, force: true })
|
|
await mkdir(paths.viewsRoot, { recursive: true })
|
|
await mkdir(paths.deploymentViewsRoot, { recursive: true })
|
|
await mkdir(paths.systemPluginViewsRoot, { recursive: true })
|
|
await mkdir(paths.systemSkillsView, { recursive: true })
|
|
for (const name of ['environment', 'runtime']) {
|
|
await symlink(join('..', 'deployment', name), join(paths.viewsRoot, name), 'dir')
|
|
}
|
|
await symlink(join('..', 'system-plugin-views', 'current'), join(paths.viewsRoot, 'system-plugins'), 'dir')
|
|
await symlink(join('..', 'deployment', 'system-plugins'), join(paths.systemPluginViewsRoot, 'current'), 'dir')
|
|
if (process.getuid?.() === 0) {
|
|
await chown(paths.runRoot, 0, 1000)
|
|
// Isolated service identities must traverse to their dedicated subtrees and
|
|
// immutable Bootstrap entrypoints without gaining the node group's socket access.
|
|
await chmod(paths.runRoot, 0o1771)
|
|
await chown(paths.viewsRoot, 0, 0)
|
|
await chmod(paths.viewsRoot, 0o755)
|
|
await chown(paths.deploymentViewsRoot, 1000, 1000)
|
|
await chmod(paths.deploymentViewsRoot, 0o755)
|
|
await chown(paths.systemPluginViewsRoot, 1000, 1000)
|
|
await chmod(paths.systemPluginViewsRoot, 0o755)
|
|
await chown(paths.systemSkillsView, 1000, 1000)
|
|
await chmod(paths.systemSkillsView, 0o755)
|
|
}
|
|
}
|
|
|
|
export async function replaceRuntimeView(paths, name, target) {
|
|
if (name !== 'bootstrap') {
|
|
throw new Error(`runtime view ${name} is invalid`)
|
|
}
|
|
const path = join(paths.viewsRoot, name)
|
|
const temporary = join(dirname(path), `.${name}.${randomUUID()}.tmp`)
|
|
await symlink(resolve(target), temporary, 'dir')
|
|
await rename(temporary, path)
|
|
return path
|
|
}
|
|
|
|
export async function replaceDeploymentView(paths, target) {
|
|
const temporary = join(paths.runRoot, `.deployment.${randomUUID()}.tmp`)
|
|
await symlink(resolve(target), temporary, 'dir')
|
|
await rename(temporary, paths.deploymentView)
|
|
return paths.deploymentView
|
|
}
|
|
|
|
export async function replaceSystemPluginView(paths, target = join(paths.deploymentView, 'system-plugins')) {
|
|
const path = join(paths.systemPluginViewsRoot, 'current')
|
|
const temporary = join(paths.systemPluginViewsRoot, `.current.${randomUUID()}.tmp`)
|
|
await symlink(resolve(target), temporary, 'dir')
|
|
await rename(temporary, path)
|
|
return path
|
|
}
|