The assignment's own root is a place, and the manifest's maps are placed #70
@@ -224,6 +224,21 @@ func (r Resolution) Compose(with Rendering) (Composed, error) {
|
||||
}
|
||||
|
||||
func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[string]any, error) {
|
||||
// Every manifest is placed first (novox/hq ADR 0112): the maps naming where its bindings,
|
||||
// credentials and contributions land are resolved against this node's directories, so every
|
||||
// reader below — the binding files, the sealed secrets, the grant paths a contribution
|
||||
// names — sees a concrete place and none learns the vocabulary.
|
||||
// Into a fresh slice, never the caller's: one resolution may compose for many nodes, and a
|
||||
// slice element written in place would carry the first node's places into the second's.
|
||||
placed := make([]Manifest, len(r.Modules))
|
||||
for i, m := range r.Modules {
|
||||
var err error
|
||||
if placed[i], err = placedManifest(m, with); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
r.Modules = placed
|
||||
|
||||
// Where each provision's credentials land, so a contribution can name the file rather than
|
||||
// carry a value the mesh does not have.
|
||||
directories := map[string]string{}
|
||||
|
||||
+126
-15
@@ -39,6 +39,11 @@ func dataRoot(with Rendering) string {
|
||||
}
|
||||
|
||||
// dirsFor is every placed directory of a module, id → the path it resolves to on this node.
|
||||
//
|
||||
// A pathless directory saying `"place": "."` is the assignment's own root, <root>/<module> —
|
||||
// to-be 27's one directory per assignment, which every other placed thing sits beneath. At most
|
||||
// one makes sense; nothing enforces one, because two ids resolving to one path is a mistake the
|
||||
// module's own files make visible immediately.
|
||||
func dirsFor(m Manifest, with Rendering) map[string]string {
|
||||
dirs := map[string]string{}
|
||||
for _, r := range m.Resources {
|
||||
@@ -50,11 +55,88 @@ func dirsFor(m Manifest, with Rendering) map[string]string {
|
||||
dirs[id] = strings.TrimRight(path, "/")
|
||||
continue
|
||||
}
|
||||
if place, said := r["place"].(string); said && place == "." {
|
||||
dirs[id] = dataRoot(with) + "/" + m.Module
|
||||
continue
|
||||
}
|
||||
dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// placedOrAbsolute says a path is usable where the mesh needs one: absolute already, or
|
||||
// beginning with a placed reference — resolution makes it absolute before anything reads it.
|
||||
// (unknownDirRefs is what checks the reference names a real directory.)
|
||||
func placedOrAbsolute(path string) bool {
|
||||
return strings.HasPrefix(path, "/") ||
|
||||
(strings.HasPrefix(path, "${dir:") && dirRef.MatchString(path))
|
||||
}
|
||||
|
||||
// dirFill resolves every ${dir:…} in one string, or refuses a reference naming no directory.
|
||||
func dirFill(s string, dirs map[string]string, module string) (string, error) {
|
||||
var missing error
|
||||
out := dirRef.ReplaceAllStringFunc(s, func(ref string) string {
|
||||
id := dirRef.FindStringSubmatch(ref)[1]
|
||||
path, has := dirs[id]
|
||||
if !has {
|
||||
missing = fmt.Errorf(
|
||||
"%s says ${dir:%s}, and %s declares no directory %q. It declares %s",
|
||||
module, id, module, id, orNothing(namesOfDirs(dirs)))
|
||||
return ref
|
||||
}
|
||||
return path
|
||||
})
|
||||
return out, missing
|
||||
}
|
||||
|
||||
// placedManifest is the manifest with every path the mesh resolves already resolved: the maps
|
||||
// naming where bindings, credentials and contributions land are filled against this node's
|
||||
// placed directories, so everything downstream — the generated binding files, the sealed
|
||||
// secrets, the grant directories — reads a concrete place and learns nothing new.
|
||||
func placedManifest(m Manifest, with Rendering) (Manifest, error) {
|
||||
dirs := dirsFor(m, with)
|
||||
fillMap := func(in map[string]string) (map[string]string, error) {
|
||||
if len(in) == 0 {
|
||||
return in, nil
|
||||
}
|
||||
out := make(map[string]string, len(in))
|
||||
for key, value := range in {
|
||||
filled, err := dirFill(value, dirs, m.Module)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[key] = filled
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
var err error
|
||||
if m.Receives, err = fillMap(m.Receives); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if m.Binds, err = fillMap(m.Binds); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if m.Secrets, err = fillMap(m.Secrets); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if m.OwnSecrets, err = fillMap(m.OwnSecrets); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if m.Grants, err = fillMap(m.Grants); err != nil {
|
||||
return m, err
|
||||
}
|
||||
if len(m.SecretsMany) > 0 {
|
||||
many := make(map[string]map[string]string, len(m.SecretsMany))
|
||||
for to, locals := range m.SecretsMany {
|
||||
if many[to], err = fillMap(locals); err != nil {
|
||||
return m, err
|
||||
}
|
||||
}
|
||||
m.SecretsMany = many
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// dirInto places a resource: a pathless directory is given the path the mesh resolved for it,
|
||||
// and every ${dir:…} the resource carries — in its path, its content, its mounts, its
|
||||
// environment and its env-files — becomes that path.
|
||||
@@ -63,27 +145,16 @@ func dirsFor(m Manifest, with Rendering) map[string]string {
|
||||
// `${dir:x}` would reach the machine as a path, and the runtime would create and mount a
|
||||
// directory called `${dir:x}` — real, wrong, and named after the mistake.
|
||||
func dirInto(resource map[string]any, dirs map[string]string, module string) error {
|
||||
fill := func(s string) (string, error) {
|
||||
var missing error
|
||||
out := dirRef.ReplaceAllStringFunc(s, func(ref string) string {
|
||||
id := dirRef.FindStringSubmatch(ref)[1]
|
||||
path, has := dirs[id]
|
||||
if !has {
|
||||
missing = fmt.Errorf(
|
||||
"%s says ${dir:%s}, and %s declares no directory %q. It declares %s",
|
||||
module, id, module, id, orNothing(namesOfDirs(dirs)))
|
||||
return ref
|
||||
}
|
||||
return path
|
||||
})
|
||||
return out, missing
|
||||
}
|
||||
fill := func(s string) (string, error) { return dirFill(s, dirs, module) }
|
||||
|
||||
if fmt.Sprint(resource["type"]) == "directory" {
|
||||
id := fmt.Sprint(resource["id"])
|
||||
if path, stated := resource["path"].(string); !stated || path == "" {
|
||||
resource["path"] = dirs[id]
|
||||
}
|
||||
// Said in the catalogue, not on the machine: the host parses strictly and knows no
|
||||
// such field — resolved, the place IS the path.
|
||||
delete(resource, "place")
|
||||
}
|
||||
|
||||
var err error
|
||||
@@ -157,6 +228,28 @@ func (m Manifest) unknownDirRefs() []string {
|
||||
return ids
|
||||
}
|
||||
var problems []string
|
||||
for _, r := range m.Resources {
|
||||
place, said := r["place"].(string)
|
||||
if !said {
|
||||
continue
|
||||
}
|
||||
if fmt.Sprint(r["type"]) != "directory" {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s says place on %v, which is not a directory — only a directory is placed",
|
||||
m.Module, r["id"]))
|
||||
continue
|
||||
}
|
||||
if path, stated := r["path"].(string); stated && path != "" {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s states both path and place on %v — a stated path IS the placement",
|
||||
m.Module, r["id"]))
|
||||
}
|
||||
if place != "." {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s says place %q on %v, and the only place is %q — the assignment's own root",
|
||||
m.Module, place, r["id"], "."))
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
refuse := func(id string, where any) {
|
||||
if declared[id] || seen[id] {
|
||||
@@ -197,6 +290,24 @@ func (m Manifest) unknownDirRefs() []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
maps := map[string]map[string]string{
|
||||
"receives": m.Receives, "binds": m.Binds, "secrets": m.Secrets,
|
||||
"own-secrets": m.OwnSecrets, "grants": m.Grants,
|
||||
}
|
||||
for field, entries := range maps {
|
||||
for _, value := range entries {
|
||||
for _, id := range referenced(value) {
|
||||
refuse(id, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
for to, locals := range m.SecretsMany {
|
||||
for _, value := range locals {
|
||||
for _, id := range referenced(value) {
|
||||
refuse(id, "secrets."+to)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(problems)
|
||||
return problems
|
||||
}
|
||||
|
||||
@@ -132,6 +132,95 @@ func TestAReferenceToNoDirectoryRefusesAtCompositionToo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheAssignmentsOwnRootIsAPlace(t *testing.T) {
|
||||
m := Manifest{Module: "mailu", Resources: []map[string]any{
|
||||
{"id": "state", "type": "directory", "place": ".", "mode": "0700"},
|
||||
{"id": "data-mail", "type": "directory"},
|
||||
}}
|
||||
dirs := dirsFor(m, Rendering{})
|
||||
if dirs["state"] != "/var/lib/mailu" {
|
||||
t.Fatalf("place %q is the assignment's root; got %q", ".", dirs["state"])
|
||||
}
|
||||
if dirs["data-mail"] != "/var/lib/mailu/data-mail" {
|
||||
t.Fatalf("everything else sits beneath it; got %q", dirs["data-mail"])
|
||||
}
|
||||
root := shallowCopy(m.Resources[0])
|
||||
if err := dirInto(root, dirs, m.Module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if root["path"] != "/var/lib/mailu" {
|
||||
t.Fatalf("the root receives its path; got %v", root["path"])
|
||||
}
|
||||
if _, still := root["place"]; still {
|
||||
t.Fatal("place must never reach the host, which parses strictly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheManifestsMapsArePlaced(t *testing.T) {
|
||||
m := Manifest{
|
||||
Module: "photos",
|
||||
Resources: []map[string]any{
|
||||
{"id": "state", "type": "directory", "place": "."},
|
||||
},
|
||||
Binds: map[string]string{"route": "${dir:state}/route.json"},
|
||||
Secrets: map[string]string{"mongodb-database": "${dir:state}/database.secret"},
|
||||
OwnSecrets: map[string]string{"admin-key": "${dir:state}/admin-key.secret"},
|
||||
Receives: map[string]string{"route": "${dir:state}/grants/mesh.json"},
|
||||
}
|
||||
placed, err := placedManifest(m, Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if placed.Binds["route"] != "/var/lib/photos/route.json" {
|
||||
t.Fatalf("binds are placed; got %v", placed.Binds)
|
||||
}
|
||||
if placed.Secrets["mongodb-database"] != "/var/lib/photos/database.secret" {
|
||||
t.Fatalf("secrets are placed; got %v", placed.Secrets)
|
||||
}
|
||||
if placed.OwnSecrets["admin-key"] != "/var/lib/photos/admin-key.secret" {
|
||||
t.Fatalf("own-secrets are placed; got %v", placed.OwnSecrets)
|
||||
}
|
||||
if placed.Receives["route"] != "/var/lib/photos/grants/mesh.json" {
|
||||
t.Fatalf("receives are placed; got %v", placed.Receives)
|
||||
}
|
||||
if m.Binds["route"] != "${dir:state}/route.json" {
|
||||
t.Fatalf("the manifest itself stays a template; got %v", m.Binds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMapReferenceToNoDirectoryRefusesAtTheManifest(t *testing.T) {
|
||||
m := Manifest{
|
||||
Module: "photos",
|
||||
Resources: []map[string]any{{"id": "state", "type": "directory", "place": "."}},
|
||||
Binds: map[string]string{"route": "${dir:stat}/route.json"},
|
||||
}
|
||||
problems := m.unknownDirRefs()
|
||||
if len(problems) != 1 || !strings.Contains(problems[0], `${dir:stat}`) {
|
||||
t.Fatalf("a map naming no directory is a manifest problem; got %v", problems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceIsValidatedAtTheManifest(t *testing.T) {
|
||||
both := Manifest{Module: "x", Resources: []map[string]any{
|
||||
{"id": "d", "type": "directory", "place": ".", "path": "/somewhere"},
|
||||
}}
|
||||
if got := both.unknownDirRefs(); len(got) != 1 || !strings.Contains(got[0], "both path and place") {
|
||||
t.Fatalf("path beside place refuses; got %v", got)
|
||||
}
|
||||
elsewhere := Manifest{Module: "x", Resources: []map[string]any{
|
||||
{"id": "f", "type": "file", "place": ".", "path": "/somewhere", "content": ""},
|
||||
}}
|
||||
if got := elsewhere.unknownDirRefs(); len(got) != 1 || !strings.Contains(got[0], "not a directory") {
|
||||
t.Fatalf("place on a file refuses; got %v", got)
|
||||
}
|
||||
wrong := Manifest{Module: "x", Resources: []map[string]any{
|
||||
{"id": "d", "type": "directory", "place": "sub/dir"},
|
||||
}}
|
||||
if got := wrong.unknownDirRefs(); len(got) != 1 || !strings.Contains(got[0], `the only place is "."`) {
|
||||
t.Fatalf("a place that is not the root refuses; got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func shallowCopy(resource map[string]any) map[string]any {
|
||||
copied := map[string]any{}
|
||||
for k, v := range resource {
|
||||
|
||||
@@ -1049,9 +1049,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
}
|
||||
}
|
||||
for to, where := range m.Binds {
|
||||
if !strings.HasPrefix(where, "/") {
|
||||
if !placedOrAbsolute(where) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s binds %q at %q, which is not an absolute path", m.Module, to, where))
|
||||
"%s binds %q at %q, which is neither an absolute path nor a placed one", m.Module, to, where))
|
||||
}
|
||||
var wanted bool
|
||||
for _, w := range m.Wants() {
|
||||
@@ -1183,9 +1183,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
}
|
||||
}
|
||||
for name, where := range m.OwnSecrets {
|
||||
if !strings.HasPrefix(where, "/") {
|
||||
if !placedOrAbsolute(where) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s needs %q at %q, which is not an absolute path", m.Module, name, where))
|
||||
"%s needs %q at %q, which is neither an absolute path nor a placed one", m.Module, name, where))
|
||||
}
|
||||
if name == "" {
|
||||
problems = append(problems, m.Module+" needs a secret with no name")
|
||||
@@ -1200,9 +1200,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
}
|
||||
}
|
||||
for _, f := range m.SecretFiles(to) {
|
||||
if !strings.HasPrefix(f.Path, "/") {
|
||||
if !placedOrAbsolute(f.Path) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s keeps the credential for %q at %q, which is not an absolute path",
|
||||
"%s keeps the credential for %q at %q, which is neither an absolute path nor a placed one",
|
||||
m.Module, SecretLocal(to, f.Local), f.Path))
|
||||
}
|
||||
if f.Local != "" && !name.MatchString(f.Local) {
|
||||
@@ -1252,9 +1252,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
}
|
||||
}
|
||||
for to, where := range m.Grants {
|
||||
if !strings.HasPrefix(where, "/") {
|
||||
if !placedOrAbsolute(where) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s grants %q into %q, which is not an absolute path", m.Module, to, where))
|
||||
"%s grants %q into %q, which is neither an absolute path nor a placed one", m.Module, to, where))
|
||||
}
|
||||
var offered bool
|
||||
for _, o := range m.Offers() {
|
||||
@@ -1275,9 +1275,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
if !name.MatchString(to) {
|
||||
problems = append(problems, fmt.Sprintf("%q is not a usable name to receive", to))
|
||||
}
|
||||
if !strings.HasPrefix(where, "/") {
|
||||
if !placedOrAbsolute(where) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s receives %q at %q, which is not an absolute path", m.Module, to, where))
|
||||
"%s receives %q at %q, which is neither an absolute path nor a placed one", m.Module, to, where))
|
||||
}
|
||||
var offered bool
|
||||
for _, o := range m.Offers() {
|
||||
|
||||
Reference in New Issue
Block a user