Files
oikos/internal/httpapi/impl.go
dtoro 5e10437fe3
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
Phase 4 (Performance) + Phase 6 (Infrastructure) completion
Phase 4 — Performance:
- F1: SSH DialPool with key-by-host pooling and 5min idle TTL
- F2: In-memory entity lookup cache (TTL 60s, HTTP resolveEntityID)
- F3: Trigram GIN indexes on entities.slug and entities.name (migration 031)
- F4: Partial index on executions(classification_id) for auto-act (migration 032)
- Added missing RunOutput and RunStreaming in actuator/ (E3 gap fill)

Phase 6 — Infrastructure:
- H1: Infisical image pinned to v0.99.1
- H2: execworker daemon — polls pending executions with per-execution
  advisory locks, recovers orphaned running executions, wired as
  docker-compose service
- H3: splitSQL hardened with block comment and string-literal support,
  6 new edge-case tests (11 total)
- H4: Scheduler acquires pg_try_advisory_lock(0x01c05e6) at startup
2026-08-08 23:46:43 +02:00

115 lines
3.1 KiB
Go

package httpapi
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/dtoro/oikos/internal/db/sqlcgen"
"github.com/dtoro/oikos/internal/domain"
"github.com/dtoro/oikos/internal/httpapi/gen"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
const (
defaultLimit = 50
maxLimit = 200
// graphNodeCap bounds the whole-graph view. The cognition transactional
// types (execution, task) are audit records, not topology, and previously
// crowded out every host/lxc/service; the default whole-graph view below
// excludes them so the cap is spent on the actual fleet graph. Operators
// still reach executions/tasks via list_entities.
graphNodeCap = 2000
)
// actorInfo returns the caller's (type, label) from the request context,
// falling back to operator/unknown when unset.
func actorInfo(ctx context.Context) (string, string) {
if a := GetActor(ctx); a != nil {
typ := a.Type
if typ == "" {
typ = "operator"
}
label := a.Label
if label == "" {
label = a.ID
}
return typ, label
}
return "operator", "unknown"
}
func clampLimit(l *int) int {
if l == nil {
return defaultLimit
}
if *l < 1 {
return 1
}
if *l > maxLimit {
return maxLimit
}
return *l
}
// resolveEntityID resolves a UUID-or-slug path/query value to the entity UUID.
func (s *Server) resolveEntityID(ctx context.Context, idOrSlug string) (uuid.UUID, error) {
if id, err := uuid.Parse(idOrSlug); err == nil {
if _, ok := s.entityCache.GetSlug(id.String()); ok {
return id, nil
}
entity, err := sqlcgen.New(s.pool).GetEntityByID(ctx, id)
if err != nil {
return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug)
}
s.entityCache.Set(entity.Slug, entity.ID.String(), "")
return entity.ID, nil
}
if cachedID, ok := s.entityCache.GetID(idOrSlug); ok {
id, parseErr := uuid.Parse(cachedID)
if parseErr == nil {
return id, nil
}
}
entity, err := sqlcgen.New(s.pool).GetEntityBySlug(ctx, idOrSlug)
if err != nil {
return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug)
}
s.entityCache.Set(entity.Slug, entity.ID.String(), "")
return entity.ID, nil
}
// entityCols requires the entities table to be aliased as `e`, with
// entity_status left-joined and aliased as `st` (see withEntityStatus).
const entityCols = `e.id, e.slug, e.type, e.name, e.state, e.attributes,
e.maintenance_until, e.version, e.created_at, e.updated_at,
st.health, st.last_check_at`
func scanEntity(row pgx.Row) (gen.Entity, error) {
var e gen.Entity
var state *string
var attrsJSON []byte
var maint *time.Time
var health *string
var lastCheckAt *time.Time
err := row.Scan(&e.Id, &e.Slug, &e.Type, &e.Name, &state, &attrsJSON,
&maint, &e.Version, &e.CreatedAt, &e.UpdatedAt, &health, &lastCheckAt)
if err != nil {
return e, err
}
e.State = state
e.MaintenanceUntil = maint
if health != nil {
h := gen.EntityHealth(*health)
e.Health = &h
}
e.LastCheckAt = lastCheckAt
var attrs map[string]any
if len(attrsJSON) > 0 && json.Unmarshal(attrsJSON, &attrs) == nil && len(attrs) > 0 {
e.Attributes = &attrs
}
return e, nil
}