Carry what a consumer contributes in its plan entry as one opaque payload, so a provider serving from the seat's plans reads it with its own contract and the provisioner knows no provision's fields (issue 774, research 072 row 34)
mesh/delivery merged: source.main moved to novox/mesh-sdk@fbbfae7629bf
mesh/delivery-group group feat/072-moves merged: every member is merged
mesh/check failed: shanks: the check of novox/mesh-controller#487 failed: go mod download in modules/route-proxy: go: git.novox.be/novox/mesh-controll…
mesh/delivery merged: source.main moved to novox/mesh-sdk@fbbfae7629bf
mesh/delivery-group group feat/072-moves merged: every member is merged
mesh/check failed: shanks: the check of novox/mesh-controller#487 failed: go mod download in modules/route-proxy: go: git.novox.be/novox/mesh-controll…
This commit is contained in:
@@ -1080,6 +1080,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)
|
||||||
|
contributed, err := r.contributedOf(m, to, with.Settings[m.Module])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handed.contribute(m.Module, to, contributed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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
|
||||||
@@ -1827,6 +1832,47 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// contributedOf 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 several by
|
||||||
|
// local name), with where m's machine is, encoded for PlanProvision.Contributed (issue 774): one composition, so a
|
||||||
|
// plan and a file cannot say different things. Nil where m contributes nothing to it.
|
||||||
|
func (r Resolution) contributedOf(m Manifest, to string, layers []Layer) (json.RawMessage, error) {
|
||||||
|
form := ContributedForm{At: r.At}
|
||||||
|
add := func(local string, values map[string]any) error {
|
||||||
|
raw, err := json.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s contributing to %s: %w", m.Module, to, err)
|
||||||
|
}
|
||||||
|
form.Each = append(form.Each, ContributedOne{Local: local, Values: raw})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if err := add("", values); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if err := add(local, values); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(form.Each) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return EncodeContributed(form)
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -135,6 +135,80 @@ 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"`
|
||||||
|
// Contributed is what the consumer gives its provider through this provision, opaque here (issue 774, research
|
||||||
|
// 072 row 34): the bytes of a ContributedForm, each contribution composed as its provider receives it and where
|
||||||
|
// the consumer's machine is. What a contribution's values mean is the provider's own contract, published by the
|
||||||
|
// provider's repository (route-proxy's for a route), which decodes them; the seat, its plans, its client and the
|
||||||
|
// helper carry them unread. On the provision's first entry alone; nil where the consumer contributes nothing to it.
|
||||||
|
Contributed json.RawMessage `json:"contributed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContributedVersion is the version of ContributedForm this SDK writes and reads.
|
||||||
|
const ContributedVersion = 1
|
||||||
|
|
||||||
|
// ContributedForm is the generic form of PlanProvision.Contributed: every contribution one consumer makes to one
|
||||||
|
// requirement, composed (settled with its node's settings, its endpoint's port filled in, its names joined to the
|
||||||
|
// node's domains: Resolution.composed, the one composition a received file was written from), and At, where the
|
||||||
|
// consumer's machine is on the private network, empty when it is on none. The provisioner knows no field of any
|
||||||
|
// contribution: Values are the bytes of an object in the provider's contract.
|
||||||
|
type ContributedForm struct {
|
||||||
|
V int `json:"v"`
|
||||||
|
At string `json:"at,omitempty"`
|
||||||
|
Each []ContributedOne `json:"each"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContributedOne is one contribution: Local its local name among several (`contributes` naming several), empty for a
|
||||||
|
// requirement's one, and Values the contribution's object.
|
||||||
|
type ContributedOne struct {
|
||||||
|
Local string `json:"local,omitempty"`
|
||||||
|
Values json.RawMessage `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeContributed is f as the bytes PlanProvision.Contributed carries, refused where DecodeContributed would refuse
|
||||||
|
// them, so a plan never carries what its reader cannot take.
|
||||||
|
func EncodeContributed(f ContributedForm) (json.RawMessage, error) {
|
||||||
|
f.V = ContributedVersion
|
||||||
|
raw, err := json.Marshal(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := DecodeContributed(raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeContributed reads PlanProvision.Contributed strictly: this version, no field it does not know, at least one
|
||||||
|
// contribution, each local name once, each value a JSON object. What a value says is not judged here.
|
||||||
|
func DecodeContributed(raw json.RawMessage) (ContributedForm, error) {
|
||||||
|
var f ContributedForm
|
||||||
|
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
||||||
|
dec.DisallowUnknownFields()
|
||||||
|
if err := dec.Decode(&f); err != nil {
|
||||||
|
return ContributedForm{}, fmt.Errorf("what a consumer contributed is not readable: %w", err)
|
||||||
|
}
|
||||||
|
if dec.More() {
|
||||||
|
return ContributedForm{}, errors.New("what a consumer contributed carries more than one document")
|
||||||
|
}
|
||||||
|
if f.V != ContributedVersion {
|
||||||
|
return ContributedForm{}, fmt.Errorf("what a consumer contributed is of version %d, this SDK reads %d", f.V,
|
||||||
|
ContributedVersion)
|
||||||
|
}
|
||||||
|
if len(f.Each) == 0 {
|
||||||
|
return ContributedForm{}, errors.New("what a consumer contributed holds no contribution")
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, one := range f.Each {
|
||||||
|
if seen[one.Local] {
|
||||||
|
return ContributedForm{}, fmt.Errorf("what a consumer contributed names %q twice", one.Local)
|
||||||
|
}
|
||||||
|
seen[one.Local] = true
|
||||||
|
var object map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(one.Values, &object); err != nil || object == nil {
|
||||||
|
return ContributedForm{}, fmt.Errorf("the contribution %q is not a JSON object", one.Local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -631,6 +705,22 @@ 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
|
||||||
|
// contributed is what each handed module gives each requirement's provider, encoded (PlanProvision.Contributed).
|
||||||
|
contributed map[string]map[string]json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// contribute records what a handed module gives one requirement's provider, encoded; nothing for none.
|
||||||
|
func (h *handedFiles) contribute(module, provision string, raw json.RawMessage) {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if h.contributed == nil {
|
||||||
|
h.contributed = map[string]map[string]json.RawMessage{}
|
||||||
|
}
|
||||||
|
if h.contributed[module] == nil {
|
||||||
|
h.contributed[module] = map[string]json.RawMessage{}
|
||||||
|
}
|
||||||
|
h.contributed[module][provision] = raw
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -858,6 +948,15 @@ 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, 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 raw := h.contributed[module][provisions[i].Provision]; len(raw) > 0 && !gave[provisions[i].Provision] {
|
||||||
|
gave[provisions[i].Provision] = true
|
||||||
|
provisions[i].Contributed = slices.Clone(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
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: opaque bytes (issue 774).
|
||||||
|
Contributed: json.RawMessage(`{"v":1,"at":"workstation.internal","each":[{"values":{"name":"nextcloud"}}]}`)}}
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package catalogue
|
package catalogue
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -75,8 +76,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 +106,29 @@ 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, as opaque bytes the
|
||||||
|
// provider's own contract reads (issue 774, research 072 row 34): route-proxy serves the route from the plan alone.
|
||||||
|
given, err := DecodeContributed(plan.Provisions[0].Contributed)
|
||||||
|
if err != nil || given.At != "workstation.internal" || len(given.Each) != 2 || given.Each[0].Local != "" ||
|
||||||
|
string(given.Each[0].Values) != `{"host":"notify.example.org"}` || given.Each[1].Local != "api" ||
|
||||||
|
string(given.Each[1].Values) != `{"internal-name":"notify-api.anchor.internal","label":"notify-api","name":"notify-api.example.org","port":9000}` {
|
||||||
|
t.Fatalf("the plan gives %+v (%v)", given, err)
|
||||||
|
}
|
||||||
|
// Read strictly: a form the SDK does not write is refused, whatever its contributions say.
|
||||||
|
for raw, refused := range map[string]string{
|
||||||
|
`{"v":2,"each":[{"values":{}}]}`: "version 2",
|
||||||
|
`{"v":1,"each":[]}`: "no contribution",
|
||||||
|
`{"v":1,"each":[{"values":{}}],"route":{}}`: "unknown field",
|
||||||
|
`{"v":1,"each":[{"values":{}},{"values":{}}]}`: "twice",
|
||||||
|
`{"v":1,"each":[{"local":"api","values":["a"]}]}`: "not a JSON object",
|
||||||
|
`{"v":1,"each":[{"values":{}}]} {}`: "more than one",
|
||||||
|
`{"v":1,"at":"10.0.0.2","each":[{"values":{"x":true}}]}`: "",
|
||||||
|
} {
|
||||||
|
_, err := DecodeContributed(json.RawMessage(raw))
|
||||||
|
if (refused == "") != (err == nil) || err != nil && !strings.Contains(err.Error(), refused) {
|
||||||
|
t.Errorf("DecodeContributed(%s) = %v, want %q", raw, err, refused)
|
||||||
|
}
|
||||||
|
}
|
||||||
// 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user