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.
This commit is contained in:
@@ -13,14 +13,15 @@ import (
|
||||
|
||||
"github.com/dtoro/oikos/internal/config"
|
||||
"github.com/dtoro/oikos/internal/core/app"
|
||||
"github.com/dtoro/oikos/internal/core/ports"
|
||||
"github.com/dtoro/oikos/internal/adapters/postgres"
|
||||
"github.com/dtoro/oikos/internal/adapters/remote"
|
||||
"github.com/dtoro/oikos/internal/adapters/ssh"
|
||||
"github.com/dtoro/oikos/internal/execworker"
|
||||
"github.com/dtoro/oikos/internal/httpapi"
|
||||
"github.com/dtoro/oikos/internal/knowledge"
|
||||
"github.com/dtoro/oikos/internal/observability"
|
||||
"github.com/dtoro/oikos/internal/scheduler"
|
||||
"github.com/dtoro/oikos/internal/secrets"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var schedulerRunner = scheduler.RunnerForMain()
|
||||
@@ -111,15 +112,9 @@ func main() {
|
||||
go schedulerRunner(ctx, pool, cfg)
|
||||
go execWorkerRunner(ctx, pool, cfg)
|
||||
|
||||
// Build composition-root dependencies (ADR 0016).
|
||||
entityRepo := db.NewEntityRepo(pool)
|
||||
onto := db.NewOntologyRepo(pool, time.Minute)
|
||||
readModels := db.NewEntityReader(pool)
|
||||
entities := app.NewEntityService(entityRepo, onto)
|
||||
relService := app.NewRelationshipService(db.NewRelRepo(pool), onto)
|
||||
|
||||
slog.Info("all: starting api with scheduler + execution-worker in background")
|
||||
if err := httpapi.ListenAndServe(ctx, pool, cfg, entities, entityRepo, readModels, relService); err != nil {
|
||||
svc := buildAPIServices(pool)
|
||||
if err := httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds); err != nil {
|
||||
slog.Error("api failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -177,6 +172,9 @@ func runMigrate(ctx context.Context, cfg config.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// runSeed ingests the seed YAMLs via SeedService (Phase 7): the CLI is
|
||||
// an adapter over the service; ordering, idempotency (content-hash
|
||||
// no-op), and per-file logging live in the service/repository pair.
|
||||
func runSeed(ctx context.Context, cfg config.Config) error {
|
||||
pool, err := db.New(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
@@ -194,96 +192,7 @@ func runSeed(ctx context.Context, cfg config.Config) error {
|
||||
seedsDir = "seeds"
|
||||
}
|
||||
|
||||
// Ingest ontology seed
|
||||
ontoContent, err := os.ReadFile(seedsDir + "/ontology.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read ontology seed: %w", err)
|
||||
}
|
||||
err = pool.SeedIngest(ctx, "ontology.yaml", ontoContent,
|
||||
func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
|
||||
r, err := db.IngestOntologySeed(ctx, tx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("ontology ingested",
|
||||
"lifecycles", r.Lifecycles,
|
||||
"entity_types", r.EntityTypes,
|
||||
"relationship_types", r.RelationshipTypes)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ingest inventory seed
|
||||
invContent, err := os.ReadFile(seedsDir + "/inventory.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read inventory seed: %w", err)
|
||||
}
|
||||
err = pool.SeedIngest(ctx, "inventory.yaml", invContent,
|
||||
func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
|
||||
r, err := db.IngestInventorySeed(ctx, tx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("inventory ingested",
|
||||
"entities", r.Entities,
|
||||
"relationships", r.Relationships)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ingest policy seed
|
||||
polContent, err := os.ReadFile(seedsDir + "/policy.yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("read policy seed: %w", err)
|
||||
}
|
||||
err = pool.SeedIngest(ctx, "policy.yaml", polContent,
|
||||
func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
|
||||
r, err := db.IngestPolicySeed(ctx, tx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("policy ingested",
|
||||
"risk_classes", r.RiskClasses,
|
||||
"approval_rules", r.ApprovalRules,
|
||||
"autonomy_settings", r.AutonomySettings)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ingest knowledge seed (documents, investigations, runbooks)
|
||||
knContent, err := os.ReadFile(seedsDir + "/knowledge.yaml")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
slog.Info("knowledge seed not found, skipping")
|
||||
} else {
|
||||
return fmt.Errorf("read knowledge seed: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = pool.SeedIngest(ctx, "knowledge.yaml", knContent,
|
||||
func(ctx context.Context, tx pgx.Tx, data map[string]any) error {
|
||||
r, err := knowledge.Ingest(ctx, tx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("knowledge ingested",
|
||||
"documents", r.Documents,
|
||||
"investigations", r.Investigations,
|
||||
"runbooks", r.Runbooks)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("seed ingest complete")
|
||||
return nil
|
||||
return app.NewSeedService(db.NewSeedRepo(pool)).Ingest(ctx, seedsDir)
|
||||
}
|
||||
|
||||
func runAPI(ctx context.Context, cfg config.Config) error {
|
||||
@@ -297,18 +206,48 @@ func runAPI(ctx context.Context, cfg config.Config) error {
|
||||
return fmt.Errorf("migrations: %w", err)
|
||||
}
|
||||
|
||||
// Composition root — build the service dependencies (ADR 0016, plan §3.5).
|
||||
svc := buildAPIServices(pool)
|
||||
err = httpapi.ListenAndServe(ctx, pool, cfg, svc.entities, svc.entityRepo, svc.readModels, svc.relService, svc.provisioning, svc.seeds)
|
||||
if err == http.ErrServerClosed {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// apiServices is the composition-root dependency set for the API surface
|
||||
// (ADR 0016, plan §3.5): repositories, the ssh executor and provisioner,
|
||||
// the target resolver, and the use-case services built on them.
|
||||
type apiServices struct {
|
||||
entities *app.EntityService
|
||||
entityRepo *db.EntityRepo
|
||||
readModels ports.ReadModels
|
||||
relService *app.RelationshipService
|
||||
provisioning *app.ProvisioningService
|
||||
seeds *app.SeedService
|
||||
}
|
||||
|
||||
func buildAPIServices(pool *db.Pool) apiServices {
|
||||
entityRepo := db.NewEntityRepo(pool)
|
||||
onto := db.NewOntologyRepo(pool, time.Minute)
|
||||
readModels := db.NewEntityReader(pool)
|
||||
entities := app.NewEntityService(entityRepo, onto)
|
||||
relService := app.NewRelationshipService(db.NewRelRepo(pool), onto)
|
||||
|
||||
err = httpapi.ListenAndServe(ctx, pool, cfg, entities, entityRepo, readModels, relService)
|
||||
if err == http.ErrServerClosed {
|
||||
return nil
|
||||
executor := ssh.NewExecutor(ssh.FileSignerSource(), 5*time.Minute)
|
||||
provisioner := ssh.NewProvisioner(executor)
|
||||
resolver := remote.NewResolver(pool)
|
||||
provisioning := app.NewProvisioningService(provisioner, resolver, entityRepo, db.NewRelRepo(pool))
|
||||
|
||||
seeds := app.NewSeedService(db.NewSeedRepo(pool))
|
||||
|
||||
return apiServices{
|
||||
entities: entities,
|
||||
entityRepo: entityRepo,
|
||||
readModels: readModels,
|
||||
relService: relService,
|
||||
provisioning: provisioning,
|
||||
seeds: seeds,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func runWithPool(ctx context.Context, cfg config.Config, name string, fn func(context.Context, *db.Pool, config.Config)) {
|
||||
@@ -329,13 +268,15 @@ func runWithPool(ctx context.Context, cfg config.Config, name string, fn func(co
|
||||
|
||||
func runSecret(ctx context.Context, cfg config.Config) {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Fprintln(os.Stderr, "usage: oikos secret <get|set|list|migrate|export-sops>")
|
||||
fmt.Fprintln(os.Stderr, "usage: oikos secret <get|set|list|verify|audit|migrate|export-sops>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sub := os.Args[2]
|
||||
|
||||
// For get/set/list: use Infisical directly
|
||||
// get/set/list go through SecretsService (Phase 7); the CLI is an
|
||||
// adapter over the service. verify/audit/migrate/export-sops remain
|
||||
// backend-specific diagnostics.
|
||||
switch sub {
|
||||
case "get":
|
||||
if len(os.Args) < 4 {
|
||||
@@ -343,8 +284,8 @@ func runSecret(ctx context.Context, cfg config.Config) {
|
||||
os.Exit(1)
|
||||
}
|
||||
key := os.Args[3]
|
||||
backend := newInfisicalBackendOrFail(cfg)
|
||||
val, err := backend.Get(ctx, key)
|
||||
svc := app.NewSecretsService(newInfisicalBackendOrFail(cfg))
|
||||
val, err := svc.Get(ctx, key)
|
||||
if err != nil {
|
||||
slog.Error("secret get", "key", key, "error", err)
|
||||
os.Exit(1)
|
||||
@@ -358,16 +299,16 @@ func runSecret(ctx context.Context, cfg config.Config) {
|
||||
}
|
||||
key := os.Args[3]
|
||||
value := os.Args[4]
|
||||
backend := newInfisicalBackendOrFail(cfg)
|
||||
if err := backend.Set(ctx, key, value); err != nil {
|
||||
svc := app.NewSecretsService(newInfisicalBackendOrFail(cfg))
|
||||
if err := svc.Set(ctx, key, value); err != nil {
|
||||
slog.Error("secret set", "key", key, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("stored: %s\n", key)
|
||||
|
||||
case "list":
|
||||
backend := newInfisicalBackendOrFail(cfg)
|
||||
keys, err := backend.List(ctx)
|
||||
svc := app.NewSecretsService(newInfisicalBackendOrFail(cfg))
|
||||
keys, err := svc.List(ctx)
|
||||
if err != nil {
|
||||
slog.Error("secret list", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -544,6 +485,8 @@ func runSecretLegacy(ctx context.Context, cfg config.Config, sub string) {
|
||||
}
|
||||
}
|
||||
|
||||
// runExport regenerates the seed YAMLs from the DB via SeedService and
|
||||
// writes them into the seeds dir (DR / version control).
|
||||
func runExport(ctx context.Context, cfg config.Config) error {
|
||||
pool, err := db.New(ctx, cfg.DatabaseURL)
|
||||
if err != nil {
|
||||
@@ -551,7 +494,7 @@ func runExport(ctx context.Context, cfg config.Config) error {
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
exports, err := db.ExportToYAML(ctx, pool)
|
||||
exports, err := app.NewSeedService(db.NewSeedRepo(pool)).Export(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user