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 // `_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: `.`. 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, .", 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, "; ") }