v0.328.0: the catalog allow-list — the box refuses container rights a template asks for and the box does not give (R-971, D2 = A)
gates / gates (push) Successful in 45s
gates / gates (push) Successful in 45s
internal/privpolicy checks the RENDERED compose (privileged, cap_add, devices, host namespaces, security_opt,
sysctls, volumes_from, build/include/extends, env_file/config files outside the stack dir, aliasing named volumes,
binds outside the box's data roots, the Docker socket, propagation, ${VAR} in a privileged key) against an
allow-list compiled into the controller (one entry: homepage's read-only socket). Asked before every compose
write: the 15-minute sync, the guarded update/ladder step, the install, both settings changes, both restores
(before the stop). A refusal writes nothing, keeps the app on its last good compose, raises the operator-only
event app_definition_refused once per definition and puts one line on the app page. The sync never copies a
template named like a controller infra stack. Structural guard TestAllowList_IsWiredIntoEveryComposeWriter.
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -189,9 +189,12 @@ type Stack struct {
|
||||
HoldNoWholeCopy bool `json:"hold_no_whole_copy,omitempty"`
|
||||
// HoldKind (v0.269.0): which hold — "update_failed", "unhealthy_stop" (decision 28: the page offers
|
||||
// Start, not a restore), "restore"; "" when none.
|
||||
HoldKind string `json:"hold_kind,omitempty"`
|
||||
HealthProbe *HealthProbeResult `json:"health_probe,omitempty"` // controller-side probe result
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
HoldKind string `json:"hold_kind,omitempty"`
|
||||
// DefinitionRefused (R-971) is the box's refusal of the app's newest catalog definition — the app keeps its last
|
||||
// good compose. Filled on every read (allowlist.go); "" when none.
|
||||
DefinitionRefused string `json:"definition_refused,omitempty"`
|
||||
HealthProbe *HealthProbeResult `json:"health_probe,omitempty"` // controller-side probe result
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
// RestartingSince (C9-F2) is when this stack was FIRST observed in StateRestarting during the
|
||||
// current restarting run; zero whenever the stack is in any other state. It is what turns a brief
|
||||
// restart (normal: deploy, update, quiesce restart) into a distinguishable crash loop — see
|
||||
@@ -267,6 +270,10 @@ type Manager struct {
|
||||
// this and nothing else: it used to be sent beside the 202 that merely accepted the request, so
|
||||
// an install interrupted five seconds later stayed on the timeline as a completed one.
|
||||
deployDoneHook func(name string, ok bool, detail string)
|
||||
// defRefusedHook (R-971) raises the operator event for a definition the allow-list refused (allowlist.go).
|
||||
defRefusedHook func(name, detail string)
|
||||
defRefusedMu sync.Mutex
|
||||
defRefused map[string]defRefusal
|
||||
testSeams *migSeams // nil in production; tests inject fakes
|
||||
// R-51: docker restart policies for DOWN members of mixed stacks. Keyed by
|
||||
// containerName+"|"+state so a transitioned or recreated container re-reads rather than
|
||||
@@ -1123,6 +1130,7 @@ func (m *Manager) GetStacks() []Stack {
|
||||
m.mu.RUnlock()
|
||||
for i := range result {
|
||||
fillHoldReason(g, &result[i])
|
||||
result[i].DefinitionRefused = m.definitionRefused(result[i].Name)
|
||||
}
|
||||
|
||||
// Sort alphabetically by display name for consistent UI ordering
|
||||
@@ -1143,6 +1151,7 @@ func (m *Manager) GetStack(name string) (*Stack, bool) {
|
||||
}
|
||||
cp := deepCopyStack(s)
|
||||
fillHoldReason(m.updateGuards, &cp)
|
||||
cp.DefinitionRefused = m.definitionRefused(name)
|
||||
return &cp, true
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user