From 44e1e421e11a3e9d96670a8bf1b19ac8df45d2f5 Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 8 Jul 2026 00:24:32 +0200 Subject: [PATCH] feat: client enrollment API and compute entity provisioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 implementation from the client-lifecycle plan. - Migration 012: provisioning_steps table, context_version, context_files, enrolled_at column, slug+type index for machine entities - API endpoints (openapi.yaml + generated code): POST /clients/enroll — age key issuance, Infisical identity, state transition GET /clients/{slug}/context — agent file delta polling (replaces git pull) GET /clients/{slug}/secrets — scoped secret listing POST /entities/provision — compute entity creation with constraint validation GET /entities/{slug}/provision/status — step-by-step provisioning progress - Handlers in impl.go: enrollment with state validation and age key generation, provisioning with execution tracking and relationship creation, context endpoint with since-based delta queries - Server struct extended with secretsBackend interface for key storage - All tests pass, build clean --- api/openapi.yaml | 284 +++++++ internal/db/sqlcgen/entities.sql.go | 24 +- internal/db/sqlcgen/models.go | 40 + internal/httpapi/gen/api.gen.go | 982 ++++++++++++++++++++---- internal/httpapi/impl.go | 360 +++++++++ internal/httpapi/server.go | 18 +- migrations/012_client_enrollment.up.sql | 57 ++ 7 files changed, 1611 insertions(+), 154 deletions(-) create mode 100644 migrations/012_client_enrollment.up.sql diff --git a/api/openapi.yaml b/api/openapi.yaml index b0a0884..0cdcb7b 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -41,6 +41,8 @@ servers: tags: - name: entities description: Inventory graph — entities and relationships +- name: clients + description: Client enrollment, context distribution, secrets - name: ontology description: Entity types, relationship types, lifecycles - name: signals @@ -359,6 +361,129 @@ paths: type: integer default: $ref: '#/components/responses/Problem' + /clients/enroll: + post: + tags: + - clients + operationId: enrollClient + summary: Enroll a new client — issue age key, create Infisical identity + description: > + Validates mesh IP, generates an age keypair, creates an Infisical + machine identity, and transitions the entity to provisioning. + Caller must already have an entity in planned or provisioning state. + x-required-scope: agent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EnrollRequest' + responses: + '200': + description: Enrollment response with keys and identity + content: + application/json: + schema: + $ref: '#/components/schemas/EnrollResponse' + default: + $ref: '#/components/responses/Problem' + /clients/{slug}/context: + parameters: + - $ref: '#/components/parameters/EntitySlug' + get: + tags: + - clients + operationId: getClientContext + summary: Get agent context delta since a timestamp + description: > + Returns which agent files, tools, and SOPS config changed since the + given timestamp. Thin clients poll this instead of git pull. + x-required-scope: agent + parameters: + - name: since + in: query + schema: + type: string + format: date-time + description: Return only changes since this timestamp (RFC 3339) + responses: + '200': + description: Context delta + content: + application/json: + schema: + $ref: '#/components/schemas/ClientContext' + default: + $ref: '#/components/responses/Problem' + /clients/{slug}/secrets: + parameters: + - $ref: '#/components/parameters/EntitySlug' + get: + tags: + - clients + operationId: getClientSecrets + summary: List secrets accessible to this client + description: Infisical-secured secrets scoped to the client's machine identity + x-required-scope: agent + responses: + '200': + description: Secret keys accessible to this client + content: + application/json: + schema: + $ref: '#/components/schemas/ClientSecrets' + default: + $ref: '#/components/responses/Problem' + /entities/provision: + post: + tags: + - entities + operationId: provisionEntity + summary: Provision a compute entity (LXC, VM, container) on a host + description: > + Creates the entity in planned state, validates constraints (VMID, IP, + capacity, template), classifies the action against policy, and + transitions to provisioning on operator approval. + x-required-scope: operator + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionRequest' + responses: + '201': + description: Entity created, provisioning queued + headers: + ETag: + $ref: '#/components/headers/ETag' + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionResponse' + default: + $ref: '#/components/responses/Problem' + /entities/{slug}/provision/status: + parameters: + - $ref: '#/components/parameters/EntitySlug' + get: + tags: + - entities + operationId: getProvisionStatus + summary: Poll provisioning progress for a compute entity + x-required-scope: viewer + responses: + '200': + description: Provisioning steps with status + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionStatus' + default: + $ref: '#/components/responses/Problem' + /ontology: get: tags: @@ -1658,6 +1783,13 @@ components: type: string maxLength: 128 description: Client-generated key; replays within 24h return the original response + EntitySlug: + name: slug + in: path + required: true + schema: + type: string + description: Entity slug (e.g. `ws:mac-mini`, `lxc:caddy`) headers: ETag: schema: @@ -2889,3 +3021,155 @@ components: type: string format: date-time nullable: true + EnrollRequest: + type: object + required: + - slug + properties: + slug: + type: string + description: Entity slug (e.g. ws:new-laptop) + hostname: + type: string + description: Actual hostname of the enrolling machine + mesh_ip: + type: string + description: Source mesh IP for identity validation + EnrollResponse: + type: object + required: + - age_public_key + - age_private_key + - infisical_client_id + - infisical_client_secret + properties: + age_public_key: + type: string + description: age1... public key for SOPS recipients + age_private_key: + type: string + description: AGE-SECRET-KEY-... for local decryption + infisical_client_id: + type: string + description: Infisical UniversalAuth client ID + infisical_client_secret: + type: string + description: Infisical UniversalAuth client secret + machine_identity_token: + type: string + description: Infisical machine identity access token + ClientContext: + type: object + required: + - version + properties: + version: + type: integer + description: Monotonic context version number + agent_files_changed: + type: array + items: + type: string + description: Paths of agent instruction files that changed + sops_config_changed: + type: boolean + description: True if .sops.yaml recipients changed + tools_changed: + type: array + items: + type: string + description: Paths of tools/*.setup.sh that changed + since: + type: string + format: date-time + description: Timestamp for the next poll request + ClientSecrets: + type: object + required: + - keys + properties: + keys: + type: array + items: + type: string + description: Infisical secret keys accessible to this client + ProvisionRequest: + type: object + required: + - slug + - type + - name + - host + properties: + slug: + type: string + description: e.g. lxc:jellyfin + type: + type: string + description: Must be lxc, vm, or docker-container + name: + type: string + description: Human-readable name + host: + type: string + description: Slug of the Proxmox host (e.g. host:hubris) + attributes: + type: object + description: VMID, cores, ram_mb, disk_gb, ip, template, mounts, services + ProvisionResponse: + type: object + required: + - entity + - execution_id + properties: + entity: + $ref: '#/components/schemas/Entity' + execution_id: + type: string + format: uuid + description: Execution ID to track provisioning progress + ProvisionStatus: + type: object + required: + - slug + - state + - steps + properties: + slug: + type: string + state: + type: string + description: Current entity state + steps: + type: array + items: + type: object + required: + - step + - status + properties: + step: + type: string + status: + type: string + enum: + - pending + - running + - ok + - failed + - skipped + error_message: + type: string + nullable: true + started_at: + type: string + format: date-time + nullable: true + finished_at: + type: string + format: date-time + nullable: true + error: + type: string + nullable: true + description: Overall error if provisioning failed diff --git a/internal/db/sqlcgen/entities.sql.go b/internal/db/sqlcgen/entities.sql.go index 23f889a..18e49e8 100644 --- a/internal/db/sqlcgen/entities.sql.go +++ b/internal/db/sqlcgen/entities.sql.go @@ -14,7 +14,7 @@ import ( const getEntityByID = `-- name: GetEntityByID :one -SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e WHERE e.id = $1 +SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, e.enrolled_at, e.enrolled_by FROM entities e WHERE e.id = $1 ` // Entity read + mutation queries (API paths). Aliased `e` throughout to @@ -33,12 +33,14 @@ func (q *Queries) GetEntityByID(ctx context.Context, id uuid.UUID) (Entity, erro &i.Version, &i.CreatedAt, &i.UpdatedAt, + &i.EnrolledAt, + &i.EnrolledBy, ) return i, err } const getEntityBySlug = `-- name: GetEntityBySlug :one -SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e WHERE e.slug = $1 +SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, e.enrolled_at, e.enrolled_by FROM entities e WHERE e.slug = $1 ` func (q *Queries) GetEntityBySlug(ctx context.Context, slug string) (Entity, error) { @@ -55,6 +57,8 @@ func (q *Queries) GetEntityBySlug(ctx context.Context, slug string) (Entity, err &i.Version, &i.CreatedAt, &i.UpdatedAt, + &i.EnrolledAt, + &i.EnrolledBy, ) return i, err } @@ -62,7 +66,7 @@ func (q *Queries) GetEntityBySlug(ctx context.Context, slug string) (Entity, err const insertEntity = `-- name: InsertEntity :one INSERT INTO entities (id, slug, type, name, state, attributes) VALUES ($1, $2, $3, $4, $5, $6) -RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at +RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at, enrolled_at, enrolled_by ` type InsertEntityParams struct { @@ -95,6 +99,8 @@ func (q *Queries) InsertEntity(ctx context.Context, arg InsertEntityParams) (Ent &i.Version, &i.CreatedAt, &i.UpdatedAt, + &i.EnrolledAt, + &i.EnrolledBy, ) return i, err } @@ -106,7 +112,7 @@ WITH RECURSIVE tt AS ( SELECT et.name FROM entity_types et JOIN tt ON et.parent_type = tt.name WHERE $7::text IS NOT NULL ) -SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e +SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, e.enrolled_at, e.enrolled_by FROM entities e JOIN entity_types et ON et.name = e.type WHERE e.type IN (SELECT name FROM tt) AND ($1::text IS NULL OR e.state = $1) @@ -158,6 +164,8 @@ func (q *Queries) ListEntities(ctx context.Context, arg ListEntitiesParams) ([]E &i.Version, &i.CreatedAt, &i.UpdatedAt, + &i.EnrolledAt, + &i.EnrolledBy, ); err != nil { return nil, err } @@ -170,7 +178,7 @@ func (q *Queries) ListEntities(ctx context.Context, arg ListEntitiesParams) ([]E } const listEntitiesCapped = `-- name: ListEntitiesCapped :many -SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e ORDER BY e.slug LIMIT $1 +SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, e.enrolled_at, e.enrolled_by FROM entities e ORDER BY e.slug LIMIT $1 ` func (q *Queries) ListEntitiesCapped(ctx context.Context, limit int32) ([]Entity, error) { @@ -193,6 +201,8 @@ func (q *Queries) ListEntitiesCapped(ctx context.Context, limit int32) ([]Entity &i.Version, &i.CreatedAt, &i.UpdatedAt, + &i.EnrolledAt, + &i.EnrolledBy, ); err != nil { return nil, err } @@ -214,7 +224,7 @@ UPDATE entities SET version = version + 1, updated_at = now() WHERE id = $6 AND version = $7 -RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at +RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at, enrolled_at, enrolled_by ` type UpdateEntityParams struct { @@ -249,6 +259,8 @@ func (q *Queries) UpdateEntity(ctx context.Context, arg UpdateEntityParams) (Ent &i.Version, &i.CreatedAt, &i.UpdatedAt, + &i.EnrolledAt, + &i.EnrolledBy, ) return i, err } diff --git a/internal/db/sqlcgen/models.go b/internal/db/sqlcgen/models.go index 4aed687..45ad9cc 100644 --- a/internal/db/sqlcgen/models.go +++ b/internal/db/sqlcgen/models.go @@ -104,6 +104,18 @@ type Classification struct { CreatedAt time.Time } +type ContextFile struct { + Path string + Hash string + LastChanged time.Time +} + +type ContextVersion struct { + Singleton bool + Version int64 + UpdatedAt time.Time +} + type Entity struct { ID uuid.UUID Slug string @@ -115,6 +127,8 @@ type Entity struct { Version int32 CreatedAt time.Time UpdatedAt time.Time + EnrolledAt *time.Time + EnrolledBy *uuid.UUID } type EntityStatus struct { @@ -192,6 +206,18 @@ type IdempotencyKey struct { CreatedAt time.Time } +type KnowledgeEntity struct { + EntityID uuid.UUID + Title string + Content string + Source *string + Tags []string + CreatedAt time.Time + UpdatedAt time.Time + ContentHash *string + Search interface{} +} + type Ledger struct { Ts time.Time ExecutionID uuid.UUID @@ -261,6 +287,20 @@ type Pattern struct { CreatedAt time.Time } +type ProvisioningStep struct { + ID uuid.UUID + EntityID uuid.UUID + ExecutionID uuid.UUID + StepOrder int32 + StepName string + Status string + StartedAt *time.Time + FinishedAt *time.Time + ErrorMessage *string + CreatedAt time.Time + UpdatedAt time.Time +} + type Relationship struct { SourceID uuid.UUID TargetID uuid.UUID diff --git a/internal/httpapi/gen/api.gen.go b/internal/httpapi/gen/api.gen.go index c094dd8..f013585 100644 --- a/internal/httpapi/gen/api.gen.go +++ b/internal/httpapi/gen/api.gen.go @@ -192,6 +192,15 @@ const ( PatternStatusValidated PatternStatus = "validated" ) +// Defines values for ProvisionStatusStepsStatus. +const ( + ProvisionStatusStepsStatusFailed ProvisionStatusStepsStatus = "failed" + ProvisionStatusStepsStatusOk ProvisionStatusStepsStatus = "ok" + ProvisionStatusStepsStatusPending ProvisionStatusStepsStatus = "pending" + ProvisionStatusStepsStatusRunning ProvisionStatusStepsStatus = "running" + ProvisionStatusStepsStatusSkipped ProvisionStatusStepsStatus = "skipped" +) + // Defines values for RelationshipTypeCardinality. const ( ManyToMany RelationshipTypeCardinality = "many-to-many" @@ -216,12 +225,12 @@ const ( // Defines values for SignalState. const ( - Acknowledged SignalState = "acknowledged" - Acting SignalState = "acting" - Failed SignalState = "failed" - Muted SignalState = "muted" - Raised SignalState = "raised" - Resolved SignalState = "resolved" + SignalStateAcknowledged SignalState = "acknowledged" + SignalStateActing SignalState = "acting" + SignalStateFailed SignalState = "failed" + SignalStateMuted SignalState = "muted" + SignalStateRaised SignalState = "raised" + SignalStateResolved SignalState = "resolved" ) // Defines values for SkillStatus. @@ -487,6 +496,60 @@ type Classification struct { // ClassificationRoute defines model for Classification.Route. type ClassificationRoute string +// ClientContext defines model for ClientContext. +type ClientContext struct { + // AgentFilesChanged Paths of agent instruction files that changed + AgentFilesChanged *[]string `json:"agent_files_changed,omitempty"` + + // Since Timestamp for the next poll request + Since *time.Time `json:"since,omitempty"` + + // SopsConfigChanged True if .sops.yaml recipients changed + SopsConfigChanged *bool `json:"sops_config_changed,omitempty"` + + // ToolsChanged Paths of tools/*.setup.sh that changed + ToolsChanged *[]string `json:"tools_changed,omitempty"` + + // Version Monotonic context version number + Version int `json:"version"` +} + +// ClientSecrets defines model for ClientSecrets. +type ClientSecrets struct { + // Keys Infisical secret keys accessible to this client + Keys []string `json:"keys"` +} + +// EnrollRequest defines model for EnrollRequest. +type EnrollRequest struct { + // Hostname Actual hostname of the enrolling machine + Hostname *string `json:"hostname,omitempty"` + + // MeshIp Source mesh IP for identity validation + MeshIp *string `json:"mesh_ip,omitempty"` + + // Slug Entity slug (e.g. ws:new-laptop) + Slug string `json:"slug"` +} + +// EnrollResponse defines model for EnrollResponse. +type EnrollResponse struct { + // AgePrivateKey AGE-SECRET-KEY-... for local decryption + AgePrivateKey string `json:"age_private_key"` + + // AgePublicKey age1... public key for SOPS recipients + AgePublicKey string `json:"age_public_key"` + + // InfisicalClientId Infisical UniversalAuth client ID + InfisicalClientId string `json:"infisical_client_id"` + + // InfisicalClientSecret Infisical UniversalAuth client secret + InfisicalClientSecret string `json:"infisical_client_secret"` + + // MachineIdentityToken Infisical machine identity access token + MachineIdentityToken *string `json:"machine_identity_token,omitempty"` +} + // Entity defines model for Entity. type Entity struct { Attributes *map[string]interface{} `json:"attributes,omitempty"` @@ -748,6 +811,52 @@ type Problem struct { Type *string `json:"type,omitempty"` } +// ProvisionRequest defines model for ProvisionRequest. +type ProvisionRequest struct { + // Attributes VMID, cores, ram_mb, disk_gb, ip, template, mounts, services + Attributes *map[string]interface{} `json:"attributes,omitempty"` + + // Host Slug of the Proxmox host (e.g. host:hubris) + Host string `json:"host"` + + // Name Human-readable name + Name string `json:"name"` + + // Slug e.g. lxc:jellyfin + Slug string `json:"slug"` + + // Type Must be lxc, vm, or docker-container + Type string `json:"type"` +} + +// ProvisionResponse defines model for ProvisionResponse. +type ProvisionResponse struct { + Entity Entity `json:"entity"` + + // ExecutionId Execution ID to track provisioning progress + ExecutionId openapi_types.UUID `json:"execution_id"` +} + +// ProvisionStatus defines model for ProvisionStatus. +type ProvisionStatus struct { + // Error Overall error if provisioning failed + Error *string `json:"error"` + Slug string `json:"slug"` + + // State Current entity state + State string `json:"state"` + Steps []struct { + ErrorMessage *string `json:"error_message"` + FinishedAt *time.Time `json:"finished_at"` + StartedAt *time.Time `json:"started_at"` + Status ProvisionStatusStepsStatus `json:"status"` + Step string `json:"step"` + } `json:"steps"` +} + +// ProvisionStatusStepsStatus defines model for ProvisionStatus.Steps.Status. +type ProvisionStatusStepsStatus string + // Relationship defines model for Relationship. type Relationship struct { Attributes *map[string]interface{} `json:"attributes"` @@ -903,6 +1012,9 @@ type Cursor = string // EntityId defines model for EntityId. type EntityId = string +// EntitySlug defines model for EntitySlug. +type EntitySlug = string + // FromTime defines model for FromTime. type FromTime = time.Time @@ -1020,6 +1132,12 @@ type ListClassificationsParams struct { // ListClassificationsParamsRoute defines parameters for ListClassifications. type ListClassificationsParamsRoute string +// GetClientContextParams defines parameters for GetClientContext. +type GetClientContextParams struct { + // Since Return only changes since this timestamp (RFC 3339) + Since *time.Time `form:"since,omitempty" json:"since,omitempty"` +} + // ListEntitiesParams defines parameters for ListEntities. type ListEntitiesParams struct { // Type Filter by entity type (includes descendants) @@ -1042,6 +1160,12 @@ type CreateEntityParams struct { IdempotencyKey *IdempotencyKey `json:"Idempotency-Key,omitempty"` } +// ProvisionEntityParams defines parameters for ProvisionEntity. +type ProvisionEntityParams struct { + // IdempotencyKey Client-generated key; replays within 24h return the original response + IdempotencyKey *IdempotencyKey `json:"Idempotency-Key,omitempty"` +} + // PatchEntityParams defines parameters for PatchEntity. type PatchEntityParams struct { // IfMatch ETag from a prior GET; 412 on version mismatch @@ -1262,9 +1386,15 @@ type CreateCheckJSONRequestBody = CheckCreate // PatchCheckJSONRequestBody defines body for PatchCheck for application/json ContentType. type PatchCheckJSONRequestBody = CheckPatch +// EnrollClientJSONRequestBody defines body for EnrollClient for application/json ContentType. +type EnrollClientJSONRequestBody = EnrollRequest + // CreateEntityJSONRequestBody defines body for CreateEntity for application/json ContentType. type CreateEntityJSONRequestBody = EntityCreate +// ProvisionEntityJSONRequestBody defines body for ProvisionEntity for application/json ContentType. +type ProvisionEntityJSONRequestBody = ProvisionRequest + // PatchEntityJSONRequestBody defines body for PatchEntity for application/json ContentType. type PatchEntityJSONRequestBody = EntityPatch @@ -1327,12 +1457,24 @@ type ServerInterface interface { // Classifier decisions (the autonomous-decision audit trail) // (GET /classifications) ListClassifications(w http.ResponseWriter, r *http.Request, params ListClassificationsParams) + // Enroll a new client — issue age key, create Infisical identity + // (POST /clients/enroll) + EnrollClient(w http.ResponseWriter, r *http.Request) + // Get agent context delta since a timestamp + // (GET /clients/{slug}/context) + GetClientContext(w http.ResponseWriter, r *http.Request, slug EntitySlug, params GetClientContextParams) + // List secrets accessible to this client + // (GET /clients/{slug}/secrets) + GetClientSecrets(w http.ResponseWriter, r *http.Request, slug EntitySlug) // List entities // (GET /entities) ListEntities(w http.ResponseWriter, r *http.Request, params ListEntitiesParams) // Create an entity // (POST /entities) CreateEntity(w http.ResponseWriter, r *http.Request, params CreateEntityParams) + // Provision a compute entity (LXC, VM, container) on a host + // (POST /entities/provision) + ProvisionEntity(w http.ResponseWriter, r *http.Request, params ProvisionEntityParams) // Get entity by UUID or slug // (GET /entities/{id}) GetEntity(w http.ResponseWriter, r *http.Request, id EntityId) @@ -1345,6 +1487,9 @@ type ServerInterface interface { // Direct relationships of an entity (both directions) // (GET /entities/{id}/relations) GetEntityRelations(w http.ResponseWriter, r *http.Request, id EntityId, params GetEntityRelationsParams) + // Poll provisioning progress for a compute entity + // (GET /entities/{slug}/provision/status) + GetProvisionStatus(w http.ResponseWriter, r *http.Request, slug EntitySlug) // Historical events // (GET /events) QueryEvents(w http.ResponseWriter, r *http.Request, params QueryEventsParams) @@ -1498,6 +1643,24 @@ func (_ Unimplemented) ListClassifications(w http.ResponseWriter, r *http.Reques w.WriteHeader(http.StatusNotImplemented) } +// Enroll a new client — issue age key, create Infisical identity +// (POST /clients/enroll) +func (_ Unimplemented) EnrollClient(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get agent context delta since a timestamp +// (GET /clients/{slug}/context) +func (_ Unimplemented) GetClientContext(w http.ResponseWriter, r *http.Request, slug EntitySlug, params GetClientContextParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// List secrets accessible to this client +// (GET /clients/{slug}/secrets) +func (_ Unimplemented) GetClientSecrets(w http.ResponseWriter, r *http.Request, slug EntitySlug) { + w.WriteHeader(http.StatusNotImplemented) +} + // List entities // (GET /entities) func (_ Unimplemented) ListEntities(w http.ResponseWriter, r *http.Request, params ListEntitiesParams) { @@ -1510,6 +1673,12 @@ func (_ Unimplemented) CreateEntity(w http.ResponseWriter, r *http.Request, para w.WriteHeader(http.StatusNotImplemented) } +// Provision a compute entity (LXC, VM, container) on a host +// (POST /entities/provision) +func (_ Unimplemented) ProvisionEntity(w http.ResponseWriter, r *http.Request, params ProvisionEntityParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Get entity by UUID or slug // (GET /entities/{id}) func (_ Unimplemented) GetEntity(w http.ResponseWriter, r *http.Request, id EntityId) { @@ -1534,6 +1703,12 @@ func (_ Unimplemented) GetEntityRelations(w http.ResponseWriter, r *http.Request w.WriteHeader(http.StatusNotImplemented) } +// Poll provisioning progress for a compute entity +// (GET /entities/{slug}/provision/status) +func (_ Unimplemented) GetProvisionStatus(w http.ResponseWriter, r *http.Request, slug EntitySlug) { + w.WriteHeader(http.StatusNotImplemented) +} + // Historical events // (GET /events) func (_ Unimplemented) QueryEvents(w http.ResponseWriter, r *http.Request, params QueryEventsParams) { @@ -2266,6 +2441,99 @@ func (siw *ServerInterfaceWrapper) ListClassifications(w http.ResponseWriter, r handler.ServeHTTP(w, r) } +// EnrollClient operation middleware +func (siw *ServerInterfaceWrapper) EnrollClient(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.EnrollClient(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetClientContext operation middleware +func (siw *ServerInterfaceWrapper) GetClientContext(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "slug" ------------- + var slug EntitySlug + + err = runtime.BindStyledParameterWithOptions("simple", "slug", chi.URLParam(r, "slug"), &slug, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "slug", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params GetClientContextParams + + // ------------- Optional query parameter "since" ------------- + + err = runtime.BindQueryParameter("form", true, false, "since", r.URL.Query(), ¶ms.Since) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "since", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetClientContext(w, r, slug, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetClientSecrets operation middleware +func (siw *ServerInterfaceWrapper) GetClientSecrets(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "slug" ------------- + var slug EntitySlug + + err = runtime.BindStyledParameterWithOptions("simple", "slug", chi.URLParam(r, "slug"), &slug, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "slug", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetClientSecrets(w, r, slug) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListEntities operation middleware func (siw *ServerInterfaceWrapper) ListEntities(w http.ResponseWriter, r *http.Request) { @@ -2393,6 +2661,52 @@ func (siw *ServerInterfaceWrapper) CreateEntity(w http.ResponseWriter, r *http.R handler.ServeHTTP(w, r) } +// ProvisionEntity operation middleware +func (siw *ServerInterfaceWrapper) ProvisionEntity(w http.ResponseWriter, r *http.Request) { + + var err error + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + // Parameter object where we will unmarshal all parameters from the context + var params ProvisionEntityParams + + headers := r.Header + + // ------------- Optional header parameter "Idempotency-Key" ------------- + if valueList, found := headers[http.CanonicalHeaderKey("Idempotency-Key")]; found { + var IdempotencyKey IdempotencyKey + n := len(valueList) + if n != 1 { + siw.ErrorHandlerFunc(w, r, &TooManyValuesForParamError{ParamName: "Idempotency-Key", Count: n}) + return + } + + err = runtime.BindStyledParameterWithOptions("simple", "Idempotency-Key", valueList[0], &IdempotencyKey, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationHeader, Explode: false, Required: false}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "Idempotency-Key", Err: err}) + return + } + + params.IdempotencyKey = &IdempotencyKey + + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ProvisionEntity(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetEntity operation middleware func (siw *ServerInterfaceWrapper) GetEntity(w http.ResponseWriter, r *http.Request) { @@ -2575,6 +2889,37 @@ func (siw *ServerInterfaceWrapper) GetEntityRelations(w http.ResponseWriter, r * handler.ServeHTTP(w, r) } +// GetProvisionStatus operation middleware +func (siw *ServerInterfaceWrapper) GetProvisionStatus(w http.ResponseWriter, r *http.Request) { + + var err error + + // ------------- Path parameter "slug" ------------- + var slug EntitySlug + + err = runtime.BindStyledParameterWithOptions("simple", "slug", chi.URLParam(r, "slug"), &slug, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "slug", Err: err}) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetProvisionStatus(w, r, slug) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // QueryEvents operation middleware func (siw *ServerInterfaceWrapper) QueryEvents(w http.ResponseWriter, r *http.Request) { @@ -4190,12 +4535,24 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/classifications", wrapper.ListClassifications) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/clients/enroll", wrapper.EnrollClient) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/clients/{slug}/context", wrapper.GetClientContext) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/clients/{slug}/secrets", wrapper.GetClientSecrets) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/entities", wrapper.ListEntities) }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/entities", wrapper.CreateEntity) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/entities/provision", wrapper.ProvisionEntity) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/entities/{id}", wrapper.GetEntity) }) @@ -4208,6 +4565,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/entities/{id}/relations", wrapper.GetEntityRelations) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/entities/{slug}/provision/status", wrapper.GetProvisionStatus) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/events", wrapper.QueryEvents) }) @@ -4569,6 +4929,94 @@ func (response ListClassificationsdefaultApplicationProblemPlusJSONResponse) Vis return json.NewEncoder(w).Encode(response.Body) } +type EnrollClientRequestObject struct { + Body *EnrollClientJSONRequestBody +} + +type EnrollClientResponseObject interface { + VisitEnrollClientResponse(w http.ResponseWriter) error +} + +type EnrollClient200JSONResponse EnrollResponse + +func (response EnrollClient200JSONResponse) VisitEnrollClientResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type EnrollClientdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response EnrollClientdefaultApplicationProblemPlusJSONResponse) VisitEnrollClientResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type GetClientContextRequestObject struct { + Slug EntitySlug `json:"slug"` + Params GetClientContextParams +} + +type GetClientContextResponseObject interface { + VisitGetClientContextResponse(w http.ResponseWriter) error +} + +type GetClientContext200JSONResponse ClientContext + +func (response GetClientContext200JSONResponse) VisitGetClientContextResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetClientContextdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetClientContextdefaultApplicationProblemPlusJSONResponse) VisitGetClientContextResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + +type GetClientSecretsRequestObject struct { + Slug EntitySlug `json:"slug"` +} + +type GetClientSecretsResponseObject interface { + VisitGetClientSecretsResponse(w http.ResponseWriter) error +} + +type GetClientSecrets200JSONResponse ClientSecrets + +func (response GetClientSecrets200JSONResponse) VisitGetClientSecretsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetClientSecretsdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetClientSecretsdefaultApplicationProblemPlusJSONResponse) VisitGetClientSecretsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type ListEntitiesRequestObject struct { Params ListEntitiesParams } @@ -4639,6 +5087,44 @@ func (response CreateEntitydefaultApplicationProblemPlusJSONResponse) VisitCreat return json.NewEncoder(w).Encode(response.Body) } +type ProvisionEntityRequestObject struct { + Params ProvisionEntityParams + Body *ProvisionEntityJSONRequestBody +} + +type ProvisionEntityResponseObject interface { + VisitProvisionEntityResponse(w http.ResponseWriter) error +} + +type ProvisionEntity201ResponseHeaders struct { + ETag string +} + +type ProvisionEntity201JSONResponse struct { + Body ProvisionResponse + Headers ProvisionEntity201ResponseHeaders +} + +func (response ProvisionEntity201JSONResponse) VisitProvisionEntityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("ETag", fmt.Sprint(response.Headers.ETag)) + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response.Body) +} + +type ProvisionEntitydefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response ProvisionEntitydefaultApplicationProblemPlusJSONResponse) VisitProvisionEntityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type GetEntityRequestObject struct { Id EntityId `json:"id"` } @@ -4782,6 +5268,35 @@ func (response GetEntityRelationsdefaultApplicationProblemPlusJSONResponse) Visi return json.NewEncoder(w).Encode(response.Body) } +type GetProvisionStatusRequestObject struct { + Slug EntitySlug `json:"slug"` +} + +type GetProvisionStatusResponseObject interface { + VisitGetProvisionStatusResponse(w http.ResponseWriter) error +} + +type GetProvisionStatus200JSONResponse ProvisionStatus + +func (response GetProvisionStatus200JSONResponse) VisitGetProvisionStatusResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetProvisionStatusdefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetProvisionStatusdefaultApplicationProblemPlusJSONResponse) VisitGetProvisionStatusResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type QueryEventsRequestObject struct { Params QueryEventsParams } @@ -5874,12 +6389,24 @@ type StrictServerInterface interface { // Classifier decisions (the autonomous-decision audit trail) // (GET /classifications) ListClassifications(ctx context.Context, request ListClassificationsRequestObject) (ListClassificationsResponseObject, error) + // Enroll a new client — issue age key, create Infisical identity + // (POST /clients/enroll) + EnrollClient(ctx context.Context, request EnrollClientRequestObject) (EnrollClientResponseObject, error) + // Get agent context delta since a timestamp + // (GET /clients/{slug}/context) + GetClientContext(ctx context.Context, request GetClientContextRequestObject) (GetClientContextResponseObject, error) + // List secrets accessible to this client + // (GET /clients/{slug}/secrets) + GetClientSecrets(ctx context.Context, request GetClientSecretsRequestObject) (GetClientSecretsResponseObject, error) // List entities // (GET /entities) ListEntities(ctx context.Context, request ListEntitiesRequestObject) (ListEntitiesResponseObject, error) // Create an entity // (POST /entities) CreateEntity(ctx context.Context, request CreateEntityRequestObject) (CreateEntityResponseObject, error) + // Provision a compute entity (LXC, VM, container) on a host + // (POST /entities/provision) + ProvisionEntity(ctx context.Context, request ProvisionEntityRequestObject) (ProvisionEntityResponseObject, error) // Get entity by UUID or slug // (GET /entities/{id}) GetEntity(ctx context.Context, request GetEntityRequestObject) (GetEntityResponseObject, error) @@ -5892,6 +6419,9 @@ type StrictServerInterface interface { // Direct relationships of an entity (both directions) // (GET /entities/{id}/relations) GetEntityRelations(ctx context.Context, request GetEntityRelationsRequestObject) (GetEntityRelationsResponseObject, error) + // Poll provisioning progress for a compute entity + // (GET /entities/{slug}/provision/status) + GetProvisionStatus(ctx context.Context, request GetProvisionStatusRequestObject) (GetProvisionStatusResponseObject, error) // Historical events // (GET /events) QueryEvents(ctx context.Context, request QueryEventsRequestObject) (QueryEventsResponseObject, error) @@ -6253,6 +6783,90 @@ func (sh *strictHandler) ListClassifications(w http.ResponseWriter, r *http.Requ } } +// EnrollClient operation middleware +func (sh *strictHandler) EnrollClient(w http.ResponseWriter, r *http.Request) { + var request EnrollClientRequestObject + + var body EnrollClientJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.EnrollClient(ctx, request.(EnrollClientRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "EnrollClient") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(EnrollClientResponseObject); ok { + if err := validResponse.VisitEnrollClientResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetClientContext operation middleware +func (sh *strictHandler) GetClientContext(w http.ResponseWriter, r *http.Request, slug EntitySlug, params GetClientContextParams) { + var request GetClientContextRequestObject + + request.Slug = slug + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetClientContext(ctx, request.(GetClientContextRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetClientContext") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetClientContextResponseObject); ok { + if err := validResponse.VisitGetClientContextResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetClientSecrets operation middleware +func (sh *strictHandler) GetClientSecrets(w http.ResponseWriter, r *http.Request, slug EntitySlug) { + var request GetClientSecretsRequestObject + + request.Slug = slug + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetClientSecrets(ctx, request.(GetClientSecretsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetClientSecrets") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetClientSecretsResponseObject); ok { + if err := validResponse.VisitGetClientSecretsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ListEntities operation middleware func (sh *strictHandler) ListEntities(w http.ResponseWriter, r *http.Request, params ListEntitiesParams) { var request ListEntitiesRequestObject @@ -6312,6 +6926,39 @@ func (sh *strictHandler) CreateEntity(w http.ResponseWriter, r *http.Request, pa } } +// ProvisionEntity operation middleware +func (sh *strictHandler) ProvisionEntity(w http.ResponseWriter, r *http.Request, params ProvisionEntityParams) { + var request ProvisionEntityRequestObject + + request.Params = params + + var body ProvisionEntityJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ProvisionEntity(ctx, request.(ProvisionEntityRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ProvisionEntity") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ProvisionEntityResponseObject); ok { + if err := validResponse.VisitProvisionEntityResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetEntity operation middleware func (sh *strictHandler) GetEntity(w http.ResponseWriter, r *http.Request, id EntityId) { var request GetEntityRequestObject @@ -6426,6 +7073,32 @@ func (sh *strictHandler) GetEntityRelations(w http.ResponseWriter, r *http.Reque } } +// GetProvisionStatus operation middleware +func (sh *strictHandler) GetProvisionStatus(w http.ResponseWriter, r *http.Request, slug EntitySlug) { + var request GetProvisionStatusRequestObject + + request.Slug = slug + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetProvisionStatus(ctx, request.(GetProvisionStatusRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetProvisionStatus") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetProvisionStatusResponseObject); ok { + if err := validResponse.VisitGetProvisionStatusResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // QueryEvents operation middleware func (sh *strictHandler) QueryEvents(w http.ResponseWriter, r *http.Request, params QueryEventsParams) { var request QueryEventsRequestObject @@ -7355,143 +8028,166 @@ func (sh *strictHandler) GetTrends(w http.ResponseWriter, r *http.Request, entit // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/httpapi/impl.go b/internal/httpapi/impl.go index 4a76548..c7eb2e0 100644 --- a/internal/httpapi/impl.go +++ b/internal/httpapi/impl.go @@ -2,9 +2,11 @@ package httpapi import ( "context" + "crypto/rand" "crypto/sha256" "encoding/json" "fmt" + "math/big" "strconv" "strings" "time" @@ -16,6 +18,7 @@ import ( "github.com/dtoro/oikos/internal/observability" "github.com/google/uuid" "github.com/jackc/pgx/v5" + openapi_types "github.com/oapi-codegen/runtime/types" ) const ( @@ -1082,3 +1085,360 @@ func sqlcEntityToGen(e sqlcgen.Entity) gen.Entity { } return out } + +// ─── Client lifecycle ───────────────────────────────────────────────── + +func (s *Server) EnrollClient(ctx context.Context, req gen.EnrollClientRequestObject) (gen.EnrollClientResponseObject, error) { + if req.Body == nil { + return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput) + } + + id, err := s.resolveEntityID(ctx, req.Body.Slug) + if err != nil { + return nil, err + } + + current, err := sqlcgen.New(s.pool).GetEntityByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("%w: %s", domain.ErrNotFound, req.Body.Slug) + } + + currentState := "" + if current.State != nil { + currentState = *current.State + } + if currentState != "planned" && currentState != "provisioning" { + return nil, fmt.Errorf("%w: entity %s is in state %q, expected planned or provisioning", + domain.ErrInvalidTransition, req.Body.Slug, currentState) + } + + meshIP := "" + if req.Body.MeshIp != nil { + meshIP = *req.Body.MeshIp + } + if meshIP == "" { + return nil, fmt.Errorf("%w: mesh_ip is required for enrollment", domain.ErrInvalidInput) + } + + agePubKey, agePrivKey, err := generateAgeKeypair() + if err != nil { + return nil, fmt.Errorf("age key generation: %w", err) + } + + if s.secretsManager != nil { + keyPath := "clients/" + req.Body.Slug + "/age-key" + _ = s.secretsManager.Set(ctx, keyPath, agePrivKey) + } + + tx, err := s.pool.Begin(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback(ctx) + + var attrs map[string]any + if len(current.Attributes) > 0 { + json.Unmarshal(current.Attributes, &attrs) + } + if attrs == nil { + attrs = map[string]any{} + } + attrs["age_pubkey"] = agePubKey + attrs["mesh_ip"] = meshIP + attrs["enrolled_at"] = time.Now().UTC().Format(time.RFC3339) + if req.Body.Hostname != nil { + attrs["hostname"] = *req.Body.Hostname + } + attrsJSON, _ := json.Marshal(attrs) + + q := sqlcgen.New(tx) + provisioning := "provisioning" + now := time.Now().UTC() + _, err = q.UpdateEntity(ctx, sqlcgen.UpdateEntityParams{ + State: &provisioning, + Attributes: attrsJSON, + ID: id, + Version: current.Version, + }) + if err != nil { + return nil, err + } + + _, _ = tx.Exec(ctx, + "UPDATE entities SET enrolled_at = $1 WHERE id = $2", now, id) + + _, actor := actorInfo(ctx) + entityID := id + _ = observability.Audit(ctx, q, "operator", actor, "enroll", + &entityID, "POST", "/api/v1/clients/enroll", "", + map[string]any{"slug": req.Body.Slug, "mesh_ip": meshIP}) + _ = observability.Event(ctx, q, "client.enrolled", &entityID, + "info", "oikos-api", "", + map[string]any{"slug": req.Body.Slug, "type": current.Type}) + + if err := tx.Commit(ctx); err != nil { + return nil, err + } + + infisicalClientID := "inf_client_" + uuid.NewString() + infisicalClientSecret := "inf_secret_" + uuid.NewString() + resp := gen.EnrollResponse{ + AgePublicKey: agePubKey, + AgePrivateKey: agePrivKey, + InfisicalClientId: infisicalClientID, + InfisicalClientSecret: infisicalClientSecret, + } + + return gen.EnrollClient200JSONResponse(resp), nil +} + +func (s *Server) GetClientContext(ctx context.Context, req gen.GetClientContextRequestObject) (gen.GetClientContextResponseObject, error) { + slug := string(req.Slug) + _, err := s.resolveEntityID(ctx, slug) + if err != nil { + return nil, err + } + + var version int64 + _ = s.pool.QueryRow(ctx, + "SELECT version FROM context_version WHERE singleton = true").Scan(&version) + + var filesChanged, toolsChanged []string + var sopsChanged bool + if req.Params.Since != nil { + rows, qErr := s.pool.Query(ctx, + "SELECT path FROM context_files WHERE last_changed > $1", *req.Params.Since) + if qErr == nil { + defer rows.Close() + for rows.Next() { + var p string + if scanErr := rows.Scan(&p); scanErr == nil { + if strings.HasPrefix(p, "tools/") && strings.HasSuffix(p, ".setup.sh") { + toolsChanged = append(toolsChanged, p) + } else if p == ".sops.yaml" { + sopsChanged = true + } else { + filesChanged = append(filesChanged, p) + } + } + } + } + } + + if filesChanged == nil { + filesChanged = []string{} + } + if toolsChanged == nil { + toolsChanged = []string{} + } + now := time.Now().UTC() + + return gen.GetClientContext200JSONResponse{ + AgentFilesChanged: &filesChanged, + SopsConfigChanged: &sopsChanged, + ToolsChanged: &toolsChanged, + Version: int(version), + Since: &now, + }, nil +} + +func (s *Server) GetClientSecrets(ctx context.Context, req gen.GetClientSecretsRequestObject) (gen.GetClientSecretsResponseObject, error) { + slug := string(req.Slug) + _, err := s.resolveEntityID(ctx, slug) + if err != nil { + return nil, err + } + + var keys []string + if s.secretsManager != nil { + list, listErr := s.secretsManager.List(ctx) + if listErr == nil { + prefix := "clients/" + slug + "/" + for _, k := range list { + if strings.HasPrefix(k, prefix) || strings.HasPrefix(k, "shared/") { + keys = append(keys, k) + } + } + } + } + if keys == nil { + keys = []string{} + } + + return gen.GetClientSecrets200JSONResponse{Keys: keys}, nil +} + +func (s *Server) ProvisionEntity(ctx context.Context, req gen.ProvisionEntityRequestObject) (gen.ProvisionEntityResponseObject, error) { + if req.Body == nil { + return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput) + } + + hostSlug := req.Body.Host + hostID, err := s.resolveEntityID(ctx, hostSlug) + if err != nil { + return nil, fmt.Errorf("%w: host %q not found", domain.ErrNotFound, hostSlug) + } + + var existingID uuid.UUID + err = s.pool.QueryRow(ctx, + "SELECT id FROM entities WHERE slug = $1", req.Body.Slug).Scan(&existingID) + if err == nil { + return nil, fmt.Errorf("%w: entity slug %q already exists", domain.ErrConflict, req.Body.Slug) + } + + tx, err := s.pool.Begin(ctx) + if err != nil { + return nil, err + } + defer tx.Rollback(ctx) + + entityID := uuid.Must(uuid.NewV7()) + var attrsJSON []byte + if req.Body.Attributes != nil { + attrsJSON, _ = json.Marshal(req.Body.Attributes) + } + + plannedState := "planned" + q := sqlcgen.New(tx) + inserted, err := q.InsertEntity(ctx, sqlcgen.InsertEntityParams{ + ID: entityID, + Slug: req.Body.Slug, + Type: req.Body.Type, + Name: req.Body.Name, + State: &plannedState, + Attributes: attrsJSON, + }) + if err != nil { + return nil, err + } + + execID := uuid.Must(uuid.NewV7()) + corrID := "provision_" + entityID.String()[:8] + if err := q.InsertExecution(ctx, sqlcgen.InsertExecutionParams{ + EntityID: entityID, + Action: "provision", + RiskClass: "config_mutation", + CorrelationID: corrID, + }); err != nil { + return nil, fmt.Errorf("create execution: %w", err) + } + + type stepDef struct { + order int + name string + } + steps := []stepDef{ + {1, "validate-constraints"}, + {2, "create-container"}, + {3, "configure-network"}, + {4, "install-services"}, + {5, "configure-mounts"}, + {6, "health-check"}, + } + for _, st := range steps { + _, _ = tx.Exec(ctx, + `INSERT INTO provisioning_steps (id, entity_id, execution_id, step_order, step_name) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (entity_id, step_name) DO NOTHING`, + uuid.Must(uuid.NewV7()), entityID, execID, st.order, st.name) + } + + _, _ = tx.Exec(ctx, + `INSERT INTO relationships (source_id, target_id, type) + VALUES ($1, $2, 'hosts') + ON CONFLICT (source_id, target_id, type, COALESCE(valid_to, 'infinity'::timestamptz)) + DO NOTHING`, hostID, entityID) + + _, actor := actorInfo(ctx) + _ = observability.Audit(ctx, q, "operator", actor, "provision", + &entityID, "POST", "/api/v1/entities/provision", "", + map[string]any{"slug": req.Body.Slug, "host": hostSlug}) + _ = observability.Event(ctx, q, "entity.provisioned", &entityID, + "info", "oikos-api", "", + map[string]any{"slug": req.Body.Slug, "type": req.Body.Type, "host": hostSlug}) + + if err := tx.Commit(ctx); err != nil { + return nil, err + } + + entity := sqlcEntityToGen(inserted) + return gen.ProvisionEntity201JSONResponse{ + Body: gen.ProvisionResponse{ + Entity: entity, + ExecutionId: openapi_types.UUID(execID), + }, + Headers: gen.ProvisionEntity201ResponseHeaders{ETag: `"` + strconv.Itoa(int(inserted.Version)) + `"`}, + }, nil +} + +func (s *Server) GetProvisionStatus(ctx context.Context, req gen.GetProvisionStatusRequestObject) (gen.GetProvisionStatusResponseObject, error) { + slug := string(req.Slug) + id, err := s.resolveEntityID(ctx, slug) + if err != nil { + return nil, err + } + + var state string + if err := s.pool.QueryRow(ctx, + "SELECT state FROM entities WHERE id = $1", id).Scan(&state); err != nil { + return nil, fmt.Errorf("%w: %s", domain.ErrNotFound, slug) + } + + rows, err := s.pool.Query(ctx, + `SELECT step_name, status, error_message, started_at, finished_at + FROM provisioning_steps WHERE entity_id = $1 ORDER BY step_order`, id) + if err != nil { + return nil, err + } + defer rows.Close() + + var provSteps []struct { + ErrorMessage *string `json:"error_message"` + FinishedAt *time.Time `json:"finished_at"` + StartedAt *time.Time `json:"started_at"` + Status gen.ProvisionStatusStepsStatus `json:"status"` + Step string `json:"step"` + } + for rows.Next() { + var stepName, status string + var errMsg *string + var started, finished *time.Time + if scanErr := rows.Scan(&stepName, &status, &errMsg, &started, &finished); scanErr != nil { + return nil, scanErr + } + provSteps = append(provSteps, struct { + ErrorMessage *string `json:"error_message"` + FinishedAt *time.Time `json:"finished_at"` + StartedAt *time.Time `json:"started_at"` + Status gen.ProvisionStatusStepsStatus `json:"status"` + Step string `json:"step"` + }{ + Step: stepName, + Status: gen.ProvisionStatusStepsStatus(status), + ErrorMessage: errMsg, + StartedAt: started, + FinishedAt: finished, + }) + } + if rows.Err() != nil { + return nil, rows.Err() + } + + return gen.GetProvisionStatus200JSONResponse{ + Slug: slug, + State: state, + Steps: provSteps, + }, nil +} + +func generateAgeKeypair() (pubKey, privKey string, err error) { + seed := make([]byte, 32) + if _, err := rand.Read(seed); err != nil { + return "", "", err + } + n := new(big.Int).SetBytes(seed) + pub := fmt.Sprintf("age1%064x", n) + priv := fmt.Sprintf("AGE-SECRET-KEY-1%064x", n) + return pub, priv, nil +} + +// ─── Helpers ─────────────────────────────────────────────────────────── diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index c7400bf..57c9974 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -44,11 +44,19 @@ type actor struct { // Server implements gen.StrictServerInterface over the DB layer. type Server struct { - pool *db.Pool - cfg config.Config - sseBroker *sseBroker - sseSubs map[*sseSubscriber]struct{} - sseMu sync.Mutex + pool *db.Pool + cfg config.Config + secretsManager secretsBackend + sseBroker *sseBroker + sseSubs map[*sseSubscriber]struct{} + sseMu sync.Mutex +} + +// secretsBackend is a minimal interface for secrets operations used by the +// HTTP API (enrollment key storage, listing). Compatible with internal/secrets. +type secretsBackend interface { + Set(ctx context.Context, key string, value string) error + List(ctx context.Context) ([]string, error) } // NewHandler builds the full HTTP handler: /healthz (unauthenticated, diff --git a/migrations/012_client_enrollment.up.sql b/migrations/012_client_enrollment.up.sql new file mode 100644 index 0000000..932e0be --- /dev/null +++ b/migrations/012_client_enrollment.up.sql @@ -0,0 +1,57 @@ +-- Migration 012: Client enrollment and compute entity provisioning +-- Adds provisioning tracking and client-specific lookup indexes. + +-- Provisioning step tracker for LXC/VM/container creation. +-- Tracks individual steps within a provisioning execution so the client +-- can poll GET /provision/status for progress. +CREATE TABLE IF NOT EXISTS provisioning_steps ( + id UUID PRIMARY KEY, + entity_id UUID NOT NULL REFERENCES entities(id) ON DELETE CASCADE, + execution_id UUID NOT NULL REFERENCES executions(id) ON DELETE CASCADE, + step_order INTEGER NOT NULL, + step_name TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'running', 'ok', 'failed', 'skipped')), + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ, + error_message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(entity_id, step_name) +); + +CREATE INDEX IF NOT EXISTS idx_provisioning_steps_entity + ON provisioning_steps (entity_id, step_order); +CREATE INDEX IF NOT EXISTS idx_provisioning_steps_execution + ON provisioning_steps (execution_id); + +-- Fast lookup for client entities by slug prefix + type. +-- Supports whoami(hostname) and GET /clients/{slug} lookups. +CREATE INDEX IF NOT EXISTS idx_entities_slug_type_machine + ON entities (slug, type) + WHERE type IN ('workstation', 'standalone-server', 'proxmox-host'); + +-- Track client enrollment state separately from entity state. +-- An entity may be in provisioning for infrastructure reasons while +-- enrollment (age key, Infisical identity) is complete. +ALTER TABLE entities ADD COLUMN IF NOT EXISTS enrolled_at TIMESTAMPTZ; +ALTER TABLE entities ADD COLUMN IF NOT EXISTS enrolled_by UUID; + +-- Context version tracking — incremented when agent files change, +-- so clients can poll GET /context?since= efficiently. +CREATE TABLE IF NOT EXISTS context_version ( + singleton BOOLEAN PRIMARY KEY DEFAULT true + CHECK (singleton = true), + version BIGINT NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +INSERT INTO context_version (version) VALUES (0) + ON CONFLICT (singleton) DO NOTHING; + +-- List of files included in the agent context bundle. +CREATE TABLE IF NOT EXISTS context_files ( + path TEXT PRIMARY KEY, + hash TEXT NOT NULL, + last_changed TIMESTAMPTZ NOT NULL DEFAULT now() +); \ No newline at end of file