package main import ( "context" "encoding/json" "errors" "fmt" "slices" "sort" "strings" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-sdk/go/asks" sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // The operator's Approve of a delivery group registers a moved module from its new repository (novox/hq ADR 0420 // §7, issue 1025). ADR 0266 §44 lets a build's outcome register a known module only from the repository the // catalogue already builds it from. The one exception was a build asked at the controller's terminal. This is a // second, for a move: mesh-delivery announces each merge it makes with the ask whose Approve it merged on; the // controller keeps the announcement, and when the build of a merge of a known module comes from another repository // it reads that ask's warrant from the router and accepts only the operator's Approve of the merged head. A verb // alone, or a merge with no such warrant, never moves a module. // theDelivery is the module that merges, and whose asks these are. const theDelivery = "mesh-delivery" // optionApprove is the option of mesh-delivery's ask that merges (its OptionApprove). const optionApprove = "approve" // deliveryMerge is mesh-delivery's announcement of a merge it made: the pull request and the head it merged, the // group it belongs to, and the ask whose answer it merged on with the option chosen and the heads that answer was // bound to (delivery id → commit). type deliveryMerge struct { Owner string `json:"owner"` Repo string `json:"repo"` Number int `json:"number"` HeadSHA string `json:"head_sha"` Group string `json:"group,omitempty"` AskID string `json:"ask_id"` Option string `json:"option"` Heads map[string]string `json:"heads"` // Asker is the module that asked the ask, whose warrant is read from the router under that name (novox/hq ADR // 0437 §6): empty is today's module, mesh-delivery. Asker string `json:"asker,omitempty"` } // askerOf is the asker whose warrant the merge is read under: the one the announcement names, mesh-delivery when it // names none. func (d deliveryMerge) askerOf() string { if d.Asker == "" { return theDelivery } return d.Asker } // askerAccepted is whether the controller takes a warrant from this asker for a merge: the teller, and mesh-delivery // until the gate takes the seat over (novox/hq ADR 0437 §6). Any other registers no move. func askerAccepted(asker string) error { if asker == theDelivery || asker == sdkdelivery.TellerModule { return nil } return fmt.Errorf("the merge names the asker %q, and the controller takes a warrant only from %s and %s", asker, sdkdelivery.TellerModule, theDelivery) } // parseDeliveryMerge reads an announcement, refusing one that names no pull request, head or ask. func parseDeliveryMerge(body []byte) (deliveryMerge, error) { var d deliveryMerge if err := json.Unmarshal(body, &d); err != nil { return d, fmt.Errorf("mesh-delivery's word on a merge cannot be read: %w", err) } if d.Owner == "" || d.Repo == "" || d.Number <= 0 || d.HeadSHA == "" || d.AskID == "" || d.Option == "" { return d, errors.New("mesh-delivery's word on a merge names no pull request, head, ask or option") } return d, nil } // deliveryMergeKey is where a merge is kept: bus keys allow no `#` or `@`, so the pull request and its head are // path-like tokens, as the forge spells the repository. func deliveryMergeKey(owner, repo string, number int, head string) string { return fmt.Sprintf("%s/%s/%d/%s", owner, repo, number, head) } // mergeAct is what mesh-delivery's answer binds, spelled exactly as mesh-delivery spells it (its approval.go: // mergeAct and headsString): this ask, the option and the heads it covers. func mergeAct(ask, option string, heads map[string]string) asks.Act { var out []string for id, sha := range heads { out = append(out, id+"="+sha) } sort.Strings(out) return asks.Act{"verb": theDelivery + ".merge", "ask": ask, "option": option, "heads": strings.Join(out, ",")} } // movedOnApprove judges the warrant of the ask a merge was made on: it holds only if it is the operator's Approve, // proved by the channel it came through, of this very ask as it was published (digest), bound to the heads the // announcement names, and those heads hold the head merged. Answers why it holds, or why not. func movedOnApprove(d deliveryMerge, ask *asks.Ask, w *asks.Warrant) (string, error) { asker := d.askerOf() if err := askerAccepted(asker); err != nil { return "", err } switch { case ask == nil || w == nil: return "", errors.New("the router holds no closed record of the ask the merge was made on") case ask.Who != asks.Operator: return "", fmt.Errorf("the ask %s is for %q to answer, not the operator", ask.ID, ask.Who) case d.Option != optionApprove: return "", fmt.Errorf("the merge was made on the option %q, not an Approve", d.Option) } option, err := w.For(asker, *ask) if err != nil { return "", err } if option.ID != optionApprove || w.Option != optionApprove || d.Option != option.ID { return "", fmt.Errorf("the operator chose %q on the ask %s, not an Approve", w.Option, w.Ask) } if w.By == nil || w.By.Who != asks.Operator || w.By.Verified == "" { return "", fmt.Errorf("the ask %s was not answered by the operator on a channel that proves who answered", w.Ask) } if err := option.Performs(mergeAct(d.AskID, option.ID, d.Heads)); err != nil { return "", err } named := false for _, sha := range d.Heads { named = named || sha == d.HeadSHA } if !named { return "", fmt.Errorf("the Approve covers %v, and the head merged, %.8s, is not among them", d.Heads, d.HeadSHA) } return w.Says() + " (ask " + w.Ask + ")", nil } // ---- keeping what mesh-delivery announced ---------------------------------------------------------------------- // keepingDeliveryMerges keeps each merge mesh-delivery announces in the controller's bucket for them. type keepingDeliveryMerges struct { js func() *broker.JetStream // catalog is the catalogue, whose modules claiming the delivery seat are the only ones whose word on a merge is // kept (novox/hq ADR 0437 §3). catalog func(ctx context.Context) ([]inventory.Entry, error) // kept, when set, is told each merge once it is kept: a first merge waiting for the word asks its builds then // (novox/hq ADR 0432, issue 1146). kept func(ctx context.Context, d deliveryMerge) error } func (k keepingDeliveryMerges) KeepDeliveryMerge(ctx context.Context, emitter string, body []byte) error { if k.catalog == nil { return fmt.Errorf("%w: the catalogue is not read", link.ErrTryAgain) } entries, err := k.catalog(ctx) if err != nil { return fmt.Errorf("%w: %w", link.ErrTryAgain, err) } if !slices.Contains(claimantsOf(entries, catalogue.DeliverySeat), emitter) { return fmt.Errorf("%w: %q is not among %v", link.ErrNotTheDeliverysWord, emitter, claimantsOf(entries, catalogue.DeliverySeat)) } d, err := parseDeliveryMerge(body) if err != nil { return err } js := k.js() if js == nil { return fmt.Errorf("%w: the bus is not held", link.ErrTryAgain) } api, err := jetstream.New(js.Conn()) if err != nil { return fmt.Errorf("%w: %w", link.ErrTryAgain, err) } kv, err := api.KeyValue(ctx, broker.DeliveryMergesBucket) if err != nil { return fmt.Errorf("%w: the bucket of mesh-delivery's merges: %w", link.ErrTryAgain, err) } if _, err := kv.Put(ctx, deliveryMergeKey(d.Owner, d.Repo, d.Number, d.HeadSHA), body); err != nil { return fmt.Errorf("%w: %w", link.ErrTryAgain, err) } if k.kept != nil { return k.kept(ctx, d) } return nil } // keptDeliveryMerge is the merge mesh-delivery announced for a pull request's head; false when none was kept. // A variable so a test can hand it one. var keptDeliveryMerge = func(ctx context.Context, owner, repo string, number int, head string) (deliveryMerge, bool, error) { serving := servingBus.Load() if serving == nil { return deliveryMerge{}, false, errors.New("only the serving controller keeps what mesh-delivery announced") } api, err := jetstream.New(serving.Conn()) if err != nil { return deliveryMerge{}, false, err } kv, err := api.KeyValue(ctx, broker.DeliveryMergesBucket) if err != nil { return deliveryMerge{}, false, err } e, err := kv.Get(ctx, deliveryMergeKey(owner, repo, number, head)) if errors.Is(err, jetstream.ErrKeyNotFound) { return deliveryMerge{}, false, nil } if err != nil { return deliveryMerge{}, false, err } d, err := parseDeliveryMerge(e.Value()) return d, err == nil, err } // routerAskOf is the router's record of one of the delivery's asks, under its asker: the ask as published and its warrant. A // variable so a test can hand it one. var routerAskOf = func(ctx context.Context, inv catalogueReader, asker, id string) (*asks.Ask, *asks.Warrant, error) { serving := servingBus.Load() if serving == nil { return nil, nil, errors.New("only the serving controller reads the router's record") } bucket, err := asksRecords(ctx, inv) if err != nil { return nil, nil, err } if bucket == "" { return nil, nil, errors.New("no module declares the operator channel's records") } _, ask, w, err := readRouterEntry(ctx, serving.Conn(), bucket, asker, id) return ask, w, err } // readRouterEntry reads the router's record of one asker's ask: its state, the ask as published, and its warrant; // "" when there is none. The controller's grant reaches the JetStream API whole (`$JS.API.>`), so it reads any // asker's record. func readRouterEntry(ctx context.Context, conn *nats.Conn, bucket, asker, ask string) (string, *asks.Ask, *asks.Warrant, error) { reply, err := conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+asker+"."+ask, nil) if err != nil { return "", nil, nil, err } if status := reply.Header.Get("Status"); status != "" { if status == "404" { return "", nil, nil, nil } return "", nil, nil, fmt.Errorf("the router's record could not be read: %s %s", status, reply.Header.Get("Description")) } var rec struct { State string `json:"state"` Ask *asks.Ask `json:"ask"` Warrant *asks.Warrant `json:"warrant"` } if err := json.Unmarshal(reply.Data, &rec); err != nil { return "", nil, nil, fmt.Errorf("the router's record of %s's ask %s cannot be read: %w", asker, ask, err) } return rec.State, rec.Ask, rec.Warrant, nil } // errNotAMerge says a build was not asked for a merge, so there is no move to look for. var errNotAMerge = errors.New("the build is not of a merge") // approvedMove says whether a build of a known module from another repository is the move the operator approved // (ADR 0420 §7): the build is of a merge, that merge is a pull request mesh-delivery announced it made, and the // ask it made it on holds an Approve by the operator of that head. Answers why it was accepted; an error says why // it was not, and the refusal of ADR 0266 stands. func approvedMove(ctx context.Context, inv registrations, forms *sourceForms, buildID string, built inventory.Source) (string, error) { r, found, err := inv.BuildRequestByID(ctx, buildID) if err != nil { return "", err } if !found || r.For != "merge" || r.Commit == "" { return "", errNotAMerge } owner, name, onForge := forms.onTheForge(built.Repository, built.Seat) if !onForge { return "", errors.New("the repository is not on the mesh's forge") } merged, known, err := inv.MergeOf(ctx, owner+"/"+name, r.Commit) if err != nil { return "", err } if !known { return "", fmt.Errorf("the controller kept no merge of %s/%s at %.8s", owner, name, r.Commit) } return approvedAnnounced(ctx, inv, owner, name, announcedOf(merged)) } // errWordNotAnApprove says the merge cannot be shown to be the operator's Approve of its head: its announcement names no // pull request, or the word on record is not that Approve. var errWordNotAnApprove = errors.New("the word on record is no Approve of the head merged") // errNotAnnounced says mesh-delivery's word on a merge is not kept (yet): the merge may have been made by someone else, // or its word may still be on its way. var errNotAnnounced = errors.New("mesh-delivery announced no such merge") // approvedAnnounced is the warrant of the merge the forge announced as k: mesh-delivery announced it, and the ask it made // it on holds the operator's Approve of that head. Why it holds, or why not. Shared by the approved move of a known // module and the first build of a new repository's modules (ADR 0420 §7, ADR 0432). func approvedAnnounced(ctx context.Context, inv catalogueReader, owner, name string, k keptMerge) (string, error) { if k.Number <= 0 || k.HeadSHA == "" { return "", fmt.Errorf("%w: the forge's announcement of the merge names no pull request or head", errWordNotAnApprove) } d, kept, err := keptDeliveryMerge(ctx, owner, name, k.Number, k.HeadSHA) if err != nil { return "", err } if !kept { return "", fmt.Errorf("%w: %s/%s#%d at %.8s", errNotAnnounced, owner, name, k.Number, k.HeadSHA) } // The warrant is read under the asker the announcement names, and under none the controller does not take. if err := askerAccepted(d.askerOf()); err != nil { return "", fmt.Errorf("%w: %w", errWordNotAnApprove, err) } ask, w, err := routerAskOf(ctx, inv, d.askerOf(), d.AskID) if err != nil { return "", err } why, err := movedOnApprove(d, ask, w) if err != nil { return "", fmt.Errorf("%w: %w", errWordNotAnApprove, err) } return fmt.Sprintf("%s/%s#%d merged at %.8s on %s", owner, name, k.Number, k.HeadSHA, why), nil }