Merge pull request 'A module's own refusal refuses that module, not the node; contributions say unset values in config words; a folded module owns its manifest's paths (issue 1126, group fix/1126-seed-8)' (#117) from fix/1126-seed-8 into main
This commit was merged in pull request #117.
This commit is contained in:
@@ -445,6 +445,19 @@ func TestAnUnsetRequiredValueIsRefusedInTheConfigFormsWords(t *testing.T) {
|
|||||||
t.Fatalf("not typed as a value nobody gave: %v", err)
|
t.Fatalf("not typed as a value nobody gave: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A contribution's value too (issue 1126, seed-8: de-spiegel's route names were refused with ${setting:…}).
|
||||||
|
withRoute := inConfigForm()
|
||||||
|
withRoute["contributes"] = map[string]any{"route": map[string]any{"site": map[string]any{"name": "${config:sender}", "port": 80}}}
|
||||||
|
routed, err := parseRaw(t, withRoute)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = Resolution{}.composed(routed, "route", routed.ContributesMany["route"]["site"], nil, "notifier contributing site to route")
|
||||||
|
if !errors.As(err, &unset) || unset.Module != "notifier" || !strings.Contains(err.Error(),
|
||||||
|
`notifier has a contribution site to route, in "name", that says ${config:sender}`) || strings.Contains(err.Error(), "${setting:") {
|
||||||
|
t.Fatalf("a contribution's unset value is not said in the config form's words: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
settingsForm := notifier()
|
settingsForm := notifier()
|
||||||
settingsForm.Resources[0]["content"] = "from = ${setting:domain}\n"
|
settingsForm.Resources[0]["content"] = "from = ${setting:domain}\n"
|
||||||
err = unsetInConfigWords(settingInto(settingsForm.Resources[0], nil, "notifier"), settingsForm)
|
err = unsetInConfigWords(settingInto(settingsForm.Resources[0], nil, "notifier"), settingsForm)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -1832,7 +1833,14 @@ func (r Resolution) composed(m Manifest, to string, raw map[string]any, layers [
|
|||||||
// module's files is no part of it (novox/hq 04-ISSUES/173).
|
// module's files is no part of it (novox/hq 04-ISSUES/173).
|
||||||
values, err := overridden(raw, WithDefaults(m, layers), what)
|
values, err := overridden(raw, WithDefaults(m, layers), what)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s: %w", what, err)
|
// A required value nothing sets is said in the words of the module's form, as a file's, a unit's and an owner's
|
||||||
|
// are (issue 1126, seed-8: de-spiegel in the config form was refused with ${setting:…} and `settings set`).
|
||||||
|
var unset *UnsetSettingError
|
||||||
|
if errors.As(err, &unset) && unset.Module == "" {
|
||||||
|
unset.Module = m.Module
|
||||||
|
unset.where = fmt.Sprintf("a contribution%s, in %q,", strings.TrimPrefix(what, m.Module+" contributing"), unset.field)
|
||||||
|
}
|
||||||
|
return nil, unsetInConfigWords(fmt.Errorf("%s: %w", what, err), m)
|
||||||
}
|
}
|
||||||
reaches, err := Reaches(m, layers)
|
reaches, err := Reaches(m, layers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -111,11 +111,14 @@ func TestTheHolderOfNodeFilesIsToldSecretPlacesAndOwnedPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
seats = compiled
|
seats = compiled
|
||||||
// Issue 1126, C5: a module carried as it runs (compose.Carried) owns the paths it runs at, beside its resources'.
|
// Issue 1126, C5 and seed-8: a module carried as it runs (compose.Carried) owns its manifest's paths (OwnsPaths), as
|
||||||
carried := Manifest{Module: "de-spiegel", Version: "1", RunsAt: []string{"/var/lib/de-spiegel/server.env"}}
|
// the controller tells them, and not the paths its running resources are at (a directory the mesh placed).
|
||||||
|
carried := Manifest{Module: "de-spiegel", Version: "1", OwnsPaths: []string{"/var/lib/de-spiegel/server.env"},
|
||||||
|
Resources: []map[string]any{{"id": "state", "type": "directory", "path": "/var/lib/de-spiegel"}}}
|
||||||
w, err := Resolution{Node: "anchor", Modules: []Manifest{files, carried}}.filesWords(files, nil, Rendering{})
|
w, err := Resolution{Node: "anchor", Modules: []Manifest{files, carried}}.filesWords(files, nil, Rendering{})
|
||||||
if err != nil || !strings.Contains(w[FilesOwnedPaths], `"/var/lib/de-spiegel/server.env":"de-spiegel (its own tools)"`) {
|
if err != nil || !strings.Contains(w[FilesOwnedPaths], `"/var/lib/de-spiegel/server.env":"de-spiegel (its own tools)"`) ||
|
||||||
t.Errorf("the paths a carried module runs at are not owned: %v %v", w[FilesOwnedPaths], err)
|
strings.Contains(w[FilesOwnedPaths], `"/var/lib/de-spiegel":`) {
|
||||||
|
t.Errorf("a carried module does not own its manifest's paths alone: %v %v", w[FilesOwnedPaths], err)
|
||||||
}
|
}
|
||||||
if w, _ := r.filesWords(app, nil, Rendering{}); w[FilesOwnedPaths] != "" {
|
if w, _ := r.filesWords(app, nil, Rendering{}); w[FilesOwnedPaths] != "" {
|
||||||
t.Error("a module not holding node-files is told the owned paths")
|
t.Error("a module not holding node-files is told the owned paths")
|
||||||
|
|||||||
@@ -864,10 +864,11 @@ type Manifest struct {
|
|||||||
// that could only see its own ports would write a rule set that closed everything else.
|
// that could only see its own ports would write a rule set that closed everything else.
|
||||||
Filtering *Filtering `json:"filtering,omitempty"`
|
Filtering *Filtering `json:"filtering,omitempty"`
|
||||||
|
|
||||||
// RunsAt is, for a module composed from what runs of it (compose.Carried: folded into its node's own composition
|
// OwnsPaths is, for a module composed from what runs of it (compose.Carried: folded into its node's own composition
|
||||||
// at a build of the old form, issue 1126, C5), the paths its running resources are at: paths it owns beside its
|
// at a build of the old form, issue 1126, C5), the paths it owns: its manifest's (DeclaredPaths), as the controller
|
||||||
// resources' (declaredPaths), so the holder of node-files is told them. Never in a manifest's JSON.
|
// tells the holder of node-files them, in place of its resources', which are what runs (a directory placed at a
|
||||||
RunsAt []string `json:"-"`
|
// path its manifest leaves to the mesh, a binding). Nil for any other module. Never in a manifest's JSON.
|
||||||
|
OwnsPaths []string `json:"-"`
|
||||||
|
|
||||||
// Jails are the fail2ban jails this module declares for its own service (novox/hq to-be 31).
|
// Jails are the fail2ban jails this module declares for its own service (novox/hq to-be 31).
|
||||||
// Written into whichever node runs the module, the same way `listens` become that node's rules.
|
// Written into whichever node runs the module, the same way `listens` become that node's rules.
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ func namesWithoutSteppingBack(s Seat, p string) bool {
|
|||||||
// as written would pass for a path nobody named.
|
// as written would pass for a path nobody named.
|
||||||
func declaredPaths(m Manifest) map[string]bool {
|
func declaredPaths(m Manifest) map[string]bool {
|
||||||
out := map[string]bool{}
|
out := map[string]bool{}
|
||||||
|
if m.OwnsPaths != nil {
|
||||||
|
// Carried: what its manifest declares, never what runs of it (issue 1126, seed-8: f1-circuits, snake and txt-game
|
||||||
|
// on ace owned their /var/lib paths on the node and none on the controller).
|
||||||
|
for _, p := range m.OwnsPaths {
|
||||||
|
out[p] = true
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
for _, r := range m.Resources {
|
for _, r := range m.Resources {
|
||||||
if p, ok := r["path"].(string); ok && strings.HasPrefix(p, "/") {
|
if p, ok := r["path"].(string); ok && strings.HasPrefix(p, "/") {
|
||||||
out[path.Clean(p)] = true
|
out[path.Clean(p)] = true
|
||||||
@@ -133,14 +141,15 @@ func declaredPaths(m Manifest) map[string]bool {
|
|||||||
out[path.Clean(f.Path)] = true
|
out[path.Clean(f.Path)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, p := range m.RunsAt {
|
|
||||||
if strings.HasPrefix(p, "/") {
|
|
||||||
out[path.Clean(p)] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeclaredPaths is every machine path a module's manifest declares it owns (declaredPaths), sorted and never nil: what
|
||||||
|
// a carried module owns (Manifest.OwnsPaths).
|
||||||
|
func DeclaredPaths(m Manifest) []string {
|
||||||
|
return append([]string{}, sortedKeys(declaredPaths(m))...)
|
||||||
|
}
|
||||||
|
|
||||||
// declaresPath says whether a module declares one path, as a resource or as a file the mesh renders.
|
// declaresPath says whether a module declares one path, as a resource or as a file the mesh renders.
|
||||||
func declaresPath(m Manifest, p string) bool { return declaredPaths(m)[p] }
|
func declaresPath(m Manifest, p string) bool { return declaredPaths(m)[p] }
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ type UnsetSettingError struct {
|
|||||||
// where says what asks for it — "a file", "a service whose unit", "a directory <id> whose owner" — and
|
// where says what asks for it — "a file", "a service whose unit", "a directory <id> whose owner" — and
|
||||||
// setToday which of the module's keys a layer sets, for saying it again in the config form's words.
|
// setToday which of the module's keys a layer sets, for saying it again in the config form's words.
|
||||||
where, setToday string
|
where, setToday string
|
||||||
|
// field is the key of a contribution whose value asks for it, where the contribution's words are made by its caller.
|
||||||
|
field string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *UnsetSettingError) Error() string { return e.said }
|
func (e *UnsetSettingError) Error() string { return e.said }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -129,7 +130,12 @@ func ApplySettings(resource map[string]any, layers []Layer) (map[string]any, err
|
|||||||
// contract. Until issue 696, a layer replaced any served key it named; influxdb on ace was the one
|
// contract. Until issue 696, a layer replaced any served key it named; influxdb on ace was the one
|
||||||
// layer relying on it, and serves `${setting:org}` and `${setting:bucket}` since.
|
// layer relying on it, and serves `${setting:org}` and `${setting:bucket}` since.
|
||||||
func Settle(base map[string]any, layers []Layer) (map[string]any, error) {
|
func Settle(base map[string]any, layers []Layer) (map[string]any, error) {
|
||||||
return filledFromSettings(deepCopy(base), layers, "what is served")
|
out, err := filledFromSettings(deepCopy(base), layers, "what is served")
|
||||||
|
if err != nil {
|
||||||
|
// Said as it was, untyped: what is served names no module here to type it by.
|
||||||
|
return nil, errors.New(err.Error())
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServedSettings is every key a module's served values and its zone's name fill from its settings (`${setting:<key>}`
|
// ServedSettings is every key a module's served values and its zone's name fill from its settings (`${setting:<key>}`
|
||||||
@@ -193,10 +199,12 @@ func filledFromSettings(merged map[string]any, layers []Layer, what string) (map
|
|||||||
for _, asked := range settingsUsed(s) {
|
for _, asked := range settingsUsed(s) {
|
||||||
v, set := settingValue(layers, asked)
|
v, set := settingValue(layers, asked)
|
||||||
if !set {
|
if !set {
|
||||||
return nil, fmt.Errorf(
|
// Typed, its module and where left to the caller that knows them (Resolution.composed), so a module in
|
||||||
|
// the config form is refused in its own words (unsetInConfigWords; issue 1126, seed-8).
|
||||||
|
return nil, &UnsetSettingError{Setting: asked, field: key, setToday: orNoSettings(layers), said: fmt.Sprintf(
|
||||||
"%s says ${setting:%s} for %q, and nothing sets %q — an operator's value is the "+
|
"%s says ${setting:%s} for %q, and nothing sets %q — an operator's value is the "+
|
||||||
"assignment's, never the definition's (novox/hq ADR 0112)%s",
|
"assignment's, never the definition's (novox/hq ADR 0112)%s",
|
||||||
what, asked, key, asked, orNoSettings(layers))
|
what, asked, key, asked, orNoSettings(layers))}
|
||||||
}
|
}
|
||||||
s = strings.ReplaceAll(s, "${setting:"+asked+"}", plainly(v))
|
s = strings.ReplaceAll(s, "${setting:"+asked+"}", plainly(v))
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-13
@@ -263,6 +263,10 @@ func NamesBuild(m catalogue.Manifest) bool {
|
|||||||
// and nothing else of its own is composed (issue 1126: nats's ${dir:mesh-state} on novox). A module refused in a
|
// and nothing else of its own is composed (issue 1126: nats's ${dir:mesh-state} on novox). A module refused in a
|
||||||
// node's answer is composed so; a folded one the node runs is Carried.
|
// node's answer is composed so; a folded one the node runs is Carried.
|
||||||
func Stub(m catalogue.Manifest) catalogue.Manifest {
|
func Stub(m catalogue.Manifest) catalogue.Manifest {
|
||||||
|
// The paths it owns stay its manifest's, as the controller tells the holder of node-files them (issue 1126, seed-8).
|
||||||
|
if m.OwnsPaths == nil {
|
||||||
|
m.OwnsPaths = catalogue.DeclaredPaths(m)
|
||||||
|
}
|
||||||
m.Build = nil
|
m.Build = nil
|
||||||
var dirs []map[string]any
|
var dirs []map[string]any
|
||||||
for _, r := range m.Resources {
|
for _, r := range m.Resources {
|
||||||
@@ -276,19 +280,21 @@ func Stub(m catalogue.Manifest) catalogue.Manifest {
|
|||||||
|
|
||||||
// Carried is m, folded into its node's own composition at a build of the old form, as the node runs it (issue 1126,
|
// Carried is m, folded into its node's own composition at a build of the old form, as the node runs it (issue 1126,
|
||||||
// C5): nothing to build; its containers and directories those the node's own composition (runs) carries of it, their
|
// C5): nothing to build; its containers and directories those the node's own composition (runs) carries of it, their
|
||||||
// module's word taken off their ids, with the manifest's directories it does not run; and the path of every resource it
|
// module's word taken off their ids, with the manifest's directories it does not run. So what other modules read of it
|
||||||
// runs (catalogue.Manifest.RunsAt). So what other modules read of it is what runs: the machine ports its containers
|
// is what runs: the machine ports its containers publish (the filter's rules, the routes reaching it: MachineSide). The
|
||||||
// publish (the filter's rules, the routes reaching it: MachineSide) and the paths it owns (the holder of node-files is
|
// paths it owns, which the holder of node-files is told, are its manifest's, as for its Stub: the controller composes
|
||||||
// told them). Its other resources are not taken, since the composition makes some of them again from its manifest (its
|
// them from the manifest, not from every path a resource of it runs at (issue 1126, seed-8: f1-circuits, snake and
|
||||||
// secrets' files) and one id twice is refused; none of them is composed into what is applied either way (compositions
|
// txt-game on ace, whose manifests leave their directories to the mesh). Its other resources are not taken, since the
|
||||||
// leaves a carried module's out, and carries the ones that run). What it provides, requires, listens on and grants is
|
// composition makes some of them again from its manifest (its secrets' files) and one id twice is refused; none of them
|
||||||
// its manifest's. A module whose resources runs holds none of is its Stub, as before.
|
// is composed into what is applied either way (compositions leaves a carried module's out, and carries the ones that
|
||||||
|
// run). What it provides, requires, listens on and grants is its manifest's. A module whose resources runs holds none
|
||||||
|
// of is its Stub, as before.
|
||||||
func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catalogue.Manifest {
|
func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catalogue.Manifest {
|
||||||
if runs == nil {
|
if runs == nil {
|
||||||
return Stub(m)
|
return Stub(m)
|
||||||
}
|
}
|
||||||
var taken []map[string]any
|
var taken []map[string]any
|
||||||
var at []string
|
runsAny := false
|
||||||
ids := map[string]bool{}
|
ids := map[string]bool{}
|
||||||
for _, raw := range runs.Resources {
|
for _, raw := range runs.Resources {
|
||||||
var r map[string]any
|
var r map[string]any
|
||||||
@@ -303,9 +309,7 @@ func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catal
|
|||||||
if owner != module {
|
if owner != module {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if p, ok := r["path"].(string); ok && p != "" {
|
runsAny = true
|
||||||
at = append(at, p)
|
|
||||||
}
|
|
||||||
if typ := r["type"]; typ != "container" && typ != "directory" {
|
if typ := r["type"]; typ != "container" && typ != "directory" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -314,7 +318,7 @@ func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catal
|
|||||||
ids[r["id"].(string)] = true
|
ids[r["id"].(string)] = true
|
||||||
taken = append(taken, r)
|
taken = append(taken, r)
|
||||||
}
|
}
|
||||||
if len(at) == 0 && len(taken) == 0 {
|
if !runsAny {
|
||||||
return Stub(m)
|
return Stub(m)
|
||||||
}
|
}
|
||||||
stub := Stub(m)
|
stub := Stub(m)
|
||||||
@@ -324,7 +328,6 @@ func Carried(m catalogue.Manifest, module string, runs *delivery.Composed) catal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
stub.Resources = taken
|
stub.Resources = taken
|
||||||
stub.RunsAt = at
|
|
||||||
return stub
|
return stub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
|
|||||||
}
|
}
|
||||||
composed, skipped, stood, err := compose.CompositionsShadow(in, s.Node, fps)
|
composed, skipped, stood, err := compose.CompositionsShadow(in, s.Node, fps)
|
||||||
for err != nil {
|
for err != nil {
|
||||||
module := culprit(in, s.Node, fps, failed, err)
|
module, bare := culprit(in, s.Node, fps, failed, err)
|
||||||
if module == "" {
|
if module == "" {
|
||||||
// Not one module's: the node's own (a value served elsewhere, the mesh's network), said once as the node's.
|
// Not one module's: the node's own (a value served elsewhere, the mesh's network), said once as the node's.
|
||||||
if len(failed) == 0 {
|
if len(failed) == 0 {
|
||||||
@@ -83,7 +83,7 @@ func Answer(ctx context.Context, r Reader, mesh Mesh, s Scope, fps map[string]st
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
failed[module] = err.Error()
|
failed[module] = err.Error()
|
||||||
in = stubbed(in, s.Node, module)
|
in = stubbed(in, s.Node, module, bare)
|
||||||
composed, skipped, stood, err = compose.CompositionsShadow(in, s.Node, fps)
|
composed, skipped, stood, err = compose.CompositionsShadow(in, s.Node, fps)
|
||||||
}
|
}
|
||||||
// The node's own composition is judged only where the node runs one: one that runs none applies it only once a want
|
// The node's own composition is judged only where the node runs one: one that runs none applies it only once a want
|
||||||
@@ -246,8 +246,10 @@ func standsInFor(x, node string) (string, string) {
|
|||||||
|
|
||||||
// culprit is the module placed on node whose composition the error is of: the first, in placement order, whose stub
|
// culprit is the module placed on node whose composition the error is of: the first, in placement order, whose stub
|
||||||
// (compose.Stub) makes the composition succeed or fail otherwise; empty when none does (the error is not one module's).
|
// (compose.Stub) makes the composition succeed or fail otherwise; empty when none does (the error is not one module's).
|
||||||
// Modules already refused are passed over.
|
// Modules already refused are passed over. A module whose stub still fails the same is tried bare (its stub contributing
|
||||||
func culprit(in compose.Input, node string, fps map[string]string, failed map[string]string, err error) string {
|
// nothing, bare true): a refusal of what it contributes is its own, which the stub keeps (issue 1126, seed-8: de-spiegel's
|
||||||
|
// route names on novox, an unset required value, refused the whole node).
|
||||||
|
func culprit(in compose.Input, node string, fps map[string]string, failed map[string]string, err error) (string, bool) {
|
||||||
var assigned []string
|
var assigned []string
|
||||||
for _, n := range in.Nodes {
|
for _, n := range in.Nodes {
|
||||||
if n.Name == node {
|
if n.Name == node {
|
||||||
@@ -263,19 +265,32 @@ func culprit(in compose.Input, node string, fps map[string]string, failed map[st
|
|||||||
order = append(order, m)
|
order = append(order, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
changes := func(m string, bare bool) bool {
|
||||||
|
_, _, _, again := compose.CompositionsShadow(stubbed(in, node, m, bare), node, fps)
|
||||||
|
return again == nil || again.Error() != err.Error()
|
||||||
|
}
|
||||||
for _, m := range order {
|
for _, m := range order {
|
||||||
if _, done := failed[m]; done {
|
if _, done := failed[m]; done {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, _, _, again := compose.CompositionsShadow(stubbed(in, node, m), node, fps); again == nil || again.Error() != err.Error() {
|
if changes(m, false) {
|
||||||
return m
|
return m, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
for _, m := range order {
|
||||||
|
if _, done := failed[m]; done {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if changes(m, true) {
|
||||||
|
return m, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// stubbed is in with module composed as a stub on node alone (compose.Stub, as node's Kept).
|
// stubbed is in with module composed as a stub on node alone (compose.Stub, as node's Kept); bare, a stub that also
|
||||||
func stubbed(in compose.Input, node, module string) compose.Input {
|
// contributes nothing to what it requires.
|
||||||
|
func stubbed(in compose.Input, node, module string, bare bool) compose.Input {
|
||||||
out := in
|
out := in
|
||||||
out.Nodes = append([]compose.Node(nil), in.Nodes...)
|
out.Nodes = append([]compose.Node(nil), in.Nodes...)
|
||||||
for i := range out.Nodes {
|
for i := range out.Nodes {
|
||||||
@@ -292,7 +307,11 @@ func stubbed(in compose.Input, node, module string) compose.Input {
|
|||||||
for k, v := range out.Nodes[i].Kept {
|
for k, v := range out.Nodes[i].Kept {
|
||||||
kept[k] = v
|
kept[k] = v
|
||||||
}
|
}
|
||||||
kept[module] = compose.Stub(man)
|
stub := compose.Stub(man)
|
||||||
|
if bare {
|
||||||
|
stub.Contributes, stub.ContributesMany = nil, nil
|
||||||
|
}
|
||||||
|
kept[module] = stub
|
||||||
out.Nodes[i].Kept = kept
|
out.Nodes[i].Kept = kept
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -110,7 +111,7 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// A refusal naming no module placed here is no module's: no compose is spent stubbing each in turn (issue 1126: ace's
|
// A refusal naming no module placed here is no module's: no compose is spent stubbing each in turn (issue 1126: ace's
|
||||||
// answer took 7m46s, one compose per module), though here a stub would compose.
|
// answer took 7m46s, one compose per module), though here a stub would compose.
|
||||||
if m := culprit(in, "n1", fps, map[string]string{}, fmt.Errorf("nothing in this mesh provides \"artifact-store\"")); m != "" {
|
if m, _ := culprit(in, "n1", fps, map[string]string{}, fmt.Errorf("nothing in this mesh provides \"artifact-store\"")); m != "" {
|
||||||
t.Errorf("a refusal naming no placed module was laid on %s", m)
|
t.Errorf("a refusal naming no placed module was laid on %s", m)
|
||||||
}
|
}
|
||||||
ownKey, _ := delivery.ComposedNodeKey("n1")
|
ownKey, _ := delivery.ComposedNodeKey("n1")
|
||||||
@@ -240,17 +241,73 @@ func answerNamesEveryRefusalAndLeavesAnOldBuildToTheNode(t *testing.T) {
|
|||||||
if !strings.Contains(rules, "# spiegel — the site") || !strings.Contains(rules, "dport 20005 accept") || strings.Contains(rules, "dport 8080") {
|
if !strings.Contains(rules, "# spiegel — the site") || !strings.Contains(rules, "dport 20005 accept") || strings.Contains(rules, "dport 8080") {
|
||||||
t.Errorf("the filter does not open the machine port spiegel runs at:\n%s", rules)
|
t.Errorf("the filter does not open the machine port spiegel runs at:\n%s", rules)
|
||||||
}
|
}
|
||||||
|
// The paths a carried module owns are its manifest's, as the controller composes them, never what runs of it (issue
|
||||||
|
// 1126, seed-8 #542: on ace, the folded f1-circuits, snake and txt-game, whose manifests leave their directories and
|
||||||
|
// files to ${dir:…}, owned their /var/lib paths on the node and none on the controller, shifting node-tools'
|
||||||
|
// MESH_OWNED_PATHS). f1-circuits' manifest at the indexer's commit (c0ea2c94) is captured; what runs of it is
|
||||||
|
// composed from that manifest by the controller's rule (the directory at /var/lib/f1-circuits, its file, its container).
|
||||||
for _, c := range running {
|
for _, c := range running {
|
||||||
if c.Module == "" && !slices.Contains(compose.Carried(in.Catalogue["spiegel"], "spiegel", &c).RunsAt, "/var/lib/spiegel/server.env") {
|
if c.Module == "" && !reflect.DeepEqual(compose.Carried(in.Catalogue["spiegel"], "spiegel", &c).OwnsPaths, []string{"/var/lib/spiegel"}) {
|
||||||
t.Errorf("spiegel carried does not own the path its running file is at")
|
t.Errorf("spiegel carried owns %v, want its manifest's /var/lib/spiegel alone", compose.Carried(in.Catalogue["spiegel"], "spiegel", &c).OwnsPaths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
f1raw, err := os.ReadFile("testdata/f1-circuits-c0ea2c94.module.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f1, err := catalogue.ParseManifest(f1raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f1runs := delivery.Composed{Node: "ace", Resources: []json.RawMessage{
|
||||||
|
json.RawMessage(`{"id":"f1-circuits.state","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits","mode":"0700"}`),
|
||||||
|
json.RawMessage(`{"id":"f1-circuits.cache","module":"f1-circuits","type":"directory","path":"/var/lib/f1-circuits/cache","mode":"0700"}`),
|
||||||
|
json.RawMessage(`{"id":"f1-circuits.site-env","module":"f1-circuits","type":"file","path":"/var/lib/f1-circuits/site.env","mode":"0644","content":"F1_CIRCUITS_BASE_URL=https://f1.example\n"}`),
|
||||||
|
json.RawMessage(`{"id":"f1-circuits.server","module":"f1-circuits","type":"container","name":"f1-circuits","image":"registry.example/f1@sha256:` + strings.Repeat("c", 64) + `"}`)}}
|
||||||
|
if got, want := catalogue.DeclaredPaths(compose.Carried(f1, "f1-circuits", &f1runs)), catalogue.DeclaredPaths(f1); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("f1-circuits carried owns %v, want what the controller composes from its manifest, %v", got, want)
|
||||||
|
}
|
||||||
var k1, k2 any
|
var k1, k2 any
|
||||||
_ = json.Unmarshal(keptComposed, &k1)
|
_ = json.Unmarshal(keptComposed, &k1)
|
||||||
_ = json.Unmarshal(keptFile, &k2)
|
_ = json.Unmarshal(keptFile, &k2)
|
||||||
if !reflect.DeepEqual(k1, k2) {
|
if !reflect.DeepEqual(k1, k2) {
|
||||||
t.Errorf("the operator export is composed as %s, want the file that runs %s", keptComposed, keptFile)
|
t.Errorf("the operator export is composed as %s, want the file that runs %s", keptComposed, keptFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (D) Issue 1126, seed-8 (mesh-catalog #542): a module's own refusal refuses that module, never the node. novox runs
|
||||||
|
// de-spiegel folded, and its manifest at the indexer's commit (e25e1beb, captured) asks four required config values
|
||||||
|
// for its route names that nothing sets. novox answered the whole candidate refused ("de-spiegel contributing
|
||||||
|
// praktijk to route says ${setting:praktijk-name} ...": its stub still contributed the route). Only de-spiegel is
|
||||||
|
// refused, in the config form's words, and the rest is judged.
|
||||||
|
spiegel, err := os.ReadFile("testdata/de-spiegel-e25e1beb.module.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
proxy := []byte(`{"module":"proxy","version":"1","capabilities":["systemd"],"provides":[{"name":"route","scope":"mesh"},{"name":"smtp","scope":"mesh"}],` +
|
||||||
|
`"resources":[{"id":"service","type":"service","unit":"proxy.service","state":"running","boot":"enabled"}]}`)
|
||||||
|
b = mk("lemurs", overlay.Name, "proxy", "de-spiegel")
|
||||||
|
for m, raw := range map[string][]byte{"de-spiegel": spiegel, "proxy": proxy} {
|
||||||
|
b.manifests[delivery.ManifestDigest(raw)] = raw
|
||||||
|
b.modules[m] = delivery.Module{Manifest: delivery.ManifestDigest(raw)}
|
||||||
|
}
|
||||||
|
p = b.placements["n1"]
|
||||||
|
p.Decided.Endpoint = "n1.example:51820"
|
||||||
|
b.placements["n1"] = p
|
||||||
|
fps = map[string]string{"lemurs": fp("e"), "proxy": fp("f")}
|
||||||
|
a = Answer(ctx, b, Mesh{}, Scope{Node: "n1", Placements: given(b), InOwn: []string{"de-spiegel"}}, fps, nil, "h1", 3)
|
||||||
|
refused = nil
|
||||||
|
for _, r := range a.Refusals {
|
||||||
|
refused = append(refused, r.Module)
|
||||||
|
if r.Module == "de-spiegel" && (!strings.Contains(r.Why, "${config:praktijk-name}") || strings.Contains(r.Why, "${setting:")) {
|
||||||
|
t.Errorf("de-spiegel is refused in other words than its config form's: %s", r.Why)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.Refused != "" || !reflect.DeepEqual(refused, []string{"de-spiegel"}) {
|
||||||
|
t.Errorf("a module's own refusal: %+v, want de-spiegel refused alone, the node not", a)
|
||||||
|
}
|
||||||
|
if !slices.ContainsFunc(a.Added, func(m delivery.ModuleAdded) bool { return m.Module == "proxy" }) {
|
||||||
|
t.Errorf("proxy, which composes, is not judged beside de-spiegel's refusal: %+v", a)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A module another node cannot run in this composition offers nothing in it: the refusal names it and why
|
// A module another node cannot run in this composition offers nothing in it: the refusal names it and why
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"module": "de-spiegel",
|
||||||
|
"version": "1",
|
||||||
|
"slug": "spiegel",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route",
|
||||||
|
"smtp"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"site": {
|
||||||
|
"label": "de-spiegel",
|
||||||
|
"port": 35621
|
||||||
|
},
|
||||||
|
"praktijk": {
|
||||||
|
"name": "${config:praktijk-name}",
|
||||||
|
"port": 35621
|
||||||
|
},
|
||||||
|
"praktijk-www": {
|
||||||
|
"name": "${config:praktijk-www-name}",
|
||||||
|
"redirect": "https://${config:praktijk-name}"
|
||||||
|
},
|
||||||
|
"psychologie": {
|
||||||
|
"name": "${config:psychologie-name}",
|
||||||
|
"port": 35621
|
||||||
|
},
|
||||||
|
"psychologie-www": {
|
||||||
|
"name": "${config:psychologie-www-name}",
|
||||||
|
"redirect": "https://${config:psychologie-name}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smtp": {
|
||||||
|
"account": "de-spiegel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "/var/lib/de-spiegel/route.json",
|
||||||
|
"smtp": "/var/lib/de-spiegel/smtp.json"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"praktijk-name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The public name the practice's site is served on; the operator's, with no default (novox/hq ADR 0112).",
|
||||||
|
"trusted": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"praktijk-www-name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The public name that redirects to the practice's site (its www form); the operator's, with no default (novox/hq ADR 0112).",
|
||||||
|
"trusted": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"psychologie-name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The public name the psychology practice's site is served on; the operator's, with no default (novox/hq ADR 0112).",
|
||||||
|
"trusted": true,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"psychologie-www-name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The public name that redirects to the psychology practice's site (its www form); the operator's, with no default (novox/hq ADR 0112).",
|
||||||
|
"trusted": true,
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"port": 35621,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the de-spiegel site and its /contact endpoint over http; the practice's three public names are route grants, and the proxy reaches every one of them here"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"path": "/var/lib/de-spiegel",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "/var/lib/de-spiegel/server.env",
|
||||||
|
"mode": "0600",
|
||||||
|
"content": "SMTP_HOST=${bound:smtp:at}\nSMTP_PORT=${bound:smtp:port}\nSMTP_AUTH_USER=de-spiegel@${bound:smtp:domain}\nSMTP_AUTH_PASS=${secret:smtp}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "net",
|
||||||
|
"type": "network",
|
||||||
|
"name": "de-spiegel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "de-spiegel",
|
||||||
|
"artifact": "site",
|
||||||
|
"network": "de-spiegel",
|
||||||
|
"env-file": [
|
||||||
|
"/var/lib/de-spiegel/server.env"
|
||||||
|
],
|
||||||
|
"ports": [
|
||||||
|
"35621"
|
||||||
|
],
|
||||||
|
"secrets-in-environment": "the application's own code reads SMTP_AUTH_USER/PASS from the environment (server/index.js); converting is this repository's change"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"on": [
|
||||||
|
{
|
||||||
|
"arg": "NODE_BASE",
|
||||||
|
"image": "node@sha256:48e4b67d85f87bd551df43704e24d252f56cc5f8e9718841aace50f19948f0f9"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "site",
|
||||||
|
"kind": "image",
|
||||||
|
"from": "Dockerfile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
"smtp": "/var/lib/de-spiegel/smtp.secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
"module": "f1-circuits",
|
||||||
|
"slug": "f1",
|
||||||
|
"version": "1",
|
||||||
|
"capabilities": [
|
||||||
|
"container-runtime"
|
||||||
|
],
|
||||||
|
"requires": [
|
||||||
|
"route"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"route": {
|
||||||
|
"label": "f1-circuits",
|
||||||
|
"endpoint": "web"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binds": {
|
||||||
|
"route": "${dir:state}/route.json"
|
||||||
|
},
|
||||||
|
"listens": [
|
||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"port": 8000,
|
||||||
|
"protocol": "tcp",
|
||||||
|
"from": "mesh",
|
||||||
|
"why": "the track-layout CDN, the race posters and the Kometa metadata over http; its public name is a route grant and the proxy reaches it here. Kometa fetches the metadata and every poster by that public name"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"id": "state",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700",
|
||||||
|
"place": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cache",
|
||||||
|
"type": "directory",
|
||||||
|
"mode": "0700"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "site-env",
|
||||||
|
"type": "file",
|
||||||
|
"path": "${dir:state}/site.env",
|
||||||
|
"mode": "0644",
|
||||||
|
"content": "F1_CIRCUITS_BASE_URL=https://${bound:route:name}\n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "server",
|
||||||
|
"type": "container",
|
||||||
|
"name": "f1-circuits",
|
||||||
|
"artifact": "cdn",
|
||||||
|
"ports": [
|
||||||
|
"8000"
|
||||||
|
],
|
||||||
|
"env-file": [
|
||||||
|
"${dir:state}/site.env"
|
||||||
|
],
|
||||||
|
"volumes": [
|
||||||
|
"${dir:cache}:/app/cache"
|
||||||
|
],
|
||||||
|
"restart-on": [
|
||||||
|
"site-env"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"build": {
|
||||||
|
"on": [
|
||||||
|
{
|
||||||
|
"arg": "PYTHON_BASE",
|
||||||
|
"image": "python@sha256:6d43704baacd1bfbe7c295d7f13079d5d8104ed33568873133f8fc69980419df"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"name": "cdn",
|
||||||
|
"kind": "image",
|
||||||
|
"from": "service/Dockerfile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user