aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/vm/Docker.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/modules/vm/Docker.qml')
-rw-r--r--desktop/modules/vm/Docker.qml93
1 files changed, 89 insertions, 4 deletions
diff --git a/desktop/modules/vm/Docker.qml b/desktop/modules/vm/Docker.qml
index 10199b2..aa3272c 100644
--- a/desktop/modules/vm/Docker.qml
+++ b/desktop/modules/vm/Docker.qml
@@ -34,19 +34,31 @@ Singleton {
// --- discovery -------------------------------------------------------
+ // Published host TCP ports, from `{{.Ports}}`: "0.0.0.0:3000->8080/tcp,
+ // [::]:3000->8080/tcp, 9000/tcp" gives [3000]. An unpublished port has no
+ // arrow, and the v4 and v6 bindings of one port are the same port.
+ function _ports(s) {
+ const re = /:(\d+)->\d+\/tcp/g, out = [];
+ let m;
+ while ((m = re.exec(s)) !== null)
+ if (!out.includes(+m[1])) out.push(+m[1]);
+ return out;
+ }
+
Process {
id: listProc
command: ["docker", "ps", "-a", "--format",
- '{{.ID}}\t{{.Names}}\t{{.State}}\t{{.Status}}\t{{.Label "com.docker.compose.project"}}\t{{.Image}}']
+ '{{.ID}}\t{{.Names}}\t{{.State}}\t{{.Status}}\t{{.Label "com.docker.compose.project"}}\t{{.Image}}\t{{.Label "com.docker.compose.project.config_files"}}\t{{.Ports}}']
stdout: StdioCollector {
onStreamFinished: {
const g = {};
for (const line of text.split("\n")) {
const f = line.split("\t");
- if (f.length < 6) continue;
+ if (f.length < 8) continue;
const p = f[4] || f[1];
(g[p] = g[p] ?? []).push({ id: f[0], name: f[1], state: f[2],
- status: f[3], image: f[5] });
+ status: f[3], image: f[5], files: f[6],
+ ports: root._ports(f[7]) });
}
for (const p in g) g[p].sort((a, b) => a.name.localeCompare(b.name));
root.groups = g;
@@ -112,9 +124,82 @@ Singleton {
}
}
+ // Pull and recreate a compose stack from its own compose files, read off
+ // the container labels. `up -d` recreates only what changed, and starts
+ // any stopped service of the stack. Its own Process, because a pull runs
+ // for minutes and must not be clobbered by a start or stop meanwhile.
+ property string updating: "" // the project being updated
+
+ function update(project, files) {
+ const cmd = ["docker", "compose", "-p", project];
+ for (const f of files.split(",")) cmd.push("-f", f);
+ updProc.command = cmd.concat(["up", "-d", "--pull", "always"]);
+ root.updating = project;
+ updProc.running = false;
+ updProc.running = true;
+ }
+
+ Process {
+ id: updProc
+ stderr: StdioCollector { id: updErr }
+ onExited: code => {
+ if (code !== 0)
+ root.actionFailed(root.updating, "update",
+ updErr.text.trim().split("\n").pop() || `exited ${code}`);
+ root.updating = "";
+ root.refreshList();
+ }
+ }
+
+ // Images no container uses, running or stopped: `prune -a`, not just the
+ // dangling ones. A stopped stack keeps its images.
+ property bool pruning: false
+ property string pruned: "" // docker's "Total reclaimed space" line
+
+ property var unused: [] // [{ name, size }], what prune would delete
+
+ Process {
+ id: imgProc
+ command: ["docker", "image", "ls", "--format",
+ "{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.Containers}}\t{{.ID}}"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const u = [];
+ for (const line of text.split("\n")) {
+ const f = line.split("\t");
+ if (f.length < 4 || f[2] !== "0") continue;
+ u.push({ name: f[0] === "<none>:<none>" ? f[3] : f[0], size: f[1] });
+ }
+ root.unused = u;
+ }
+ }
+ }
+
+ function prune() {
+ root.pruning = true;
+ root.pruned = "";
+ pruneProc.running = false;
+ pruneProc.running = true;
+ }
+
+ Process {
+ id: pruneProc
+ command: ["docker", "image", "prune", "-a", "-f"]
+ stdout: StdioCollector {
+ onStreamFinished: root.pruned = text.trim().split("\n").pop()
+ }
+ stderr: StdioCollector { id: pruneErr }
+ onExited: code => {
+ if (code !== 0)
+ root.actionFailed("images", "prune", pruneErr.text.trim() || `exited ${code}`);
+ root.pruning = false;
+ root.refreshList();
+ }
+ }
+
// --- refresh ---------------------------------------------------------
- function refreshList() { listProc.running = true; }
+ function refreshList() { listProc.running = true; imgProc.running = true; }
Timer {
interval: 2000