124 lines
4.2 KiB
Go
Executable File
124 lines
4.2 KiB
Go
Executable File
// Package inventory implements the Gaze query engine.
|
|
//
|
|
// Gaze is the "all-seeing eye" of the Coven. It answers:
|
|
//
|
|
// gaze install <spell> — list every file owned by an Essence
|
|
// gaze tablet <spell> — show the y/n answers stored in the Tablet
|
|
// gaze depends <spell> — print the live DAG of a spell's build tree
|
|
// gaze essence <hash> — show which spell and config produced this blob
|
|
// gaze whereis <file> — reverse lookup: which spell owns /usr/bin/wget
|
|
// gaze sbom [essence_id] — produce a CycloneDX SBOM on stdout
|
|
package inventory
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
|
|
"dcos.net/sorcery-go/pkg/dag"
|
|
"dcos.net/sorcery-go/pkg/legal"
|
|
"dcos.net/sorcery-go/pkg/state"
|
|
"dcos.net/sorcery-go/pkg/tomb"
|
|
)
|
|
|
|
// Inventory wraps the state and tomb so Gaze can answer queries fast.
|
|
type Inventory struct {
|
|
State *state.Manager
|
|
Tomb *tomb.Tomb
|
|
Graph *dag.Graph
|
|
}
|
|
|
|
// New returns an Inventory.
|
|
func New(s *state.Manager, t *tomb.Tomb, g *dag.Graph) *Inventory {
|
|
return &Inventory{State: s, Tomb: t, Graph: g}
|
|
}
|
|
|
|
// InstallList returns every file the spell variant currently owns.
|
|
func (i *Inventory) InstallList(spell, variant string) ([]string, error) {
|
|
return i.State.GetManifest(spell, variant)
|
|
}
|
|
|
|
// Depends walks the Graph from `spell` and returns a flat list of
|
|
// dependencies (transitive). Optional+disabled edges are skipped.
|
|
func (i *Inventory) Depends(spell string) ([]string, error) {
|
|
if i.Graph == nil {
|
|
return nil, fmt.Errorf("gaze: no graph loaded")
|
|
}
|
|
root, ok := i.Graph.Nodes[spell]
|
|
if !ok {
|
|
return nil, fmt.Errorf("gaze: %s not in grimoire", spell)
|
|
}
|
|
seen := make(map[string]bool)
|
|
var walk func(*dag.Node)
|
|
walk = func(n *dag.Node) {
|
|
if seen[n.Name] {
|
|
return
|
|
}
|
|
seen[n.Name] = true
|
|
for _, e := range n.Edges {
|
|
if e.Type == dag.OptionalDep && !e.Enabled {
|
|
continue
|
|
}
|
|
walk(e.Target)
|
|
}
|
|
}
|
|
walk(root)
|
|
delete(seen, spell)
|
|
out := make([]string, 0, len(seen))
|
|
for k := range seen {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
// EssenceInfo is the reverse lookup: given a file hash, find which spell
|
|
// produced it.
|
|
func (i *Inventory) EssenceInfo(hash string) (string, error) {
|
|
if i.Tomb == nil {
|
|
return "", fmt.Errorf("gaze: no tomb loaded")
|
|
}
|
|
all, err := i.Tomb.List()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, s := range all {
|
|
for _, h := range s.Files {
|
|
if h == hash {
|
|
return s.SpellName + " (" + s.Version + ")", nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("gaze: no essence contains hash %s", hash)
|
|
}
|
|
|
|
// WhereIs reverse-lookup: given a filesystem path, find which spell owns it.
|
|
// Uses the state DB's reverse index for O(1) lookup.
|
|
func (i *Inventory) WhereIs(path string) (string, error) {
|
|
if i.State == nil {
|
|
return "", fmt.Errorf("gaze: no state loaded")
|
|
}
|
|
spell, variant, err := i.State.WhoOwns(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("gaze: %s: %w", path, err)
|
|
}
|
|
return spell + " (" + variant + ")", nil
|
|
}
|
|
|
|
// Sbom collects every component in the Tomb and returns it as a
|
|
// legal.Component slice ready for CycloneDX / SPDX export.
|
|
func (i *Inventory) Sbom() []legal.Component {
|
|
if i.Tomb == nil {
|
|
return nil
|
|
}
|
|
all, _ := i.Tomb.List()
|
|
out := make([]legal.Component, 0, len(all))
|
|
for _, s := range all {
|
|
out = append(out, legal.Component{
|
|
Name: s.SpellName, Version: s.Version,
|
|
License: s.License, Hash: s.EssenceID,
|
|
Purl: fmt.Sprintf("pkg:sorcery/%s@%s", s.SpellName, s.Version),
|
|
})
|
|
}
|
|
return out
|
|
}
|