257 lines
10 KiB
Go
257 lines
10 KiB
Go
package inventory
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// The values of the controller's record mirrors, built from the store (novox/hq ADR 0289, design 54 §1,
|
|
// design 57 package F3).
|
|
//
|
|
// **Each value is built from a named list of fields, and nothing else** (design 54 §1): no epoch, no time
|
|
// of writing and no time of the last look, so any lease holder builds the same bytes from the same record
|
|
// and a pass over unchanged records writes nothing (ADR 0289 §1, §4). The encoding is fixed: the fields in
|
|
// their struct's order, a declaration's maps sorted.
|
|
|
|
// AssignmentMirrored is one assignment as the assignments mirror holds it: the module, the node, and since
|
|
// when it is assigned there. Design 54 §1 also lists whether it is pending, the builds sent and running and
|
|
// the seats and provisions it holds there; those join with issue 419, which builds the rest of design 54.
|
|
type AssignmentMirrored struct {
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Assigned time.Time `json:"assigned,omitzero"`
|
|
// RequiredBy is, for a module only pulled in by another and handed to the provisioner on this node (issue 765),
|
|
// the module that requires it there; such a value has no time assigned. Absent for an assignment.
|
|
RequiredBy string `json:"required-by,omitempty"`
|
|
// Provisioning is the plan of a module handed to the provisioner on this node (novox/hq ADR 0349 clause 4,
|
|
// design 57 P5): absent for every other, so their values are the bytes they were.
|
|
Provisioning *Provisioning `json:"provisioning,omitempty"`
|
|
}
|
|
|
|
// DeclaredConfigMirrored is one registered module's config declarations as the declared-config mirror holds
|
|
// them: the module, and each key its manifest declares — under `config`, or under its old name `settings` for a
|
|
// module not yet moved (ADR 0348 §1) — with that declaration's listed fields. An empty object when the module
|
|
// declares none: registered, declaring nothing.
|
|
type DeclaredConfigMirrored struct {
|
|
Module string `json:"module"`
|
|
Settings map[string]DeclaredMirrored `json:"settings"`
|
|
}
|
|
|
|
// DeclaredMirrored is one key's declaration as the mirror holds it: **its listed fields and no other**
|
|
// (ADR 0289 §12), and every field a declaration has (catalogue.SettingDeclaration), under the same names. The
|
|
// mirror holds what the module's manifest declares about its config values, and mesh-config validates and
|
|
// resolves from it (ADR 0356 §4, ADR 0348 §4, §9), so it carries the whole declaration: kind, default and why
|
|
// of the settings form, and type, description, validation, trusted, predefined, required and runtime of the
|
|
// config form. None is a secret: a secret lives in the vault, never in config, and the catalogue refuses a
|
|
// default for a key whose name says secret. A field the manifest gains reaches the bus only once it is added
|
|
// here, deliberately: a test fails until it is listed. The config form's fields are left out when unset, so a
|
|
// module not yet moved is mirrored as it was before they were carried.
|
|
type DeclaredMirrored struct {
|
|
Kind string `json:"kind"`
|
|
Default json.RawMessage `json:"default"`
|
|
Why string `json:"why"`
|
|
Type string `json:"type,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Enum json.RawMessage `json:"enum,omitempty"`
|
|
Minimum *float64 `json:"minimum,omitempty"`
|
|
Maximum *float64 `json:"maximum,omitempty"`
|
|
Pattern string `json:"pattern,omitempty"`
|
|
Trusted bool `json:"trusted,omitempty"`
|
|
Predefined string `json:"predefined,omitempty"`
|
|
Required bool `json:"required,omitempty"`
|
|
Runtime bool `json:"runtime,omitempty"`
|
|
}
|
|
|
|
// DeclaredMirroredFields are the fields of a declaration the mirror carries, by their manifest names.
|
|
var DeclaredMirroredFields = []string{"kind", "default", "why", "type", "description", "enum", "minimum", "maximum",
|
|
"pattern", "trusted", "predefined", "required", "runtime"}
|
|
|
|
// assignmentsMirrored is every standing assignment as the provisioner's plans are matched to it (SentProvisioningPlans,
|
|
// KeepAssignmentPlans).
|
|
func (i *Inventory) assignmentsMirrored(ctx context.Context) ([]AssignmentMirrored, error) {
|
|
rows, err := i.assignments(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]AssignmentMirrored, 0, len(rows))
|
|
for _, a := range rows {
|
|
out = append(out, AssignmentMirrored{Node: a.Node, Module: a.Module, Assigned: a.Assigned.UTC()})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// DeclaredConfigMirror is every registered module's mirrored config declarations, keyed by the module.
|
|
func (i *Inventory) DeclaredConfigMirror(ctx context.Context) (map[string][]byte, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select name, coalesce(manifest->'config', manifest->'settings', '{}'::jsonb) from module`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string][]byte{}
|
|
for rows.Next() {
|
|
var (
|
|
d DeclaredConfigMirrored
|
|
raw []byte
|
|
)
|
|
if err := rows.Scan(&d.Module, &raw); err != nil {
|
|
return nil, err
|
|
}
|
|
if d.Settings, err = declaredMirrored(raw); err != nil {
|
|
return nil, fmt.Errorf("the declarations of %s cannot be read: %w", d.Module, err)
|
|
}
|
|
value, err := json.Marshal(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out[d.Module] = value
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// declaredMirrored is a manifest's config declarations, keyed by the value, as the mirror holds them: each
|
|
// with its listed fields, a default and an enum encoded the one way.
|
|
func declaredMirrored(raw []byte) (map[string]DeclaredMirrored, error) {
|
|
var declared map[string]DeclaredMirrored
|
|
if err := json.Unmarshal(raw, &declared); err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]DeclaredMirrored{}
|
|
for key, decl := range declared {
|
|
var err error
|
|
if len(decl.Default) > 0 {
|
|
if decl.Default, err = canonical(decl.Default); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if len(decl.Enum) > 0 {
|
|
if decl.Enum, err = canonical(decl.Enum); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
out[key] = decl
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// canonical is a JSON document encoded the one way: objects' keys sorted, numbers as written.
|
|
func canonical(raw []byte) ([]byte, error) {
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.UseNumber()
|
|
var v any
|
|
if err := dec.Decode(&v); err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
// PredefinedMirrorKey is the predefined-keys mirror's one key: the list kept is one record.
|
|
const PredefinedMirrorKey = "list"
|
|
|
|
// PredefinedMirrored is the list of predefined keys kept as its mirror holds it (design 57 K3, issue 662): the
|
|
// keys, canonical, and the trunk build of the catalogue they came from, by its commit and generation, so a reader
|
|
// says which list it holds. Not when it was kept: any lease holder builds the same bytes from the same record.
|
|
type PredefinedMirrored struct {
|
|
Keys json.RawMessage `json:"keys"`
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
Generation int `json:"generation"`
|
|
Build string `json:"build"`
|
|
}
|
|
|
|
// PredefinedMirror is the list of predefined keys kept, under PredefinedMirrorKey; no key while none is kept.
|
|
func (i *Inventory) PredefinedMirror(ctx context.Context) (map[string][]byte, error) {
|
|
kept, has, err := i.PredefinedKept(ctx)
|
|
if err != nil || !has {
|
|
return map[string][]byte{}, err
|
|
}
|
|
keys, err := kept.Keys.Canonical()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
value, err := json.Marshal(PredefinedMirrored{Keys: keys, Repository: kept.Repository, Commit: kept.Commit,
|
|
Generation: kept.Generation, Build: kept.Build})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string][]byte{PredefinedMirrorKey: value}, nil
|
|
}
|
|
|
|
// DeclaredOf is a manifest's config declarations as the mirror holds them (DeclaredMirrored), so a pull request's
|
|
// declarations are told to mesh-config in the one shape it reads the trunk's in (novox/hq ADR 0502 clause 2).
|
|
func DeclaredOf(settings map[string]catalogue.SettingDeclaration) (map[string]DeclaredMirrored, error) {
|
|
raw, err := json.Marshal(settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return declaredMirrored(raw)
|
|
}
|
|
|
|
// PackageDeclared is one package a node's modules declare, as the declared-packages mirror holds it (ADR 0507 §5): its
|
|
// name, the modules that declare it that way, sorted, and whether they declare it absent. A package some modules declare
|
|
// present and others absent is two entries; the composition refuses that pair, so it is seen only while it is refused.
|
|
type PackageDeclared struct {
|
|
Package string `json:"package"`
|
|
Modules []string `json:"modules"`
|
|
Absent bool `json:"absent"`
|
|
}
|
|
|
|
// DeclaredPackagesMirrored is one node's value of the declared-packages mirror, keyed by the node: its packages sorted by
|
|
// name (absent after present). Fixed fields and no time, so the same declaration is the same bytes (ADR 0289 §1).
|
|
type DeclaredPackagesMirrored struct {
|
|
Node string `json:"node"`
|
|
Packages []PackageDeclared `json:"packages"`
|
|
}
|
|
|
|
// DeclaredPackagesValue is the mirror's value for one node, from the resources its composition declares and the module that
|
|
// owns each (Composed.Owner, by resource id). A resource that is no package, or names none, is left out.
|
|
func DeclaredPackagesValue(node string, resources []map[string]any, owner map[string]string) ([]byte, error) {
|
|
type key struct {
|
|
name string
|
|
absent bool
|
|
}
|
|
by := map[key]map[string]bool{}
|
|
for _, r := range resources {
|
|
if fmt.Sprint(r["type"]) != "package" {
|
|
continue
|
|
}
|
|
name, _ := r["package"].(string)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
k := key{name, r["absent"] == true}
|
|
if by[k] == nil {
|
|
by[k] = map[string]bool{}
|
|
}
|
|
if module := owner[fmt.Sprint(r["id"])]; module != "" {
|
|
by[k][module] = true
|
|
}
|
|
}
|
|
keys := make([]key, 0, len(by))
|
|
for k := range by {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
if keys[i].name != keys[j].name {
|
|
return keys[i].name < keys[j].name
|
|
}
|
|
return !keys[i].absent && keys[j].absent
|
|
})
|
|
v := DeclaredPackagesMirrored{Node: node, Packages: []PackageDeclared{}}
|
|
for _, k := range keys {
|
|
modules := make([]string, 0, len(by[k]))
|
|
for m := range by[k] {
|
|
modules = append(modules, m)
|
|
}
|
|
sort.Strings(modules)
|
|
v.Packages = append(v.Packages, PackageDeclared{Package: k.name, Modules: modules, Absent: k.absent})
|
|
}
|
|
return json.Marshal(v)
|
|
}
|