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- and mesh- 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 }