mesh/check failed: integration: github.com/novox/mesh-controller/internal/identity.TestACertificateChainsToTheMeshsOwnAuthority: authority_test.go:45:…
mesh/repo-check head 4357b3688f58 merged into trunk 3d4ad6fefb7c — pass: its merge-check.sh passed
mesh/merge-gate head 4357b3688f58 merged into trunk 3d4ad6fefb7c — pass: group fix/1826-held-config composed: every machine composes with the change as it …
mesh/delivery delivered
mesh/delivery-group group fix/1826-held-config delivered: every member is delivered
212 lines
7.7 KiB
Go
212 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"reflect"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// What the controller tells mesh-config of an open pull request, and asks it back (novox/hq ADR 0502 clauses 2 and 4,
|
|
// issue 1826): a config key a pull request introduces is not declared on the trunk, so a value for it can be judged
|
|
// only against the pull request's own declaration, which the core reads from the forge and mesh-config never does
|
|
// (ADR 0495 §1). The call is mesh-config's `config` verb in the controller's own name, actions `pull-declares` and
|
|
// `held`; the shapes are those of the interface the two sides agreed.
|
|
|
|
// meshConfigWait is how long the controller waits for mesh-config before it goes on without: a check is never held up
|
|
// by a module that does not answer.
|
|
const meshConfigWait = 10 * time.Second
|
|
|
|
// meshConfigAsk is one call of mesh-config's `config` verb in the controller's own name, answering its result. A
|
|
// variable so a test needs no bus.
|
|
var meshConfigAsk = func(ctx context.Context, args map[string]any) ([]byte, error) {
|
|
var result []byte
|
|
err := onTheBus(func(conn *nats.Conn) error {
|
|
answer, err := link.AskMeshSeatToolAs(ctx, conn, broker.ControllerName, "mesh-config", "config", args, meshConfigWait)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if answer.Error != "" {
|
|
return fmt.Errorf("%s", answer.Error)
|
|
}
|
|
result = answer.Result
|
|
return nil
|
|
})
|
|
return result, err
|
|
}
|
|
|
|
// pullAt is an open pull request at a head: the change to read, and its number.
|
|
type pullAt struct {
|
|
changeSource
|
|
Number int
|
|
}
|
|
|
|
// pullAtOf is a pull request announced, as the change it makes over what it merges into.
|
|
func pullAtOf(p link.PullUpdated) pullAt {
|
|
old := p.BaseSHA
|
|
if old == "" {
|
|
old = p.Base
|
|
}
|
|
return pullAt{Number: p.Number, changeSource: changeSource{Owner: p.Owner, Repo: p.Repo, New: p.Commit, Old: old,
|
|
Paths: p.Paths, Truncated: p.PathsTruncated, Removed: p.Removed}}
|
|
}
|
|
|
|
func pullKey(owner, repo string, number int) string {
|
|
return fmt.Sprintf("%s/%s#%d", owner, repo, number)
|
|
}
|
|
|
|
func (p pullAt) key() string { return pullKey(p.Owner, p.Repo, p.Number) }
|
|
|
|
// pullDeclarations are the config declarations a pull request's head adds or changes over what it merges into, by
|
|
// module, in the shape of the declared-config mirror. The bool says the head could be read: false for a pull request whose
|
|
// paths are cut (the caller cuts them to the manifests of the modules it moves) or whose base is unknown, which is then
|
|
// told nothing, never an empty set.
|
|
func pullDeclarations(ctx context.Context, p pullAt) ([]inventory.DeclaredConfigMirrored, bool, error) {
|
|
var out []inventory.DeclaredConfigMirrored
|
|
if p.Truncated || p.Old == "" {
|
|
return nil, false, nil
|
|
}
|
|
for _, file := range p.Paths {
|
|
if path.Base(file) != moduleManifestFile || slices.Contains(p.Removed, file) {
|
|
continue
|
|
}
|
|
now, exists, err := manifestCached(ctx, p.Owner, p.Repo, file, p.New)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if !exists || now.Module == "" {
|
|
continue
|
|
}
|
|
was, _, err := manifestCached(ctx, p.Owner, p.Repo, file, p.Old)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
have, err := inventory.DeclaredOf(now.Settings)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("%s at %.8s: %w", file, p.New, err)
|
|
}
|
|
had, err := inventory.DeclaredOf(was.Settings)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("%s at %.8s: %w", file, p.Old, err)
|
|
}
|
|
changed := map[string]inventory.DeclaredMirrored{}
|
|
for key, d := range have {
|
|
if before, there := had[key]; !there || !reflect.DeepEqual(before, d) {
|
|
changed[key] = d
|
|
}
|
|
}
|
|
if len(changed) > 0 {
|
|
out = append(out, inventory.DeclaredConfigMirrored{Module: now.Module, Settings: changed})
|
|
}
|
|
}
|
|
return out, true, nil
|
|
}
|
|
|
|
// tellPullDeclares tells mesh-config, for each pull request at every announced head, what the head declares, an empty set
|
|
// when it declares nothing new: mesh-config re-judges the values held at every head, so a head that reverts the change
|
|
// drops them (ADR 0502 clause 6). A
|
|
// failure is said to w and the check goes on: the held values are then none (ADR 0502 clause 2).
|
|
func tellPullDeclares(ctx context.Context, w io.Writer, pulls []pullAt) {
|
|
for _, p := range pulls {
|
|
mods, readable, err := pullDeclarations(ctx, p)
|
|
if err != nil {
|
|
fmt.Fprintf(w, "%s: what its head declares cannot be read, so mesh-config is told nothing: %v\n", p.key(), err)
|
|
continue
|
|
}
|
|
if !readable {
|
|
continue
|
|
}
|
|
declares := map[string]map[string]inventory.DeclaredMirrored{}
|
|
for _, m := range mods {
|
|
declares[m.Module] = m.Settings
|
|
}
|
|
if _, err := meshConfigAsk(ctx, map[string]any{"action": "pull-declares", "pull": p.key(), "head": p.New,
|
|
"declares": declares}); err != nil {
|
|
fmt.Fprintf(w, "%s: mesh-config cannot be told what its head declares (%v); the check goes on without held values\n",
|
|
p.key(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// heldFor asks mesh-config for the approved held values of these pull requests at their heads (ADR 0502 clause 4). Only
|
|
// those of the pull requests asked about are carried. A failure is said to w and answers none.
|
|
func heldFor(ctx context.Context, w io.Writer, pulls []pullAt) []link.HeldConfig {
|
|
if len(pulls) == 0 {
|
|
return nil
|
|
}
|
|
type pullHead struct {
|
|
Pull string `json:"pull"`
|
|
Head string `json:"head"`
|
|
}
|
|
asked := make([]pullHead, 0, len(pulls))
|
|
for _, p := range pulls {
|
|
asked = append(asked, pullHead{p.key(), p.New})
|
|
}
|
|
raw, err := meshConfigAsk(ctx, map[string]any{"action": "held", "pulls": asked})
|
|
if err != nil {
|
|
fmt.Fprintf(w, "mesh-config cannot say what it holds for these pull requests (%v); the check goes on without held values\n", err)
|
|
return nil // empty-on-error: said above; a check without held values fails for want of the value and says so, it never passes on a guess
|
|
}
|
|
var answer struct {
|
|
Held []link.HeldConfig `json:"held"`
|
|
Skipped []struct {
|
|
Pull string `json:"pull"`
|
|
Why string `json:"why"`
|
|
} `json:"skipped"`
|
|
}
|
|
if len(raw) == 0 {
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(raw, &answer); err != nil {
|
|
fmt.Fprintf(w, "mesh-config's held values cannot be read (%v); the check goes on without them\n", err)
|
|
return nil // empty-on-error: said above; a check without held values fails for want of the value and says so, it never passes on a guess
|
|
}
|
|
for _, s := range answer.Skipped {
|
|
fmt.Fprintf(w, "%s: mesh-config holds a value it does not carry: %s\n", s.Pull, s.Why)
|
|
}
|
|
var out []link.HeldConfig
|
|
for _, h := range answer.Held {
|
|
for _, p := range pulls {
|
|
if h.Pull == p.key() {
|
|
out = append(out, h)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// withHeldConfig tells mesh-config what these pull requests declare and puts the approved held values of them on the
|
|
// check request (ADR 0502 clause 4). With carry false, a member of a group whose composed check composes, only the
|
|
// declarations are told.
|
|
func withHeldConfig(ctx context.Context, w io.Writer, request *link.BuildRequest, pulls []pullAt, carry bool) {
|
|
tellPullDeclares(ctx, w, pulls)
|
|
if carry {
|
|
carryHeld(ctx, w, request, pulls)
|
|
}
|
|
}
|
|
|
|
// carryHeld puts the approved held values of these pull requests on the check request.
|
|
func carryHeld(ctx context.Context, w io.Writer, request *link.BuildRequest, pulls []pullAt) {
|
|
if request.Check != nil {
|
|
request.Check.HeldConfig = heldFor(ctx, w, pulls)
|
|
}
|
|
}
|
|
|
|
// holdsModules is whether a module of the mesh is built from this repository.
|
|
func holdsModules(entries []inventory.Entry, owner, repo string) bool {
|
|
return slices.ContainsFunc(entries, func(e inventory.Entry) bool {
|
|
return sameRepository(e.Source.Repository, link.SourceMoved{Owner: owner, Repo: repo})
|
|
})
|
|
}
|