146 lines
5.3 KiB
Go
146 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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})
|
|
})
|
|
}
|