Carry each contribution, composed, and the consumer's address in its plan entry, and let a process read another module's state, so route-proxy serves routes from the seat's plans alone (issue 774, research 072 row 34)
This commit is contained in:
@@ -1076,6 +1076,11 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
handed.name(m.Module, to, own)
|
handed.name(m.Module, to, own)
|
||||||
|
given, err := r.givenOf(m, to, with.Settings[m.Module])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handed.give(m.Module, to, r.At, given)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A handed collector is filled from items alone: each old form its collections would have held is said
|
// A handed collector is filled from items alone: each old form its collections would have held is said
|
||||||
@@ -1823,6 +1828,32 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// givenOf is each contribution m makes to one requirement, composed as its provider receives it in a received file
|
||||||
|
// (contributions): the one of `contributes` with what the module shares of its bucket, then each of
|
||||||
|
// `contributes-many` by local name. The plan carries them (PlanProvision.Given, issue 774), so the two forms cannot
|
||||||
|
// say different things.
|
||||||
|
func (r Resolution) givenOf(m Manifest, to string, layers []Layer) ([]PlanGiven, error) {
|
||||||
|
var out []PlanGiven
|
||||||
|
if raw, ok := m.Contributes[to]; ok {
|
||||||
|
values, err := r.composed(m, to, raw, layers, m.Module+" contributing to "+to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if values, err = withShares(m, to, values); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, PlanGiven{Values: values})
|
||||||
|
}
|
||||||
|
for _, local := range sortedKeys(m.ContributesMany[to]) {
|
||||||
|
values, err := r.composed(m, to, m.ContributesMany[to][local], layers, m.Module+" contributing "+local+" to "+to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, PlanGiven{Local: local, Values: values})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// composed is one contribution as its provider receives it: settled with this node's settings, its
|
// composed is one contribution as its provider receives it: settled with this node's settings, its
|
||||||
// endpoint's port filled in, and its names composed from the label.
|
// endpoint's port filled in, and its names composed from the label.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -134,6 +134,21 @@ type PlanProvision struct {
|
|||||||
// alone knows the node's domains, and carried unchanged by the provider's helper into the consumer's entry. Nil
|
// alone knows the node's domains, and carried unchanged by the provider's helper into the consumer's entry. Nil
|
||||||
// where the consumer contributes nothing to the provision, and the entry is then as it was.
|
// where the consumer contributes nothing to the provision, and the entry is then as it was.
|
||||||
Names map[string]string `json:"names,omitempty"`
|
Names map[string]string `json:"names,omitempty"`
|
||||||
|
// Given is each contribution of the consumer to this provision as its provider receives it (issue 774, research
|
||||||
|
// 072 row 34: a route is a provision, served from the plans alone): settled with the node's settings, its
|
||||||
|
// endpoint's port filled in and its names composed, by the same composition a received file was written from.
|
||||||
|
// ConsumerAt is where the consumer's machine is on the private network, empty when it is not on one: a provider
|
||||||
|
// that reaches back to its consumer (a reverse proxy) is told it rather than deriving it from a node's name. Both
|
||||||
|
// are on the first entry of the provision alone, nil where the consumer contributes nothing to it.
|
||||||
|
Given []PlanGiven `json:"given,omitempty"`
|
||||||
|
ConsumerAt string `json:"consumer-at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanGiven is one contribution of a consumer to a provision, composed: Local is its local name among several
|
||||||
|
// (`contributes-many`), empty for the one of `contributes`.
|
||||||
|
type PlanGiven struct {
|
||||||
|
Local string `json:"local,omitempty"`
|
||||||
|
Values map[string]any `json:"values"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// provisionsOf is, from a resolution's needs, each provision module requires and its provider, by provision then
|
// provisionsOf is, from a resolution's needs, each provision module requires and its provider, by provision then
|
||||||
@@ -603,6 +618,25 @@ type handedFiles struct {
|
|||||||
// keyedOnly is each module with a plan only for what Keyed keys of a consumer not handed: a provision its
|
// keyedOnly is each module with a plan only for what Keyed keys of a consumer not handed: a provision its
|
||||||
// provider's helper answers (issue 774), such as a route.
|
// provider's helper answers (issue 774), such as a route.
|
||||||
keyedOnly map[string]bool
|
keyedOnly map[string]bool
|
||||||
|
// given is each handed module's contributions composed, by requirement, and at its machine's private name
|
||||||
|
// (PlanProvision.Given, issue 774).
|
||||||
|
given map[string]map[string][]PlanGiven
|
||||||
|
at map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// give records a handed module's contributions to one requirement, composed, and where its machine is.
|
||||||
|
func (h *handedFiles) give(module, provision, at string, given []PlanGiven) {
|
||||||
|
if len(given) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.given == nil {
|
||||||
|
h.given, h.at = map[string]map[string][]PlanGiven{}, map[string]string{}
|
||||||
|
}
|
||||||
|
if h.given[module] == nil {
|
||||||
|
h.given[module] = map[string][]PlanGiven{}
|
||||||
|
}
|
||||||
|
h.given[module][provision] = given
|
||||||
|
h.at[module] = at
|
||||||
}
|
}
|
||||||
|
|
||||||
// keyed starts the plan of a module begin started none for, when the composition keys any of its needs as a consumer
|
// keyed starts the plan of a module begin started none for, when the composition keys any of its needs as a consumer
|
||||||
@@ -830,6 +864,16 @@ func (h *handedFiles) finished(needs []Needed) map[string]Plan {
|
|||||||
provisions[i].Names = maps.Clone(h.names[module][provisions[i].Provision])
|
provisions[i].Names = maps.Clone(h.names[module][provisions[i].Provision])
|
||||||
}
|
}
|
||||||
sortProvisions(provisions)
|
sortProvisions(provisions)
|
||||||
|
// **What the consumer contributes, composed, on the provision's first entry** (issue 774): a provider serving
|
||||||
|
// from the plans alone reads each contribution once, however many local names the requirement has.
|
||||||
|
gave := map[string]bool{}
|
||||||
|
for i := range provisions {
|
||||||
|
if given := h.given[module][provisions[i].Provision]; len(given) > 0 && !gave[provisions[i].Provision] {
|
||||||
|
gave[provisions[i].Provision] = true
|
||||||
|
provisions[i].Given = slices.Clone(given)
|
||||||
|
provisions[i].ConsumerAt = h.at[module]
|
||||||
|
}
|
||||||
|
}
|
||||||
plan.Provisions = provisions
|
plan.Provisions = provisions
|
||||||
h.plans[module] = plan
|
h.plans[module] = plan
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,7 +187,9 @@ func TestAHandedModulesPlanEntryCarriesWhatItContributesToItsProvider(t *testing
|
|||||||
want := []PlanProvision{{Provision: "postgres-database", Provider: "anchor", ProviderModule: "store",
|
want := []PlanProvision{{Provision: "postgres-database", Provider: "anchor", ProviderModule: "store",
|
||||||
CredentialForm: CredentialFormPair, Contributes: map[string]any{"name": "nextcloud"}, At: "anchor.internal", From: "anchor", As: ConsumerIdentity("workstation", "notify"),
|
CredentialForm: CredentialFormPair, Contributes: map[string]any{"name": "nextcloud"}, At: "anchor.internal", From: "anchor", As: ConsumerIdentity("workstation", "notify"),
|
||||||
// What the binding file says as `${bound:postgres-database:name}` (issue 792).
|
// What the binding file says as `${bound:postgres-database:name}` (issue 792).
|
||||||
Names: map[string]string{"name": "nextcloud"}}}
|
Names: map[string]string{"name": "nextcloud"},
|
||||||
|
// And what a received file carried of it, composed, with where its machine is (issue 774).
|
||||||
|
Given: []PlanGiven{{Values: map[string]any{"name": "nextcloud"}}}, ConsumerAt: "workstation.internal"}}
|
||||||
if got := c.Plans["notify"].Provisions; !reflect.DeepEqual(got, want) {
|
if got := c.Plans["notify"].Provisions; !reflect.DeepEqual(got, want) {
|
||||||
t.Fatalf("the plan names %+v, want %+v", got, want)
|
t.Fatalf("the plan names %+v, want %+v", got, want)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) {
|
|||||||
consumer := notifierConsumer(t, false, false)
|
consumer := notifierConsumer(t, false, false)
|
||||||
consumer.Requires = append(consumer.Requires, "route")
|
consumer.Requires = append(consumer.Requires, "route")
|
||||||
consumer.Contributes = map[string]map[string]any{"route": {"host": "notify.example.org"}}
|
consumer.Contributes = map[string]map[string]any{"route": {"host": "notify.example.org"}}
|
||||||
|
consumer.ContributesMany = map[string]map[string]map[string]any{"route": {"api": {"label": "notify-api", "port": 9000}}}
|
||||||
node := workstation()
|
node := workstation()
|
||||||
node.At = "workstation.internal"
|
node.At, node.PublicDomain = "workstation.internal", "example.org"
|
||||||
proxy := Manifest{Module: "proxy", Version: "1", Provides: []Offer{{Name: "route", Scope: ScopeMesh, AnsweredBy: AnsweredByHelper}}}
|
proxy := Manifest{Module: "proxy", Version: "1", Provides: []Offer{{Name: "route", Scope: ScopeMesh, AnsweredBy: AnsweredByHelper}}}
|
||||||
store := Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh,
|
store := Manifest{Module: "store", Version: "1", Provides: []Offer{{Name: "postgres-database", Scope: ScopeMesh,
|
||||||
Credential: &OfferCredential{Form: CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}}
|
Credential: &OfferCredential{Form: CredentialFormPair}}}, Serves: map[string]map[string]any{"postgres-database": {"port": 5432}}}
|
||||||
@@ -104,6 +105,13 @@ func TestAModuleNotHandedHasAPlanForWhatAHelperAnswers(t *testing.T) {
|
|||||||
!strings.Contains(plan.Provisions[0].As, "notify") || plan.Provisions[0].Contributes["host"] != "notify.example.org" {
|
!strings.Contains(plan.Provisions[0].As, "notify") || plan.Provisions[0].Contributes["host"] != "notify.example.org" {
|
||||||
t.Fatalf("the plan names %+v", plan)
|
t.Fatalf("the plan names %+v", plan)
|
||||||
}
|
}
|
||||||
|
// What it contributes, composed as a received file carried it, and where its machine is (issue 774, research 072
|
||||||
|
// row 34): route-proxy serves the route from the plan alone.
|
||||||
|
if g := plan.Provisions[0].Given; len(g) != 2 || g[0].Local != "" || g[0].Values["host"] != "notify.example.org" ||
|
||||||
|
g[1].Local != "api" || g[1].Values["name"] != "notify-api.example.org" || g[1].Values["port"] != 9000 ||
|
||||||
|
plan.Provisions[0].ConsumerAt != "workstation.internal" {
|
||||||
|
t.Fatalf("the plan gives %+v at %q", g, plan.Provisions[0].ConsumerAt)
|
||||||
|
}
|
||||||
// And it hands nothing: Plan.Hands, the one rule every reader of a plan asks, by each part a plan can name.
|
// 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() {
|
if plan.Hands() {
|
||||||
t.Fatalf("a plan naming the route alone hands its module: %+v", plan)
|
t.Fatalf("a plan naming the route alone hands its module: %+v", plan)
|
||||||
|
|||||||
+22
-3
@@ -43,9 +43,28 @@ func (s State) bucket() string { return s.Module + "_" + s.Name }
|
|||||||
// (js.KeyValue) is what asks $JS.API.STREAM.INFO.KV_<bucket>, so it is what withRefusal watches: a bucket
|
// (js.KeyValue) is what asks $JS.API.STREAM.INFO.KV_<bucket>, so it is what withRefusal watches: a bucket
|
||||||
// not granted is refused there, promptly, as nats.ErrPermissionViolation.
|
// not granted is refused there, promptly, as nats.ErrPermissionViolation.
|
||||||
func (c *Conn) State(name string) State {
|
func (c *Conn) State(name string) State {
|
||||||
bucket := c.account.Module + "_" + name
|
return c.bound(c.account.Module, name, func() error { return c.refusedUnannounced(name) })
|
||||||
return State{Module: c.account.Module, Name: name, closed: func() bool { return c.nc.IsClosed() }, kv: func(ctx context.Context) (jetstream.KeyValue, error) {
|
}
|
||||||
if err := c.refusedUnannounced(name); err != nil {
|
|
||||||
|
// Read is another module's state this connection's module reads, named as its manifest's `reads` names it
|
||||||
|
// (`<module>.<state>`, novox/hq ADR 0495): bound like State to the bucket <module>_<state>, so Get, Keys and Watch
|
||||||
|
// work as far as the account's read grant goes, and a write is refused by the bus (issue 774: route-proxy follows
|
||||||
|
// the seat mesh-provisioner's plans). A name that is not `<module>.<state>` fails at the call that needed it.
|
||||||
|
func (c *Conn) Read(read string) State {
|
||||||
|
module, name, ok := strings.Cut(read, ".")
|
||||||
|
return c.bound(module, name, func() error {
|
||||||
|
if !ok || module == "" || name == "" || strings.Contains(name, ".") {
|
||||||
|
return fmt.Errorf("%q is not a state another module keeps, <module>.<state>", read)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// bound is module's state name on this connection, checked by refused before every bind.
|
||||||
|
func (c *Conn) bound(module, name string, refused func() error) State {
|
||||||
|
bucket := module + "_" + name
|
||||||
|
return State{Module: module, Name: name, closed: func() bool { return c.nc.IsClosed() }, kv: func(ctx context.Context) (jetstream.KeyValue, error) {
|
||||||
|
if err := refused(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
js, err := jetstream.New(c.nc)
|
js, err := jetstream.New(c.nc)
|
||||||
|
|||||||
@@ -353,6 +353,18 @@ func TestStateWatchRefusesAnUngrantedBucketNamingIt(t *testing.T) {
|
|||||||
if err == nil || !errors.Is(err, nats.ErrPermissionViolation) || !strings.Contains(err.Error(), "mesh-mail_unrelated") {
|
if err == nil || !errors.Is(err, nats.ErrPermissionViolation) || !strings.Contains(err.Error(), "mesh-mail_unrelated") {
|
||||||
t.Fatalf("Watch on an ungranted bucket: %v, want a refusal naming mesh-mail_unrelated", err)
|
t.Fatalf("Watch on an ungranted bucket: %v, want a refusal naming mesh-mail_unrelated", err)
|
||||||
}
|
}
|
||||||
|
// Another module's state, read as `reads` names it (issue 774): bound to its bucket, refused naming it when no read
|
||||||
|
// is granted; a name that is not <module>.<state> is refused before the bus is asked.
|
||||||
|
if _, err := adminJetStream.CreateKeyValue(ctxFor(t), jetstream.KeyValueConfig{Bucket: "mesh-provisioner_plans"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = conn.Read("mesh-provisioner.plans").Watch("", func(stdio.StateChange) error { return nil })
|
||||||
|
if err == nil || !errors.Is(err, nats.ErrPermissionViolation) || !strings.Contains(err.Error(), "mesh-provisioner_plans") {
|
||||||
|
t.Fatalf("Read of an ungranted state: %v, want a refusal naming mesh-provisioner_plans", err)
|
||||||
|
}
|
||||||
|
if _, err := conn.Read("plans").Get("x"); err == nil || !strings.Contains(err.Error(), "<module>.<state>") {
|
||||||
|
t.Fatalf("Read of a name that is no other module's state: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1840/S1: given a process connection granted its bucket, when it deletes a key, then the key is gone and
|
// 1840/S1: given a process connection granted its bucket, when it deletes a key, then the key is gone and
|
||||||
|
|||||||
Reference in New Issue
Block a user