docker: the eight things Dockhand did that Ham Lab did not

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>
This commit is contained in:
Mitchell-Johnson
2026-09-24 20:11:36 -07:00
co-authored by Claude Opus 5
parent a70f3e2ed2
commit 192504ead3
12 changed files with 783 additions and 69 deletions
+19 -2
View File
@@ -452,8 +452,25 @@ func (m *Manager) WriteCronFile(ctx context.Context, policies []Policy, user, ex
return m.writeRoot(ctx, CronFile, text)
}
// writeRoot puts text at a root-owned path, through the same privilege escalation every
// other write here uses.
// WriteRootFile puts text at a root-owned path, through the same privilege escalation every
// other write here uses. Shared by the snapshot schedules and the image-update schedules,
// which both own a file in /etc/cron.d.
func (m *Manager) WriteRootFile(ctx context.Context, path, text string) error {
return m.writeRoot(ctx, path, text)
}
// RemoveRootFile deletes one, for when the last schedule of a kind goes away.
func (m *Manager) RemoveRootFile(ctx context.Context, path string) error {
out, code, err := m.run(ctx, "rm", "-f", path)
if err != nil {
return err
}
if code != 0 {
return errors.New(strings.TrimSpace(string(out)))
}
return nil
}
func (m *Manager) writeRoot(ctx context.Context, path, text string) error {
args := []string{"tee", path}
if m.sudo {