// // Wrapper around the `container` command line tool. // // Every list command is asked for JSON; Apple Container's JSON is a single // document rather than the line delimited JSON Docker emits, so output is // buffered and parsed once the process exits. // const { exec, execJSON } = require("../Utilities/Process"); const SEARCH_PATHS = [ "/usr/local/bin/container", "/opt/homebrew/bin/container", "/usr/bin/container" ]; class ContainerCLI { // Resolves the tool as a [path, leadingArgs] pair. Nova does not inherit a // login shell's PATH reliably, so a configured or well known absolute path // is preferred and `/usr/bin/env` is only the last resort. static resolve() { let configured = nova.config.get("apple_container.cli.container", "string"); if (configured && configured.trim().length > 0) { return [configured.trim(), []]; } for (let path of SEARCH_PATHS) { if (nova.fs.access(path, nova.fs.X_OK)) { return [path, []]; } } return ["/usr/bin/env", ["container"]]; } // Absolute path to the tool, or null when it could not be found. Used to // build shell commands for the terminal. static path() { let [path, leading] = this.resolve(); return leading.length > 0 ? "container" : path; } static run(args, options = {}) { let [path, leading] = this.resolve(); return exec(path, leading.concat(args), options); } static runJSON(args, options = {}) { let [path, leading] = this.resolve(); return execJSON(path, leading.concat(args), options); } // System --------------------------------------------------------------- // Resolves to a status object even when the services are down, so the // sidebar can render the stopped state rather than an empty section. static async systemStatus() { try { let status = await this.runJSON(["system", "status", "--format", "json"]); return status && typeof status === "object" ? status : { status: "unknown" }; } catch (error) { return { status: "stopped", error: error.message }; } } static systemStart() { return this.run(["system", "start"]); } static systemStop() { return this.run(["system", "stop"]); } static async systemDiskUsage() { try { return await this.runJSON(["system", "df", "--format", "json"]); } catch (error) { return null; } } static async dnsDomains() { try { let result = await this.run(["system", "dns", "ls"]); return result.stdout .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) .slice(1); } catch (error) { return []; } } static version() { return this.run(["--version"]); } // Builder -------------------------------------------------------------- // `container builder status` reports through its message rather than a // machine readable field, so the text is matched. static async builderRunning() { try { let result = await this.run(["builder", "status"], { ignoreStatus: true }); let output = `${result.stdout} ${result.stderr}`.toLowerCase(); if (output.includes("not running") || output.includes("does not exist")) { return false; } return result.status === 0; } catch (error) { return false; } } static builderStart() { return this.run(["builder", "start"]); } static builderStop() { return this.run(["builder", "stop"]); } static builderDelete() { return this.run(["builder", "delete"]); } // Containers ----------------------------------------------------------- static listContainers() { return this.runJSON(["list", "--all", "--format", "json"]); } static startContainers(ids) { // `container start` takes a single container, so fan out. return Promise.all(ids.map((id) => this.run(["start", id]))); } static stopContainers(ids) { return this.run(["stop"].concat(ids)); } static killContainers(ids) { return this.run(["kill"].concat(ids)); } static async restartContainers(ids) { await this.stopContainers(ids); return this.startContainers(ids); } static deleteContainers(ids, force = false) { let args = force ? ["delete", "--force"] : ["delete"]; return this.run(args.concat(ids)); } static pruneContainers() { return this.run(["prune"]); } static async inspectContainer(id) { let result = await this.run(["inspect", id]); return result.stdout; } // Images --------------------------------------------------------------- static listImages() { return this.runJSON(["image", "list", "--format", "json"]); } static pullImage(reference) { return this.run(["image", "pull", reference]); } static pushImage(reference) { return this.run(["image", "push", reference]); } static tagImage(reference, target) { return this.run(["image", "tag", reference, target]); } static deleteImages(references) { return this.run(["image", "delete"].concat(references)); } static pruneImages() { return this.run(["image", "prune"]); } static async inspectImage(reference) { let result = await this.run(["image", "inspect", reference]); return result.stdout; } // Volumes -------------------------------------------------------------- static listVolumes() { return this.runJSON(["volume", "list", "--format", "json"]); } static createVolume(nameOrArgs) { let args = Array.isArray(nameOrArgs) ? nameOrArgs : [nameOrArgs]; return this.run(["volume", "create"].concat(args)); } static deleteVolumes(names) { return this.run(["volume", "delete"].concat(names)); } static pruneVolumes() { return this.run(["volume", "prune"]); } static async inspectVolume(name) { let result = await this.run(["volume", "inspect", name]); return result.stdout; } // Networks ------------------------------------------------------------- static listNetworks() { return this.runJSON(["network", "list", "--format", "json"]); } // Accepts a bare name or a full argument list such as // ["backend", "--subnet", "192.168.70.0/24"]. static createNetwork(nameOrArgs) { let args = Array.isArray(nameOrArgs) ? nameOrArgs : [nameOrArgs]; return this.run(["network", "create"].concat(args)); } static deleteNetworks(names) { return this.run(["network", "delete"].concat(names)); } static pruneNetworks() { return this.run(["network", "prune"]); } static async inspectNetwork(name) { let result = await this.run(["network", "inspect", name]); return result.stdout; } } module.exports = { ContainerCLI };