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}) }) }