A container reflects its config: restart-on for containers (04-ISSUES/009)

A container reads a mounted file once, at start; its spec (image, env, volumes)
does not include a mounted file's content, so a settings change that re-renders the
file left the running process holding the old value while every check passed. Give
Container the restart-on field a Service already has, and recreate the container
when a named resource changed this pass. Unit-tested (recreated on change, left
alone otherwise) and proven in the mesh-lab: a running grafana runtime picked up a
token change on the next push.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-04 23:39:47 +02:00
parent 8211d8b6fb
commit aa441bac19
3 changed files with 100 additions and 11 deletions
+29 -11
View File
@@ -241,7 +241,7 @@ func applyOne(ctx context.Context, sys system.System, r declaration.Resource, ru
case *declaration.Package:
return applyPackage(ctx, sys, res, run)
case *declaration.Container:
return applyContainer(ctx, res, run)
return applyContainer(ctx, res, run, changed)
case *declaration.User:
return applyUser(ctx, sys, res, run)
case *declaration.Archive:
@@ -521,13 +521,7 @@ func reflects(r *declaration.Service, changed map[string]bool) bool {
// reflected is which of them changed, so the outcome can say why the service was restarted. A
// restart with no reason given is indistinguishable from a service that keeps falling over.
func reflected(r *declaration.Service, changed map[string]bool) []string {
var which []string
for _, id := range r.RestartOn {
if changed[id] {
which = append(which, id)
}
}
return which
return restartedBy(r.RestartOn, changed)
}
func applyService(ctx context.Context, sys system.System, r *declaration.Service, run Runner,
@@ -886,7 +880,7 @@ func applyNetwork(ctx context.Context, r *declaration.Network, run Runner) (Outc
return out, nil
}
func applyContainer(ctx context.Context, r *declaration.Container, run Runner) (Outcome, error) {
func applyContainer(ctx context.Context, r *declaration.Container, run Runner, changed map[string]bool) (Outcome, error) {
out := begin(r)
want := containerSpec(r)
@@ -898,8 +892,16 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner) (
before, err := containerState(ctx, r.Name, run)
existed := err == nil
// A container reads a mounted file once, at start. When one of its restart-on resources changed
// this pass — a settings-merged config the runtime read, say — the file on disk is new and the
// running process still holds the old value, and the spec (image, env, volumes) has not moved,
// so the plain "spec matches, leave it" below would keep the stale process for ever
// (novox/hq 04-ISSUES/009). Recreating is how a container gets restart-on, which a service
// already has.
reasons := restartedBy(r.RestartOn, changed)
switch {
case existed && before.Spec == want && before.Running:
case existed && before.Spec == want && before.Running && len(reasons) == 0:
out.Action = "unchanged"
return out, nil
case existed:
@@ -959,11 +961,27 @@ func applyContainer(ctx context.Context, r *declaration.Container, run Runner) (
out.Action = "created"
if existed {
out.Action = "updated"
out.Detail = "replaced; a container's configuration is fixed when it is created"
if len(reasons) > 0 {
out.Detail = "recreated to pick up " + strings.Join(reasons, ", ")
} else {
out.Detail = "replaced; a container's configuration is fixed when it is created"
}
}
return out, nil
}
// restartedBy is which of the named resources changed this pass — the reason a container or service
// must be brought back rather than left as it is (novox/hq 04-ISSUES/009).
func restartedBy(restartOn []string, changed map[string]bool) []string {
var which []string
for _, id := range restartOn {
if changed[id] {
which = append(which, id)
}
}
return which
}
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {