268 lines
9.0 KiB
Go
268 lines
9.0 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// Joining modules' numbers, library groups, taken libraries and the libraries themselves (novox/hq ADR 0328 §1, §3,
|
|
// §4, §7, §15; migration 0096).
|
|
|
|
// AllocateIdentity is a joining module's number, allocated now when it has none: one number from the mesh's range,
|
|
// mesh-wide, kept for ever, its user's and its own group's. Two controllers allocating at once agree: the second's
|
|
// insert finds the first's row.
|
|
func (i *Inventory) AllocateIdentity(ctx context.Context, module string) (catalogue.ModuleIdentity, error) {
|
|
if strings.TrimSpace(module) == "" {
|
|
return catalogue.ModuleIdentity{}, errors.New("a number is allocated for a module, and none was named")
|
|
}
|
|
if id, had, err := i.IdentityOf(ctx, module); err != nil || had {
|
|
return id, err
|
|
}
|
|
// Drawn only when the row is new; a draw lost to a concurrent insert is a gap in the range, never a number given
|
|
// twice.
|
|
if _, err := i.store.Pool().Exec(ctx,
|
|
`insert into module_identity (module, uid) select $1, nextval('mesh_number')::integer
|
|
on conflict (module) do nothing`, module); err != nil {
|
|
return catalogue.ModuleIdentity{}, fmt.Errorf("%s could not be given its number: %w", module, err)
|
|
}
|
|
id, had, err := i.IdentityOf(ctx, module)
|
|
if err == nil && !had {
|
|
err = fmt.Errorf("%s was given its number and it cannot be read back", module)
|
|
}
|
|
return id, err
|
|
}
|
|
|
|
// IdentityOf is a module's number, and false when it was never allocated one.
|
|
func (i *Inventory) IdentityOf(ctx context.Context, module string) (catalogue.ModuleIdentity, bool, error) {
|
|
id := catalogue.ModuleIdentity{Module: module}
|
|
err := i.store.Pool().QueryRow(ctx, `select uid from module_identity where module = $1`, module).Scan(&id.UID)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return catalogue.ModuleIdentity{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return catalogue.ModuleIdentity{}, false, err
|
|
}
|
|
return id, true, nil
|
|
}
|
|
|
|
// Identities is every joining module's number, by module.
|
|
func (i *Inventory) Identities(ctx context.Context) (map[string]catalogue.ModuleIdentity, error) {
|
|
rows, err := i.store.Pool().Query(ctx, `select module, uid from module_identity`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string]catalogue.ModuleIdentity{}
|
|
for rows.Next() {
|
|
var id catalogue.ModuleIdentity
|
|
if err := rows.Scan(&id.Module, &id.UID); err != nil {
|
|
return nil, err
|
|
}
|
|
out[id.Module] = id
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// AllocateLibraryGroup is a library's group number, allocated now when it has none, from the same sequence as the
|
|
// modules' numbers (§3).
|
|
func (i *Inventory) AllocateLibraryGroup(ctx context.Context, library string) (int, error) {
|
|
if strings.TrimSpace(library) == "" {
|
|
return 0, errors.New("a group number is allocated for a library, and none was named")
|
|
}
|
|
groups, err := i.LibraryGroups(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if gid, had := groups[library]; had {
|
|
return gid, nil
|
|
}
|
|
if _, err := i.store.Pool().Exec(ctx,
|
|
`insert into library_group (library, gid) select $1, nextval('mesh_number')::integer
|
|
on conflict (library) do nothing`, library); err != nil {
|
|
return 0, fmt.Errorf("the library %s could not be given its group's number: %w", library, err)
|
|
}
|
|
if groups, err = i.LibraryGroups(ctx); err != nil {
|
|
return 0, err
|
|
}
|
|
gid, has := groups[library]
|
|
if !has {
|
|
return 0, fmt.Errorf("the library %s was given its group's number and it cannot be read back", library)
|
|
}
|
|
return gid, nil
|
|
}
|
|
|
|
// LibraryGroups is every library's group number, by library.
|
|
func (i *Inventory) LibraryGroups(ctx context.Context) (map[string]int, error) {
|
|
rows, err := i.store.Pool().Query(ctx, `select library, gid from library_group`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string]int{}
|
|
for rows.Next() {
|
|
var name string
|
|
var gid int
|
|
if err := rows.Scan(&name, &gid); err != nil {
|
|
return nil, err
|
|
}
|
|
out[name] = gid
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Libraries is every library the operator defined (§1): the mesh-wide `libraries` layer of the controller's module.
|
|
// None when it was never set.
|
|
func (i *Inventory) Libraries(ctx context.Context) (map[string]catalogue.Library, error) {
|
|
var raw []byte
|
|
err := i.store.Pool().QueryRow(ctx,
|
|
`select values from settings where module = $1 and node is null`, catalogue.LibrariesModule).Scan(&raw)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var layer map[string]any
|
|
if err := json.Unmarshal(raw, &layer); err != nil {
|
|
return nil, err
|
|
}
|
|
return catalogue.ParseLibraries(layer[catalogue.LibrariesSetting])
|
|
}
|
|
|
|
// judgeLibraries is what a layer of the controller's module is held to when it defines libraries (§1, §4, §13): only
|
|
// mesh-wide, well formed, no library named as a registered module, none overlapping another on a machine.
|
|
func (i *Inventory) judgeLibraries(ctx context.Context, nodeName, module string, values map[string]any) error {
|
|
v, defines := values[catalogue.LibrariesSetting]
|
|
if module != catalogue.LibrariesModule || !defines {
|
|
return nil
|
|
}
|
|
if nodeName != "" {
|
|
return fmt.Errorf("refused: %s is the mesh's, set on the whole mesh and never on one machine (%s §1)",
|
|
catalogue.LibrariesSetting, catalogue.ADR0328)
|
|
}
|
|
libs, err := catalogue.ParseLibraries(v)
|
|
if err != nil {
|
|
return fmt.Errorf("refused: %w", err)
|
|
}
|
|
shelf, err := i.Catalogue(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
modules := make([]string, 0, len(shelf))
|
|
for name := range shelf {
|
|
modules = append(modules, name)
|
|
}
|
|
if problems := catalogue.LibrariesProblems(libs, modules); len(problems) > 0 {
|
|
return fmt.Errorf("refused: %s", strings.Join(problems, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LibraryNamed refuses a module whose name is a defined library's (§4): mesh-<module> and mesh-<library> never collide.
|
|
func (i *Inventory) LibraryNamed(ctx context.Context, module string) error {
|
|
libs, err := i.Libraries(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, clash := libs[module]; clash {
|
|
return fmt.Errorf("the module %s has the name of the library %s: %s would be both (%s §4)", module, module,
|
|
catalogue.MeshName(module), catalogue.ADR0328)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LibraryPaths is every sub-path a module assigned anywhere asks of each library (§6): what its home keeps.
|
|
func (i *Inventory) LibraryPaths(ctx context.Context) (map[string][]string, error) {
|
|
shelf, err := i.Catalogue(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := i.store.Pool().Query(ctx, `select distinct module from assignment`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
seen := map[string]map[string]bool{}
|
|
for rows.Next() {
|
|
var module string
|
|
if err := rows.Scan(&module); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, u := range shelf[module].Libraries {
|
|
if seen[u.Library] == nil {
|
|
seen[u.Library] = map[string]bool{}
|
|
}
|
|
seen[u.Library][filepath.Clean(u.Path)] = true
|
|
}
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string][]string{}
|
|
for lib, paths := range seen {
|
|
for p := range paths {
|
|
out[lib] = append(out[lib], p)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// TakenLibrary is one library the operator let the mesh take, on its home (§7).
|
|
type TakenLibrary struct {
|
|
Library string
|
|
Home string
|
|
Placed string
|
|
By string
|
|
Ask string
|
|
Warrant string
|
|
Said string
|
|
}
|
|
|
|
// TakenLibraries is every library taken, by name.
|
|
func (i *Inventory) TakenLibraries(ctx context.Context) (map[string]TakenLibrary, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select library, home, placed, taken_by, ask, warrant, said from kept_library`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string]TakenLibrary{}
|
|
for rows.Next() {
|
|
var t TakenLibrary
|
|
if err := rows.Scan(&t.Library, &t.Home, &t.Placed, &t.By, &t.Ask, &t.Warrant, &t.Said); err != nil {
|
|
return nil, err
|
|
}
|
|
out[t.Library] = t
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// RecordTaken keeps that a library was taken on its home, under the operator's warrant. Once per home and filesystem:
|
|
// a second take of the same keeps the first, and one on another home or filesystem (it moved, and the operator was
|
|
// asked again) replaces it.
|
|
func (i *Inventory) RecordTaken(ctx context.Context, t TakenLibrary) error {
|
|
switch {
|
|
case t.Library == "" || t.Home == "":
|
|
return errors.New("a taken library names itself and its home")
|
|
case strings.TrimSpace(t.Warrant) == "" || strings.TrimSpace(t.Ask) == "":
|
|
return errors.New("a library is taken only on the operator's warrant, and this names none")
|
|
}
|
|
_, err := i.store.Pool().Exec(ctx,
|
|
`insert into kept_library (library, home, placed, taken_by, ask, warrant, said)
|
|
values ($1, $2, $3, $4, $5, $6, $7)
|
|
on conflict (library) do update
|
|
set home = excluded.home, placed = excluded.placed, taken_at = now(), taken_by = excluded.taken_by,
|
|
ask = excluded.ask, warrant = excluded.warrant, said = excluded.said
|
|
where kept_library.home <> excluded.home or kept_library.placed <> excluded.placed`,
|
|
t.Library, t.Home, t.Placed, t.By, t.Ask, t.Warrant, t.Said)
|
|
return err
|
|
}
|