deployer: switch to @cmd, use nix --json output
parent
bcb93ac28a
commit
6a62e97ac3
|
@ -3,12 +3,19 @@ import { ssh, SSH } from './ssh.js'
|
|||
import { Expression, BuiltOutput } from './nix.js'
|
||||
|
||||
class Cmd {
|
||||
registry: {[index: string]: (() => Promise<void>)} = {};
|
||||
constructor() {
|
||||
}
|
||||
registry: Record<string, () => Promise<void>> = {};
|
||||
register(obj: string, action: string, fn: () => Promise<void>) {
|
||||
this.registry[`${obj}.${action}`] = fn
|
||||
}
|
||||
registerAll(obj: string, intf: Object & { _commands?: string[] }) {
|
||||
if (!intf._commands) return
|
||||
const proto = Object.getPrototypeOf(intf)
|
||||
for (const name of intf._commands) {
|
||||
if (name != "constructor" && typeof proto[name] === "function") {
|
||||
this.register(obj, name, proto[name].bind(intf))
|
||||
}
|
||||
}
|
||||
}
|
||||
async run() {
|
||||
const opt = argv._[0]
|
||||
if (opt && this.registry[opt]) await this.registry[opt]()
|
||||
|
@ -48,15 +55,15 @@ class Machine {
|
|||
return this.hostname
|
||||
} else {
|
||||
// todo: directify
|
||||
return `${name}.vpn.yori.cc`
|
||||
return `${this.name}.vpn.yori.cc`
|
||||
}
|
||||
}
|
||||
async findIP(): Promise<typeof Machine.prototype.ssh> {
|
||||
const host = await this.sshTarget()
|
||||
return <R>(user?: string, cb?: () => Promise<R>): Promise<SSH|R> => {
|
||||
const sshTarget = user ? `${user}@${host}` : host
|
||||
if (cb !== undefined) return ssh(host, cb)
|
||||
return ssh(host)
|
||||
if (cb !== undefined) return ssh(sshTarget, cb)
|
||||
return ssh(sshTarget)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,53 +88,75 @@ const machines = {
|
|||
for (const [name, machine] of Object.entries(machines))
|
||||
machine.name = name
|
||||
|
||||
function cmd<R>(target: { _commands?: string[] }, propertyKey: string, descriptor: PropertyDescriptor): void {
|
||||
if (target._commands == undefined) target._commands = []
|
||||
target._commands.push(propertyKey)
|
||||
}
|
||||
|
||||
for (const machine of Object.values(machines)) {
|
||||
action.register(machine.name, "ssh", async function() {
|
||||
(await machine.ssh()).interactive()
|
||||
})
|
||||
action.register(machine.name, "gc", async () => {
|
||||
machine.ssh("root", async () => {
|
||||
await $`nix-collect-garbage -d`
|
||||
class MachineInterface {
|
||||
machine: Machine
|
||||
_commands?: string[]
|
||||
constructor(machine: Machine) {
|
||||
this.machine = machine
|
||||
}
|
||||
@cmd
|
||||
async ssh() {
|
||||
(await this.machine.ssh()).interactive()
|
||||
}
|
||||
@cmd
|
||||
gc() {
|
||||
return this.machine.ssh("root", () => $`nix-collect-garbage -d`)
|
||||
}
|
||||
@cmd
|
||||
eval() {
|
||||
return Promise.all(this.machine.targets.map(x => x.derive()))
|
||||
}
|
||||
@cmd
|
||||
build() {
|
||||
return Promise.all(this.machine.targets.map(x => x.build()))
|
||||
}
|
||||
@cmd
|
||||
status() {
|
||||
return this.machine.ssh(undefined, async () => {
|
||||
await $`systemctl is-system-running`
|
||||
await $`zpool status -x`
|
||||
await $`realpath /run/current-system /nix/var/nix/profiles/system`
|
||||
})
|
||||
})
|
||||
action.register(machine.name, "eval", async () => {
|
||||
await Promise.all(machine.targets.map(x => x.derive()))
|
||||
})
|
||||
action.register(machine.name, "build", async () => {
|
||||
await Promise.all(machine.targets.map(x => x.build()))
|
||||
})
|
||||
action.register(machine.name, "copy", async () => {
|
||||
const machineSSH = await machine.findIP()
|
||||
// todo merge builds
|
||||
const outputs = (await Promise.all(machine.targets.map(x => x.build()))).flat()
|
||||
if (machine.isLocal()) {
|
||||
}
|
||||
|
||||
@cmd
|
||||
async copy() {
|
||||
const machineSSH = await this.machine.findIP()
|
||||
const outputs = (await Promise.all(this.machine.targets.map(x => x.build().then(Object.values)))).flat()
|
||||
if (this.machine.isLocal()) {
|
||||
console.log("skipping copy, is localhost")
|
||||
return
|
||||
}
|
||||
const conn = await machineSSH()
|
||||
await Promise.all(outputs.map(x => x.copy(conn)))
|
||||
await Promise.all(Object.values(outputs).map(x => x.copy(conn)))
|
||||
// machine.toplevel.buildAndCopy(machine.ssh)
|
||||
// Home.buildAndCopy(machine.ssh)
|
||||
})
|
||||
action.register(machine.name, "boot-deploy", async () => {
|
||||
const machineSSH = await machine.findIP()
|
||||
const path = (await machine.attr.build()).out
|
||||
if (!machine.isLocal()) {
|
||||
}
|
||||
@cmd
|
||||
async "boot-deploy"() {
|
||||
const machineSSH = await this.machine.findIP()
|
||||
const path = (await this.machine.attr.build()).out
|
||||
if (!this.machine.isLocal()) {
|
||||
const conn = await machineSSH()
|
||||
await path.copy(conn)
|
||||
}
|
||||
// machine.toplevel.buildAndCopy(machine.ssh)
|
||||
// machine.ssh.within(machine.toplevel.activate("boot"))
|
||||
await machineSSH("root", async () => {
|
||||
await $`nix-env -p /nix/var/nix/profiles/system --set ${path}`
|
||||
await $`nix-env -p /nix/var/nix/profiles/system --set ${path.path}`
|
||||
await $`${path}/bin/switch-to-configuration boot`
|
||||
})
|
||||
})
|
||||
action.register(machine.name, "switch", async () => {
|
||||
const machineSSH = await machine.findIP()
|
||||
const path = (await machine.attr.build()).out
|
||||
if (!machine.isLocal()) {
|
||||
}
|
||||
@cmd
|
||||
async "switch"() {
|
||||
const machineSSH = await this.machine.findIP()
|
||||
const path = (await this.machine.attr.build()).out
|
||||
if (!this.machine.isLocal()) {
|
||||
const conn = await machineSSH()
|
||||
await path.copy(conn)
|
||||
}
|
||||
|
@ -135,32 +164,32 @@ for (const machine of Object.values(machines)) {
|
|||
await machineSSH("root", async () => {
|
||||
const old_kernel = (await $`readlink /run/booted-system/kernel`).stdout
|
||||
if (new_kernel !== old_kernel) {
|
||||
console.error(`[${machine.name}] requires reboot because of kernel update`)
|
||||
console.error(`[${this.machine.name}] requires reboot because of kernel update`)
|
||||
process.exit(1)
|
||||
}
|
||||
await $`nix-env -p /nix/var/nix/profiles/system --set ${path.path}`
|
||||
await $`${path}/bin/switch-to-configuration switch`
|
||||
})
|
||||
})
|
||||
if (machine.hasHome) {
|
||||
action.register(machine.name, "update-home", async () => {
|
||||
const new_path = (await Home.build()).out
|
||||
if (machine.isLocal()) {
|
||||
await $`${new_path.path}/activate`
|
||||
} else {
|
||||
const conn = await machine.ssh()
|
||||
await new_path.copy(conn)
|
||||
await conn.within(() => $`${new_path.path}/activate`)
|
||||
}
|
||||
await $`${path.path}/bin/switch-to-configuration switch`
|
||||
})
|
||||
}
|
||||
action.register(machine.name, "status", async () => {
|
||||
await machine.ssh(undefined, async () => {
|
||||
await $`systemctl is-system-running`
|
||||
await $`zpool status -x`
|
||||
await $`realpath /run/current-system /nix/var/nix/profiles/system`
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
class MachineInterfaceHome extends MachineInterface {
|
||||
@cmd
|
||||
async "update-home"() {
|
||||
const new_path = (await Home.build()).out
|
||||
if (this.machine.isLocal()) {
|
||||
await $`${new_path.path}/activate`
|
||||
} else {
|
||||
const conn = await this.machine.ssh()
|
||||
await new_path.copy(conn)
|
||||
await conn.within(() => $`${new_path.path}/activate`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const machine of Object.values(machines)) {
|
||||
action.registerAll(machine.name, machine.hasHome ? new MachineInterfaceHome(machine) : new MachineInterface(machine))
|
||||
}
|
||||
|
||||
action.register("all", "build", async () => {
|
||||
|
|
107
deployer/nix.ts
107
deployer/nix.ts
|
@ -8,14 +8,21 @@ type NixPath = DrvPath | OutPath
|
|||
type ShownDerivation = {
|
||||
args: Array<string>,
|
||||
builder: string,
|
||||
env: { [index: string]: string } ,
|
||||
inputDrvs: { [index: DrvPath]: Array<String> },
|
||||
env: Record<string, string>,
|
||||
inputDrvs: Record<DrvPath, string[]>,
|
||||
inputSrcs: Array<OutPath>,
|
||||
outputs: { [index: string]: { path: OutPath }},
|
||||
outputs: Record<string, { path: OutPath }>,
|
||||
system: "x86-64_linux"
|
||||
};
|
||||
type ShowDerivationOutput = {
|
||||
[index: DrvPath]: ShownDerivation
|
||||
type ShowDerivationOutput = Record<DrvPath, ShownDerivation>
|
||||
|
||||
type OutputSpec<R> = { out: R } & Record<string, R>
|
||||
|
||||
type BuildOutput = {
|
||||
drvPath: DrvPath,
|
||||
outputs: OutputSpec<OutPath>,
|
||||
startTime?: number,
|
||||
stopTime?: number
|
||||
}
|
||||
|
||||
export class Expression {
|
||||
|
@ -24,17 +31,20 @@ export class Expression {
|
|||
this.expr = expr
|
||||
}
|
||||
async derive(): Promise<Derivation> {
|
||||
const drvPath = await nix.eval(this.expr)
|
||||
const {drvPath} = await nix.derive(this.expr)
|
||||
const drv = await nix.showDerivation(drvPath)
|
||||
return new Derivation(drvPath, drv[drvPath])
|
||||
}
|
||||
async build(): Promise<{out: BuiltOutput} & Array<BuiltOutput>> {
|
||||
async build(): Promise<OutputSpec<BuiltOutput>> {
|
||||
const outputs = await nix.build(this.expr)
|
||||
const drvMetaJson = await nix.showDerivation(outputs[0])
|
||||
const drvMetaJson = await nix.showDerivation(outputs.drvPath)
|
||||
const [drvPath, drvMeta] = Object.entries(drvMetaJson)[0]
|
||||
const drv = new Derivation(drvPath, drvMeta)
|
||||
const ret = outputs.map(x => new BuiltOutput(x, drv))
|
||||
return Object.assign(ret, { out: new BuiltOutput(drv.meta.outputs.out.path, drv) })
|
||||
const ret: Record<string, BuiltOutput> = {}
|
||||
for (const [k, v] of Object.entries(outputs.outputs)) {
|
||||
ret[k] = new BuiltOutput(v, drv)
|
||||
}
|
||||
return Object.assign(ret, { out: ret.out })
|
||||
}
|
||||
}
|
||||
export class Derivation {
|
||||
|
@ -46,8 +56,8 @@ export class Derivation {
|
|||
this.meta = meta
|
||||
}
|
||||
async build(): Promise<{out: BuiltOutput} & Array<BuiltOutput>> {
|
||||
const outputs = await nix.build(this.path)
|
||||
const ret = outputs.map(x => new BuiltOutput(x, this))
|
||||
const outputs: BuildOutput = await nix.build(this.path)
|
||||
const ret = Object.values(outputs.outputs).map(x => new BuiltOutput(x, this))
|
||||
return Object.assign(ret, { out: new BuiltOutput(this.meta.outputs.out.path, this) })
|
||||
}
|
||||
}
|
||||
|
@ -63,22 +73,65 @@ export class BuiltOutput {
|
|||
}
|
||||
}
|
||||
|
||||
export const nix = {
|
||||
async build(attr: string | Array<string>, name="result"): Promise<Array<OutPath>> {
|
||||
const tmp = (await $`mktemp -d`).stdout.trim()
|
||||
process.on('exit', () => fs.removeSync(tmp))
|
||||
await $`nom build ${attr} -o ${tmp}/${name}`
|
||||
const files = await fs.readdir(`${tmp}`)
|
||||
return Promise.all(files.map(f => fs.readlink(`${tmp}/${f}`)))
|
||||
},
|
||||
async showDerivation(path: NixPath): Promise<ShowDerivationOutput> {
|
||||
function dedupe<A, B, Rest extends any[]>(fn: (xs: A[], ...rest: Rest) => Promise<B[]>):
|
||||
{ (x: A, ...rest: Rest): Promise<B>; (x: A[], ...rest: Rest): Promise<B[]>; } {
|
||||
type QueueEntry = {
|
||||
x: A,
|
||||
resolve: (res: B) => void,
|
||||
reject: (err: any) => void
|
||||
}
|
||||
const queues = new Map<string, QueueEntry[]>()
|
||||
function inner(x: A, ...rest: Rest): Promise<B>
|
||||
function inner(x: A[], ...rest: Rest): Promise<B[]>
|
||||
function inner(x: A | A[], ...rest: Rest) {
|
||||
// todo: also dedupe array results
|
||||
if (Array.isArray(x)) return fn(x, ...rest)
|
||||
// map queue by rest
|
||||
const stringified = JSON.stringify(rest)
|
||||
const had = queues.has(stringified)
|
||||
const queue = queues.get(stringified) || []
|
||||
const ret = new Promise<B>((resolve, reject) => {
|
||||
queue.push({x, resolve, reject})
|
||||
})
|
||||
if (!had) {
|
||||
queues.set(stringified, queue)
|
||||
setImmediate(() => {
|
||||
queues.delete(stringified)
|
||||
fn(queue.map(({x}) => x), ...rest)
|
||||
.then(results => {
|
||||
for (const [i, {resolve}] of queue.entries()) resolve(results[i])
|
||||
})
|
||||
.catch(e => {
|
||||
for (const {reject} of queue) reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
return inner
|
||||
}
|
||||
|
||||
//function nixBuild(attr: string[]): Promise<BuildOutput[]>
|
||||
//function nixBuild(attr: string): Promise<BuildOutput>
|
||||
async function nixBuild(attr: string[]): Promise<BuildOutput[]> {
|
||||
const tmp = (await $`mktemp -d`).stdout.trim()
|
||||
process.on('exit', () => fs.removeSync(tmp))
|
||||
const ret = JSON.parse((await $`nom build --json ${attr} -o ${tmp}/result`).stdout)
|
||||
if (Array.isArray(attr)) return ret
|
||||
return ret[0]
|
||||
}
|
||||
|
||||
namespace nix {
|
||||
export const build = dedupe(nixBuild)
|
||||
export async function showDerivation(path: NixPath): Promise<ShowDerivationOutput> {
|
||||
return JSON.parse((await $`nix show-derivation ${path}`.quiet()).stdout)
|
||||
},
|
||||
async eval(attr: string) : Promise<DrvPath> {
|
||||
return JSON.parse((await $`nix eval ${attr}.drvPath --json`).stdout)
|
||||
},
|
||||
async copy(attrs: string | Array<string>, target: SSH): Promise<void> {
|
||||
}
|
||||
export const derive = dedupe(async (attr: string[]): Promise<BuildOutput[]> => {
|
||||
return JSON.parse((await $`nom build --json --dry-run ${attr}`).stdout)
|
||||
})
|
||||
export const copy = dedupe(async (attrs: string[], target: SSH): Promise<void[]> => {
|
||||
process.env.NIX_SSHOPTS = "-o compression=no";
|
||||
await $`nix copy ${attrs} --to ssh://${target.host}`
|
||||
}
|
||||
return Array(attrs.length)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "es2022",
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"files": [
|
||||
"ssh.ts",
|
||||
|
|
Loading…
Reference in New Issue