deployer: switch to @cmd, use nix --json output

master
Yorick van Pelt 2023-03-01 11:40:56 +01:00
parent bcb93ac28a
commit 6a62e97ac3
Signed by: yorick
GPG Key ID: A36E70F9DC014A15
3 changed files with 169 additions and 86 deletions

View File

@ -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 () => {

View File

@ -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)
})
}

View File

@ -4,7 +4,8 @@
"allowSyntheticDefaultImports": true,
"moduleResolution": "nodenext",
"target": "es2022",
"strict": true
"strict": true,
"experimentalDecorators": true
},
"files": [
"ssh.ts",