From 17fc9b9b811590e1a91f2273392f9dc54e5a1df3 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 30 Oct 2026 23:50:09 +0100 Subject: [PATCH] Say whether a plan hands its module in one rule, Plan.Hands, so no reader takes a plan naming provisions alone for a handed module (issue 774) --- go/catalogue/handed.go | 10 ++++++++++ go/catalogue/keyed_test.go | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/go/catalogue/handed.go b/go/catalogue/handed.go index 0f5e3ec..bda7dcd 100644 --- a/go/catalogue/handed.go +++ b/go/catalogue/handed.go @@ -66,6 +66,16 @@ type Plan struct { Provisions []PlanProvision `json:"provisions,omitempty"` } +// Hands is whether the plan hands its module to the provisioner: it names something the node's provisioner client fills +// (a template archive, a file, an own secret or an item), and the node composes none of it. A plan naming provisions alone +// hands nothing (catalogue.Keyed, issue 774): the seat keys what a provider's helper answers of a module not handed, and +// that module's files and own secrets stay its node's composition. The one rule every reader of a plan asks; whether a +// plan exists is never it (on 2026-10-30 the vault read every route-only plan as handed and answered twenty modules' +// own secrets to nobody). +func (p Plan) Hands() bool { + return p.Templates.Source != "" || len(p.Files) > 0 || len(p.Secrets) > 0 || len(p.Items) > 0 +} + // PlanItem is one item the module contributes: to which collection, and its template inside the archive. // // A named item has its Name and Attributes (design 57 L3, issue 777): the client records them with the item and diff --git a/go/catalogue/keyed_test.go b/go/catalogue/keyed_test.go index 4fffa28..d917d42 100644 --- a/go/catalogue/keyed_test.go +++ b/go/catalogue/keyed_test.go @@ -104,6 +104,28 @@ func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) { !strings.Contains(plan.Provisions[0].As, "notify") || plan.Provisions[0].Contributes["host"] != "notify.example.org" { t.Fatalf("the plan names %+v", plan) } + // And it hands nothing: Plan.Hands, the one rule every reader of a plan asks, by each part a plan can name. + if plan.Hands() { + t.Fatalf("a plan naming the route alone hands its module: %+v", plan) + } + for _, c := range []struct { + name string + plan Plan + hands bool + }{ + {"empty", Plan{}, false}, + {"provisions alone", Plan{Provisions: plan.Provisions}, false}, + {"a digest without an archive", Plan{Templates: PlanTemplates{Digest: "sha256:1"}}, false}, + {"templates alone", Plan{Templates: PlanTemplates{Source: "notify.tar", Digest: "sha256:1"}}, true}, + {"files alone", Plan{Files: []PlanFile{{ID: "conf", Path: "/etc/notify.conf"}}}, true}, + {"secrets alone", Plan{Secrets: []PlanSecret{{Name: "admin"}}}, true}, + {"items alone", Plan{Items: []PlanItem{{Collection: "routes", Template: "route.tmpl"}}}, true}, + {"secrets and provisions", Plan{Secrets: []PlanSecret{{Name: "admin"}}, Provisions: plan.Provisions}, true}, + } { + if got := c.plan.Hands(); got != c.hands { + t.Errorf("%s: Hands() is %v, want %v", c.name, got, c.hands) + } + } if want := Awaited(consumer, func(p string) []Manifest { return map[string][]Manifest{"route": {proxy}, "postgres-database": {store}}[p] }); !reflect.DeepEqual(want, []string{"route"}) {