Compose and nothing else, for the two workstations, where it is already installed by hand; the runtime, buildx and the group stay the docker module's. Nine Go tools: projects (with their directories from the containers' labels), ps, logs, a rendered config with secret-looking values redacted, and up, down, restart and pull by directory or name. Acts run as jobs inside the bundle, waited on for 18 s and followed with docker_compose_job, because an up that pulls outlasts a call. down never removes volumes.
200 lines
6.6 KiB
Go
200 lines
6.6 KiB
Go
// The docker-compose module's tools (novox/hq research 027/02, 026/05): the compose projects on this
|
|
// machine, their containers, logs and rendered configuration, and bringing one up, down or round
|
|
// again by its directory. A Go bundle the node's runtime launches over stdio (ADR 0188, ADR 0193); it
|
|
// runs as the operator account, which reaches the container runtime through the docker group.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
var providedBy = map[string]string{
|
|
"docker": "the docker module installs the container runtime; this module adds compose to it",
|
|
}
|
|
|
|
func main() {
|
|
if err := stdio.Serve("", tools()); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// where is the argument schema every tool that acts on one project takes.
|
|
var where = map[string]any{
|
|
"dir": map[string]any{"type": "string", "description": "the project's directory, absolute: where its compose file is"},
|
|
"project": map[string]any{"type": "string", "description": "the project's name, for a project docker already knows (instead of dir)"},
|
|
}
|
|
|
|
func with(extra map[string]any) map[string]any {
|
|
out := map[string]any{}
|
|
for k, v := range where {
|
|
out[k] = v
|
|
}
|
|
for k, v := range extra {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
var servicesArg = map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "only these services (default all)"}
|
|
|
|
func tools() []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{
|
|
Name: "docker_compose_projects",
|
|
Description: "The compose projects docker knows on this machine, running or stopped: name, status, working " +
|
|
"directory, compose files, services, and containers running of total. (r)",
|
|
Input: map[string]any{},
|
|
Run: func(map[string]any) (any, error) { return Projects() },
|
|
},
|
|
{
|
|
Name: "docker_compose_ps",
|
|
Description: "One project's containers: service, state, health, exit code, image and published ports. (r)",
|
|
Input: with(nil),
|
|
Run: func(args map[string]any) (any, error) {
|
|
t, err := targetOf(args, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Ps(t)
|
|
},
|
|
},
|
|
{
|
|
Name: "docker_compose_logs",
|
|
Description: "One project's logs, the last lines of each service (default 200, at most 5000), optionally since a " +
|
|
"time (\"10m\", \"2026-10-04T12:00:00\"). Cut at 256 KiB. (r)",
|
|
Input: with(map[string]any{
|
|
"services": servicesArg,
|
|
"tail": map[string]any{"type": "integer", "description": "lines per service (default 200, at most 5000)"},
|
|
"since": map[string]any{"type": "string", "description": "only lines since this: a duration such as 10m or a timestamp"},
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
t, err := targetOf(args, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services, err := optList(args, "services")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := optWhole(args, "tail", 200, 1, 5000)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
since, err := optText(args, "since", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Logs(t, services, n, since)
|
|
},
|
|
},
|
|
{
|
|
Name: "docker_compose_config",
|
|
Description: "A project's configuration as compose renders it: files merged, variables filled. Values of " +
|
|
"environment variables, build arguments and labels whose names suggest a secret are replaced with " +
|
|
"[redacted]. An invalid file is answered as the error compose gives. (r)",
|
|
Input: with(nil),
|
|
Run: func(args map[string]any) (any, error) {
|
|
t, err := targetOf(args, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Config(t)
|
|
},
|
|
},
|
|
{
|
|
Name: "docker_compose_up",
|
|
Description: "Bring a project up, detached: create and start its containers, building or pulling what is " +
|
|
"missing. Answers when finished, or after 18 s with a job to follow with docker_compose_job. (a)",
|
|
Input: with(map[string]any{
|
|
"services": servicesArg,
|
|
"build": map[string]any{"type": "boolean", "description": "build images before starting (--build)"},
|
|
"pull": map[string]any{"type": "string", "enum": []string{"missing", "always", "never"}, "description": "pull policy (default missing)"},
|
|
}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
t, err := targetOf(args, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services, err := optList(args, "services")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
build, err := optFlag(args, "build", false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pull, err := optText(args, "pull", "missing")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Up(t, services, build, pull)
|
|
},
|
|
},
|
|
{
|
|
Name: "docker_compose_down",
|
|
Description: "Stop and remove a project's containers and networks. Its volumes are kept: removing data is not " +
|
|
"this tool's. Answers when finished, or with a job to follow. (a)",
|
|
Input: with(nil),
|
|
Run: func(args map[string]any) (any, error) {
|
|
t, err := targetOf(args, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Down(t)
|
|
},
|
|
},
|
|
{
|
|
Name: "docker_compose_restart",
|
|
Description: "Restart a project's containers, or some of its services. Answers when finished, or with a job to follow. (a)",
|
|
Input: with(map[string]any{"services": servicesArg}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
t, err := targetOf(args, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services, err := optList(args, "services")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Restart(t, services)
|
|
},
|
|
},
|
|
{
|
|
Name: "docker_compose_pull",
|
|
Description: "Pull a project's images, or some services', without starting anything. Answers when finished, or with a job to follow. (a)",
|
|
Input: with(map[string]any{"services": servicesArg}),
|
|
Run: func(args map[string]any) (any, error) {
|
|
t, err := targetOf(args, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
services, err := optList(args, "services")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Pull(t, services)
|
|
},
|
|
},
|
|
{
|
|
Name: "docker_compose_job",
|
|
Description: "A long act this module started (up, down, restart, pull): running or finished, its exit status " +
|
|
"and the end of its output. Without job, every act this process knows, newest first. (r)",
|
|
Input: map[string]any{"job": map[string]any{"type": "string", "description": "the job id an act answered"}},
|
|
Run: func(args map[string]any) (any, error) {
|
|
id, err := optText(args, "job", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if id == "" {
|
|
return map[string]any{"jobs": listJobs()}, nil
|
|
}
|
|
return jobByID(id)
|
|
},
|
|
},
|
|
}
|
|
}
|