Problem: the hexagon's Phase 2 (plans/2026-08-15-hexagonal-architecture.md) must give the use-cases-to-be their contract surface: driven-port interfaces, test fakes, the secrets interface moved into core, and the postgres package inside the adapters tree — before the first vertical slice (Phase 3) can wire a composition root. Change: - internal/core/ports: full driven-port catalog per plan §3.3 — repositories as transaction-scoped aggregates whose inputs carry derived checks, audit, and events (§3.6), plus CommandExecutor, TargetResolver, Checker, Secrets, EventPublisher, Provisioner. Port-local payload types (Event, AuditEntry, CheckDef, KnowledgeEntry, ExecResult) keep signatures off infrastructure; TypeTree aliases internal/ontology (pure over domain) until checkdefaults is absorbed. ReadModels intentionally not declared yet — it materializes with the Phase 3 slice and grows as report handlers rewire. - secrets.Backend is now an alias of ports.Secrets; implementations (Infisical, SOPS, Manager) unchanged. mcp's local secretBackend subset is deleted; tool constructors take ports.Secrets. - internal/db → internal/adapters/postgres (mechanical import rewrite; package identifier stays db until the Phase 3 repository split). sqlc.yaml, Makefile, golangci exclusions, and docs follow the move; make generate-check verified. - internal/adapters/ssh: Executor implements ports.CommandExecutor over the actuator dial pool + RunStreaming (10-min default timeout carried over from the httpapi path). - internal/adapters/remote: Resolver implements ports.TargetResolver delegating to internal/remote (still pool-based; drops onto ports.EntityRepository when repositories land in Phase 3 — documented transitional import). - internal/core/ports/portstest: importable fakes — in-memory EntityRepo (with check-then-act SetState, side-effect recording), RecordingExecutor, FakeChecker, SpyPublisher; port-satisfaction guards; tests. Risk: ports are declared ahead of implementations — signatures firm up per phase as slices land (documented in the package doc); the remote→postgres transitional import is explicit and dissolves in Phase 3. Verification: go vet, make test (race, 19 packages), generate-check, golangci on core+adapters — 0 issues; full-repo baseline down 365→344.
80 lines
2.4 KiB
Go
80 lines
2.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/dtoro/oikos/internal/adapters/postgres"
|
|
"github.com/dtoro/oikos/internal/httpapi/gen"
|
|
)
|
|
|
|
func (s *Server) GetFleetHealth(ctx context.Context, req gen.GetFleetHealthRequestObject) (gen.GetFleetHealthResponseObject, error) {
|
|
resp := gen.GetFleetHealth200JSONResponse{}
|
|
resp.Entities = []struct {
|
|
Health gen.HealthSummaryEntitiesHealth `json:"health"`
|
|
LastCheckAt *time.Time `json:"last_check_at"`
|
|
Slug string `json:"slug"`
|
|
Trend *gen.HealthSummaryEntitiesTrend `json:"trend"`
|
|
Type string `json:"type"`
|
|
}{}
|
|
|
|
// Exclude 'check' entities (internal probes) — only entities actually
|
|
// being monitored should count toward fleet health.
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT e.slug, e.type, st.health, st.last_check_at
|
|
FROM entity_status st JOIN entities e ON e.id = st.entity_id
|
|
WHERE e.type <> 'check'
|
|
ORDER BY e.slug`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
stale := 0
|
|
for rows.Next() {
|
|
var slug, typ, health string
|
|
var lastCheck *time.Time
|
|
if err := rows.Scan(&slug, &typ, &health, &lastCheck); err != nil {
|
|
return nil, err
|
|
}
|
|
switch health {
|
|
case "healthy":
|
|
resp.Summary.Healthy++
|
|
case "degraded":
|
|
resp.Summary.Degraded++
|
|
case "down":
|
|
resp.Summary.Down++
|
|
case "stale":
|
|
stale++
|
|
default:
|
|
resp.Summary.Unknown++
|
|
}
|
|
resp.Entities = append(resp.Entities, struct {
|
|
Health gen.HealthSummaryEntitiesHealth `json:"health"`
|
|
LastCheckAt *time.Time `json:"last_check_at"`
|
|
Slug string `json:"slug"`
|
|
Trend *gen.HealthSummaryEntitiesTrend `json:"trend"`
|
|
Type string `json:"type"`
|
|
}{
|
|
Health: gen.HealthSummaryEntitiesHealth(health),
|
|
LastCheckAt: lastCheck,
|
|
Slug: slug,
|
|
Type: typ,
|
|
})
|
|
}
|
|
if stale > 0 {
|
|
resp.Summary.Stale = &stale
|
|
}
|
|
return resp, rows.Err()
|
|
}
|
|
|
|
func (s *Server) ExportSeeds(ctx context.Context, req gen.ExportSeedsRequestObject) (gen.ExportSeedsResponseObject, error) {
|
|
exports, err := db.ExportToYAML(ctx, s.pool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gen.ExportSeeds200JSONResponse{
|
|
Ontology: string(exports["ontology.yaml"]),
|
|
Inventory: string(exports["inventory.yaml"]),
|
|
Policy: string(exports["policy.yaml"]),
|
|
}, nil
|
|
} |