feat: client enrollment API and compute entity provisioning

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
This commit is contained in:
2026-07-08 00:24:32 +02:00
parent 8653f3036d
commit 44e1e421e1
7 changed files with 1611 additions and 154 deletions

View File

@@ -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