Seed ingest/export moves behind ports.SeedRepository (SeedRepo in the postgres adapter; knowledge ingest absorbed from internal/knowledge, package deleted). pct_create flow (defaults, template/VMID/gateway pre-flights, pct create, graph registration) moves from httpapi's approved-execution path into app.ProvisioningService + the ssh provisioner adapter; CLI seed/export/secret become adapters over the services. EntityCreateInput gains EnrolledAt. Plan status corrected: phases 0-7 shipped, 8 + 9 gates open. VERSION 0.34.1.
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
|
|
"github.com/dtoro/oikos/internal/core/ports"
|
|
)
|
|
|
|
// SeedService is the bootstrap/DR use-case: ingest the seeds/*.yaml files
|
|
// into the DB (idempotent, ontology-validated) and regenerate them from
|
|
// the DB for version control. Absorbed the ingest orchestration that
|
|
// lived in cmd/oikos runSeed and the export entry point in runExport
|
|
// (Phase 7); the SQL stays behind ports.SeedRepository in the postgres
|
|
// adapter.
|
|
type SeedService struct {
|
|
repo ports.SeedRepository
|
|
}
|
|
|
|
// NewSeedService wires the service.
|
|
func NewSeedService(repo ports.SeedRepository) *SeedService {
|
|
return &SeedService{repo: repo}
|
|
}
|
|
|
|
// Ingest reads the seed YAMLs from dir and ingests them in dependency
|
|
// order: ontology (types) before inventory (entities/edges validated
|
|
// against them), then policy, then knowledge — which is optional and
|
|
// skipped silently when absent. Files whose content hash is unchanged
|
|
// are no-ops at the repository layer.
|
|
func (s *SeedService) Ingest(ctx context.Context, dir string) error {
|
|
type step struct {
|
|
file string
|
|
ingest func(ctx context.Context, filename string, content []byte) (ports.SeedCounts, bool, error)
|
|
optional bool
|
|
}
|
|
steps := []step{
|
|
{"ontology.yaml", s.repo.IngestOntology, false},
|
|
{"inventory.yaml", s.repo.IngestInventory, false},
|
|
{"policy.yaml", s.repo.IngestPolicy, false},
|
|
{"knowledge.yaml", s.repo.IngestKnowledge, true},
|
|
}
|
|
|
|
for _, st := range steps {
|
|
content, err := os.ReadFile(dir + "/" + st.file)
|
|
if err != nil {
|
|
if os.IsNotExist(err) && st.optional {
|
|
slog.Info("knowledge seed not found, skipping")
|
|
continue
|
|
}
|
|
return fmt.Errorf("read %s seed: %w", st.file, err)
|
|
}
|
|
|
|
counts, applied, err := st.ingest(ctx, st.file, content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !applied {
|
|
continue
|
|
}
|
|
|
|
switch st.file {
|
|
case "ontology.yaml":
|
|
slog.Info("ontology ingested",
|
|
"lifecycles", counts.Lifecycles,
|
|
"entity_types", counts.EntityTypes,
|
|
"relationship_types", counts.RelationshipTypes)
|
|
case "inventory.yaml":
|
|
slog.Info("inventory ingested",
|
|
"entities", counts.Entities,
|
|
"relationships", counts.Relationships)
|
|
case "policy.yaml":
|
|
slog.Info("policy ingested",
|
|
"risk_classes", counts.RiskClasses,
|
|
"approval_rules", counts.ApprovalRules,
|
|
"autonomy_settings", counts.AutonomySettings)
|
|
case "knowledge.yaml":
|
|
slog.Info("knowledge ingested",
|
|
"documents", counts.Documents,
|
|
"investigations", counts.Investigations,
|
|
"runbooks", counts.Runbooks)
|
|
}
|
|
}
|
|
|
|
slog.Info("seed ingest complete")
|
|
return nil
|
|
}
|
|
|
|
// Export regenerates the seed YAMLs from the DB (DR / version control).
|
|
// Deterministic: the repository orders lists and sorts map keys so
|
|
// export → ingest → export is byte-stable.
|
|
func (s *SeedService) Export(ctx context.Context) (map[string][]byte, error) {
|
|
return s.repo.Export(ctx)
|
|
}
|