forked from wrenn/wrenn
Refactored to maintain a separate cloud version
Moves 12 packages from internal/ to pkg/ (config, id, validate, events, db, auth, lifecycle, scheduler, channels, audit, service) so they can be imported by the enterprise repo as a Go module dependency. Introduces pkg/cpextension (shared Extension interface + ServerContext) and pkg/cpserver (Run() entrypoint with functional options) so the enterprise main.go can call cpserver.Run(cpserver.WithExtensions(...)) without duplicating the 20-step server bootstrap. Adds db/migrations/embed.go for go:embed access to OSS SQL migrations from the enterprise module. cmd/control-plane/main.go is reduced to a 10-line wrapper around cpserver.Run.
This commit is contained in:
@ -13,6 +13,7 @@ WRENN_DIR=/var/lib/wrenn
|
|||||||
WRENN_HOST_INTERFACE=eth0
|
WRENN_HOST_INTERFACE=eth0
|
||||||
WRENN_CP_URL=http://localhost:9725
|
WRENN_CP_URL=http://localhost:9725
|
||||||
WRENN_DEFAULT_ROOTFS_SIZE=5Gi
|
WRENN_DEFAULT_ROOTFS_SIZE=5Gi
|
||||||
|
WRENN_FIRECRACKER_BIN=/usr/local/bin/firecracker
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
|
|||||||
10
Makefile
10
Makefile
@ -4,6 +4,10 @@
|
|||||||
DATABASE_URL ?= postgres://wrenn:wrenn@localhost:5432/wrenn?sslmode=disable
|
DATABASE_URL ?= postgres://wrenn:wrenn@localhost:5432/wrenn?sslmode=disable
|
||||||
GOBIN := $(shell pwd)/builds
|
GOBIN := $(shell pwd)/builds
|
||||||
ENVD_DIR := envd
|
ENVD_DIR := envd
|
||||||
|
COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
VERSION_CP := $(shell cat VERSION_CP 2>/dev/null | tr -d '[:space:]' || echo "0.0.0-dev")
|
||||||
|
VERSION_AGENT := $(shell cat VERSION_AGENT 2>/dev/null | tr -d '[:space:]' || echo "0.0.0-dev")
|
||||||
|
VERSION_ENVD := $(shell cat envd/VERSION 2>/dev/null | tr -d '[:space:]' || echo "0.0.0-dev")
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════
|
||||||
@ -17,14 +21,14 @@ build-frontend:
|
|||||||
cd frontend && pnpm install --frozen-lockfile && pnpm build
|
cd frontend && pnpm install --frozen-lockfile && pnpm build
|
||||||
|
|
||||||
build-cp:
|
build-cp:
|
||||||
go build -v -ldflags="$(LDFLAGS)" -o $(GOBIN)/wrenn-cp ./cmd/control-plane
|
go build -v -ldflags="$(LDFLAGS) -X main.version=$(VERSION_CP) -X main.commit=$(COMMIT)" -o $(GOBIN)/wrenn-cp ./cmd/control-plane
|
||||||
|
|
||||||
build-agent:
|
build-agent:
|
||||||
go build -v -ldflags="$(LDFLAGS)" -o $(GOBIN)/wrenn-agent ./cmd/host-agent
|
go build -v -ldflags="$(LDFLAGS) -X main.version=$(VERSION_AGENT) -X main.commit=$(COMMIT)" -o $(GOBIN)/wrenn-agent ./cmd/host-agent
|
||||||
|
|
||||||
build-envd:
|
build-envd:
|
||||||
cd $(ENVD_DIR) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
cd $(ENVD_DIR) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
go build -ldflags="$(LDFLAGS)" -o $(GOBIN)/envd .
|
go build -ldflags="$(LDFLAGS) -X main.Version=$(VERSION_ENVD) -X main.commitSHA=$(COMMIT)" -o $(GOBIN)/envd .
|
||||||
@file $(GOBIN)/envd | grep -q "statically linked" || \
|
@file $(GOBIN)/envd | grep -q "statically linked" || \
|
||||||
(echo "ERROR: envd is not statically linked!" && exit 1)
|
(echo "ERROR: envd is not statically linked!" && exit 1)
|
||||||
|
|
||||||
|
|||||||
1
VERSION_AGENT
Normal file
1
VERSION_AGENT
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
||||||
1
VERSION_CP
Normal file
1
VERSION_CP
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
||||||
@ -1,191 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "git.omukk.dev/wrenn/wrenn/pkg/cpserver"
|
||||||
"context"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
// Set via -ldflags at build time.
|
||||||
"github.com/redis/go-redis/v9"
|
var (
|
||||||
|
version = "dev"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/api"
|
commit = "unknown"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth/oauth"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/channels"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/config"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/scheduler"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
cpserver.Run(
|
||||||
Level: slog.LevelDebug,
|
cpserver.WithVersion(version, commit),
|
||||||
})))
|
)
|
||||||
|
|
||||||
cfg := config.Load()
|
|
||||||
|
|
||||||
if len(cfg.JWTSecret) < 32 {
|
|
||||||
slog.Error("JWT_SECRET must be at least 32 characters")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Database connection pool.
|
|
||||||
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to connect to database", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
if err := pool.Ping(ctx); err != nil {
|
|
||||||
slog.Error("failed to ping database", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
slog.Info("connected to database")
|
|
||||||
|
|
||||||
queries := db.New(pool)
|
|
||||||
|
|
||||||
// Redis client.
|
|
||||||
redisOpts, err := redis.ParseURL(cfg.RedisURL)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to parse REDIS_URL", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
rdb := redis.NewClient(redisOpts)
|
|
||||||
defer rdb.Close()
|
|
||||||
|
|
||||||
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
||||||
slog.Error("failed to ping redis", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
slog.Info("connected to redis")
|
|
||||||
|
|
||||||
// mTLS is mandatory — parse internal CA for CP↔agent communication.
|
|
||||||
if cfg.CACert == "" || cfg.CAKey == "" {
|
|
||||||
slog.Error("WRENN_CA_CERT and WRENN_CA_KEY are required — mTLS is mandatory for CP↔agent communication")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
ca, err := auth.ParseCA(cfg.CACert, cfg.CAKey)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to parse mTLS CA from environment", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
slog.Info("mTLS enabled: CA loaded")
|
|
||||||
|
|
||||||
// Host client pool — manages Connect RPC clients to host agents.
|
|
||||||
cpCertStore, err := auth.NewCPCertStore(ca)
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("failed to issue CP client certificate", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
// Renew the CP client certificate periodically so it never expires
|
|
||||||
// while the control plane is running (TTL = 24h, renewal = every 12h).
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(auth.CPCertRenewInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
if err := cpCertStore.Refresh(); err != nil {
|
|
||||||
slog.Error("failed to renew CP client certificate", "error", err)
|
|
||||||
} else {
|
|
||||||
slog.Info("CP client certificate renewed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
hostPool := lifecycle.NewHostClientPoolTLS(auth.CPClientTLSConfig(ca, cpCertStore))
|
|
||||||
slog.Info("host client pool: mTLS enabled")
|
|
||||||
|
|
||||||
// Scheduler — picks a host for each new sandbox (least-loaded, bottleneck-first).
|
|
||||||
hostScheduler := scheduler.NewLeastLoadedScheduler(queries)
|
|
||||||
|
|
||||||
// OAuth provider registry.
|
|
||||||
oauthRegistry := oauth.NewRegistry()
|
|
||||||
if cfg.OAuthGitHubClientID != "" && cfg.OAuthGitHubClientSecret != "" {
|
|
||||||
if cfg.CPPublicURL == "" {
|
|
||||||
slog.Error("CP_PUBLIC_URL must be set when OAuth providers are configured")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
callbackURL := strings.TrimRight(cfg.CPPublicURL, "/") + "/auth/oauth/github/callback"
|
|
||||||
ghProvider := oauth.NewGitHubProvider(cfg.OAuthGitHubClientID, cfg.OAuthGitHubClientSecret, callbackURL)
|
|
||||||
oauthRegistry.Register(ghProvider)
|
|
||||||
slog.Info("registered OAuth provider", "provider", "github")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Channels: publisher, service, dispatcher.
|
|
||||||
if len(cfg.EncryptionKeyHex) != 64 {
|
|
||||||
slog.Error("WRENN_ENCRYPTION_KEY must be a hex-encoded 32-byte key (64 hex chars)")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
channelPub := channels.NewPublisher(rdb)
|
|
||||||
channelSvc := &channels.Service{DB: queries, EncKey: cfg.EncryptionKey}
|
|
||||||
channelDispatcher := channels.NewDispatcher(rdb, queries, cfg.EncryptionKey)
|
|
||||||
|
|
||||||
// Shared audit logger with event publishing.
|
|
||||||
al := audit.NewWithPublisher(queries, channelPub)
|
|
||||||
|
|
||||||
// API server.
|
|
||||||
srv := api.New(queries, hostPool, hostScheduler, pool, rdb, []byte(cfg.JWTSecret), oauthRegistry, cfg.OAuthRedirectURL, ca, al, channelSvc)
|
|
||||||
|
|
||||||
// Start template build workers (2 concurrent).
|
|
||||||
stopBuildWorkers := srv.BuildSvc.StartWorkers(ctx, 2)
|
|
||||||
defer stopBuildWorkers()
|
|
||||||
|
|
||||||
// Start channel event dispatcher.
|
|
||||||
channelDispatcher.Start(ctx)
|
|
||||||
|
|
||||||
// Start host monitor (passive + active reconciliation every 30s).
|
|
||||||
monitor := api.NewHostMonitor(queries, hostPool, al, 30*time.Second)
|
|
||||||
monitor.Start(ctx)
|
|
||||||
|
|
||||||
// Start metrics sampler (records per-team sandbox stats every 10s).
|
|
||||||
sampler := api.NewMetricsSampler(queries, 10*time.Second)
|
|
||||||
sampler.Start(ctx)
|
|
||||||
|
|
||||||
// Wrap the API handler with the sandbox proxy so that requests with
|
|
||||||
// {port}-{sandbox_id}.{domain} Host headers are routed to the sandbox's
|
|
||||||
// host agent. All other requests pass through to the normal API router.
|
|
||||||
proxyWrapper := api.NewSandboxProxyWrapper(srv.Handler(), queries, hostPool)
|
|
||||||
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: cfg.ListenAddr,
|
|
||||||
Handler: proxyWrapper,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Graceful shutdown on signal.
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
go func() {
|
|
||||||
sig := <-sigCh
|
|
||||||
slog.Info("received signal, shutting down", "signal", sig)
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer shutdownCancel()
|
|
||||||
|
|
||||||
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
|
||||||
slog.Error("http server shutdown error", "error", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
slog.Info("control plane starting", "addr", cfg.ListenAddr)
|
|
||||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
slog.Error("http server error", "error", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Info("control plane stopped")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,14 +15,21 @@ import (
|
|||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/devicemapper"
|
"git.omukk.dev/wrenn/wrenn/internal/devicemapper"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/hostagent"
|
"git.omukk.dev/wrenn/wrenn/internal/hostagent"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/network"
|
"git.omukk.dev/wrenn/wrenn/internal/network"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/sandbox"
|
"git.omukk.dev/wrenn/wrenn/internal/sandbox"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Set via -ldflags at build time.
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Best-effort load — missing .env file is fine.
|
// Best-effort load — missing .env file is fine.
|
||||||
_ = godotenv.Load()
|
_ = godotenv.Load()
|
||||||
@ -82,9 +89,31 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve latest kernel version.
|
||||||
|
kernelPath, kernelVersion, err := layout.LatestKernel(rootDir)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to find kernel", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info("resolved kernel", "version", kernelVersion, "path", kernelPath)
|
||||||
|
|
||||||
|
// Detect firecracker version.
|
||||||
|
fcBin := envOrDefault("WRENN_FIRECRACKER_BIN", "/usr/local/bin/firecracker")
|
||||||
|
fcVersion, err := sandbox.DetectFirecrackerVersion(fcBin)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to detect firecracker version", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info("resolved firecracker", "version", fcVersion, "path", fcBin)
|
||||||
|
|
||||||
cfg := sandbox.Config{
|
cfg := sandbox.Config{
|
||||||
WrennDir: rootDir,
|
WrennDir: rootDir,
|
||||||
DefaultRootfsSizeMB: defaultRootfsSizeMB,
|
DefaultRootfsSizeMB: defaultRootfsSizeMB,
|
||||||
|
KernelPath: kernelPath,
|
||||||
|
KernelVersion: kernelVersion,
|
||||||
|
FirecrackerBin: fcBin,
|
||||||
|
FirecrackerVersion: fcVersion,
|
||||||
|
AgentVersion: version,
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr := sandbox.New(cfg)
|
mgr := sandbox.New(cfg)
|
||||||
@ -193,7 +222,7 @@ func main() {
|
|||||||
doShutdown("signal: " + sig.String())
|
doShutdown("signal: " + sig.String())
|
||||||
}()
|
}()
|
||||||
|
|
||||||
slog.Info("host agent starting", "addr", listenAddr, "host_id", creds.HostID)
|
slog.Info("host agent starting", "addr", listenAddr, "host_id", creds.HostID, "version", version, "commit", commit)
|
||||||
// TLSConfig is always set (mTLS is mandatory). Create the TLS listener
|
// TLSConfig is always set (mTLS is mandatory). Create the TLS listener
|
||||||
// manually because ListenAndServeTLS requires on-disk cert/key paths
|
// manually because ListenAndServeTLS requires on-disk cert/key paths
|
||||||
// but we use GetCertificate callback for hot-swap support.
|
// but we use GetCertificate callback for hot-swap support.
|
||||||
|
|||||||
9
db/migrations/20260415134310_add_metadata.sql
Normal file
9
db/migrations/20260415134310_add_metadata.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE sandboxes ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}';
|
||||||
|
ALTER TABLE templates ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}';
|
||||||
|
ALTER TABLE template_builds ADD COLUMN metadata JSONB NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE sandboxes DROP COLUMN metadata;
|
||||||
|
ALTER TABLE templates DROP COLUMN metadata;
|
||||||
|
ALTER TABLE template_builds DROP COLUMN metadata;
|
||||||
10
db/migrations/embed.go
Normal file
10
db/migrations/embed.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Package migrations embeds the SQL migration files so that external modules
|
||||||
|
// (such as the enterprise edition) can access them programmatically.
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
// FS contains all SQL migration files.
|
||||||
|
//
|
||||||
|
//go:embed *.sql
|
||||||
|
var FS embed.FS
|
||||||
@ -1,6 +1,6 @@
|
|||||||
-- name: InsertSandbox :one
|
-- name: InsertSandbox :one
|
||||||
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id)
|
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id, metadata)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetSandbox :one
|
-- name: GetSandbox :one
|
||||||
@ -74,6 +74,12 @@ SET status = 'missing',
|
|||||||
last_updated = NOW()
|
last_updated = NOW()
|
||||||
WHERE host_id = $1 AND status IN ('running', 'starting', 'pending');
|
WHERE host_id = $1 AND status IN ('running', 'starting', 'pending');
|
||||||
|
|
||||||
|
-- name: UpdateSandboxMetadata :exec
|
||||||
|
UPDATE sandboxes
|
||||||
|
SET metadata = $2,
|
||||||
|
last_updated = NOW()
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: BulkRestoreRunning :exec
|
-- name: BulkRestoreRunning :exec
|
||||||
-- Called by the reconciler when a host comes back online and its sandboxes are
|
-- Called by the reconciler when a host comes back online and its sandboxes are
|
||||||
-- confirmed alive. Restores only sandboxes that are in 'missing' state.
|
-- confirmed alive. Restores only sandboxes that are in 'missing' state.
|
||||||
|
|||||||
@ -34,5 +34,5 @@ WHERE id = $1;
|
|||||||
|
|
||||||
-- name: UpdateBuildDefaults :exec
|
-- name: UpdateBuildDefaults :exec
|
||||||
UPDATE template_builds
|
UPDATE template_builds
|
||||||
SET default_user = $2, default_env = $3
|
SET default_user = $2, default_env = $3, metadata = $4
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
-- name: InsertTemplate :one
|
-- name: InsertTemplate :one
|
||||||
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id, default_user, default_env)
|
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id, default_user, default_env, metadata)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetTemplate :one
|
-- name: GetTemplate :one
|
||||||
|
|||||||
1
envd/VERSION
Normal file
1
envd/VERSION
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.1.0
|
||||||
@ -99,7 +99,7 @@ func TestGetFilesContentDisposition(t *testing.T) {
|
|||||||
EnvVars: utils.NewMap[string, string](),
|
EnvVars: utils.NewMap[string, string](),
|
||||||
User: currentUser.Username,
|
User: currentUser.Username,
|
||||||
}
|
}
|
||||||
api := New(&logger, defaults, nil, false, context.Background(), nil)
|
api := New(&logger, defaults, nil, false, context.Background(), nil, "test")
|
||||||
|
|
||||||
// Create request and response recorder
|
// Create request and response recorder
|
||||||
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
||||||
@ -148,7 +148,7 @@ func TestGetFilesContentDispositionWithNestedPath(t *testing.T) {
|
|||||||
EnvVars: utils.NewMap[string, string](),
|
EnvVars: utils.NewMap[string, string](),
|
||||||
User: currentUser.Username,
|
User: currentUser.Username,
|
||||||
}
|
}
|
||||||
api := New(&logger, defaults, nil, false, context.Background(), nil)
|
api := New(&logger, defaults, nil, false, context.Background(), nil, "test")
|
||||||
|
|
||||||
// Create request and response recorder
|
// Create request and response recorder
|
||||||
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
||||||
@ -191,7 +191,7 @@ func TestGetFiles_GzipEncoding_ExplicitIdentityOffWithRange(t *testing.T) {
|
|||||||
EnvVars: utils.NewMap[string, string](),
|
EnvVars: utils.NewMap[string, string](),
|
||||||
User: currentUser.Username,
|
User: currentUser.Username,
|
||||||
}
|
}
|
||||||
api := New(&logger, defaults, nil, false, context.Background(), nil)
|
api := New(&logger, defaults, nil, false, context.Background(), nil, "test")
|
||||||
|
|
||||||
// Create request and response recorder
|
// Create request and response recorder
|
||||||
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
||||||
@ -232,7 +232,7 @@ func TestGetFiles_GzipDownload(t *testing.T) {
|
|||||||
EnvVars: utils.NewMap[string, string](),
|
EnvVars: utils.NewMap[string, string](),
|
||||||
User: currentUser.Username,
|
User: currentUser.Username,
|
||||||
}
|
}
|
||||||
api := New(&logger, defaults, nil, false, context.Background(), nil)
|
api := New(&logger, defaults, nil, false, context.Background(), nil, "test")
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
req := httptest.NewRequest(http.MethodGet, "/files?path="+url.QueryEscape(tempFile), nil)
|
||||||
req.Header.Set("Accept-Encoding", "gzip")
|
req.Header.Set("Accept-Encoding", "gzip")
|
||||||
@ -297,7 +297,7 @@ func TestPostFiles_GzipUpload(t *testing.T) {
|
|||||||
EnvVars: utils.NewMap[string, string](),
|
EnvVars: utils.NewMap[string, string](),
|
||||||
User: currentUser.Username,
|
User: currentUser.Username,
|
||||||
}
|
}
|
||||||
api := New(&logger, defaults, nil, false, context.Background(), nil)
|
api := New(&logger, defaults, nil, false, context.Background(), nil, "test")
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/files?path="+url.QueryEscape(destPath), &gzBuf)
|
req := httptest.NewRequest(http.MethodPost, "/files?path="+url.QueryEscape(destPath), &gzBuf)
|
||||||
req.Header.Set("Content-Type", mpWriter.FormDataContentType())
|
req.Header.Set("Content-Type", mpWriter.FormDataContentType())
|
||||||
@ -357,7 +357,7 @@ func TestGzipUploadThenGzipDownload(t *testing.T) {
|
|||||||
EnvVars: utils.NewMap[string, string](),
|
EnvVars: utils.NewMap[string, string](),
|
||||||
User: currentUser.Username,
|
User: currentUser.Username,
|
||||||
}
|
}
|
||||||
api := New(&logger, defaults, nil, false, context.Background(), nil)
|
api := New(&logger, defaults, nil, false, context.Background(), nil, "test")
|
||||||
|
|
||||||
uploadReq := httptest.NewRequest(http.MethodPost, "/files?path="+url.QueryEscape(destPath), &gzBuf)
|
uploadReq := httptest.NewRequest(http.MethodPost, "/files?path="+url.QueryEscape(destPath), &gzBuf)
|
||||||
uploadReq.Header.Set("Content-Type", mpWriter.FormDataContentType())
|
uploadReq.Header.Set("Content-Type", mpWriter.FormDataContentType())
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func newTestAPI(accessToken *SecureToken, mmdsClient MMDSClient) *API {
|
|||||||
defaults := &execcontext.Defaults{
|
defaults := &execcontext.Defaults{
|
||||||
EnvVars: utils.NewMap[string, string](),
|
EnvVars: utils.NewMap[string, string](),
|
||||||
}
|
}
|
||||||
api := New(&logger, defaults, nil, false, context.Background(), nil)
|
api := New(&logger, defaults, nil, false, context.Background(), nil, "test")
|
||||||
if accessToken != nil {
|
if accessToken != nil {
|
||||||
api.accessToken.TakeFrom(accessToken)
|
api.accessToken.TakeFrom(accessToken)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ type API struct {
|
|||||||
logger *zerolog.Logger
|
logger *zerolog.Logger
|
||||||
accessToken *SecureToken
|
accessToken *SecureToken
|
||||||
defaults *execcontext.Defaults
|
defaults *execcontext.Defaults
|
||||||
|
version string
|
||||||
|
|
||||||
mmdsChan chan *host.MMDSOpts
|
mmdsChan chan *host.MMDSOpts
|
||||||
hyperloopLock sync.Mutex
|
hyperloopLock sync.Mutex
|
||||||
@ -48,7 +49,7 @@ type API struct {
|
|||||||
portSubsystem *publicport.PortSubsystem
|
portSubsystem *publicport.PortSubsystem
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host.MMDSOpts, isNotFC bool, rootCtx context.Context, portSubsystem *publicport.PortSubsystem) *API {
|
func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host.MMDSOpts, isNotFC bool, rootCtx context.Context, portSubsystem *publicport.PortSubsystem, version string) *API {
|
||||||
return &API{
|
return &API{
|
||||||
logger: l,
|
logger: l,
|
||||||
defaults: defaults,
|
defaults: defaults,
|
||||||
@ -59,6 +60,7 @@ func New(l *zerolog.Logger, defaults *execcontext.Defaults, mmdsChan chan *host.
|
|||||||
accessToken: &SecureToken{},
|
accessToken: &SecureToken{},
|
||||||
rootCtx: rootCtx,
|
rootCtx: rootCtx,
|
||||||
portSubsystem: portSubsystem,
|
portSubsystem: portSubsystem,
|
||||||
|
version: version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +70,11 @@ func (a *API) GetHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
a.logger.Trace().Msg("Health check")
|
a.logger.Trace().Msg("Health check")
|
||||||
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
w.Header().Set("Content-Type", "")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"version": a.version,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *API) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
func (a *API) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "0.5.4"
|
Version = "0.1.0"
|
||||||
|
|
||||||
commitSHA string
|
commitSHA string
|
||||||
|
|
||||||
@ -197,7 +197,7 @@ func main() {
|
|||||||
portSubsystem.Start(ctx)
|
portSubsystem.Start(ctx)
|
||||||
defer portSubsystem.Stop()
|
defer portSubsystem.Stop()
|
||||||
|
|
||||||
service := api.New(&envLogger, defaults, mmdsChan, isNotFC, ctx, portSubsystem)
|
service := api.New(&envLogger, defaults, mmdsChan, isNotFC, ctx, portSubsystem, Version)
|
||||||
handler := api.HandlerFromMux(service, m)
|
handler := api.HandlerFromMux(service, m)
|
||||||
middleware := authn.NewMiddleware(permissions.AuthenticateUsername)
|
middleware := authn.NewMiddleware(permissions.AuthenticateUsername)
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -17,9 +17,9 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sentinel errors returned by proxyTarget, used to map to HTTP status codes
|
// Sentinel errors returned by proxyTarget, used to map to HTTP status codes
|
||||||
|
|||||||
@ -11,13 +11,13 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/validate"
|
"git.omukk.dev/wrenn/wrenn/pkg/validate"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -224,6 +224,7 @@ func (h *adminCapsuleHandler) Snapshot(w http.ResponseWriter, r *http.Request) {
|
|||||||
TeamID: id.PlatformTeamID,
|
TeamID: id.PlatformTeamID,
|
||||||
DefaultUser: "root",
|
DefaultUser: "root",
|
||||||
DefaultEnv: []byte("{}"),
|
DefaultEnv: []byte("{}"),
|
||||||
|
Metadata: sb.Metadata,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to insert template record", "name", req.Name, "error", err)
|
slog.Error("failed to insert template record", "name", req.Name, "error", err)
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiKeyHandler struct {
|
type apiKeyHandler struct {
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type auditHandler struct {
|
type auditHandler struct {
|
||||||
|
|||||||
@ -12,9 +12,9 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loginTeam returns the team and role to stamp into a login JWT.
|
// loginTeam returns the team and role to stamp into a login JWT.
|
||||||
|
|||||||
@ -12,12 +12,12 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/validate"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/validate"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -8,11 +8,11 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/channels"
|
"git.omukk.dev/wrenn/wrenn/pkg/channels"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type channelHandler struct {
|
type channelHandler struct {
|
||||||
|
|||||||
@ -12,10 +12,10 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -12,10 +12,10 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -10,10 +10,10 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -10,11 +10,11 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type hostHandler struct {
|
type hostHandler struct {
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -16,10 +16,10 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth/oauth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth/oauth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type oauthHandler struct {
|
type oauthHandler struct {
|
||||||
|
|||||||
@ -12,10 +12,10 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -14,10 +14,10 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,11 +7,11 @@ import (
|
|||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type sandboxHandler struct {
|
type sandboxHandler struct {
|
||||||
@ -31,18 +31,19 @@ type createSandboxRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type sandboxResponse struct {
|
type sandboxResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Template string `json:"template"`
|
Template string `json:"template"`
|
||||||
VCPUs int32 `json:"vcpus"`
|
VCPUs int32 `json:"vcpus"`
|
||||||
MemoryMB int32 `json:"memory_mb"`
|
MemoryMB int32 `json:"memory_mb"`
|
||||||
TimeoutSec int32 `json:"timeout_sec"`
|
TimeoutSec int32 `json:"timeout_sec"`
|
||||||
GuestIP string `json:"guest_ip,omitempty"`
|
GuestIP string `json:"guest_ip,omitempty"`
|
||||||
HostIP string `json:"host_ip,omitempty"`
|
HostIP string `json:"host_ip,omitempty"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
StartedAt *string `json:"started_at,omitempty"`
|
StartedAt *string `json:"started_at,omitempty"`
|
||||||
LastActiveAt *string `json:"last_active_at,omitempty"`
|
LastActiveAt *string `json:"last_active_at,omitempty"`
|
||||||
LastUpdated string `json:"last_updated"`
|
LastUpdated string `json:"last_updated"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func sandboxToResponse(sb db.Sandbox) sandboxResponse {
|
func sandboxToResponse(sb db.Sandbox) sandboxResponse {
|
||||||
@ -56,6 +57,12 @@ func sandboxToResponse(sb db.Sandbox) sandboxResponse {
|
|||||||
GuestIP: sb.GuestIp,
|
GuestIP: sb.GuestIp,
|
||||||
HostIP: sb.HostIp,
|
HostIP: sb.HostIp,
|
||||||
}
|
}
|
||||||
|
if len(sb.Metadata) > 0 {
|
||||||
|
var meta map[string]string
|
||||||
|
if err := json.Unmarshal(sb.Metadata, &meta); err == nil && len(meta) > 0 {
|
||||||
|
resp.Metadata = meta
|
||||||
|
}
|
||||||
|
}
|
||||||
if sb.CreatedAt.Valid {
|
if sb.CreatedAt.Valid {
|
||||||
resp.CreatedAt = sb.CreatedAt.Time.Format(time.RFC3339)
|
resp.CreatedAt = sb.CreatedAt.Time.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,14 +13,14 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/validate"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/validate"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,13 +69,14 @@ type createSnapshotRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type snapshotResponse struct {
|
type snapshotResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
VCPUs *int32 `json:"vcpus,omitempty"`
|
VCPUs *int32 `json:"vcpus,omitempty"`
|
||||||
MemoryMB *int32 `json:"memory_mb,omitempty"`
|
MemoryMB *int32 `json:"memory_mb,omitempty"`
|
||||||
SizeBytes int64 `json:"size_bytes"`
|
SizeBytes int64 `json:"size_bytes"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
Platform bool `json:"platform"`
|
Platform bool `json:"platform"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func templateToResponse(t db.Template) snapshotResponse {
|
func templateToResponse(t db.Template) snapshotResponse {
|
||||||
@ -94,6 +95,12 @@ func templateToResponse(t db.Template) snapshotResponse {
|
|||||||
if t.CreatedAt.Valid {
|
if t.CreatedAt.Valid {
|
||||||
resp.CreatedAt = t.CreatedAt.Time.Format(time.RFC3339)
|
resp.CreatedAt = t.CreatedAt.Time.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
if len(t.Metadata) > 0 {
|
||||||
|
var meta map[string]string
|
||||||
|
if err := json.Unmarshal(t.Metadata, &meta); err == nil && len(meta) > 0 {
|
||||||
|
resp.Metadata = meta
|
||||||
|
}
|
||||||
|
}
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +226,7 @@ func (h *snapshotHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|||||||
TeamID: ac.TeamID,
|
TeamID: ac.TeamID,
|
||||||
DefaultUser: "root",
|
DefaultUser: "root",
|
||||||
DefaultEnv: []byte("{}"),
|
DefaultEnv: []byte("{}"),
|
||||||
|
Metadata: sb.Metadata,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to insert template record", "name", req.Name, "error", err)
|
slog.Error("failed to insert template record", "name", req.Name, "error", err)
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type statsHandler struct {
|
type statsHandler struct {
|
||||||
|
|||||||
@ -10,11 +10,11 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type teamHandler struct {
|
type teamHandler struct {
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type usersHandler struct {
|
type usersHandler struct {
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MetricsSampler records per-team sandbox resource usage to
|
// MetricsSampler records per-team sandbox resource usage to
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import (
|
|||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorResponse struct {
|
type errorResponse struct {
|
||||||
|
|||||||
@ -3,9 +3,9 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// injectPlatformTeam overwrites the AuthContext's TeamID with the platform
|
// injectPlatformTeam overwrites the AuthContext's TeamID with the platform
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// requireAPIKeyOrJWT accepts either X-API-Key header or Authorization: Bearer JWT.
|
// requireAPIKeyOrJWT accepts either X-API-Key header or Authorization: Bearer JWT.
|
||||||
|
|||||||
@ -3,8 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// requireHostToken validates the X-Host-Token header containing a host JWT,
|
// requireHostToken validates the X-Host-Token header containing a host JWT,
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// requireJWT validates a JWT from the Authorization: Bearer header or the
|
// requireJWT validates a JWT from the Authorization: Bearer header or the
|
||||||
|
|||||||
@ -9,14 +9,15 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/audit"
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth/oauth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth/oauth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/channels"
|
"git.omukk.dev/wrenn/wrenn/pkg/channels"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/cpextension"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/lifecycle"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/scheduler"
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/service"
|
"git.omukk.dev/wrenn/wrenn/pkg/scheduler"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed openapi.yaml
|
//go:embed openapi.yaml
|
||||||
@ -29,6 +30,8 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New constructs the chi router and registers all routes.
|
// New constructs the chi router and registers all routes.
|
||||||
|
// Extensions are called after core routes are registered, allowing enterprise
|
||||||
|
// or third-party code to add routes and middleware.
|
||||||
func New(
|
func New(
|
||||||
queries *db.Queries,
|
queries *db.Queries,
|
||||||
pool *lifecycle.HostClientPool,
|
pool *lifecycle.HostClientPool,
|
||||||
@ -41,6 +44,8 @@ func New(
|
|||||||
ca *auth.CA,
|
ca *auth.CA,
|
||||||
al *audit.AuditLogger,
|
al *audit.AuditLogger,
|
||||||
channelSvc *channels.Service,
|
channelSvc *channels.Service,
|
||||||
|
extensions []cpextension.Extension,
|
||||||
|
sctx cpextension.ServerContext,
|
||||||
) *Server {
|
) *Server {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(requestLogger())
|
r.Use(requestLogger())
|
||||||
@ -239,6 +244,11 @@ func New(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Let extensions register their routes after all core routes.
|
||||||
|
for _, ext := range extensions {
|
||||||
|
ext.RegisterRoutes(r, sctx)
|
||||||
|
}
|
||||||
|
|
||||||
return &Server{router: r, BuildSvc: buildSvc}
|
return &Server{router: r, BuildSvc: buildSvc}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,6 +257,11 @@ func (s *Server) Handler() http.Handler {
|
|||||||
return s.router
|
return s.router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Router returns the underlying chi.Router for direct access.
|
||||||
|
func (s *Server) Router() chi.Router {
|
||||||
|
return s.router
|
||||||
|
}
|
||||||
|
|
||||||
func serveOpenAPI(w http.ResponseWriter, r *http.Request) {
|
func serveOpenAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/yaml")
|
w.Header().Set("Content-Type", "application/yaml")
|
||||||
_, _ = w.Write(openapiYAML)
|
_, _ = w.Write(openapiYAML)
|
||||||
|
|||||||
@ -2,7 +2,9 @@ package envdclient
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@ -31,6 +33,38 @@ func (c *Client) WaitUntilReady(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchVersion queries envd's health endpoint and returns the reported version.
|
||||||
|
func (c *Client) FetchVersion(ctx context.Context) (string, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.healthURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("build health request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("fetch envd version: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||||
|
return "", fmt.Errorf("health check returned %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil || len(body) == 0 {
|
||||||
|
return "", nil // envd may not support version reporting yet
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return "", nil // non-JSON response, old envd
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Version, nil
|
||||||
|
}
|
||||||
|
|
||||||
// healthCheck sends a single GET /health request to envd.
|
// healthCheck sends a single GET /health request to envd.
|
||||||
func (c *Client) healthCheck(ctx context.Context) error {
|
func (c *Client) healthCheck(ctx context.Context) error {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.healthURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.healthURL, nil)
|
||||||
|
|||||||
@ -80,6 +80,7 @@ func (s *Server) CreateSandbox(
|
|||||||
SandboxId: sb.ID,
|
SandboxId: sb.ID,
|
||||||
Status: string(sb.Status),
|
Status: string(sb.Status),
|
||||||
HostIp: sb.HostIP.String(),
|
HostIp: sb.HostIP.String(),
|
||||||
|
Metadata: sb.Metadata,
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ func (s *Server) ResumeSandbox(
|
|||||||
req *connect.Request[pb.ResumeSandboxRequest],
|
req *connect.Request[pb.ResumeSandboxRequest],
|
||||||
) (*connect.Response[pb.ResumeSandboxResponse], error) {
|
) (*connect.Response[pb.ResumeSandboxResponse], error) {
|
||||||
msg := req.Msg
|
msg := req.Msg
|
||||||
sb, err := s.mgr.Resume(ctx, msg.SandboxId, int(msg.TimeoutSec))
|
sb, err := s.mgr.Resume(ctx, msg.SandboxId, int(msg.TimeoutSec), msg.KernelVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, connect.NewError(connect.CodeInternal, err)
|
return nil, connect.NewError(connect.CodeInternal, err)
|
||||||
}
|
}
|
||||||
@ -124,6 +125,7 @@ func (s *Server) ResumeSandbox(
|
|||||||
SandboxId: sb.ID,
|
SandboxId: sb.ID,
|
||||||
Status: string(sb.Status),
|
Status: string(sb.Status),
|
||||||
HostIp: sb.HostIP.String(),
|
HostIp: sb.HostIP.String(),
|
||||||
|
Metadata: sb.Metadata,
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,6 +566,7 @@ func (s *Server) ListSandboxes(
|
|||||||
CreatedAtUnix: sb.CreatedAt.Unix(),
|
CreatedAtUnix: sb.CreatedAt.Unix(),
|
||||||
LastActiveAtUnix: sb.LastActiveAt.Unix(),
|
LastActiveAtUnix: sb.LastActiveAt.Unix(),
|
||||||
TimeoutSec: int32(sb.TimeoutSec),
|
TimeoutSec: int32(sb.TimeoutSec),
|
||||||
|
Metadata: sb.Metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
package layout
|
package layout
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsMinimal reports whether the given team and template IDs represent the
|
// IsMinimal reports whether the given team and template IDs represent the
|
||||||
@ -47,6 +51,75 @@ func KernelPath(wrennDir string) string {
|
|||||||
return filepath.Join(wrennDir, "kernels", "vmlinux")
|
return filepath.Join(wrennDir, "kernels", "vmlinux")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KernelPathVersioned returns the path to a specific kernel version.
|
||||||
|
func KernelPathVersioned(wrennDir, version string) string {
|
||||||
|
return filepath.Join(wrennDir, "kernels", "vmlinux-"+version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestKernel scans the kernels directory for files matching vmlinux-{semver}
|
||||||
|
// and returns the path and version of the latest one (by semver sort).
|
||||||
|
func LatestKernel(wrennDir string) (path, version string, err error) {
|
||||||
|
dir := filepath.Join(wrennDir, "kernels")
|
||||||
|
return latestVersionedFile(dir, "vmlinux-")
|
||||||
|
}
|
||||||
|
|
||||||
|
// latestVersionedFile scans dir for files with the given prefix, extracts the
|
||||||
|
// version suffix, sorts by semver, and returns the path and version of the latest.
|
||||||
|
func latestVersionedFile(dir, prefix string) (path, version string, err error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("read directory %s: %w", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var versions []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := e.Name()
|
||||||
|
if v, ok := strings.CutPrefix(name, prefix); ok && v != "" {
|
||||||
|
versions = append(versions, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(versions) == 0 {
|
||||||
|
return "", "", fmt.Errorf("no %s* files found in %s", prefix, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
|
return compareSemver(versions[i], versions[j]) < 0
|
||||||
|
})
|
||||||
|
|
||||||
|
latest := versions[len(versions)-1]
|
||||||
|
return filepath.Join(dir, prefix+latest), latest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareSemver compares two dotted-numeric version strings.
|
||||||
|
// Returns -1 if a < b, 0 if equal, 1 if a > b.
|
||||||
|
func compareSemver(a, b string) int {
|
||||||
|
aParts := strings.Split(a, ".")
|
||||||
|
bParts := strings.Split(b, ".")
|
||||||
|
|
||||||
|
maxLen := max(len(aParts), len(bParts))
|
||||||
|
|
||||||
|
for i := 0; i < maxLen; i++ {
|
||||||
|
var av, bv int
|
||||||
|
if i < len(aParts) {
|
||||||
|
_, _ = fmt.Sscanf(aParts[i], "%d", &av)
|
||||||
|
}
|
||||||
|
if i < len(bParts) {
|
||||||
|
_, _ = fmt.Sscanf(bParts[i], "%d", &bv)
|
||||||
|
}
|
||||||
|
if av < bv {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if av > bv {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// ImagesRoot returns the root images directory.
|
// ImagesRoot returns the root images directory.
|
||||||
func ImagesRoot(wrennDir string) string {
|
func ImagesRoot(wrennDir string) string {
|
||||||
return filepath.Join(wrennDir, "images")
|
return filepath.Join(wrennDir, "images")
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsMinimal(t *testing.T) {
|
func TestIsMinimal(t *testing.T) {
|
||||||
|
|||||||
@ -30,4 +30,5 @@ type Sandbox struct {
|
|||||||
RootfsPath string
|
RootfsPath string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
LastActiveAt time.Time
|
LastActiveAt time.Time
|
||||||
|
Metadata map[string]string
|
||||||
}
|
}
|
||||||
|
|||||||
30
internal/sandbox/fcversion.go
Normal file
30
internal/sandbox/fcversion.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package sandbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetectFirecrackerVersion runs the firecracker binary with --version and
|
||||||
|
// parses the semver from the output (e.g. "Firecracker v1.14.1" → "1.14.1").
|
||||||
|
func DetectFirecrackerVersion(binaryPath string) (string, error) {
|
||||||
|
out, err := exec.Command(binaryPath, "--version").Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("run %s --version: %w", binaryPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output is typically "Firecracker v1.14.1\n" or similar.
|
||||||
|
line := strings.TrimSpace(string(out))
|
||||||
|
for _, field := range strings.Fields(line) {
|
||||||
|
v := strings.TrimPrefix(field, "v")
|
||||||
|
if v != field || strings.Contains(field, ".") {
|
||||||
|
// Either had a "v" prefix or contains a dot — likely the version.
|
||||||
|
if strings.Count(v, ".") >= 1 {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not parse version from firecracker output: %q", line)
|
||||||
|
}
|
||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultDiskSizeMB is the standard disk size for base images. Images smaller
|
// DefaultDiskSizeMB is the standard disk size for base images. Images smaller
|
||||||
|
|||||||
@ -17,13 +17,13 @@ import (
|
|||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/devicemapper"
|
"git.omukk.dev/wrenn/wrenn/internal/devicemapper"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/envdclient"
|
"git.omukk.dev/wrenn/wrenn/internal/envdclient"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
"git.omukk.dev/wrenn/wrenn/internal/layout"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/models"
|
"git.omukk.dev/wrenn/wrenn/internal/models"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/network"
|
"git.omukk.dev/wrenn/wrenn/internal/network"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/snapshot"
|
"git.omukk.dev/wrenn/wrenn/internal/snapshot"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/uffd"
|
"git.omukk.dev/wrenn/wrenn/internal/uffd"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/vm"
|
"git.omukk.dev/wrenn/wrenn/internal/vm"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
envdpb "git.omukk.dev/wrenn/wrenn/proto/envd/gen"
|
envdpb "git.omukk.dev/wrenn/wrenn/proto/envd/gen"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,6 +32,13 @@ type Config struct {
|
|||||||
WrennDir string // root directory (e.g. /var/lib/wrenn); all sub-paths derived via layout package
|
WrennDir string // root directory (e.g. /var/lib/wrenn); all sub-paths derived via layout package
|
||||||
EnvdTimeout time.Duration
|
EnvdTimeout time.Duration
|
||||||
DefaultRootfsSizeMB int // target size for template rootfs images; 0 → DefaultDiskSizeMB
|
DefaultRootfsSizeMB int // target size for template rootfs images; 0 → DefaultDiskSizeMB
|
||||||
|
|
||||||
|
// Resolved at startup by the host agent.
|
||||||
|
KernelPath string // path to the latest vmlinux-x.y.z
|
||||||
|
KernelVersion string // semver extracted from filename
|
||||||
|
FirecrackerBin string // path to the firecracker binary
|
||||||
|
FirecrackerVersion string // semver from firecracker --version
|
||||||
|
AgentVersion string // host agent version (injected via ldflags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager orchestrates sandbox lifecycle: VM, network, filesystem, envd.
|
// Manager orchestrates sandbox lifecycle: VM, network, filesystem, envd.
|
||||||
@ -86,6 +93,35 @@ type snapshotParent struct {
|
|||||||
// preventing the crash.
|
// preventing the crash.
|
||||||
const maxDiffGenerations = 8
|
const maxDiffGenerations = 8
|
||||||
|
|
||||||
|
// buildMetadata constructs the metadata map with version information.
|
||||||
|
func (m *Manager) buildMetadata(envdVersion string) map[string]string {
|
||||||
|
meta := map[string]string{
|
||||||
|
"kernel_version": m.cfg.KernelVersion,
|
||||||
|
"firecracker_version": m.cfg.FirecrackerVersion,
|
||||||
|
"agent_version": m.cfg.AgentVersion,
|
||||||
|
}
|
||||||
|
if envdVersion != "" {
|
||||||
|
meta["envd_version"] = envdVersion
|
||||||
|
}
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveKernelPath returns the kernel path for the given version hint.
|
||||||
|
// If the exact version exists on disk, it is used. Otherwise, falls back to
|
||||||
|
// the latest kernel (m.cfg.KernelPath).
|
||||||
|
func (m *Manager) resolveKernelPath(versionHint string) string {
|
||||||
|
if versionHint == "" {
|
||||||
|
return m.cfg.KernelPath
|
||||||
|
}
|
||||||
|
exact := layout.KernelPathVersioned(m.cfg.WrennDir, versionHint)
|
||||||
|
if _, err := os.Stat(exact); err == nil {
|
||||||
|
return exact
|
||||||
|
}
|
||||||
|
slog.Warn("requested kernel version not found, using latest",
|
||||||
|
"requested", versionHint, "latest", m.cfg.KernelVersion)
|
||||||
|
return m.cfg.KernelPath
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a new sandbox manager.
|
// New creates a new sandbox manager.
|
||||||
func New(cfg Config) *Manager {
|
func New(cfg Config) *Manager {
|
||||||
if cfg.EnvdTimeout == 0 {
|
if cfg.EnvdTimeout == 0 {
|
||||||
@ -175,7 +211,7 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template
|
|||||||
vmCfg := vm.VMConfig{
|
vmCfg := vm.VMConfig{
|
||||||
SandboxID: sandboxID,
|
SandboxID: sandboxID,
|
||||||
TemplateID: id.UUIDString(templateID),
|
TemplateID: id.UUIDString(templateID),
|
||||||
KernelPath: layout.KernelPath(m.cfg.WrennDir),
|
KernelPath: m.cfg.KernelPath,
|
||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
VCPUs: vcpus,
|
VCPUs: vcpus,
|
||||||
MemoryMB: memoryMB,
|
MemoryMB: memoryMB,
|
||||||
@ -185,6 +221,7 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template
|
|||||||
GuestIP: slot.GuestIP,
|
GuestIP: slot.GuestIP,
|
||||||
GatewayIP: slot.TapIP,
|
GatewayIP: slot.TapIP,
|
||||||
NetMask: slot.GuestNetMask,
|
NetMask: slot.GuestNetMask,
|
||||||
|
FirecrackerBin: m.cfg.FirecrackerBin,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := m.vm.Create(ctx, vmCfg); err != nil {
|
if _, err := m.vm.Create(ctx, vmCfg); err != nil {
|
||||||
@ -211,6 +248,9 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template
|
|||||||
return nil, fmt.Errorf("wait for envd: %w", err)
|
return nil, fmt.Errorf("wait for envd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch envd version (best-effort).
|
||||||
|
envdVersion, _ := client.FetchVersion(ctx)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
sb := &sandboxState{
|
sb := &sandboxState{
|
||||||
Sandbox: models.Sandbox{
|
Sandbox: models.Sandbox{
|
||||||
@ -226,6 +266,7 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template
|
|||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
LastActiveAt: now,
|
LastActiveAt: now,
|
||||||
|
Metadata: m.buildMetadata(envdVersion),
|
||||||
},
|
},
|
||||||
slot: slot,
|
slot: slot,
|
||||||
client: client,
|
client: client,
|
||||||
@ -558,7 +599,7 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
|
|||||||
|
|
||||||
// Resume restores a paused sandbox from its snapshot using UFFD for
|
// Resume restores a paused sandbox from its snapshot using UFFD for
|
||||||
// lazy memory loading. The sandbox gets a new network slot.
|
// lazy memory loading. The sandbox gets a new network slot.
|
||||||
func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int) (*models.Sandbox, error) {
|
func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int, kernelVersion string) (*models.Sandbox, error) {
|
||||||
pauseDir := layout.PauseSnapshotDir(m.cfg.WrennDir, sandboxID)
|
pauseDir := layout.PauseSnapshotDir(m.cfg.WrennDir, sandboxID)
|
||||||
if _, err := os.Stat(pauseDir); err != nil {
|
if _, err := os.Stat(pauseDir); err != nil {
|
||||||
return nil, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
|
return nil, fmt.Errorf("no snapshot found for sandbox %s", sandboxID)
|
||||||
@ -672,7 +713,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
// Restore VM from snapshot.
|
// Restore VM from snapshot.
|
||||||
vmCfg := vm.VMConfig{
|
vmCfg := vm.VMConfig{
|
||||||
SandboxID: sandboxID,
|
SandboxID: sandboxID,
|
||||||
KernelPath: layout.KernelPath(m.cfg.WrennDir),
|
KernelPath: m.resolveKernelPath(kernelVersion),
|
||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
VCPUs: 1, // Placeholder; overridden by snapshot.
|
VCPUs: 1, // Placeholder; overridden by snapshot.
|
||||||
MemoryMB: int(header.Metadata.Size / (1024 * 1024)), // Placeholder; overridden by snapshot.
|
MemoryMB: int(header.Metadata.Size / (1024 * 1024)), // Placeholder; overridden by snapshot.
|
||||||
@ -682,6 +723,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
GuestIP: slot.GuestIP,
|
GuestIP: slot.GuestIP,
|
||||||
GatewayIP: slot.TapIP,
|
GatewayIP: slot.TapIP,
|
||||||
NetMask: slot.GuestNetMask,
|
NetMask: slot.GuestNetMask,
|
||||||
|
FirecrackerBin: m.cfg.FirecrackerBin,
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeSnapPath := filepath.Join(pauseDir, snapshot.SnapFileName)
|
resumeSnapPath := filepath.Join(pauseDir, snapshot.SnapFileName)
|
||||||
@ -718,6 +760,9 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
slog.Warn("post-init failed after resume, metadata files may be stale", "sandbox", sandboxID, "error", err)
|
slog.Warn("post-init failed after resume, metadata files may be stale", "sandbox", sandboxID, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch envd version (best-effort).
|
||||||
|
envdVersion, _ := client.FetchVersion(ctx)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
sb := &sandboxState{
|
sb := &sandboxState{
|
||||||
Sandbox: models.Sandbox{
|
Sandbox: models.Sandbox{
|
||||||
@ -731,6 +776,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int)
|
|||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
LastActiveAt: now,
|
LastActiveAt: now,
|
||||||
|
Metadata: m.buildMetadata(envdVersion),
|
||||||
},
|
},
|
||||||
slot: slot,
|
slot: slot,
|
||||||
client: client,
|
client: client,
|
||||||
@ -1099,7 +1145,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
|
|||||||
vmCfg := vm.VMConfig{
|
vmCfg := vm.VMConfig{
|
||||||
SandboxID: sandboxID,
|
SandboxID: sandboxID,
|
||||||
TemplateID: id.UUIDString(templateID),
|
TemplateID: id.UUIDString(templateID),
|
||||||
KernelPath: layout.KernelPath(m.cfg.WrennDir),
|
KernelPath: m.cfg.KernelPath,
|
||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
VCPUs: vcpus,
|
VCPUs: vcpus,
|
||||||
MemoryMB: memoryMB,
|
MemoryMB: memoryMB,
|
||||||
@ -1109,6 +1155,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
|
|||||||
GuestIP: slot.GuestIP,
|
GuestIP: slot.GuestIP,
|
||||||
GatewayIP: slot.TapIP,
|
GatewayIP: slot.TapIP,
|
||||||
NetMask: slot.GuestNetMask,
|
NetMask: slot.GuestNetMask,
|
||||||
|
FirecrackerBin: m.cfg.FirecrackerBin,
|
||||||
}
|
}
|
||||||
|
|
||||||
snapPath := filepath.Join(tmplDir, snapshot.SnapFileName)
|
snapPath := filepath.Join(tmplDir, snapshot.SnapFileName)
|
||||||
@ -1145,6 +1192,9 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
|
|||||||
slog.Warn("post-init failed after template restore, metadata files may be stale", "sandbox", sandboxID, "error", err)
|
slog.Warn("post-init failed after template restore, metadata files may be stale", "sandbox", sandboxID, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch envd version (best-effort).
|
||||||
|
envdVersion, _ := client.FetchVersion(ctx)
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
sb := &sandboxState{
|
sb := &sandboxState{
|
||||||
Sandbox: models.Sandbox{
|
Sandbox: models.Sandbox{
|
||||||
@ -1160,6 +1210,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
|
|||||||
RootfsPath: dmDev.DevicePath,
|
RootfsPath: dmDev.DevicePath,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
LastActiveAt: now,
|
LastActiveAt: now,
|
||||||
|
Metadata: m.buildMetadata(envdVersion),
|
||||||
},
|
},
|
||||||
slot: slot,
|
slot: slot,
|
||||||
client: client,
|
client: client,
|
||||||
|
|||||||
@ -7,10 +7,10 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/events"
|
"git.omukk.dev/wrenn/wrenn/pkg/events"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuditLogger writes audit log entries for user-initiated and system events.
|
// AuditLogger writes audit log entries for user-initiated and system events.
|
||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const jwtExpiry = 6 * time.Hour
|
const jwtExpiry = 6 * time.Hour
|
||||||
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/containrrr/shoutrrr"
|
"github.com/containrrr/shoutrrr"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/events"
|
"git.omukk.dev/wrenn/wrenn/pkg/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deliver sends a notification to a single provider with the given config.
|
// Deliver sends a notification to a single provider with the given config.
|
||||||
@ -8,9 +8,9 @@ import (
|
|||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/events"
|
"git.omukk.dev/wrenn/wrenn/pkg/events"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/events"
|
"git.omukk.dev/wrenn/wrenn/pkg/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FormatMessage produces a human-readable notification string containing
|
// FormatMessage produces a human-readable notification string containing
|
||||||
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/events"
|
"git.omukk.dev/wrenn/wrenn/pkg/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
const streamKey = "wrenn:events"
|
const streamKey = "wrenn:events"
|
||||||
@ -13,10 +13,10 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/events"
|
"git.omukk.dev/wrenn/wrenn/pkg/events"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/validate"
|
"git.omukk.dev/wrenn/wrenn/pkg/validate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Valid providers.
|
// Valid providers.
|
||||||
48
pkg/cpextension/extension.go
Normal file
48
pkg/cpextension/extension.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Package cpextension defines the types for extending the control plane server.
|
||||||
|
// This package is intentionally minimal and dependency-free (relative to internal/)
|
||||||
|
// to avoid import cycles between pkg/cpserver and internal/api.
|
||||||
|
package cpextension
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/config"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/scheduler"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerContext exposes the initialized dependencies that extensions can use
|
||||||
|
// to register routes and start background workers. All fields are read-only
|
||||||
|
// from the extension's perspective.
|
||||||
|
type ServerContext struct {
|
||||||
|
Queries *db.Queries
|
||||||
|
PgPool *pgxpool.Pool
|
||||||
|
Redis *redis.Client
|
||||||
|
HostPool *lifecycle.HostClientPool
|
||||||
|
Scheduler scheduler.HostScheduler
|
||||||
|
CA *auth.CA
|
||||||
|
Audit *audit.AuditLogger
|
||||||
|
JWTSecret []byte
|
||||||
|
Config config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension allows enterprise (or any external) code to plug additional
|
||||||
|
// routes and background workers into the control plane without modifying
|
||||||
|
// the core server.
|
||||||
|
type Extension interface {
|
||||||
|
// RegisterRoutes is called after all core routes are registered.
|
||||||
|
// The chi.Router supports sub-routing, middleware, etc.
|
||||||
|
RegisterRoutes(r chi.Router, ctx ServerContext)
|
||||||
|
|
||||||
|
// BackgroundWorkers returns functions that will be called once with
|
||||||
|
// the application context after the server is fully initialized.
|
||||||
|
// Each function should start its own goroutine(s) and return.
|
||||||
|
BackgroundWorkers(ctx ServerContext) []func(context.Context)
|
||||||
|
}
|
||||||
11
pkg/cpserver/extension.go
Normal file
11
pkg/cpserver/extension.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package cpserver
|
||||||
|
|
||||||
|
import "git.omukk.dev/wrenn/wrenn/pkg/cpextension"
|
||||||
|
|
||||||
|
// ServerContext is an alias for cpextension.ServerContext.
|
||||||
|
// Enterprise code should use this package (pkg/cpserver) as the main entry point.
|
||||||
|
type ServerContext = cpextension.ServerContext
|
||||||
|
|
||||||
|
// Extension is an alias for cpextension.Extension.
|
||||||
|
// Enterprise code should use this package (pkg/cpserver) as the main entry point.
|
||||||
|
type Extension = cpextension.Extension
|
||||||
27
pkg/cpserver/options.go
Normal file
27
pkg/cpserver/options.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package cpserver
|
||||||
|
|
||||||
|
// options holds the configuration for Run.
|
||||||
|
type options struct {
|
||||||
|
version string
|
||||||
|
commit string
|
||||||
|
extensions []Extension
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option configures the control plane server.
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
// WithVersion sets the version and commit strings for logging.
|
||||||
|
func WithVersion(version, commit string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.version = version
|
||||||
|
o.commit = commit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExtensions registers one or more extensions that add routes and
|
||||||
|
// background workers to the control plane.
|
||||||
|
func WithExtensions(exts ...Extension) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.extensions = append(o.extensions, exts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
224
pkg/cpserver/run.go
Normal file
224
pkg/cpserver/run.go
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
package cpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
"git.omukk.dev/wrenn/wrenn/internal/api"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/audit"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/auth/oauth"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/channels"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/config"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/lifecycle"
|
||||||
|
"git.omukk.dev/wrenn/wrenn/pkg/scheduler"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run initializes and starts the control plane server. It blocks until a
|
||||||
|
// SIGINT or SIGTERM signal is received, then shuts down gracefully.
|
||||||
|
//
|
||||||
|
// Extensions registered via WithExtensions get to add routes and start
|
||||||
|
// background workers after the core server is fully initialized.
|
||||||
|
func Run(opts ...Option) {
|
||||||
|
o := &options{
|
||||||
|
version: "dev",
|
||||||
|
commit: "unknown",
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(o)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
})))
|
||||||
|
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
if len(cfg.JWTSecret) < 32 {
|
||||||
|
slog.Error("JWT_SECRET must be at least 32 characters")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Database connection pool.
|
||||||
|
pool, err := pgxpool.New(ctx, cfg.DatabaseURL)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to connect to database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
slog.Error("failed to ping database", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info("connected to database")
|
||||||
|
|
||||||
|
queries := db.New(pool)
|
||||||
|
|
||||||
|
// Redis client.
|
||||||
|
redisOpts, err := redis.ParseURL(cfg.RedisURL)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to parse REDIS_URL", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
rdb := redis.NewClient(redisOpts)
|
||||||
|
defer rdb.Close()
|
||||||
|
|
||||||
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||||
|
slog.Error("failed to ping redis", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info("connected to redis")
|
||||||
|
|
||||||
|
// mTLS is mandatory — parse internal CA for CP↔agent communication.
|
||||||
|
if cfg.CACert == "" || cfg.CAKey == "" {
|
||||||
|
slog.Error("WRENN_CA_CERT and WRENN_CA_KEY are required — mTLS is mandatory for CP↔agent communication")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
ca, err := auth.ParseCA(cfg.CACert, cfg.CAKey)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to parse mTLS CA from environment", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
slog.Info("mTLS enabled: CA loaded")
|
||||||
|
|
||||||
|
// Host client pool — manages Connect RPC clients to host agents.
|
||||||
|
cpCertStore, err := auth.NewCPCertStore(ca)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to issue CP client certificate", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// Renew the CP client certificate periodically so it never expires
|
||||||
|
// while the control plane is running (TTL = 24h, renewal = every 12h).
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(auth.CPCertRenewInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := cpCertStore.Refresh(); err != nil {
|
||||||
|
slog.Error("failed to renew CP client certificate", "error", err)
|
||||||
|
} else {
|
||||||
|
slog.Info("CP client certificate renewed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
hostPool := lifecycle.NewHostClientPoolTLS(auth.CPClientTLSConfig(ca, cpCertStore))
|
||||||
|
slog.Info("host client pool: mTLS enabled")
|
||||||
|
|
||||||
|
// Scheduler — picks a host for each new sandbox (least-loaded, bottleneck-first).
|
||||||
|
hostScheduler := scheduler.NewLeastLoadedScheduler(queries)
|
||||||
|
|
||||||
|
// OAuth provider registry.
|
||||||
|
oauthRegistry := oauth.NewRegistry()
|
||||||
|
if cfg.OAuthGitHubClientID != "" && cfg.OAuthGitHubClientSecret != "" {
|
||||||
|
if cfg.CPPublicURL == "" {
|
||||||
|
slog.Error("CP_PUBLIC_URL must be set when OAuth providers are configured")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
callbackURL := strings.TrimRight(cfg.CPPublicURL, "/") + "/auth/oauth/github/callback"
|
||||||
|
ghProvider := oauth.NewGitHubProvider(cfg.OAuthGitHubClientID, cfg.OAuthGitHubClientSecret, callbackURL)
|
||||||
|
oauthRegistry.Register(ghProvider)
|
||||||
|
slog.Info("registered OAuth provider", "provider", "github")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channels: publisher, service, dispatcher.
|
||||||
|
if len(cfg.EncryptionKeyHex) != 64 {
|
||||||
|
slog.Error("WRENN_ENCRYPTION_KEY must be a hex-encoded 32-byte key (64 hex chars)")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
channelPub := channels.NewPublisher(rdb)
|
||||||
|
channelSvc := &channels.Service{DB: queries, EncKey: cfg.EncryptionKey}
|
||||||
|
channelDispatcher := channels.NewDispatcher(rdb, queries, cfg.EncryptionKey)
|
||||||
|
|
||||||
|
// Shared audit logger with event publishing.
|
||||||
|
al := audit.NewWithPublisher(queries, channelPub)
|
||||||
|
|
||||||
|
// Build the server context that extensions receive.
|
||||||
|
sctx := ServerContext{
|
||||||
|
Queries: queries,
|
||||||
|
PgPool: pool,
|
||||||
|
Redis: rdb,
|
||||||
|
HostPool: hostPool,
|
||||||
|
Scheduler: hostScheduler,
|
||||||
|
CA: ca,
|
||||||
|
Audit: al,
|
||||||
|
JWTSecret: []byte(cfg.JWTSecret),
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// API server.
|
||||||
|
srv := api.New(queries, hostPool, hostScheduler, pool, rdb, []byte(cfg.JWTSecret), oauthRegistry, cfg.OAuthRedirectURL, ca, al, channelSvc, o.extensions, sctx)
|
||||||
|
|
||||||
|
// Start template build workers (2 concurrent).
|
||||||
|
stopBuildWorkers := srv.BuildSvc.StartWorkers(ctx, 2)
|
||||||
|
defer stopBuildWorkers()
|
||||||
|
|
||||||
|
// Start channel event dispatcher.
|
||||||
|
channelDispatcher.Start(ctx)
|
||||||
|
|
||||||
|
// Start host monitor (passive + active reconciliation every 30s).
|
||||||
|
monitor := api.NewHostMonitor(queries, hostPool, al, 30*time.Second)
|
||||||
|
monitor.Start(ctx)
|
||||||
|
|
||||||
|
// Start metrics sampler (records per-team sandbox stats every 10s).
|
||||||
|
sampler := api.NewMetricsSampler(queries, 10*time.Second)
|
||||||
|
sampler.Start(ctx)
|
||||||
|
|
||||||
|
// Start extension background workers.
|
||||||
|
for _, ext := range o.extensions {
|
||||||
|
for _, worker := range ext.BackgroundWorkers(sctx) {
|
||||||
|
worker(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the API handler with the sandbox proxy so that requests with
|
||||||
|
// {port}-{sandbox_id}.{domain} Host headers are routed to the sandbox's
|
||||||
|
// host agent. All other requests pass through to the normal API router.
|
||||||
|
proxyWrapper := api.NewSandboxProxyWrapper(srv.Handler(), queries, hostPool)
|
||||||
|
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: cfg.ListenAddr,
|
||||||
|
Handler: proxyWrapper,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graceful shutdown on signal.
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
sig := <-sigCh
|
||||||
|
slog.Info("received signal, shutting down", "signal", sig)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer shutdownCancel()
|
||||||
|
|
||||||
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
slog.Error("http server shutdown error", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
slog.Info("control plane starting", "addr", cfg.ListenAddr, "version", o.version, "commit", o.commit)
|
||||||
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
slog.Error("http server error", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("control plane stopped")
|
||||||
|
}
|
||||||
@ -111,6 +111,7 @@ type Sandbox struct {
|
|||||||
LastUpdated pgtype.Timestamptz `json:"last_updated"`
|
LastUpdated pgtype.Timestamptz `json:"last_updated"`
|
||||||
TemplateID pgtype.UUID `json:"template_id"`
|
TemplateID pgtype.UUID `json:"template_id"`
|
||||||
TemplateTeamID pgtype.UUID `json:"template_team_id"`
|
TemplateTeamID pgtype.UUID `json:"template_team_id"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SandboxMetricPoint struct {
|
type SandboxMetricPoint struct {
|
||||||
@ -162,6 +163,7 @@ type Template struct {
|
|||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
DefaultUser string `json:"default_user"`
|
DefaultUser string `json:"default_user"`
|
||||||
DefaultEnv []byte `json:"default_env"`
|
DefaultEnv []byte `json:"default_env"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateBuild struct {
|
type TemplateBuild struct {
|
||||||
@ -187,6 +189,7 @@ type TemplateBuild struct {
|
|||||||
SkipPrePost bool `json:"skip_pre_post"`
|
SkipPrePost bool `json:"skip_pre_post"`
|
||||||
DefaultUser string `json:"default_user"`
|
DefaultUser string `json:"default_user"`
|
||||||
DefaultEnv []byte `json:"default_env"`
|
DefaultEnv []byte `json:"default_env"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@ -43,7 +43,7 @@ func (q *Queries) BulkUpdateStatusByIDs(ctx context.Context, arg BulkUpdateStatu
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getSandbox = `-- name: GetSandbox :one
|
const getSandbox = `-- name: GetSandbox :one
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id FROM sandboxes WHERE id = $1
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata FROM sandboxes WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, error) {
|
func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, error) {
|
||||||
@ -67,12 +67,13 @@ func (q *Queries) GetSandbox(ctx context.Context, id pgtype.UUID) (Sandbox, erro
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSandboxByTeam = `-- name: GetSandboxByTeam :one
|
const getSandboxByTeam = `-- name: GetSandboxByTeam :one
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id FROM sandboxes WHERE id = $1 AND team_id = $2
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata FROM sandboxes WHERE id = $1 AND team_id = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetSandboxByTeamParams struct {
|
type GetSandboxByTeamParams struct {
|
||||||
@ -101,6 +102,7 @@ func (q *Queries) GetSandboxByTeam(ctx context.Context, arg GetSandboxByTeamPara
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -127,9 +129,9 @@ func (q *Queries) GetSandboxProxyTarget(ctx context.Context, id pgtype.UUID) (Ge
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertSandbox = `-- name: InsertSandbox :one
|
const insertSandbox = `-- name: InsertSandbox :one
|
||||||
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id)
|
INSERT INTO sandboxes (id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, template_id, template_team_id, metadata)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||||
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id
|
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertSandboxParams struct {
|
type InsertSandboxParams struct {
|
||||||
@ -144,6 +146,7 @@ type InsertSandboxParams struct {
|
|||||||
DiskSizeMb int32 `json:"disk_size_mb"`
|
DiskSizeMb int32 `json:"disk_size_mb"`
|
||||||
TemplateID pgtype.UUID `json:"template_id"`
|
TemplateID pgtype.UUID `json:"template_id"`
|
||||||
TemplateTeamID pgtype.UUID `json:"template_team_id"`
|
TemplateTeamID pgtype.UUID `json:"template_team_id"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (Sandbox, error) {
|
func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (Sandbox, error) {
|
||||||
@ -159,6 +162,7 @@ func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (S
|
|||||||
arg.DiskSizeMb,
|
arg.DiskSizeMb,
|
||||||
arg.TemplateID,
|
arg.TemplateID,
|
||||||
arg.TemplateTeamID,
|
arg.TemplateTeamID,
|
||||||
|
arg.Metadata,
|
||||||
)
|
)
|
||||||
var i Sandbox
|
var i Sandbox
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@ -179,12 +183,13 @@ func (q *Queries) InsertSandbox(ctx context.Context, arg InsertSandboxParams) (S
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listActiveSandboxesByTeam = `-- name: ListActiveSandboxesByTeam :many
|
const listActiveSandboxesByTeam = `-- name: ListActiveSandboxesByTeam :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id FROM sandboxes
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata FROM sandboxes
|
||||||
WHERE team_id = $1 AND status IN ('running', 'paused', 'starting')
|
WHERE team_id = $1 AND status IN ('running', 'paused', 'starting')
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -216,6 +221,7 @@ func (q *Queries) ListActiveSandboxesByTeam(ctx context.Context, teamID pgtype.U
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -228,7 +234,7 @@ func (q *Queries) ListActiveSandboxesByTeam(ctx context.Context, teamID pgtype.U
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listSandboxes = `-- name: ListSandboxes :many
|
const listSandboxes = `-- name: ListSandboxes :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id FROM sandboxes ORDER BY created_at DESC
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata FROM sandboxes ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
||||||
@ -258,6 +264,7 @@ func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -270,7 +277,7 @@ func (q *Queries) ListSandboxes(ctx context.Context) ([]Sandbox, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listSandboxesByHostAndStatus = `-- name: ListSandboxesByHostAndStatus :many
|
const listSandboxesByHostAndStatus = `-- name: ListSandboxesByHostAndStatus :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id FROM sandboxes
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata FROM sandboxes
|
||||||
WHERE host_id = $1 AND status = ANY($2::text[])
|
WHERE host_id = $1 AND status = ANY($2::text[])
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -307,6 +314,7 @@ func (q *Queries) ListSandboxesByHostAndStatus(ctx context.Context, arg ListSand
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -319,7 +327,7 @@ func (q *Queries) ListSandboxesByHostAndStatus(ctx context.Context, arg ListSand
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listSandboxesByTeam = `-- name: ListSandboxesByTeam :many
|
const listSandboxesByTeam = `-- name: ListSandboxesByTeam :many
|
||||||
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id FROM sandboxes
|
SELECT id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata FROM sandboxes
|
||||||
WHERE team_id = $1 AND status NOT IN ('stopped', 'error')
|
WHERE team_id = $1 AND status NOT IN ('stopped', 'error')
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
@ -351,6 +359,7 @@ func (q *Queries) ListSandboxesByTeam(ctx context.Context, teamID pgtype.UUID) (
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -394,6 +403,23 @@ func (q *Queries) UpdateLastActive(ctx context.Context, arg UpdateLastActivePara
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateSandboxMetadata = `-- name: UpdateSandboxMetadata :exec
|
||||||
|
UPDATE sandboxes
|
||||||
|
SET metadata = $2,
|
||||||
|
last_updated = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateSandboxMetadataParams struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateSandboxMetadata(ctx context.Context, arg UpdateSandboxMetadataParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateSandboxMetadata, arg.ID, arg.Metadata)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const updateSandboxRunning = `-- name: UpdateSandboxRunning :one
|
const updateSandboxRunning = `-- name: UpdateSandboxRunning :one
|
||||||
UPDATE sandboxes
|
UPDATE sandboxes
|
||||||
SET status = 'running',
|
SET status = 'running',
|
||||||
@ -403,7 +429,7 @@ SET status = 'running',
|
|||||||
last_active_at = $4,
|
last_active_at = $4,
|
||||||
last_updated = NOW()
|
last_updated = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id
|
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateSandboxRunningParams struct {
|
type UpdateSandboxRunningParams struct {
|
||||||
@ -439,6 +465,7 @@ func (q *Queries) UpdateSandboxRunning(ctx context.Context, arg UpdateSandboxRun
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -448,7 +475,7 @@ UPDATE sandboxes
|
|||||||
SET status = $2,
|
SET status = $2,
|
||||||
last_updated = NOW()
|
last_updated = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id
|
RETURNING id, team_id, host_id, template, status, vcpus, memory_mb, timeout_sec, disk_size_mb, guest_ip, host_ip, created_at, started_at, last_active_at, last_updated, template_id, template_team_id, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateSandboxStatusParams struct {
|
type UpdateSandboxStatusParams struct {
|
||||||
@ -477,6 +504,7 @@ func (q *Queries) UpdateSandboxStatus(ctx context.Context, arg UpdateSandboxStat
|
|||||||
&i.LastUpdated,
|
&i.LastUpdated,
|
||||||
&i.TemplateID,
|
&i.TemplateID,
|
||||||
&i.TemplateTeamID,
|
&i.TemplateTeamID,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -12,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const getTemplateBuild = `-- name: GetTemplateBuild :one
|
const getTemplateBuild = `-- name: GetTemplateBuild :one
|
||||||
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env FROM template_builds WHERE id = $1
|
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env, metadata FROM template_builds WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (TemplateBuild, error) {
|
func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (TemplateBuild, error) {
|
||||||
@ -41,6 +41,7 @@ func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (Templat
|
|||||||
&i.SkipPrePost,
|
&i.SkipPrePost,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -48,7 +49,7 @@ func (q *Queries) GetTemplateBuild(ctx context.Context, id pgtype.UUID) (Templat
|
|||||||
const insertTemplateBuild = `-- name: InsertTemplateBuild :one
|
const insertTemplateBuild = `-- name: InsertTemplateBuild :one
|
||||||
INSERT INTO template_builds (id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, total_steps, template_id, team_id, skip_pre_post)
|
INSERT INTO template_builds (id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, total_steps, template_id, team_id, skip_pre_post)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8, $9, $10, $11)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8, $9, $10, $11)
|
||||||
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env
|
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertTemplateBuildParams struct {
|
type InsertTemplateBuildParams struct {
|
||||||
@ -103,12 +104,13 @@ func (q *Queries) InsertTemplateBuild(ctx context.Context, arg InsertTemplateBui
|
|||||||
&i.SkipPrePost,
|
&i.SkipPrePost,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listTemplateBuilds = `-- name: ListTemplateBuilds :many
|
const listTemplateBuilds = `-- name: ListTemplateBuilds :many
|
||||||
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env FROM template_builds ORDER BY created_at DESC
|
SELECT id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env, metadata FROM template_builds ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, error) {
|
func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, error) {
|
||||||
@ -143,6 +145,7 @@ func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, erro
|
|||||||
&i.SkipPrePost,
|
&i.SkipPrePost,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -156,7 +159,7 @@ func (q *Queries) ListTemplateBuilds(ctx context.Context) ([]TemplateBuild, erro
|
|||||||
|
|
||||||
const updateBuildDefaults = `-- name: UpdateBuildDefaults :exec
|
const updateBuildDefaults = `-- name: UpdateBuildDefaults :exec
|
||||||
UPDATE template_builds
|
UPDATE template_builds
|
||||||
SET default_user = $2, default_env = $3
|
SET default_user = $2, default_env = $3, metadata = $4
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -164,10 +167,16 @@ type UpdateBuildDefaultsParams struct {
|
|||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
DefaultUser string `json:"default_user"`
|
DefaultUser string `json:"default_user"`
|
||||||
DefaultEnv []byte `json:"default_env"`
|
DefaultEnv []byte `json:"default_env"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateBuildDefaults(ctx context.Context, arg UpdateBuildDefaultsParams) error {
|
func (q *Queries) UpdateBuildDefaults(ctx context.Context, arg UpdateBuildDefaultsParams) error {
|
||||||
_, err := q.db.Exec(ctx, updateBuildDefaults, arg.ID, arg.DefaultUser, arg.DefaultEnv)
|
_, err := q.db.Exec(ctx, updateBuildDefaults,
|
||||||
|
arg.ID,
|
||||||
|
arg.DefaultUser,
|
||||||
|
arg.DefaultEnv,
|
||||||
|
arg.Metadata,
|
||||||
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +236,7 @@ SET status = $2,
|
|||||||
started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END,
|
started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END,
|
||||||
completed_at = CASE WHEN $2 IN ('success', 'failed', 'cancelled') THEN NOW() ELSE completed_at END
|
completed_at = CASE WHEN $2 IN ('success', 'failed', 'cancelled') THEN NOW() ELSE completed_at END
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env
|
RETURNING id, name, base_template, recipe, healthcheck, vcpus, memory_mb, status, current_step, total_steps, logs, error, sandbox_id, host_id, created_at, started_at, completed_at, template_id, team_id, skip_pre_post, default_user, default_env, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateBuildStatusParams struct {
|
type UpdateBuildStatusParams struct {
|
||||||
@ -261,6 +270,7 @@ func (q *Queries) UpdateBuildStatus(ctx context.Context, arg UpdateBuildStatusPa
|
|||||||
&i.SkipPrePost,
|
&i.SkipPrePost,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ func (q *Queries) DeleteTemplatesByTeam(ctx context.Context, teamID pgtype.UUID)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getPlatformTemplateByName = `-- name: GetPlatformTemplateByName :one
|
const getPlatformTemplateByName = `-- name: GetPlatformTemplateByName :one
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE team_id = '00000000-0000-0000-0000-000000000000' AND name = $1
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE team_id = '00000000-0000-0000-0000-000000000000' AND name = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
// Check if a global (platform) template exists with the given name.
|
// Check if a global (platform) template exists with the given name.
|
||||||
@ -63,12 +63,13 @@ func (q *Queries) GetPlatformTemplateByName(ctx context.Context, name string) (T
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTemplate = `-- name: GetTemplate :one
|
const getTemplate = `-- name: GetTemplate :one
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE id = $1
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetTemplate(ctx context.Context, id pgtype.UUID) (Template, error) {
|
func (q *Queries) GetTemplate(ctx context.Context, id pgtype.UUID) (Template, error) {
|
||||||
@ -85,12 +86,13 @@ func (q *Queries) GetTemplate(ctx context.Context, id pgtype.UUID) (Template, er
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTemplateByName = `-- name: GetTemplateByName :one
|
const getTemplateByName = `-- name: GetTemplateByName :one
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE team_id = $1 AND name = $2
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE team_id = $1 AND name = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetTemplateByNameParams struct {
|
type GetTemplateByNameParams struct {
|
||||||
@ -113,12 +115,13 @@ func (q *Queries) GetTemplateByName(ctx context.Context, arg GetTemplateByNamePa
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTemplateByTeam = `-- name: GetTemplateByTeam :one
|
const getTemplateByTeam = `-- name: GetTemplateByTeam :one
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000')
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE name = $1 AND (team_id = $2 OR team_id = '00000000-0000-0000-0000-000000000000')
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetTemplateByTeamParams struct {
|
type GetTemplateByTeamParams struct {
|
||||||
@ -141,14 +144,15 @@ func (q *Queries) GetTemplateByTeam(ctx context.Context, arg GetTemplateByTeamPa
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertTemplate = `-- name: InsertTemplate :one
|
const insertTemplate = `-- name: InsertTemplate :one
|
||||||
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id, default_user, default_env)
|
INSERT INTO templates (id, name, type, vcpus, memory_mb, size_bytes, team_id, default_user, default_env, metadata)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env
|
RETURNING name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertTemplateParams struct {
|
type InsertTemplateParams struct {
|
||||||
@ -161,6 +165,7 @@ type InsertTemplateParams struct {
|
|||||||
TeamID pgtype.UUID `json:"team_id"`
|
TeamID pgtype.UUID `json:"team_id"`
|
||||||
DefaultUser string `json:"default_user"`
|
DefaultUser string `json:"default_user"`
|
||||||
DefaultEnv []byte `json:"default_env"`
|
DefaultEnv []byte `json:"default_env"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
|
func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams) (Template, error) {
|
||||||
@ -174,6 +179,7 @@ func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams)
|
|||||||
arg.TeamID,
|
arg.TeamID,
|
||||||
arg.DefaultUser,
|
arg.DefaultUser,
|
||||||
arg.DefaultEnv,
|
arg.DefaultEnv,
|
||||||
|
arg.Metadata,
|
||||||
)
|
)
|
||||||
var i Template
|
var i Template
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@ -187,12 +193,13 @@ func (q *Queries) InsertTemplate(ctx context.Context, arg InsertTemplateParams)
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listTemplates = `-- name: ListTemplates :many
|
const listTemplates = `-- name: ListTemplates :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
||||||
@ -215,6 +222,7 @@ func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -227,7 +235,7 @@ func (q *Queries) ListTemplates(ctx context.Context) ([]Template, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTemplatesByTeam = `-- name: ListTemplatesByTeam :many
|
const listTemplatesByTeam = `-- name: ListTemplatesByTeam :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
// Platform templates are visible to all teams.
|
// Platform templates are visible to all teams.
|
||||||
@ -251,6 +259,7 @@ func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) (
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -263,7 +272,7 @@ func (q *Queries) ListTemplatesByTeam(ctx context.Context, teamID pgtype.UUID) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTemplatesByTeamAndType = `-- name: ListTemplatesByTeamAndType :many
|
const listTemplatesByTeamAndType = `-- name: ListTemplatesByTeamAndType :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE (team_id = $1 OR team_id = '00000000-0000-0000-0000-000000000000') AND type = $2 ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListTemplatesByTeamAndTypeParams struct {
|
type ListTemplatesByTeamAndTypeParams struct {
|
||||||
@ -292,6 +301,7 @@ func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTempla
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -304,7 +314,7 @@ func (q *Queries) ListTemplatesByTeamAndType(ctx context.Context, arg ListTempla
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTemplatesByTeamOnly = `-- name: ListTemplatesByTeamOnly :many
|
const listTemplatesByTeamOnly = `-- name: ListTemplatesByTeamOnly :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE team_id = $1 ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE team_id = $1 ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
// List templates owned by a specific team (NOT including platform templates).
|
// List templates owned by a specific team (NOT including platform templates).
|
||||||
@ -328,6 +338,7 @@ func (q *Queries) ListTemplatesByTeamOnly(ctx context.Context, teamID pgtype.UUI
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -340,7 +351,7 @@ func (q *Queries) ListTemplatesByTeamOnly(ctx context.Context, teamID pgtype.UUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listTemplatesByType = `-- name: ListTemplatesByType :many
|
const listTemplatesByType = `-- name: ListTemplatesByType :many
|
||||||
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env FROM templates WHERE type = $1 ORDER BY created_at DESC
|
SELECT name, type, vcpus, memory_mb, size_bytes, created_at, team_id, id, default_user, default_env, metadata FROM templates WHERE type = $1 ORDER BY created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Template, error) {
|
func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Template, error) {
|
||||||
@ -363,6 +374,7 @@ func (q *Queries) ListTemplatesByType(ctx context.Context, type_ string) ([]Temp
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.DefaultUser,
|
&i.DefaultUser,
|
||||||
&i.DefaultEnv,
|
&i.DefaultEnv,
|
||||||
|
&i.Metadata,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -8,8 +8,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resource overhead reserved for the host OS.
|
// Resource overhead reserved for the host OS.
|
||||||
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HostScheduler selects a host for a new sandbox. Implementations may use
|
// HostScheduler selects a host for a new sandbox. Implementations may use
|
||||||
@ -6,9 +6,9 @@ import (
|
|||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/auth"
|
"git.omukk.dev/wrenn/wrenn/pkg/auth"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/db"
|
"git.omukk.dev/wrenn/wrenn/pkg/db"
|
||||||
"git.omukk.dev/wrenn/wrenn/internal/id"
|
"git.omukk.dev/wrenn/wrenn/pkg/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIKeyService provides API key operations shared between the REST API and the dashboard.
|
// APIKeyService provides API key operations shared between the REST API and the dashboard.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user