sorcery-go/pkg/inventory/gaze.go

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
}