The store keeps what the records name (hq ADR 0189)
The mesh names what may go from its own build records — a digest it did not record making is never named, which is what keeps the sweep away from the images genesis pushed. An artifact stays because a definition the mesh holds names it, or because it belongs to one of the five most recent successful builds of its module. internal/artifacts asks the store to let go of one; internal/inventory decides and remembers (migration 0055); the sweep runs after a build the mesh recorded, which is when both the bytes and the keep set moved. Never fatal to a build. And the manifest side of while-stopped, refused from the definition alone: no schedule, run-once, a container the module does not declare, itself.
This commit is contained in:
@@ -1511,6 +1511,13 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// **A scheduled step may hold this module's own containers still while it runs**
|
||||
// (novox/hq ADR 0189). What the host judges is the declaration it receives — whether each
|
||||
// id is a container placed on that machine; what belongs here is what only the definition
|
||||
// shows: that the ids are this module's, that they are containers, and that the step is
|
||||
// scheduled. A module naming a neighbour's container would be a module that can stop the
|
||||
// mesh, and the manifest is where that is visible.
|
||||
problems = append(problems, whileStoppedProblems(m, r, hasSchedule(r))...)
|
||||
}
|
||||
for name, own := range m.OwnSecrets {
|
||||
if !placedOrAbsolute(own.Path) {
|
||||
@@ -2042,3 +2049,71 @@ func (o OwnSecrets) Paths() map[string]string {
|
||||
// InstancesInterchangeable is the one value of a definition's `instances`: the module is the same
|
||||
// on every machine, so any instance may answer for the module.
|
||||
const InstancesInterchangeable = "interchangeable"
|
||||
|
||||
// WhileStopped is the resource key naming the containers a scheduled step holds still while it
|
||||
// runs (novox/hq ADR 0189). Carried to the host unchanged, like `schedule`.
|
||||
const WhileStopped = "while-stopped"
|
||||
|
||||
// hasSchedule is whether a resource declares a cadence, as a string.
|
||||
func hasSchedule(r map[string]any) bool {
|
||||
s, _ := r["schedule"].(string)
|
||||
return s != ""
|
||||
}
|
||||
|
||||
// whileStoppedProblems judges one container's maintenance window against its own definition
|
||||
// (novox/hq ADR 0189).
|
||||
//
|
||||
// Three things the manifest is the only place to see: that the step is scheduled (a one-time
|
||||
// offline job says *before* rather than *instead of* — at apply the host already has a window,
|
||||
// because the declaration is applied in order and a run-once step gates what follows); that every
|
||||
// id it names is **this module's own** container; and that it does not name itself.
|
||||
//
|
||||
// The host checks the fourth — that the container is actually placed on that machine — because
|
||||
// that is a fact about the declaration and not about the definition.
|
||||
func whileStoppedProblems(m Manifest, r map[string]any, scheduled bool) []string {
|
||||
raw, present := r[WhileStopped]
|
||||
if !present {
|
||||
return nil
|
||||
}
|
||||
ids, ok := raw.([]any)
|
||||
if !ok {
|
||||
return []string{fmt.Sprintf(
|
||||
"%s declares %s on %v as a %T; it is a list of this module's container ids",
|
||||
m.Module, WhileStopped, r["id"], raw)}
|
||||
}
|
||||
var problems []string
|
||||
if len(ids) > 0 && !scheduled {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares %s on %v, which has no schedule. A maintenance window is for a recurring "+
|
||||
"step: at apply the mesh already has one, because a run-once step gates what is "+
|
||||
"declared after it (novox/hq ADR 0189)", m.Module, WhileStopped, r["id"]))
|
||||
}
|
||||
containers := map[string]bool{}
|
||||
for _, own := range m.Resources {
|
||||
if fmt.Sprint(own["type"]) == "container" {
|
||||
containers[fmt.Sprint(own["id"])] = true
|
||||
}
|
||||
}
|
||||
for _, each := range ids {
|
||||
id, ok := each.(string)
|
||||
if !ok {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares %s on %v naming a %T; each entry is a container's id",
|
||||
m.Module, WhileStopped, r["id"], each))
|
||||
continue
|
||||
}
|
||||
if id == fmt.Sprint(r["id"]) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares %s on %v naming itself", m.Module, WhileStopped, r["id"]))
|
||||
continue
|
||||
}
|
||||
if !containers[id] {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s declares %s on %v naming %q, which is not a container this module declares. "+
|
||||
"A step may hold still its own module's containers and nobody else's — one "+
|
||||
"that could quiesce a neighbour could stop the mesh",
|
||||
m.Module, WhileStopped, r["id"], id))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A scheduled step may hold its module's own containers still while it runs (novox/hq ADR 0189).
|
||||
//
|
||||
// The host judges what it receives — whether each id is a container on that machine. What the
|
||||
// definition is the only place to see is judged here, near whoever wrote it.
|
||||
|
||||
func aStoreManifest(step map[string]any) []byte {
|
||||
m := map[string]any{
|
||||
"module": "distribution", "version": "1",
|
||||
"resources": []any{
|
||||
map[string]any{"id": "store", "type": "container", "name": "mesh-registry",
|
||||
"image": "registry@sha256:" + strings.Repeat("a", 64)},
|
||||
step,
|
||||
},
|
||||
}
|
||||
raw, _ := json.Marshal(m)
|
||||
return raw
|
||||
}
|
||||
|
||||
func TestAMaintenanceWindowOnItsOwnModulesContainerIsAccepted(t *testing.T) {
|
||||
raw := aStoreManifest(map[string]any{
|
||||
"id": "collect", "type": "container", "name": "mesh-registry-collect",
|
||||
"image": "registry@sha256:" + strings.Repeat("a", 64),
|
||||
"schedule": "30 3 * * *", "while-stopped": []any{"store"},
|
||||
})
|
||||
if _, err := ParseManifest(raw); err != nil {
|
||||
t.Fatalf("a step holding its own module's container still was refused: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMaintenanceWindowIsRefusedWhereTheDefinitionShowsItCannotMean(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
step map[string]any
|
||||
says string
|
||||
}{
|
||||
{
|
||||
"on a step with no schedule",
|
||||
map[string]any{"id": "collect", "type": "container", "name": "c",
|
||||
"image": "registry@sha256:" + strings.Repeat("a", 64),
|
||||
"while-stopped": []any{"store"}},
|
||||
"gates what is declared after it",
|
||||
},
|
||||
{
|
||||
"on a run-once step, which already has order",
|
||||
map[string]any{"id": "collect", "type": "container", "name": "c",
|
||||
"image": "registry@sha256:" + strings.Repeat("a", 64),
|
||||
"run-once": true, "while-stopped": []any{"store"}},
|
||||
"A maintenance window is for a recurring step",
|
||||
},
|
||||
{
|
||||
"naming a container this module does not declare",
|
||||
map[string]any{"id": "collect", "type": "container", "name": "c",
|
||||
"image": "registry@sha256:" + strings.Repeat("a", 64),
|
||||
"schedule": "30 3 * * *", "while-stopped": []any{"the-broker"}},
|
||||
"could quiesce a neighbour could stop the mesh",
|
||||
},
|
||||
{
|
||||
"naming itself",
|
||||
map[string]any{"id": "collect", "type": "container", "name": "c",
|
||||
"image": "registry@sha256:" + strings.Repeat("a", 64),
|
||||
"schedule": "30 3 * * *", "while-stopped": []any{"collect"}},
|
||||
"naming itself",
|
||||
},
|
||||
{
|
||||
"written as something that is not a list",
|
||||
map[string]any{"id": "collect", "type": "container", "name": "c",
|
||||
"image": "registry@sha256:" + strings.Repeat("a", 64),
|
||||
"schedule": "30 3 * * *", "while-stopped": "store"},
|
||||
"a list of this module's container ids",
|
||||
},
|
||||
} {
|
||||
_, err := ParseManifest(aStoreManifest(c.step))
|
||||
if err == nil {
|
||||
t.Errorf("%s was accepted", c.name)
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(err.Error(), c.says) {
|
||||
t.Errorf("%s: the refusal does not say %q:\n%v", c.name, c.says, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user