// // Containers section, grouped by compose project. // const { Controller } = require("./Controller"); const { ContainerTreeItem } = require("../TreeItems/ContainerTreeItem"); const { ProjectTreeItem } = require("../TreeItems/ProjectTreeItem"); const { ContainerCLI } = require("../../Interfaces/ContainerCLI"); const { ComposeCLI } = require("../../Interfaces/ComposeCLI"); const { Terminal } = require("../../Interfaces/Terminal"); const containers = require("../Models/Container"); const notify = require("../../Utilities/Notify"); const prompt = require("../../Utilities/Prompt"); class ContainersController extends Controller { constructor(sidebar) { super("apple_container.containers"); this.sidebar = sidebar; this.registerCommands(); } async fetch() { let list = await ContainerCLI.listContainers(); let group = nova.config.get("apple_container.containers.group", "boolean") !== false; let elements = containers.build(list, { group: group }); // Attach the compose file to each project so the compose commands and // the tooltip have something to work with. for (let element of elements) { if (element.compose) { element.file = this.composeFileFor(element.name); } } return elements; } getTreeItem(element) { return element instanceof containers.ProjectModel ? new ProjectTreeItem(element) : new ContainerTreeItem(element); } // Containers in the current selection, expanding any selected projects. selectedContainers() { let selected = []; for (let element of this.selection) { if (element instanceof containers.ProjectModel) { selected = selected.concat(element.children); } else { selected.push(element); } } return selected; } selectedProjects() { return this.selection.filter((element) => element instanceof containers.ProjectModel); } // Finds the workspace compose file whose project name matches, which is how // a sidebar row is tied back to a file on disk. composeFileFor(name) { for (let file of ComposeCLI.workspaceFiles()) { if (ComposeCLI.projectName(file) === name) { return file; } } let fallback = ComposeCLI.defaultFile(); if (fallback && ComposeCLI.projectName(fallback) === name) { return fallback; } return null; } // `build` receives the resolved compose file: the arguments cannot be built // before the file is known. async runCompose(project, build, title) { let file = project.file || this.composeFileFor(project.name); if (!file) { notify.failure( `No compose file for “${project.name}”`, "Open the project's workspace, or set the compose file in workspace settings.", "apple-container-compose-missing" ); return; } let directory = nova.path.dirname(file); let args = build(file); if (nova.config.get("apple_container.compose.terminal", "boolean") !== false) { await Terminal.run(Terminal.command([ComposeCLI.path()].concat(args)), directory); return; } try { await ComposeCLI.run(args, { cwd: directory }); } catch (error) { notify.failure(title, error, "apple-container-compose"); } finally { this.reload(); } } registerCommands() { this.register("apple_container.containers.reload", () => this.reload()); this.register("apple_container.containers.start", async () => { let ids = this.selectedContainers().map((container) => container.id); try { await ContainerCLI.startContainers(ids); } catch (error) { notify.failure("Unable to start container", error, "apple-container-start"); } this.reload(); }); this.register("apple_container.containers.stop", () => this.runOnSelection((ids) => ContainerCLI.stopContainers(ids), "Unable to stop container") ); this.register("apple_container.containers.restart", () => this.runOnSelection((ids) => ContainerCLI.restartContainers(ids), "Unable to restart container") ); this.register("apple_container.containers.kill", () => this.runOnSelection((ids) => ContainerCLI.killContainers(ids), "Unable to kill container") ); this.register("apple_container.containers.startGroup", async () => { let ids = this.selectedProjects() .reduce((all, project) => all.concat(project.children), []) .filter((container) => !container.running) .map((container) => container.id); try { await ContainerCLI.startContainers(ids); } catch (error) { notify.failure("Unable to start containers", error, "apple-container-start"); } this.reload(); }); this.register("apple_container.containers.stopGroup", async () => { let ids = this.selectedProjects() .reduce((all, project) => all.concat(project.children), []) .filter((container) => container.running) .map((container) => container.id); if (ids.length === 0) { return; } try { await ContainerCLI.stopContainers(ids); } catch (error) { notify.failure("Unable to stop containers", error, "apple-container-stop"); } this.reload(); }); this.register("apple_container.containers.delete", async () => { let selected = this.selectedContainers(); if (selected.length === 0) { return; } let running = selected.some((container) => container.running); let names = selected.map((container) => container.name).join(", "); let message = running ? `Delete ${names}? Running containers will be stopped first.` : `Delete ${names}?`; if (!(await prompt.confirm(message))) { return; } try { await ContainerCLI.deleteContainers(selected.map((container) => container.id), running); } catch (error) { notify.failure("Unable to delete container", error, "apple-container-delete"); } this.reload(); }); this.register("apple_container.containers.prune", async () => { if (!(await prompt.confirm("Remove all stopped containers?", "Remove"))) { return; } try { await ContainerCLI.pruneContainers(); } catch (error) { notify.failure("Unable to prune containers", error, "apple-container-prune"); } this.reload(); }); this.register("apple_container.containers.shell", async () => { let shell = nova.config.get("apple_container.cli.shell", "string") || "/bin/sh"; for (let container of this.selectedContainers()) { await Terminal.run( Terminal.command([ContainerCLI.path(), "exec", "--interactive", "--tty", container.id, shell]) ); } }); this.register("apple_container.containers.follow", async () => { for (let container of this.selectedContainers()) { await Terminal.run(Terminal.command([ContainerCLI.path(), "logs", "--follow", container.id])); } }); this.register("apple_container.containers.logs", () => this.openLogs([])); this.register("apple_container.containers.bootLogs", () => this.openLogs(["--boot"])); this.register("apple_container.containers.browse", async () => { let url = await this.chooseURL(); if (url) { nova.openURL(url); } }); this.register("apple_container.containers.inspect", async () => { for (let container of this.selectedContainers()) { try { let output = await ContainerCLI.inspectContainer(container.id); nova.workspace.openNewTextDocument({ content: output, syntax: "json" }); } catch (error) { notify.failure("Unable to inspect container", error, "apple-container-inspect"); } } }); this.register("apple_container.containers.copy", async () => { let container = this.selectedContainers()[0]; if (!container) { return; } let fields = [["Name", container.name], ["Image", container.image]]; if (container.address) { fields.push(["Address", container.address]); } if (container.hostname) { fields.push(["Hostname", container.hostname]); } if (container.url) { fields.push(["URL", container.url]); } let choice = await prompt.choose( fields.map(([label, value]) => `${label}: ${value}`), { placeholder: "Copy" } ); if (choice) { nova.clipboard.writeText(choice.slice(choice.indexOf(": ") + 2)); } }); this.register("apple_container.compose.upProject", async () => { for (let project of this.selectedProjects()) { await this.runCompose(project, (file) => ComposeCLI.upArgs(file), "Compose up failed"); } }); this.register("apple_container.compose.downProject", async () => { for (let project of this.selectedProjects()) { await this.runCompose(project, (file) => ComposeCLI.downArgs(file), "Compose down failed"); } }); this.register("apple_container.compose.buildProject", async () => { for (let project of this.selectedProjects()) { await this.runCompose(project, (file) => ComposeCLI.buildArgs(file), "Compose build failed"); } }); this.register("apple_container.compose.openFile", () => { for (let project of this.selectedProjects()) { let file = project.file || this.composeFileFor(project.name); if (file) { nova.workspace.openFile(file); } } }); for (let key of [ "apple_container.containers.group", "apple_container.containers.label", "apple_container.containers.icons", "apple_container.containers.desc.image", "apple_container.containers.desc.ports", "apple_container.containers.desc.address", "apple_container.containers.desc.state" ]) { this.watch(key, () => this.reload()); } } async runOnSelection(action, message) { let ids = this.selectedContainers().map((container) => container.id); if (ids.length === 0) { return; } try { await action(ids); } catch (error) { notify.failure(message, error, "apple-container-action"); } this.reload(); } // Picks the address to open: straight through when there is only one, and a // palette when a container publishes several ports. async chooseURL() { let container = this.selectedContainers()[0]; if (!container) { return null; } let urls = container.urls; if (urls.length === 0) { notify.info( `Nothing to open for ${container.name}`, container.running ? "The container publishes no ports and has no address yet." : "The container is not running.", "apple-container-browse" ); return null; } if (urls.length === 1) { return urls[0].url; } let choice = await prompt.choose(urls.map((entry) => entry.label), { placeholder: `Open ${container.name}` }); if (!choice) { return null; } let picked = urls.find((entry) => entry.label === choice); return picked ? picked.url : null; } async openLogs(flags) { for (let container of this.selectedContainers()) { try { let result = await ContainerCLI.run(["logs"].concat(flags, [container.id])); let output = result.stdout || result.stderr || "No log output."; nova.workspace.openNewTextDocument({ content: output }); } catch (error) { notify.failure("Unable to read logs", error, "apple-container-logs"); } } } } module.exports = { ContainersController };