// // Containers, and the compose projects they are grouped into. // // container-compose stamps com.docker.compose.project and // com.docker.compose.service on the containers it creates, which is the only // link back to a project; containers created any other way fall into an // ungrouped bucket. // const { Element } = require("./Element"); const format = require("../../Utilities/Format"); const PROJECT_LABEL = "com.docker.compose.project"; const SERVICE_LABEL = "com.docker.compose.service"; const UNGROUPED = "Containers"; class ContainerModel extends Element { constructor(json) { let configuration = (json && json.configuration) || {}; let status = (json && json.status) || {}; super(`container:${configuration.id || json.id}`); this.id = configuration.id || json.id; this.name = this.id; this.state = typeof status.state === "string" ? status.state : "unknown"; this.labels = configuration.labels || {}; this.image = (configuration.image && configuration.image.reference) || ""; this.imageName = format.reference(this.image).short; this.platform = configuration.platform || {}; this.startedDate = status.startedDate || null; this.project = this.labels[PROJECT_LABEL] || null; this.service = this.labels[SERVICE_LABEL] || null; this.cpus = (configuration.resources && configuration.resources.cpus) || null; this.memory = (configuration.resources && configuration.resources.memoryInBytes) || null; this.ports = (configuration.publishedPorts || []).map((port) => ({ hostAddress: port.hostAddress, hostPort: port.hostPort, containerPort: port.containerPort, proto: port.proto || "tcp" })); this.mounts = (configuration.mounts || []).map((mount) => ({ destination: mount.destination, source: mount.source, volume: mount.type && mount.type.volume ? mount.type.volume.name : null })); // Addresses only exist while the container is running. this.addresses = (status.networks || []).map((attachment) => ({ network: attachment.network, hostname: stripTrailingDot(attachment.hostname), address: stripPrefix(attachment.ipv4Address) })); this.hostname = this.addresses.length > 0 ? this.addresses[0].hostname : null; this.address = this.addresses.length > 0 ? this.addresses[0].address : null; } get running() { return this.state === "running"; } get revision() { return `${this.state}:${this.address || ""}:${this.portSummary}`; } get portSummary() { return this.ports .map((port) => `${port.hostPort}→${port.containerPort}/${port.proto}`) .join(" "); } // Every way this container can be reached over HTTP, most useful first: // published ports resolve from the host unconditionally, while the DNS // hostname only resolves once its domain is registered with macOS. get urls() { let urls = this.ports .filter((port) => port.proto === "tcp") .map((port) => { let host = !port.hostAddress || port.hostAddress === "0.0.0.0" ? "localhost" : port.hostAddress; return { url: `http://${host}:${port.hostPort}`, label: `http://${host}:${port.hostPort} → container port ${port.containerPort}` }; }); if (this.running && this.hostname) { urls.push({ url: `http://${this.hostname}`, label: `http://${this.hostname} (DNS name)` }); } if (this.running && this.address) { urls.push({ url: `http://${this.address}`, label: `http://${this.address} (container IP)` }); } return urls; } get url() { let urls = this.urls; return urls.length > 0 ? urls[0].url : null; } get tooltip() { let lines = [ `Name:\t${this.name}`, `Image:\t${this.image}`, `State:\t${this.state}` ]; if (this.service) { lines.push(`Service:\t${this.service}`); } if (this.address) { lines.push(`Address:\t${this.address}`); } if (this.hostname) { lines.push(`Hostname:\t${this.hostname}`); } if (this.ports.length > 0) { lines.push(`Ports:\t${this.portSummary}`); } if (this.memory) { lines.push(`Memory:\t${format.bytes(this.memory)}`); } if (this.cpus) { lines.push(`CPUs:\t${this.cpus}`); } if (this.startedDate) { lines.push(`Started:\t${format.age(this.startedDate)}`); } return lines.join("\n"); } } class ProjectModel extends Element { constructor(name, compose) { super(`project:${name}`); this.name = name; // A compose project is one the sidebar can also drive with // container-compose; the ungrouped bucket is not. this.compose = compose; this.file = null; this.expanded = true; } get running() { return this.children.some((child) => child.running); } get summary() { let running = this.children.filter((child) => child.running).length; return `${running}/${this.children.length} running`; } get revision() { return `${this.file || ""}:${this.summary}`; } } function stripTrailingDot(hostname) { if (!hostname) { return null; } return String(hostname).replace(/\.$/, ""); } function stripPrefix(cidr) { if (!cidr) { return null; } return String(cidr).split("/")[0]; } // Builds the container tree. Returns projects when grouping is on, and a flat // list of containers when it is off. function build(list, options = {}) { let containers = (list || []).map((entry) => new ContainerModel(entry)); containers.sort((left, right) => left.name.localeCompare(right.name)); if (!options.group) { return containers; } let projects = new Map(); for (let container of containers) { let name = container.project || UNGROUPED; if (!projects.has(name)) { projects.set(name, new ProjectModel(name, container.project !== null)); } projects.get(name).addChild(container); } // Ungrouped containers sort last, everything else alphabetically. return [...projects.values()].sort((left, right) => { if (left.compose !== right.compose) { return left.compose ? -1 : 1; } return left.name.localeCompare(right.name); }); } module.exports = { ContainerModel, ProjectModel, build, PROJECT_LABEL, SERVICE_LABEL };