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) {
|
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
|
// Where each provision's credentials land, so a contribution can name the file rather than
|
||||||
// carry a value the mesh does not have.
|
// carry a value the mesh does not have.
|
||||||
directories := map[string]string{}
|
directories := map[string]string{}
|
||||||
|
|||||||
@@ -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.
|
// 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 {
|
func dirsFor(m Manifest, with Rendering) map[string]string {
|
||||||
dirs := map[string]string{}
|
dirs := map[string]string{}
|
||||||
for _, r := range m.Resources {
|
for _, r := range m.Resources {
|
||||||
@@ -50,20 +55,25 @@ func dirsFor(m Manifest, with Rendering) map[string]string {
|
|||||||
dirs[id] = strings.TrimRight(path, "/")
|
dirs[id] = strings.TrimRight(path, "/")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if place, said := r["place"].(string); said && place == "." {
|
||||||
|
dirs[id] = dataRoot(with) + "/" + m.Module
|
||||||
|
continue
|
||||||
|
}
|
||||||
dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id
|
dirs[id] = dataRoot(with) + "/" + m.Module + "/" + id
|
||||||
}
|
}
|
||||||
return dirs
|
return dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
// dirInto places a resource: a pathless directory is given the path the mesh resolved for it,
|
// placedOrAbsolute says a path is usable where the mesh needs one: absolute already, or
|
||||||
// and every ${dir:…} the resource carries — in its path, its content, its mounts, its
|
// beginning with a placed reference — resolution makes it absolute before anything reads it.
|
||||||
// environment and its env-files — becomes that path.
|
// (unknownDirRefs is what checks the reference names a real directory.)
|
||||||
//
|
func placedOrAbsolute(path string) bool {
|
||||||
// A reference naming no directory of this module is refused. Left as written, the literal
|
return strings.HasPrefix(path, "/") ||
|
||||||
// `${dir:x}` would reach the machine as a path, and the runtime would create and mount a
|
(strings.HasPrefix(path, "${dir:") && dirRef.MatchString(path))
|
||||||
// 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) {
|
// 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
|
var missing error
|
||||||
out := dirRef.ReplaceAllStringFunc(s, func(ref string) string {
|
out := dirRef.ReplaceAllStringFunc(s, func(ref string) string {
|
||||||
id := dirRef.FindStringSubmatch(ref)[1]
|
id := dirRef.FindStringSubmatch(ref)[1]
|
||||||
@@ -79,11 +89,72 @@ func dirInto(resource map[string]any, dirs map[string]string, module string) err
|
|||||||
return out, missing
|
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.
|
||||||
|
//
|
||||||
|
// A reference naming no directory of this module is refused. Left as written, the literal
|
||||||
|
// `${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) { return dirFill(s, dirs, module) }
|
||||||
|
|
||||||
if fmt.Sprint(resource["type"]) == "directory" {
|
if fmt.Sprint(resource["type"]) == "directory" {
|
||||||
id := fmt.Sprint(resource["id"])
|
id := fmt.Sprint(resource["id"])
|
||||||
if path, stated := resource["path"].(string); !stated || path == "" {
|
if path, stated := resource["path"].(string); !stated || path == "" {
|
||||||
resource["path"] = dirs[id]
|
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
|
var err error
|
||||||
@@ -157,6 +228,28 @@ func (m Manifest) unknownDirRefs() []string {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
var problems []string
|
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{}
|
seen := map[string]bool{}
|
||||||
refuse := func(id string, where any) {
|
refuse := func(id string, where any) {
|
||||||
if declared[id] || seen[id] {
|
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)
|
sort.Strings(problems)
|
||||||
return 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 {
|
func shallowCopy(resource map[string]any) map[string]any {
|
||||||
copied := map[string]any{}
|
copied := map[string]any{}
|
||||||
for k, v := range resource {
|
for k, v := range resource {
|
||||||
|
|||||||
@@ -1049,9 +1049,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for to, where := range m.Binds {
|
for to, where := range m.Binds {
|
||||||
if !strings.HasPrefix(where, "/") {
|
if !placedOrAbsolute(where) {
|
||||||
problems = append(problems, fmt.Sprintf(
|
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
|
var wanted bool
|
||||||
for _, w := range m.Wants() {
|
for _, w := range m.Wants() {
|
||||||
@@ -1183,9 +1183,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name, where := range m.OwnSecrets {
|
for name, where := range m.OwnSecrets {
|
||||||
if !strings.HasPrefix(where, "/") {
|
if !placedOrAbsolute(where) {
|
||||||
problems = append(problems, fmt.Sprintf(
|
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 == "" {
|
if name == "" {
|
||||||
problems = append(problems, m.Module+" needs a secret with no 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) {
|
for _, f := range m.SecretFiles(to) {
|
||||||
if !strings.HasPrefix(f.Path, "/") {
|
if !placedOrAbsolute(f.Path) {
|
||||||
problems = append(problems, fmt.Sprintf(
|
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))
|
m.Module, SecretLocal(to, f.Local), f.Path))
|
||||||
}
|
}
|
||||||
if f.Local != "" && !name.MatchString(f.Local) {
|
if f.Local != "" && !name.MatchString(f.Local) {
|
||||||
@@ -1252,9 +1252,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for to, where := range m.Grants {
|
for to, where := range m.Grants {
|
||||||
if !strings.HasPrefix(where, "/") {
|
if !placedOrAbsolute(where) {
|
||||||
problems = append(problems, fmt.Sprintf(
|
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
|
var offered bool
|
||||||
for _, o := range m.Offers() {
|
for _, o := range m.Offers() {
|
||||||
@@ -1275,9 +1275,9 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
|||||||
if !name.MatchString(to) {
|
if !name.MatchString(to) {
|
||||||
problems = append(problems, fmt.Sprintf("%q is not a usable name to receive", 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(
|
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
|
var offered bool
|
||||||
for _, o := range m.Offers() {
|
for _, o := range m.Offers() {
|
||||||
|
|||||||
Reference in New Issue
Block a user