From 97b7da6128b09346760781356bd634cfdf84189c Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:15:36 +0100 Subject: [PATCH] =?UTF-8?q?mesh-provisioner:=20the=20provision's=20plan=20?= =?UTF-8?q?entry=20typed=20in=20the=20definition,=20and=20fixtures=20for?= =?UTF-8?q?=20its=20two=20states=20(ADR=200634=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../go/seats/meshprovisioner/contract.go | 46 ++++++- .../go/seats/meshprovisioner/contract_test.go | 66 ++++++++++ .../mesh-provisioner/fixtures/states.json | 56 ++++++++ provisioning/seats/mesh-provisioner/seat.json | 124 +++++++++++++++++- .../ts/seats/meshprovisioner/src/index.ts | 46 ++++++- 5 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 provisioning/go/seats/meshprovisioner/contract_test.go create mode 100644 provisioning/seats/mesh-provisioner/fixtures/states.json diff --git a/provisioning/go/seats/meshprovisioner/contract.go b/provisioning/go/seats/meshprovisioner/contract.go index 1bacedd..3d54da9 100644 --- a/provisioning/go/seats/meshprovisioner/contract.go +++ b/provisioning/go/seats/meshprovisioner/contract.go @@ -184,6 +184,25 @@ type ProvisionsByNodeConsumerProvisionValueConsumer struct { Node string `json:"node,omitempty"` } +// ProvisionsByNodeConsumerProvisionValuePlan is the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126). +type ProvisionsByNodeConsumerProvisionValuePlan struct { + As string `json:"as,omitempty"` + At string `json:"at,omitempty"` + // the provider's own contract's bytes, carried unread + Contributed map[string]any `json:"contributed,omitempty"` + Contributes map[string]any `json:"contributes,omitempty"` + CredentialForm any `json:"credential-form,omitempty"` + CredentialTool string `json:"credential-tool,omitempty"` + From string `json:"from,omitempty"` + Local string `json:"local,omitempty"` + Name string `json:"name,omitempty"` + Names map[string]string `json:"names,omitempty"` + Path string `json:"path,omitempty"` + Provider string `json:"provider"` + ProviderModule string `json:"provider-module"` + Provision string `json:"provision"` +} + // ProvisionsByNodeConsumerProvisionValueProvider is a type of the contract. type ProvisionsByNodeConsumerProvisionValueProvider struct { Module string `json:"module,omitempty"` @@ -211,8 +230,8 @@ type ProvisionsByNodeConsumerProvisionValue struct { Consumer ProvisionsByNodeConsumerProvisionValueConsumer `json:"consumer"` DependsOn []string `json:"depends-on,omitempty"` Local string `json:"local,omitempty"` - // the plan's provision - Plan map[string]any `json:"plan,omitempty"` + // the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) + Plan *ProvisionsByNodeConsumerProvisionValuePlan `json:"plan,omitempty"` Provider ProvisionsByNodeConsumerProvisionValueProvider `json:"provider"` Provision string `json:"provision"` Reference *ProvisionsByNodeConsumerProvisionValueReference `json:"reference,omitempty"` @@ -237,6 +256,25 @@ type ProvisionsByNodeConsumerProvisionLocalValueConsumer struct { Node string `json:"node,omitempty"` } +// ProvisionsByNodeConsumerProvisionLocalValuePlan is the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126). +type ProvisionsByNodeConsumerProvisionLocalValuePlan struct { + As string `json:"as,omitempty"` + At string `json:"at,omitempty"` + // the provider's own contract's bytes, carried unread + Contributed map[string]any `json:"contributed,omitempty"` + Contributes map[string]any `json:"contributes,omitempty"` + CredentialForm any `json:"credential-form,omitempty"` + CredentialTool string `json:"credential-tool,omitempty"` + From string `json:"from,omitempty"` + Local string `json:"local,omitempty"` + Name string `json:"name,omitempty"` + Names map[string]string `json:"names,omitempty"` + Path string `json:"path,omitempty"` + Provider string `json:"provider"` + ProviderModule string `json:"provider-module"` + Provision string `json:"provision"` +} + // ProvisionsByNodeConsumerProvisionLocalValueProvider is a type of the contract. type ProvisionsByNodeConsumerProvisionLocalValueProvider struct { Module string `json:"module,omitempty"` @@ -264,8 +302,8 @@ type ProvisionsByNodeConsumerProvisionLocalValue struct { Consumer ProvisionsByNodeConsumerProvisionLocalValueConsumer `json:"consumer"` DependsOn []string `json:"depends-on,omitempty"` Local string `json:"local,omitempty"` - // the plan's provision - Plan map[string]any `json:"plan,omitempty"` + // the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) + Plan *ProvisionsByNodeConsumerProvisionLocalValuePlan `json:"plan,omitempty"` Provider ProvisionsByNodeConsumerProvisionLocalValueProvider `json:"provider"` Provision string `json:"provision"` Reference *ProvisionsByNodeConsumerProvisionLocalValueReference `json:"reference,omitempty"` diff --git a/provisioning/go/seats/meshprovisioner/contract_test.go b/provisioning/go/seats/meshprovisioner/contract_test.go new file mode 100644 index 0000000..78917dc --- /dev/null +++ b/provisioning/go/seats/meshprovisioner/contract_test.go @@ -0,0 +1,66 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshprovisioner + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-provisioner/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 ยง6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a database provision provided to a consumer under a local name", subject: "", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"keycloak\",\"node\":\"ace\"},\"depends-on\":[\"novox.postgres.secret-postgres-database\"],\"local\":\"main\",\"plan\":{\"credential-form\":\"pair\",\"local\":\"main\",\"names\":{\"name-main\":\"keycloak.example.internal\"},\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"reference\":{\"generation\":2,\"id\":\"pair/novox/postgres-database/ace/keycloak/main\"},\"state\":\"provided\",\"v\":1}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"keycloak\",\"node\":\"ace\"},\"depends-on\":[\"novox.postgres.secret-postgres-database\"],\"local\":\"main\",\"plan\":{\"credential-form\":\"pair\",\"local\":\"main\",\"names\":{\"name-main\":\"keycloak.example.internal\"},\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"reference\":{\"generation\":2,\"id\":\"pair/novox/postgres-database/ace/keycloak/main\"},\"state\":\"provided\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v ProvisionsByNodeConsumerProvisionLocalValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a provision that waits for the operator", subject: "", given: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"bazarr\",\"node\":\"ace\"},\"provider\":{\"module\":\"mesh-vault\",\"node\":\"novox\"},\"provision\":\"api-token\",\"state\":\"waits\",\"v\":1,\"why\":\"the token is the operator's to give\"}", wire: "{\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"bazarr\",\"node\":\"ace\"},\"provider\":{\"module\":\"mesh-vault\",\"node\":\"novox\"},\"provision\":\"api-token\",\"state\":\"waits\",\"v\":1,\"why\":\"the token is the operator's to give\"}", + decode: func(b []byte) (any, error) { + var v ProvisionsByNodeConsumerProvisionValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a retiring provision with the operator's warrant", subject: "", given: "{\"ask\":\"a7\",\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"old-app\",\"node\":\"ace\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"state\":\"retiring\",\"v\":1,\"warrant\":{\"ask\":\"a7\",\"at\":\"2026-11-04T13:00:00Z\",\"by\":\"operator\",\"option\":\"retire\"},\"why\":\"no want needs it\"}", wire: "{\"ask\":\"a7\",\"at\":\"2026-11-04T12:00:00Z\",\"consumer\":{\"module\":\"old-app\",\"node\":\"ace\"},\"provider\":{\"module\":\"postgres\",\"node\":\"novox\"},\"provision\":\"postgres-database\",\"state\":\"retiring\",\"v\":1,\"warrant\":{\"ask\":\"a7\",\"at\":\"2026-11-04T13:00:00Z\",\"by\":\"operator\",\"option\":\"retire\"},\"why\":\"no want needs it\"}", + decode: func(b []byte) (any, error) { + var v ProvisionsByNodeConsumerProvisionValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module's plan on a node", subject: "", given: "{\"module\":\"keycloak\",\"node\":\"ace\",\"plan\":{\"provisions\":[{\"credential-form\":\"pair\",\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"}]},\"v\":1}", wire: "{\"module\":\"keycloak\",\"node\":\"ace\",\"plan\":{\"provisions\":[{\"credential-form\":\"pair\",\"provider\":\"novox\",\"provider-module\":\"postgres\",\"provision\":\"postgres-database\"}]},\"v\":1}", + decode: func(b []byte) (any, error) { var v PlansByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/provisioning/seats/mesh-provisioner/fixtures/states.json b/provisioning/seats/mesh-provisioner/fixtures/states.json new file mode 100644 index 0000000..1f499cd --- /dev/null +++ b/provisioning/seats/mesh-provisioner/fixtures/states.json @@ -0,0 +1,56 @@ +[ + { + "name": "a database provision provided to a consumer under a local name", + "of": "state:provisions/...", + "given": { + "v": 1, + "provision": "postgres-database", + "local": "main", + "consumer": {"module": "keycloak", "node": "ace"}, + "provider": {"module": "postgres", "node": "novox"}, + "depends-on": ["novox.postgres.secret-postgres-database"], + "state": "provided", + "reference": {"id": "pair/novox/postgres-database/ace/keycloak/main", "generation": 2}, + "plan": {"provision": "postgres-database", "local": "main", "provider": "novox", "provider-module": "postgres", "credential-form": "pair", "names": {"name-main": "keycloak.example.internal"}}, + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "a provision that waits for the operator", + "of": "state:provisions/..", + "given": { + "v": 1, + "provision": "api-token", + "consumer": {"module": "bazarr", "node": "ace"}, + "provider": {"module": "mesh-vault", "node": "novox"}, + "state": "waits", + "why": "the token is the operator's to give", + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "a retiring provision with the operator's warrant", + "of": "state:provisions/..", + "given": { + "v": 1, + "provision": "postgres-database", + "consumer": {"module": "old-app", "node": "ace"}, + "provider": {"module": "postgres", "node": "novox"}, + "state": "retiring", + "why": "no want needs it", + "ask": "a7", + "warrant": {"ask": "a7", "option": "retire", "by": "operator", "at": "2026-11-04T13:00:00Z"}, + "at": "2026-11-04T12:00:00Z" + } + }, + { + "name": "a module's plan on a node", + "of": "state:plans/.", + "given": { + "v": 1, + "node": "ace", + "module": "keycloak", + "plan": {"provisions": [{"provision": "postgres-database", "provider": "novox", "provider-module": "postgres", "credential-form": "pair"}]} + } + } +] diff --git a/provisioning/seats/mesh-provisioner/seat.json b/provisioning/seats/mesh-provisioner/seat.json index 0806593..ad9b169 100644 --- a/provisioning/seats/mesh-provisioner/seat.json +++ b/provisioning/seats/mesh-provisioner/seat.json @@ -168,7 +168,67 @@ }, "plan": { "type": "object", - "description": "the plan's provision" + "description": "the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126)", + "properties": { + "name": { + "type": "string" + }, + "provision": { + "type": "string" + }, + "local": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "provider-module": { + "type": "string" + }, + "credential-form": { + "enum": [ + "pair", + "shared", + "issued", + "given", + "none" + ] + }, + "credential-tool": { + "type": "string" + }, + "path": { + "type": "string" + }, + "contributes": { + "type": "object" + }, + "at": { + "type": "string" + }, + "from": { + "type": "string" + }, + "as": { + "type": "string" + }, + "names": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "contributed": { + "type": "object", + "description": "the provider's own contract's bytes, carried unread" + } + }, + "required": [ + "provision", + "provider", + "provider-module" + ], + "additionalProperties": false }, "at": { "type": "string", @@ -275,7 +335,67 @@ }, "plan": { "type": "object", - "description": "the plan's provision" + "description": "the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126)", + "properties": { + "name": { + "type": "string" + }, + "provision": { + "type": "string" + }, + "local": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "provider-module": { + "type": "string" + }, + "credential-form": { + "enum": [ + "pair", + "shared", + "issued", + "given", + "none" + ] + }, + "credential-tool": { + "type": "string" + }, + "path": { + "type": "string" + }, + "contributes": { + "type": "object" + }, + "at": { + "type": "string" + }, + "from": { + "type": "string" + }, + "as": { + "type": "string" + }, + "names": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "contributed": { + "type": "object", + "description": "the provider's own contract's bytes, carried unread" + } + }, + "required": [ + "provision", + "provider", + "provider-module" + ], + "additionalProperties": false }, "at": { "type": "string", diff --git a/provisioning/ts/seats/meshprovisioner/src/index.ts b/provisioning/ts/seats/meshprovisioner/src/index.ts index ae048d9..a438b91 100644 --- a/provisioning/ts/seats/meshprovisioner/src/index.ts +++ b/provisioning/ts/seats/meshprovisioner/src/index.ts @@ -111,6 +111,25 @@ export interface ProvisionsByNodeConsumerProvisionValueConsumer { "node"?: string; } +/** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ +export interface ProvisionsByNodeConsumerProvisionValuePlan { + "as"?: string; + "at"?: string; + /** the provider's own contract's bytes, carried unread */ + "contributed"?: Record; + "contributes"?: Record; + "credential-form"?: unknown; + "credential-tool"?: string; + "from"?: string; + "local"?: string; + "name"?: string; + "names"?: Record; + "path"?: string; + "provider": string; + "provider-module": string; + "provision": string; +} + export interface ProvisionsByNodeConsumerProvisionValueProvider { "module"?: string; "node"?: string; @@ -135,8 +154,8 @@ export interface ProvisionsByNodeConsumerProvisionValue { "consumer": ProvisionsByNodeConsumerProvisionValueConsumer; "depends-on"?: string[]; "local"?: string; - /** the plan's provision */ - "plan"?: Record; + /** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ + "plan"?: ProvisionsByNodeConsumerProvisionValuePlan; "provider": ProvisionsByNodeConsumerProvisionValueProvider; "provision": string; "reference"?: ProvisionsByNodeConsumerProvisionValueReference; @@ -151,6 +170,25 @@ export interface ProvisionsByNodeConsumerProvisionLocalValueConsumer { "node"?: string; } +/** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ +export interface ProvisionsByNodeConsumerProvisionLocalValuePlan { + "as"?: string; + "at"?: string; + /** the provider's own contract's bytes, carried unread */ + "contributed"?: Record; + "contributes"?: Record; + "credential-form"?: unknown; + "credential-tool"?: string; + "from"?: string; + "local"?: string; + "name"?: string; + "names"?: Record; + "path"?: string; + "provider": string; + "provider-module": string; + "provision": string; +} + export interface ProvisionsByNodeConsumerProvisionLocalValueProvider { "module"?: string; "node"?: string; @@ -175,8 +213,8 @@ export interface ProvisionsByNodeConsumerProvisionLocalValue { "consumer": ProvisionsByNodeConsumerProvisionLocalValueConsumer; "depends-on"?: string[]; "local"?: string; - /** the plan's provision */ - "plan"?: Record; + /** the plan's provision: the entry the key was composed from, at the consumer's wanted build or its newest (issue 1126) */ + "plan"?: ProvisionsByNodeConsumerProvisionLocalValuePlan; "provider": ProvisionsByNodeConsumerProvisionLocalValueProvider; "provision": string; "reference"?: ProvisionsByNodeConsumerProvisionLocalValueReference;