Carry manifests and lock hashes in the forge messages, keep a module's new-form inputs, and lay out the build modules' toolchain keys (design 47 R4c-1, issues 1442, 1443)
The indexer needs a module's manifest, lock hashes and toolchain to compute its new-form fingerprint, and has none of them (ADR 0465 clauses 1, 2, 5): the forge messages now carry the first two, a module keeps its inputs so it can be computed again when a toolchain is reported, and the build modules' toolchain keys have a layout and an agreement rule.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ToolchainsState is the name of the state a build module keeps its toolchains in; its bucket is
|
||||
// `<module>_toolchains`, built as the SDK names a module's state, by ToolchainsBucket (novox/hq ADR 0465 clause 2).
|
||||
const ToolchainsState = "toolchains"
|
||||
|
||||
// ToolchainsBucket is the bucket of a build module's toolchains.
|
||||
func ToolchainsBucket(module string) string { return module + "_" + ToolchainsState }
|
||||
|
||||
// ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain.
|
||||
const ToolchainNoReport = "toolchain not yet reported"
|
||||
|
||||
func checkToolchainLanguage(language string) error {
|
||||
switch language {
|
||||
case "go", "typescript":
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("delivery: the toolchain language %q is not go or typescript", language)
|
||||
}
|
||||
|
||||
// ToolchainKey is the key of a node's toolchain of one language: `<node>.<language>`. It refuses a node that is not a
|
||||
// name and a language other than go or typescript.
|
||||
func ToolchainKey(node, language string) (string, error) {
|
||||
if err := checkName("node", node); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := checkToolchainLanguage(language); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return node + "." + language, nil
|
||||
}
|
||||
|
||||
// ParseToolchainKey is the inverse of ToolchainKey.
|
||||
func ParseToolchainKey(key string) (node, language string, err error) {
|
||||
node, language, ok := strings.Cut(key, ".")
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("delivery: %q is not a toolchain key, <node>.<language>", key)
|
||||
}
|
||||
if _, err := ToolchainKey(node, language); err != nil {
|
||||
return "", "", fmt.Errorf("delivery: %q is not a toolchain key: %w", key, err)
|
||||
}
|
||||
return node, language, nil
|
||||
}
|
||||
|
||||
// Toolchain is the value of a toolchains key: the version of the language's toolchain installed on the node and the
|
||||
// build module's recipe version, and when the module saw them.
|
||||
type Toolchain struct {
|
||||
V int `json:"v"`
|
||||
Language string `json:"language"`
|
||||
Version string `json:"version"`
|
||||
Recipe string `json:"recipe"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
func (t Toolchain) check() error {
|
||||
if err := checkToolchainLanguage(t.Language); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Version == "" || t.Recipe == "" {
|
||||
return fmt.Errorf("delivery: a %s toolchain needs its version and its recipe", t.Language)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeToolchain is t as JSON at Version; it refuses a language other than go or typescript and an empty version or
|
||||
// recipe.
|
||||
func EncodeToolchain(t Toolchain) ([]byte, error) {
|
||||
if err := t.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.V = Version
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
// DecodeToolchain reads a toolchains value; it refuses an unknown version (ErrUnknownVersion) and what EncodeToolchain
|
||||
// refuses.
|
||||
func DecodeToolchain(data []byte) (Toolchain, error) {
|
||||
var t Toolchain
|
||||
if err := decode(data, &t); err != nil {
|
||||
return Toolchain{}, err
|
||||
}
|
||||
if err := t.check(); err != nil {
|
||||
return Toolchain{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// NodeToolchain is one node's entry of a language, as read from the toolchains bucket.
|
||||
type NodeToolchain struct {
|
||||
Node string
|
||||
Toolchain
|
||||
}
|
||||
|
||||
// AgreedToolchain is the one Toolchain2 every node's entry of a language agrees on, or none and why: "toolchain not
|
||||
// yet reported" with no entries, and "builders disagree" naming each differing version and recipe with the nodes
|
||||
// reporting it when two differ (novox/hq ADR 0405 clause 2). The answer does not depend on the entries' order. The
|
||||
// entries must all be of one language.
|
||||
func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) {
|
||||
if len(entries) == 0 {
|
||||
return Toolchain2{}, ToolchainNoReport
|
||||
}
|
||||
groups := map[Toolchain2][]string{}
|
||||
for _, e := range entries {
|
||||
k := Toolchain2{Language: e.Language, Version: e.Version, Recipe: e.Recipe}
|
||||
groups[k] = append(groups[k], e.Node)
|
||||
}
|
||||
if len(groups) == 1 {
|
||||
for k := range groups {
|
||||
return k, ""
|
||||
}
|
||||
}
|
||||
parts := make([]string, 0, len(groups))
|
||||
for k, nodes := range groups {
|
||||
sort.Strings(nodes)
|
||||
parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", ")))
|
||||
}
|
||||
sort.Strings(parts)
|
||||
return Toolchain2{}, "builders disagree: " + strings.Join(parts, "; ")
|
||||
}
|
||||
Reference in New Issue
Block a user