Some checks failed
CI/CD / detect-changes (pull_request) Successful in 23s
CI/CD / test-frontend (pull_request) Has been skipped
CI/CD / test-runners (pull_request) Has been skipped
CI/CD / e2e-smoke (pull_request) Has been skipped
CI/CD / test-go-pg (pull_request) Failing after 2m59s
CI/CD / test-sqlite (pull_request) Failing after 3m22s
CI/CD / build-and-deploy (pull_request) Has been skipped
Extensions declare environment requirements via capabilities.required and capabilities.optional in their manifest. The kernel validates at install time — required capabilities reject with HTTP 422 and rollback, optional capabilities log a warning. Runtime query via settings.has_capability(). Admin endpoint GET /api/v1/admin/capabilities re-probes live. Detected capabilities: postgres, pgvector, object_storage, s3, workspace. 18 new tests, all passing with -race. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
147 lines
4.4 KiB
Go
147 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"log"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"armature/storage"
|
|
)
|
|
|
|
// ── Capability detection ────────────────────────────────────────────
|
|
|
|
// DetectCapabilities probes the runtime environment and returns a map
|
|
// of capability name → available. Called once at startup and again on
|
|
// each admin GET /capabilities request.
|
|
func DetectCapabilities(db *sql.DB, isPostgres bool, objStore storage.ObjectStore, workspaceRoot string) map[string]bool {
|
|
caps := map[string]bool{
|
|
"postgres": false,
|
|
"pgvector": false,
|
|
"object_storage": false,
|
|
"s3": false,
|
|
"workspace": false,
|
|
}
|
|
|
|
// postgres: dialect check
|
|
if isPostgres {
|
|
caps["postgres"] = true
|
|
}
|
|
|
|
// pgvector: query pg_extension (PG only)
|
|
if isPostgres && db != nil {
|
|
var one int
|
|
err := db.QueryRow("SELECT 1 FROM pg_extension WHERE extname = 'vector'").Scan(&one)
|
|
caps["pgvector"] = err == nil
|
|
}
|
|
|
|
// object_storage: configured and healthy
|
|
if objStore != nil {
|
|
if err := objStore.Healthy(context.Background()); err == nil {
|
|
caps["object_storage"] = true
|
|
}
|
|
}
|
|
|
|
// s3: specific backend check
|
|
if objStore != nil && objStore.Backend() == "s3" {
|
|
caps["s3"] = true
|
|
}
|
|
|
|
// workspace: root exists and is writable
|
|
if workspaceRoot != "" && storage.IsPathWritable(workspaceRoot) {
|
|
caps["workspace"] = true
|
|
}
|
|
|
|
return caps
|
|
}
|
|
|
|
// ── Manifest parsing ────────────────────────────────────────────────
|
|
|
|
// Capabilities holds the parsed capabilities block from a package manifest.
|
|
type Capabilities struct {
|
|
Required []string
|
|
Optional []string
|
|
}
|
|
|
|
// ParseCapabilities extracts capabilities.required and capabilities.optional
|
|
// from a manifest map. Returns zero value + false if no capabilities block.
|
|
func ParseCapabilities(manifest map[string]any) (Capabilities, bool) {
|
|
capsRaw, ok := manifest["capabilities"].(map[string]any)
|
|
if !ok {
|
|
return Capabilities{}, false
|
|
}
|
|
|
|
var caps Capabilities
|
|
if req, ok := capsRaw["required"].([]any); ok {
|
|
for _, v := range req {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
caps.Required = append(caps.Required, s)
|
|
}
|
|
}
|
|
}
|
|
if opt, ok := capsRaw["optional"].([]any); ok {
|
|
for _, v := range opt {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
caps.Optional = append(caps.Optional, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
return caps, true
|
|
}
|
|
|
|
// CheckRequiredCapabilities returns the subset of required capabilities
|
|
// that are not present in the detected set.
|
|
func CheckRequiredCapabilities(required []string, detected map[string]bool) []string {
|
|
var missing []string
|
|
for _, r := range required {
|
|
if !detected[r] {
|
|
missing = append(missing, r)
|
|
}
|
|
}
|
|
return missing
|
|
}
|
|
|
|
// capabilityHelpText returns operator-facing remediation hints keyed by
|
|
// capability name.
|
|
func capabilityHelpText(missing []string) map[string]string {
|
|
hints := map[string]string{
|
|
"postgres": "Switch DB_DRIVER to postgres and configure DATABASE_URL.",
|
|
"pgvector": "Run `CREATE EXTENSION vector;` in your PostgreSQL database.",
|
|
"object_storage": "Configure STORAGE_BACKEND (pvc or s3) and ensure the backend is healthy.",
|
|
"s3": "Set STORAGE_BACKEND=s3 and configure S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY.",
|
|
"workspace": "Set WORKSPACE_ROOT to a writable directory path.",
|
|
}
|
|
result := make(map[string]string, len(missing))
|
|
for _, m := range missing {
|
|
if h, ok := hints[m]; ok {
|
|
result[m] = h
|
|
} else {
|
|
result[m] = "Unknown capability — check package documentation."
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ── Admin endpoint ──────────────────────────────────────────────────
|
|
|
|
// CapabilitiesHandler serves the admin capabilities endpoint.
|
|
type CapabilitiesHandler struct {
|
|
detect func() map[string]bool
|
|
}
|
|
|
|
// NewCapabilitiesHandler creates a handler that re-probes capabilities on
|
|
// each request so the response reflects current state.
|
|
func NewCapabilitiesHandler(detect func() map[string]bool) *CapabilitiesHandler {
|
|
return &CapabilitiesHandler{detect: detect}
|
|
}
|
|
|
|
// GetCapabilities returns detected environment capabilities.
|
|
// GET /api/v1/admin/capabilities
|
|
func (h *CapabilitiesHandler) GetCapabilities(c *gin.Context) {
|
|
caps := h.detect()
|
|
log.Printf("[capabilities] probed: %v", caps)
|
|
c.JSON(200, caps)
|
|
}
|