Merge pull request 'Feature providers-after-switch-on, step 1: plans keep their route names and name the credential tool (issues 923, 928)' (#363) from feat/providers-after-switch-on into main
This commit was merged in pull request #363.
This commit is contained in:
@@ -111,7 +111,7 @@ func boundNeed(n Needed, bound Chosen, catalogue map[string]Manifest, node Node,
|
|||||||
}
|
}
|
||||||
held.From, held.At, held.Module = node.Name, at, m.Module
|
held.From, held.At, held.Module = node.Name, at, m.Module
|
||||||
held.Serves, held.SharedOwn, held.Identity = servedByOne(m, n.Name), sharedByOne(m, n.Name), m.IdentityBoundOf(n.Name)
|
held.Serves, held.SharedOwn, held.Identity = servedByOne(m, n.Name), sharedByOne(m, n.Name), m.IdentityBoundOf(n.Name)
|
||||||
held.CredentialForm = m.CredentialFormOf(n.Name)
|
held.CredentialForm, held.CredentialTool = m.CredentialFormOf(n.Name), m.CredentialToolOf(n.Name)
|
||||||
return held, true, ""
|
return held, true, ""
|
||||||
}
|
}
|
||||||
matching := bound.among(world.Offered[n.Name])
|
matching := bound.among(world.Offered[n.Name])
|
||||||
@@ -125,7 +125,7 @@ func boundNeed(n Needed, bound Chosen, catalogue map[string]Manifest, node Node,
|
|||||||
identity := DefaultIdentityBound
|
identity := DefaultIdentityBound
|
||||||
if pm, known := catalogue[p.Module]; known {
|
if pm, known := catalogue[p.Module]; known {
|
||||||
held.SharedOwn, _ = pm.SharedCredentialOf(n.Name)
|
held.SharedOwn, _ = pm.SharedCredentialOf(n.Name)
|
||||||
held.CredentialForm = pm.CredentialFormOf(n.Name)
|
held.CredentialForm, held.CredentialTool = pm.CredentialFormOf(n.Name), pm.CredentialToolOf(n.Name)
|
||||||
identity = pm.IdentityBoundOf(n.Name)
|
identity = pm.IdentityBoundOf(n.Name)
|
||||||
}
|
}
|
||||||
held.From, held.At, held.Module, held.Serves, held.Identity = p.Node, p.At, p.Module, p.Serves, identity
|
held.From, held.At, held.Module, held.Serves, held.Identity = p.Node, p.At, p.Module, p.Serves, identity
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ type PlanProvision struct {
|
|||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
ProviderModule string `json:"provider-module"`
|
ProviderModule string `json:"provider-module"`
|
||||||
CredentialForm string `json:"credential-form,omitempty"`
|
CredentialForm string `json:"credential-form,omitempty"`
|
||||||
|
// CredentialTool is, for an offer whose credential the provider's software issues (issue 928), the provider's tool
|
||||||
|
// the vault asks for each holder's value; empty for any other form.
|
||||||
|
CredentialTool string `json:"credential-tool,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
// Contributes is what the consumer's manifest tells its provider for this provision (Manifest.Contributes: a
|
// Contributes is what the consumer's manifest tells its provider for this provision (Manifest.Contributes: a
|
||||||
// database's name, its extensions), so the provider's helper learns it from the bus (issue 776). Nil where the
|
// database's name, its extensions), so the provider's helper learns it from the bus (issue 776). Nil where the
|
||||||
@@ -134,7 +137,7 @@ func provisionsOf(needs []Needed, module, as string, contributes map[string]map[
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p := PlanProvision{Provision: n.Name, Local: n.Local, Provider: n.From, ProviderModule: n.Module,
|
p := PlanProvision{Provision: n.Name, Local: n.Local, Provider: n.From, ProviderModule: n.Module,
|
||||||
CredentialForm: n.CredentialForm, Contributes: maps.Clone(contributes[n.Name]),
|
CredentialForm: n.CredentialForm, CredentialTool: n.CredentialTool, Contributes: maps.Clone(contributes[n.Name]),
|
||||||
At: n.At, From: n.From, As: as}
|
At: n.At, From: n.From, As: as}
|
||||||
// **A credential only a template names** (issue 788): named like a file's, so the client fills it from the
|
// **A credential only a template names** (issue 788): named like a file's, so the client fills it from the
|
||||||
// vault's row for this holder.
|
// vault's row for this holder.
|
||||||
|
|||||||
@@ -242,3 +242,33 @@ func TestAnOfferAnsweredByTheHelperIsReadAndHandsNoFile(t *testing.T) {
|
|||||||
t.Fatalf("the flag changed what is composed or handed:\nbefore: %s\nnow: %s\n%v %v", before, got, c2.Filled, c2.Plans)
|
t.Fatalf("the flag changed what is composed or handed:\nbefore: %s\nnow: %s\n%v %v", before, got, c2.Filled, c2.Plans)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A plan entry names the tool of an offer whose credential the provider's software issues (issue 928), so the vault
|
||||||
|
// asks that tool for each holder; an offer of any other form names none.
|
||||||
|
func TestAHandedModulesPlanNamesTheCredentialToolOfASoftwareIssuedOffer(t *testing.T) {
|
||||||
|
for credential, want := range map[string]string{
|
||||||
|
``: "",
|
||||||
|
`,"credential":{"own":"admin"}`: "",
|
||||||
|
`,"credential":{"issued-by":"outside"}`: "",
|
||||||
|
`,"credential":{"issued-by":"software","tool":"gitea_agent_token"}`: "gitea_agent_token",
|
||||||
|
} {
|
||||||
|
raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` +
|
||||||
|
credential + `}],"serves":{"postgres-database":{"port":5432}}`
|
||||||
|
if strings.Contains(credential, `"own"`) {
|
||||||
|
raw += `,"own-secrets":{"admin":{"path":"/var/lib/store/admin","taken":"at-start"}}`
|
||||||
|
}
|
||||||
|
node := workstation()
|
||||||
|
node.At = "workstation.internal"
|
||||||
|
consumer := notifierConsumer(t, true, false)
|
||||||
|
r, err := Resolve(shelf(consumer, mustParse(t, raw+`}`)), []string{consumer.Module}, node, twoStores("anchor"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r.Account = "op"
|
||||||
|
c, _ := composeWith(t, r, handingTo("store:5000"))
|
||||||
|
got := c.Plans["notify"].Provisions
|
||||||
|
if len(got) != 1 || got[0].CredentialTool != want {
|
||||||
|
t.Errorf("an offer with %q: the plan names %+v, want the tool %q", credential, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -303,6 +303,13 @@ func (m Manifest) IssuedCredentialOf(provision string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CredentialToolOf is the tool a plan names for a provision, set only where the offer says the provider's software
|
||||||
|
// issues its credential (issue 928); empty otherwise.
|
||||||
|
func (m Manifest) CredentialToolOf(provision string) string {
|
||||||
|
tool, _ := m.IssuedCredentialOf(provision)
|
||||||
|
return tool
|
||||||
|
}
|
||||||
|
|
||||||
// IssuedOutsideOf is whether this module's offer of a provision says only a person outside the mesh can give its
|
// IssuedOutsideOf is whether this module's offer of a provision says only a person outside the mesh can give its
|
||||||
// credential (novox/hq ADR 0337): the mesh never makes a value for it, and a consumer holds one only once a person
|
// credential (novox/hq ADR 0337): the mesh never makes a value for it, and a consumer holds one only once a person
|
||||||
// gave it with `secret accept`.
|
// gave it with `secret accept`.
|
||||||
|
|||||||
@@ -230,6 +230,8 @@ type Needed struct {
|
|||||||
// CredentialForm is the credential form the provider's offer declares (Manifest.CredentialFormOf, issue 768),
|
// CredentialForm is the credential form the provider's offer declares (Manifest.CredentialFormOf, issue 768),
|
||||||
// named in a handed consumer's plan for the vault; empty where the provider's manifest is not in hand.
|
// named in a handed consumer's plan for the vault; empty where the provider's manifest is not in hand.
|
||||||
CredentialForm string
|
CredentialForm string
|
||||||
|
// CredentialTool is the tool of the offer that the provider's software issues the credential through (issue 928).
|
||||||
|
CredentialTool string
|
||||||
// Manager is set when this holder is a refreshable-grant licence's MANAGER, delivered the refresh
|
// Manager is set when this holder is a refreshable-grant licence's MANAGER, delivered the refresh
|
||||||
// token rather than an access token (novox/hq ADR 0050). It changes one thing downstream: an empty
|
// token rather than an access token (novox/hq ADR 0050). It changes one thing downstream: an empty
|
||||||
// Sealed is tolerated — the manager has not adopted a refresh token yet, which is a real waiting
|
// Sealed is tolerated — the manager has not adopted a refresh token yet, which is a real waiting
|
||||||
@@ -543,7 +545,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
|||||||
needs = append(needs, Needed{
|
needs = append(needs, Needed{
|
||||||
Name: want, From: node.Name, At: at, Module: by.Module,
|
Name: want, From: node.Name, At: at, Module: by.Module,
|
||||||
Serves: servedByOne(by, want), For: because[want],
|
Serves: servedByOne(by, want), For: because[want],
|
||||||
SharedOwn: sharedByOne(by, want), CredentialForm: by.CredentialFormOf(want),
|
SharedOwn: sharedByOne(by, want), CredentialForm: by.CredentialFormOf(want), CredentialTool: by.CredentialToolOf(want),
|
||||||
Identity: by.IdentityBoundOf(want)})
|
Identity: by.IdentityBoundOf(want)})
|
||||||
} else if served := servedByOne(by, want); len(served) > 0 {
|
} else if served := servedByOne(by, want); len(served) > 0 {
|
||||||
// Answered here with no credential to mint, but the provider serves facts the
|
// Answered here with no credential to mint, but the provider serves facts the
|
||||||
@@ -565,7 +567,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
|||||||
}
|
}
|
||||||
needs = append(needs, Needed{
|
needs = append(needs, Needed{
|
||||||
Name: want, From: node.Name, At: at, Serves: served, For: because[want], Module: by.Module,
|
Name: want, From: node.Name, At: at, Serves: served, For: because[want], Module: by.Module,
|
||||||
CredentialForm: by.CredentialFormOf(want)})
|
CredentialForm: by.CredentialFormOf(want), CredentialTool: by.CredentialToolOf(want)})
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -594,17 +596,17 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
|||||||
node.Name, want, p.Node, meshNetwork))
|
node.Name, want, p.Node, meshNetwork))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
shared, form := "", ""
|
shared, form, tool := "", "", ""
|
||||||
// A provider whose definition is not in hand is held to the tightest bound the
|
// A provider whose definition is not in hand is held to the tightest bound the
|
||||||
// mesh knows, not to none: what it keeps is not known here (ADR 0225).
|
// mesh knows, not to none: what it keeps is not known here (ADR 0225).
|
||||||
bound := DefaultIdentityBound
|
bound := DefaultIdentityBound
|
||||||
if pm, known := catalogue[p.Module]; known {
|
if pm, known := catalogue[p.Module]; known {
|
||||||
shared, _ = pm.SharedCredentialOf(want)
|
shared, _ = pm.SharedCredentialOf(want)
|
||||||
form = pm.CredentialFormOf(want)
|
form, tool = pm.CredentialFormOf(want), pm.CredentialToolOf(want)
|
||||||
bound = pm.IdentityBoundOf(want)
|
bound = pm.IdentityBoundOf(want)
|
||||||
}
|
}
|
||||||
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At, Module: p.Module,
|
needs = append(needs, Needed{Name: want, From: p.Node, At: p.At, Module: p.Module,
|
||||||
Serves: p.Serves, For: because[want], SharedOwn: shared, CredentialForm: form, Identity: bound,
|
Serves: p.Serves, For: because[want], SharedOwn: shared, CredentialForm: form, CredentialTool: tool, Identity: bound,
|
||||||
LeftOut: p.LeftOut})
|
LeftOut: p.LeftOut})
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -81,18 +81,10 @@ type ProvisionedCollection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ProvisionedCredential is one provision the module requires, under one local name, and its provider (issue 760);
|
// ProvisionedCredential is one provision the module requires, under one local name, and its provider (issue 760);
|
||||||
// with a name, a credential issued for this holder alone (issue 757).
|
// with a name, a credential issued for this holder alone (issue 757). It is the composition's own type, not a copy:
|
||||||
type ProvisionedCredential struct {
|
// the plan is kept and read back as the one it was sent as, so no field of it (contributes, at, from, as, names) can
|
||||||
Name string `json:"name,omitempty"`
|
// be lost between the two (issue 923).
|
||||||
Provision string `json:"provision"`
|
type ProvisionedCredential = catalogue.PlanProvision
|
||||||
Local string `json:"local,omitempty"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
ProviderModule string `json:"provider-module"`
|
|
||||||
// CredentialForm is the form the provider's offer declares for its credential (issue 768): shared, pair,
|
|
||||||
// issued or given; empty when the provider's manifest was not in hand.
|
|
||||||
CredentialForm string `json:"credential-form,omitempty"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProvisionedSecret is one of the module's own secrets, by its name in the manifest. Path, when set, is the file
|
// ProvisionedSecret is one of the module's own secrets, by its name in the manifest. Path, when set, is the file
|
||||||
// holding the secret alone (the manifest's `own-secrets`), which the client fills with its value and nothing else.
|
// holding the secret alone (the manifest's `own-secrets`), which the client fills with its value and nothing else.
|
||||||
|
|||||||
@@ -352,3 +352,42 @@ func TestAPlanMayHoldSeveralNamedItemsOfOneCollectionAndRefusesABadOne(t *testin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **A kept plan's provisions keep every field the composition sets** (issue 923): the names, the place, the machine
|
||||||
|
// and the identity the provider's helper writes into the consumer's entry, and what the manifest contributes, are
|
||||||
|
// read back as they were sent, so the assignments mirror carries them to the provider.
|
||||||
|
func TestAKeptPlansProvisionsKeepContributesAtFromAsAndNames(t *testing.T) {
|
||||||
|
sent := catalogue.PlanProvision{Provision: "route", Provider: "novox", ProviderModule: "route-proxy",
|
||||||
|
Contributes: map[string]any{"database": "gitea"}, At: "10.0.0.1", From: "novox", As: "gitea@novox",
|
||||||
|
Names: map[string]string{"name-web": "git.example.org", "internal-name": "gitea.mesh"}}
|
||||||
|
raw, err := json.Marshal(map[string]any{"gitea": map[string]any{
|
||||||
|
"provisions": []catalogue.PlanProvision{sent}, "required-by": "forge"}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var kept map[string]keptPlan
|
||||||
|
if err := json.Unmarshal(raw, &kept); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := kept["gitea"].Provisions
|
||||||
|
if len(got) != 1 || !reflect.DeepEqual(got[0], sent) {
|
||||||
|
t.Fatalf("the kept plan's provisions are %+v, want %+v", got, sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A kept plan's provision keeps the tool the vault asks for a software-issued credential (issue 928).
|
||||||
|
func TestAKeptPlansProvisionKeepsItsCredentialTool(t *testing.T) {
|
||||||
|
sent := catalogue.PlanProvision{Provision: "gitea-read", Provider: "novox", ProviderModule: "gitea",
|
||||||
|
CredentialForm: catalogue.CredentialFormIssued, CredentialTool: "gitea_agent_token"}
|
||||||
|
raw, err := json.Marshal(map[string]any{"claude-code": map[string]any{"provisions": []catalogue.PlanProvision{sent}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var kept map[string]keptPlan
|
||||||
|
if err := json.Unmarshal(raw, &kept); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := kept["claude-code"].Provisions; len(got) != 1 || got[0].CredentialTool != "gitea_agent_token" {
|
||||||
|
t.Fatalf("the kept plan's provisions are %+v, want the tool kept", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user