242 lines
9.8 KiB
Go
242 lines
9.8 KiB
Go
package assign
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/composer"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The cases of a group's composition at its mesh-sdk head: the trunk as it is, and the trunk with the change.
|
|
const (
|
|
CaseTrunk = "trunk"
|
|
CaseChange = "change"
|
|
)
|
|
|
|
// SDKDir is the directory of mesh-sdk's Go module in its repository, where the composer is built at a head; a reader's
|
|
// composer is built at its repository's root, at the mesh-sdk its go.mod pins.
|
|
const SDKDir = "go"
|
|
|
|
// The modules whose mesh-sdk reads the manifests a plan sends (ADR 0482 clause 5): the controller composes and sends
|
|
// them, the node-engine applies them. Each is judged at its repository.
|
|
var ReaderModules = map[string]string{"mesh-controller": "novox/mesh-controller", "mesh-host": "novox/mesh-host"}
|
|
|
|
// HeadJob is the compose job of a group with a mesh-sdk member (ADR 0482 clauses 3 and 4, issue 1622): the composer
|
|
// built at the member's head composes the machines the change touches, as the trunk holds them and as the change leaves
|
|
// them, every manifest named by its digest in the indexer's bucket, the seats held from the claims (the assigner's input).
|
|
func HeadJob(m Mesh, c Change) (delivery.ComposeJob, error) {
|
|
trunk, change := map[string]composer.Manifest{}, map[string]composer.Manifest{}
|
|
for name, digest := range m.Digests {
|
|
trunk[name] = composer.Manifest{Digest: digest}
|
|
change[name] = composer.Manifest{Digest: digest}
|
|
}
|
|
for name, digest := range c.Digests {
|
|
change[name] = composer.Manifest{Digest: digest}
|
|
}
|
|
for _, name := range c.Removed {
|
|
delete(change, name)
|
|
}
|
|
req := composer.Request{V: composer.Version, Input: plainInput(m.Assigned), Depth: composer.DepthResolve,
|
|
HoldingsFromClaims: true, Machines: Touched(m, c),
|
|
Cases: []composer.Case{{Name: CaseTrunk, Manifests: trunk}, {Name: CaseChange, Manifests: change}}}
|
|
return job(SDKRepository, c.SDKHead, SDKDir, req, "the mesh-sdk head of "+c.Group)
|
|
}
|
|
|
|
// ReaderJob is the compose job that reads the manifests a group changes with the mesh-sdk a reader module pins at
|
|
// commit (ADR 0482 clause 5, issue 1623): no machine is composed, every changed manifest is only read.
|
|
func ReaderJob(c Change, module, repository, commit string) (delivery.ComposeJob, error) {
|
|
changed := map[string]composer.Manifest{}
|
|
for name, digest := range c.Digests {
|
|
changed[name] = composer.Manifest{Digest: digest}
|
|
}
|
|
req := composer.Request{V: composer.Version, Cases: []composer.Case{{Name: CaseChange, Manifests: changed}}}
|
|
return job(repository, commit, delivery.RootPath, req, module+" at "+commit)
|
|
}
|
|
|
|
func job(repository, commit, dir string, req composer.Request, reader string) (delivery.ComposeJob, error) {
|
|
raw, err := json.Marshal(req)
|
|
if err != nil {
|
|
return delivery.ComposeJob{}, err
|
|
}
|
|
return delivery.ComposeJob{Fetch: map[string]any{"repository": repository, "commit": commit}, Dir: dir,
|
|
Package: composer.Package, Request: raw, Reader: reader}, nil
|
|
}
|
|
|
|
// plainInput is the composition's input without its catalogue: the nodes by name with what is assigned to them (the
|
|
// composer derives the seats held from the claims, as input does).
|
|
func plainInput(assigned map[string][]string) compose.Input {
|
|
var in compose.Input
|
|
nodes := make([]string, 0, len(assigned))
|
|
for n := range assigned {
|
|
nodes = append(nodes, n)
|
|
}
|
|
sort.Strings(nodes)
|
|
for _, n := range nodes {
|
|
in.Nodes = append(in.Nodes, compose.Node{Name: n, Assigned: slices.Clone(assigned[n])})
|
|
}
|
|
return in
|
|
}
|
|
|
|
// Ran is one compose ask's end: its step's state and reason, and the composer's verdict when the step passed.
|
|
type Ran struct {
|
|
State string
|
|
Reason string
|
|
Verdict *composer.Verdict
|
|
}
|
|
|
|
// RanOf reads a compose ask's end from its step value; a step that passed with an answer that cannot be read is a
|
|
// failure, named.
|
|
func RanOf(step delivery.Step) Ran {
|
|
r := Ran{State: step.State, Reason: step.Reason}
|
|
if step.State != delivery.StepPassed {
|
|
return r
|
|
}
|
|
var a delivery.ComposeAnswer
|
|
var v composer.Verdict
|
|
if err := json.Unmarshal(step.Result, &a); err != nil {
|
|
return Ran{State: delivery.StepFailed, Reason: "the compose step's answer cannot be read: " + err.Error()}
|
|
}
|
|
if err := json.Unmarshal(a.Verdict, &v); err != nil || v.V != composer.Version {
|
|
return Ran{State: delivery.StepFailed, Reason: fmt.Sprintf("the composer's verdict cannot be read (version %d): %v", v.V, err)}
|
|
}
|
|
r.Verdict = &v
|
|
return r
|
|
}
|
|
|
|
func caseOf(v *composer.Verdict, name string) (composer.CaseVerdict, bool) {
|
|
if v == nil {
|
|
return composer.CaseVerdict{}, false
|
|
}
|
|
for _, c := range v.Cases {
|
|
if c.Name == name {
|
|
return c, true
|
|
}
|
|
}
|
|
return composer.CaseVerdict{}, false
|
|
}
|
|
|
|
// Reader is one module whose mesh-sdk reads the group's manifests, at the commit judged: the group's member head when
|
|
// the group holds one in its repository, else a build its node runs.
|
|
type Reader struct {
|
|
Module, Commit string
|
|
Member bool
|
|
Ran Ran
|
|
}
|
|
|
|
// FromComposer is the composition check of a group with a mesh-sdk member, judged from the composer built at its head
|
|
// (ADR 0482 clauses 3 to 5, issues 1622 and 1623): the same failures as Compose, read from the composer's two cases, the
|
|
// verdict naming the mesh-sdk the composer was built with; a trunk manifest the head's mesh-sdk refuses fails too (the
|
|
// group would deliver a reader that refuses it); and each reader that refuses a changed manifest the head reads fails
|
|
// it, naming the module and its words, unless the reader's mesh-sdk has no composer to ask (before issue 1621), which is
|
|
// said and judged nothing.
|
|
func FromComposer(m Mesh, c Change, head Ran, readers []Reader, took time.Duration) delivery.CompositionVerdict {
|
|
v := delivery.CompositionVerdict{Group: c.Group, Heads: c.Heads, SDK: "mesh-sdk " + c.SDKHead, Took: took.Milliseconds()}
|
|
machines := Touched(m, c)
|
|
v.Machines = machines
|
|
var fails []string
|
|
if head.State != delivery.StepPassed {
|
|
v.Words = fmt.Sprintf("the composer at mesh-sdk %s did not run: %s %s", c.SDKHead, head.State, head.Reason)
|
|
v.State = delivery.CompositionFail
|
|
return v
|
|
}
|
|
if head.Verdict.SDK != "" {
|
|
v.SDK = head.Verdict.SDK
|
|
}
|
|
trunk, okT := caseOf(head.Verdict, CaseTrunk)
|
|
change, okC := caseOf(head.Verdict, CaseChange)
|
|
if !okT || !okC {
|
|
v.State, v.Words = delivery.CompositionFail, "the composer's verdict holds no trunk or no change case"
|
|
return v
|
|
}
|
|
failAt := func(machine, words string) {
|
|
if v.Machine == "" && machine != "" {
|
|
v.Machine = machine
|
|
}
|
|
fails = append(fails, words)
|
|
}
|
|
for _, name := range sortedKeys(c.Digests) {
|
|
if r := change.Manifests[name]; !r.Accepted {
|
|
failAt("", fmt.Sprintf("the manifest of %s cannot be read by mesh-sdk %s: %s", name, c.SDKHead, r.Refused))
|
|
}
|
|
}
|
|
for _, name := range sortedKeys(trunk.Manifests) {
|
|
if _, changed := c.Digests[name]; changed || slices.Contains(c.Removed, name) || trunk.Manifests[name].Accepted {
|
|
continue
|
|
}
|
|
if _, before := m.Unreadable[name]; before {
|
|
continue // already unreadable on the trunk: not the group's doing
|
|
}
|
|
failAt("", fmt.Sprintf("mesh-sdk %s refuses the trunk's manifest of %s, which the group does not change: %s", c.SDKHead, name, trunk.Manifests[name].Refused))
|
|
}
|
|
for _, name := range sortedKeys(c.Twice) {
|
|
failAt("", fmt.Sprintf("%s is defined twice, in %s", name, strings.Join(c.Twice[name], " and ")))
|
|
}
|
|
for _, node := range machines {
|
|
for _, name := range c.Removed {
|
|
if slices.Contains(m.Assigned[node], name) {
|
|
failAt(node, fmt.Sprintf("%s runs %s, which the change removes from its source", node, name))
|
|
}
|
|
}
|
|
before, now := trunk.Machines[node], change.Machines[node]
|
|
switch {
|
|
case now.Composes:
|
|
case !before.Composes:
|
|
v.Warnings = append(v.Warnings, fmt.Sprintf("%s does not compose on the trunk either, so the change is not blamed: %s", node, before.Reason))
|
|
default:
|
|
failAt(node, fmt.Sprintf("%s composes on the trunk and not with the change: %s", node, now.Reason))
|
|
}
|
|
}
|
|
readerFails, readerWarnings := judgeReaders(c, change, readers)
|
|
fails = append(fails, readerFails...)
|
|
v.Warnings = append(v.Warnings, readerWarnings...)
|
|
return finish(v, m, c, machines, fails)
|
|
}
|
|
|
|
// NoComposer says whether a compose step failed because the mesh-sdk it was built at has no composer command (a mesh-sdk
|
|
// from before issue 1621): Go's words for a package its module does not hold.
|
|
func NoComposer(reason string) bool {
|
|
for _, words := range []string{"cannot find package", "no required module provides package", "is not in std", "cannot find module providing package", "does not contain package"} {
|
|
if strings.Contains(reason, words) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func judgeReaders(c Change, head composer.CaseVerdict, readers []Reader) (fails, warnings []string) {
|
|
for _, r := range readers {
|
|
at := r.Module + " at " + r.Commit
|
|
if r.Member {
|
|
at = r.Module + "'s member head " + r.Commit
|
|
}
|
|
if r.Ran.State != delivery.StepPassed {
|
|
if NoComposer(r.Ran.Reason) {
|
|
warnings = append(warnings, fmt.Sprintf("not judged whether %s reads the group's manifests: the mesh-sdk it pins has no "+
|
|
"composer command (before issue 1621)", at))
|
|
continue
|
|
}
|
|
fails = append(fails, fmt.Sprintf("the composer of the mesh-sdk %s pins did not run: %s %s", at, r.Ran.State, r.Ran.Reason))
|
|
continue
|
|
}
|
|
read, ok := caseOf(r.Ran.Verdict, CaseChange)
|
|
if !ok {
|
|
fails = append(fails, fmt.Sprintf("the composer of the mesh-sdk %s pins answered no change case", at))
|
|
continue
|
|
}
|
|
for _, name := range sortedKeys(c.Digests) {
|
|
if !head.Manifests[name].Accepted || read.Manifests[name].Accepted {
|
|
continue
|
|
}
|
|
fails = append(fails, fmt.Sprintf("the manifest of %s uses what %s (mesh-sdk %s) cannot read: %s; the group must move %s onto "+
|
|
"mesh-sdk %s", name, at, r.Ran.Verdict.SDK, read.Manifests[name].Refused, r.Module, c.SDKHead))
|
|
}
|
|
}
|
|
return fails, warnings
|
|
}
|