Dockhand is being retired, so the Docker tab has to cover what it actually earned its keep doing. Eight pieces, all per container or per machine: - Stats: CPU, memory and IO over time, from sensors already collected. - Health: the container's own healthcheck, with the last few probe outputs — which is where a failing check says why. - Events: `docker events` keeps a minute of backlog and nothing more, so Ham Lab follows the stream and keeps a ring of its own. A restart at 4am is still there at 9. - Shell: `docker exec` into a container, in the terminal already here. - Limits: memory, CPU and restart policy changed live, without hand-editing the compose file for a number you want to try. - Updates: what the registry has that this machine does not, for every image at once rather than one stack at a time. - The same across every machine, asked of the hub — which is the only thing that can reach them all. - Schedules: a real line in /etc/cron.d/hamlab-docker calling the binary, in the same shape as the ZFS snapshot schedules. Dockhand updated DNS and ingress nightly from a ticker inside a web app; a cron line keeps running when the console does not, and is visible to anyone reading the machine the normal way. internal/compose is new and shared, so a deploy from the console and a deploy from cron invoke compose identically — including the case where compose has to run inside the orchestrator's own container, where the paths are that container's and not the host's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
51 lines
1.6 KiB
Go
51 lines
1.6 KiB
Go
// Package compose runs docker compose the way a stack was deployed — which is not always
|
|
// simply "on this host". A stack deployed by an orchestrator running inside its own
|
|
// container records paths in that container's filesystem, so compose has to run in there;
|
|
// a stack deployed to another machine over tcp needs DOCKER_HOST pointed at it. Both the
|
|
// console and the scheduled updates it writes must invoke it identically, or a redeploy
|
|
// from one would not match the other's labels.
|
|
package compose
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
)
|
|
|
|
// Spec is everything needed to run compose for one stack.
|
|
type Spec struct {
|
|
Project string // -p
|
|
Files []string // -f, in order
|
|
Via string // a container to run compose inside; "" is this host
|
|
Remote string // a DOCKER_HOST; "" is this machine's Docker
|
|
Tool string // "docker" or "podman"
|
|
}
|
|
|
|
// Cmd builds the command. args are compose's own, e.g. "pull" or "up", "-d".
|
|
func Cmd(ctx context.Context, s Spec, args ...string) *exec.Cmd {
|
|
tool := s.Tool
|
|
if tool == "" {
|
|
tool = "docker"
|
|
}
|
|
base := []string{"compose", "-p", s.Project}
|
|
for _, f := range s.Files {
|
|
base = append(base, "-f", f)
|
|
}
|
|
if s.Via != "" {
|
|
pre := []string{"exec"}
|
|
if s.Remote != "" {
|
|
pre = append(pre, "-e", "DOCKER_HOST="+s.Remote)
|
|
}
|
|
return exec.CommandContext(ctx, tool, append(append(append(pre, s.Via, "docker"), base...), args...)...)
|
|
}
|
|
cmd := exec.CommandContext(ctx, tool, append(base, args...)...)
|
|
if len(s.Files) > 0 {
|
|
cmd.Dir = filepath.Dir(s.Files[0])
|
|
}
|
|
if s.Remote != "" {
|
|
cmd.Env = append(os.Environ(), "DOCKER_HOST="+s.Remote)
|
|
}
|
|
return cmd
|
|
}
|