Ask mesh-sdk's Plan.Hands wherever the vault and the provisioner client judge a module handed, and refuse in the catalogue check any line that takes a plan existing for it, so a plan of provisions alone never again withholds a module's secrets (issue 774)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
module checks/planhands
|
||||
|
||||
go 1.26.0
|
||||
@@ -0,0 +1,48 @@
|
||||
// Package planhands refuses a module's line that takes a plan existing for its module handed to the provisioner (issue
|
||||
// 774). Whether a plan hands its module is mesh-sdk's one rule, catalogue.Plan.Hands: since the seat mesh-provisioner keys a
|
||||
// provision its provider's helper answers for a module not handed, a plan naming provisions alone exists for modules
|
||||
// whose files and own secrets stay their node's (on 2026-10-30 the vault read every such plan as handed and answered
|
||||
// twenty modules' own secrets to nobody, and the provisioner client refused each as a plan it could not act on). A line
|
||||
// that tests whether a plan exists either asks Hands on the same line, or says "not handing": it reads only who binds
|
||||
// whom, or that there is no plan at all.
|
||||
package planhands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// existence is a test of whether an assignment's or a plan's `provisioning` is there: nil, or an empty raw value.
|
||||
var existence = regexp.MustCompile(`Provisioning\s*[!=]=\s*nil|len\([^)]*Provisioning\)\s*[!=]=\s*0`)
|
||||
|
||||
// Problems is each such line under root's modules/ that neither asks Hands nor says "not handing", as
|
||||
// `path:line: text`, sorted. Test files are left out: a test may build a plan by hand.
|
||||
func Problems(root string) ([]string, error) {
|
||||
var out []string
|
||||
err := filepath.WalkDir(filepath.Join(root, "modules"), func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, _ := filepath.Rel(root, p)
|
||||
for i, line := range strings.Split(string(raw), "\n") {
|
||||
if existence.MatchString(line) && !strings.Contains(line, ".Hands()") && !strings.Contains(line, "not handing") {
|
||||
out = append(out, fmt.Sprintf("%s:%d: %s", rel, i+1, strings.TrimSpace(line)))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Strings(out)
|
||||
return out, err
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package planhands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A line that takes a plan existing for its module handed is refused and named; one that asks Hands, one marked "not
|
||||
// handing" and any test file pass; and the real catalogue is clean.
|
||||
func TestNoModuleTakesAPlanExistingForItsModuleHanded(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for f, body := range map[string]string{
|
||||
"modules/vault/own.go": "if a.Provisioning != nil {\n",
|
||||
"modules/client/plan.go": "if len(a.Provisioning) == 0 {\n",
|
||||
"modules/vault/rule.go": "return a.Provisioning != nil && a.Provisioning.Hands(), nil\n",
|
||||
"modules/client/binds.go": "if a.Provisioning != nil { // binds, not handing\n",
|
||||
"modules/vault/own_test.go": "if a.Provisioning == nil {\n",
|
||||
"modules/client/notgo/plan.md": "if a.Provisioning == nil {\n",
|
||||
} {
|
||||
p := filepath.Join(root, f)
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, err := Problems(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 2 || !strings.HasPrefix(got[0], "modules/client/plan.go:1:") || !strings.HasPrefix(got[1], "modules/vault/own.go:1:") {
|
||||
t.Fatalf("refused %v, wanted the client's and the vault's existence tests alone", got)
|
||||
}
|
||||
real, err := Problems(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(real) > 0 {
|
||||
t.Errorf("a module takes a plan existing for its module handed; ask catalogue.Plan.Hands, or mark a line that reads "+
|
||||
"only who binds whom \"not handing\":\n%s", strings.Join(real, "\n"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
||||
# every module's Go source, for a line that takes a plan existing for its module handed.
|
||||
modules/**/*.go
|
||||
@@ -870,4 +870,11 @@ func TestAPlanOfProvisionsAloneFetchesNoTemplates(t *testing.T) {
|
||||
if targets, wrong := fillTargets(Plan{}, texts); wrong != "" || len(targets) != 0 {
|
||||
t.Fatalf("filled %v: %s", targets, wrong)
|
||||
}
|
||||
// And such a plan hands its module to nobody (catalogue.Plan.Hands): read as no plan, never refused as one that
|
||||
// names no source, and the module's files stay its node's.
|
||||
routeOnly := `{"node":"g14","module":"grafana","provisioning":{"templates":{"digest":"","source":""},"files":null,` +
|
||||
`"provisions":[{"provision":"route","provider":"g14","provider-module":"route-proxy"}]}}`
|
||||
if module, _, err := planOf("g14", "g14.grafana", []byte(routeOnly)); module != "grafana" || !errors.Is(err, errNoPlan) {
|
||||
t.Fatalf("a plan of provisions alone reads as %s: %v, want no plan", module, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func takeBinds(s *state, key string, value []byte, gone bool) bool {
|
||||
return was != nil
|
||||
}
|
||||
b := binding{node: a.Node, module: a.Module}
|
||||
if a.Provisioning != nil {
|
||||
if a.Provisioning != nil { // binds, not handing: a plan of provisions alone still binds its consumer
|
||||
b.provisions = a.Provisioning.Provisions
|
||||
}
|
||||
s.binds[key] = b
|
||||
|
||||
@@ -178,7 +178,14 @@ func planOf(node, key string, value []byte) (string, Plan, error) {
|
||||
return "", Plan{}, fmt.Errorf("the assignment %s names node %q and module %q, not its key's", key, a.Node,
|
||||
a.Module)
|
||||
}
|
||||
if len(a.Provisioning) == 0 || string(a.Provisioning) == "null" {
|
||||
if len(a.Provisioning) == 0 || string(a.Provisioning) == "null" { // no plan at all, not handing
|
||||
return a.Module, Plan{}, errNoPlan
|
||||
}
|
||||
// **A plan that hands nothing is no plan of this client's** (mesh-sdk catalogue.Plan.Hands, issue 774): the seat keys
|
||||
// a provision its provider's helper answers for a module not handed (grafana's route), and that plan names
|
||||
// provisions alone. Read leniently for the rule; a plan that hands is read strictly below.
|
||||
var rule catalogue.Plan
|
||||
if err := json.Unmarshal(a.Provisioning, &rule); err == nil && !rule.Hands() {
|
||||
return a.Module, Plan{}, errNoPlan
|
||||
}
|
||||
var p Plan
|
||||
|
||||
@@ -206,7 +206,7 @@ func (d Declared) May(caller, name string) (bool, string) {
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Node != node || a.Module != module {
|
||||
return false, fmt.Sprintf("the assignment %s.%s is not the controller's", node, module)
|
||||
}
|
||||
if a.Provisioning == nil {
|
||||
if !hands(raw) {
|
||||
return false, fmt.Sprintf("%s on %s is not handed to the provisioner, so its secrets reach it in its "+
|
||||
"declaration (novox/hq ADR 0349 §17)", module, node)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (v *Vault) outcomes() (map[string]BoundOutcome, error) {
|
||||
continue
|
||||
}
|
||||
var a plannedProvisions
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil {
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil { // binds, not handing
|
||||
continue
|
||||
}
|
||||
consumer := provisions.End{Module: a.Module, Node: a.Node}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
@@ -348,23 +349,30 @@ func (p *planReader) handed(module string) (bool, error) {
|
||||
if raw == nil {
|
||||
return false, nil
|
||||
}
|
||||
var a struct {
|
||||
Provisioning *struct {
|
||||
Templates struct {
|
||||
Source string `json:"source"`
|
||||
} `json:"templates"`
|
||||
Files []json.RawMessage `json:"files"`
|
||||
Secrets []json.RawMessage `json:"secrets"`
|
||||
Items []json.RawMessage `json:"items"`
|
||||
} `json:"provisioning"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
hands, err := handsOf(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("the assignment %s.%s is not the controller's: %w", node, module, err)
|
||||
}
|
||||
// **A plan that names provisions alone hands nothing** (catalogue.Keyed, issue 774): the seat keys a provision its
|
||||
// provider's helper answers for a module not handed (grafana's route), and that module's own secrets are still
|
||||
// composed by its node-engine from this answer. Handed is a plan that names something the client fills.
|
||||
hands := a.Provisioning
|
||||
return hands != nil && (hands.Templates.Source != "" || len(hands.Files) > 0 || len(hands.Secrets) > 0 ||
|
||||
len(hands.Items) > 0), nil
|
||||
return hands, nil
|
||||
}
|
||||
|
||||
// handsOf is whether an assignment's value carries a plan that hands its module to the provisioner: mesh-sdk's one
|
||||
// rule, catalogue.Plan.Hands (issue 774). A plan naming provisions alone (a route the seat keys for a module not
|
||||
// handed) hands nothing, and that module's files and own secrets stay its node's composition. Whether a plan exists
|
||||
// is never the rule; a reader that asks only who binds whom reads the provisions and says so ("not handing", which
|
||||
// the catalogue check checks/planhands holds every module to).
|
||||
func handsOf(raw []byte) (bool, error) {
|
||||
var a struct {
|
||||
Provisioning *catalogue.Plan `json:"provisioning"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return a.Provisioning != nil && a.Provisioning.Hands(), nil
|
||||
}
|
||||
|
||||
// hands is handsOf for a value already read as the controller's: one that cannot be read hands nothing.
|
||||
func hands(raw []byte) bool {
|
||||
h, err := handsOf(raw)
|
||||
return err == nil && h
|
||||
}
|
||||
|
||||
@@ -242,10 +242,10 @@ func readBindings(m Mirror) (bindings, error) {
|
||||
return b, fmt.Errorf("the assignment %s is not the controller's", key)
|
||||
}
|
||||
b.assigned[key] = true
|
||||
if a.Provisioning == nil {
|
||||
if a.Provisioning == nil { // binds, not handing: a plan of provisions alone still binds its consumer
|
||||
continue
|
||||
}
|
||||
b.handed[key] = true
|
||||
b.handed[key] = hands(raw)
|
||||
for _, p := range a.Provisioning.Provisions {
|
||||
if !namePart.MatchString(p.Provider) || !namePart.MatchString(p.Provision) {
|
||||
continue
|
||||
@@ -713,7 +713,7 @@ func sharedRefusal(store *Store, read AssignmentReader, node, provider, provisio
|
||||
for _, m := range strings.Split(sh.ProviderModule, ",") {
|
||||
if raw, err := read(node + "." + m); err == nil && raw != nil {
|
||||
var a plannedProvisions
|
||||
if json.Unmarshal(raw, &a) == nil && a.Node == node && a.Module == m && a.Provisioning != nil {
|
||||
if json.Unmarshal(raw, &a) == nil && a.Node == node && a.Module == m && hands(raw) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -729,7 +729,7 @@ func sharedRefusal(store *Store, read AssignmentReader, node, provider, provisio
|
||||
continue
|
||||
}
|
||||
var a plannedProvisions
|
||||
if json.Unmarshal(raw, &a) != nil || a.Node != node || a.Module != parts[1] || a.Provisioning == nil {
|
||||
if json.Unmarshal(raw, &a) != nil || a.Node != node || a.Module != parts[1] || a.Provisioning == nil { // binds, not handing
|
||||
continue
|
||||
}
|
||||
for _, p := range a.Provisioning.Provisions {
|
||||
@@ -811,7 +811,7 @@ func pairRowRefusal(read AssignmentReader, node string, parts []string) string {
|
||||
if json.Unmarshal(raw, &a) != nil || a.Node != consumer || a.Module != module {
|
||||
return fmt.Sprintf("the assignment %s.%s is not the controller's", consumer, module)
|
||||
}
|
||||
if a.Provisioning == nil {
|
||||
if !hands(raw) {
|
||||
return fmt.Sprintf("%s on %s is not handed to the provisioner, so its login reaches it in its declaration "+
|
||||
"(ADR 0349 clause 17)", module, consumer)
|
||||
}
|
||||
@@ -827,7 +827,7 @@ func pairRowRefusal(read AssignmentReader, node string, parts []string) string {
|
||||
if p.ProviderModule != "" {
|
||||
if praw, err := read(provider + "." + p.ProviderModule); err == nil && praw != nil {
|
||||
var pa plannedProvisions
|
||||
if json.Unmarshal(praw, &pa) == nil && pa.Node == provider && pa.Provisioning != nil {
|
||||
if json.Unmarshal(praw, &pa) == nil && pa.Node == provider && hands(praw) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user