Files
oikos/internal/core/app/seed.go
dtoro 60c0432d8b feat: Phase 7 — SeedService, SecretsService, ProvisioningService + ssh Provisioner
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.
2026-08-16 09:11:26 +02:00

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)
}