Files
rose-ash/sx/sx/plans/sx-swarm.sx
giles 31a6e708fc
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 12m0s
more plans
2026-03-09 18:07:23 +00:00

172 lines
8.3 KiB
Plaintext

;; sx-swarm — SX-based Container Orchestration
;; Plan: Docker Swarm management where stack definitions, service configs,
;; and deployment logic are SX s-expressions manipulated by macros.
(defcomp ~plan-sx-swarm-content ()
(~doc-page :title "sx-swarm: Container Orchestration in SX"
(~doc-section :title "Vision" :id "vision"
(p "Replace docker-compose.yml and Docker Swarm stack files with SX. "
"Service definitions are defcomps. Environment configs are macros that expand "
"differently per target (dev, staging, production). Deployments are SX pipelines "
"executed by sx-ci. The entire infrastructure is the same language as the application.")
(p "YAML is a data format pretending to be a configuration language. "
"It has no functions, no macros, no composition, no type checking. "
"SX has all of these. A service definition is a value. A deployment is a function. "
"An environment override is a macro."))
(~doc-section :title "Why Not YAML" :id "why-not-yaml"
(p "Docker Compose and Swarm stack files share a fundamental problem: "
"they're static data with ad-hoc templating bolted on. "
"Variable substitution (${VAR}), extension fields (x-), YAML anchors (&/*) — "
"all workarounds for not having a real language.")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li "No functions — can't abstract repeated patterns")
(li "No macros — can't generate config at definition time")
(li "No conditionals — can't branch on environment")
(li "No types — a typo in an environment variable is a runtime error")
(li "No composition — merging files is fragile and order-dependent")
(li "No verification — can't check constraints before deploy"))
(p "SX solves all of these because it's a programming language, not a data format."))
(~doc-section :title "Service Definitions" :id "services"
(p "Services defined as SX, with macros for common patterns:")
(highlight ";; Base service macro — shared defaults
(defmacro defservice (name &rest body)
`(define ,name
(merge-service
{:restart-policy {:condition :on-failure :max-attempts 3}
:logging {:driver :json-file :options {:max-size \"10m\"}}
:networks (list \"internal\")}
(dict ,@body))))
;; A concrete service
(defservice blog-service
:image \"rose-ash/blog:latest\"
:ports (list \"8001:8000\")
:environment (env-for :blog)
:volumes (list \"./blog:/app/blog:ro\")
:depends-on (list :postgres :redis)
:deploy {:replicas 2
:update-config {:parallelism 1 :delay \"10s\"}})" "lisp")
(p "The " (code "defservice") " macro injects shared defaults. "
"Every service gets restart policy, logging, and network config for free. "
"Override any field by specifying it in the body."))
(~doc-section :title "Environment Macros" :id "environments"
(p "Environment-specific config via macros, not file merging:")
(highlight ";; Environment macro — expands differently per target
(defmacro env-for (service)
(let ((base (service-env-base service))
(target (current-deploy-target)))
(case target
:dev (merge base
{:DEBUG \"true\"
:DATABASE_URL (str \"postgresql://\" service \"_dev/\" service)})
:staging (merge base
{:DATABASE_URL (from-secret (str service \"-db-staging\"))})
:production (merge base
{:DATABASE_URL (from-secret (str service \"-db-prod\"))
:SENTRY_DSN (from-secret \"sentry-dsn\")}))))
;; Volume mounts differ per environment
(defmacro dev-volumes (service)
`(list ,(str \"./\" service \":/app/\" service)
,(str \"./shared:/app/shared\")
\"./dev.sh:/app/dev.sh:ro\"))
;; Production: no bind mounts, just named volumes
(defmacro prod-volumes (service)
`(list ,(str service \"-data:/data\")))" "lisp")
(p "No more docker-compose.yml + docker-compose.dev.yml + docker-compose.prod.yml. "
"One definition, macros handle the rest."))
(~doc-section :title "Stack Composition" :id "composition"
(p "Stacks compose like functions:")
(highlight ";; Infrastructure services shared across all stacks
(define infra-services
(list
(defservice postgres
:image \"postgres:16\"
:volumes (list \"pg-data:/var/lib/postgresql/data\")
:environment {:POSTGRES_PASSWORD (from-secret \"pg-pass\")})
(defservice redis
:image \"redis:7-alpine\"
:volumes (list \"redis-data:/data\"))
(defservice pgbouncer
:image \"edoburu/pgbouncer\"
:depends-on (list :postgres))))
;; Full stack = infra + app services
(define rose-ash-stack
(stack :name \"rose-ash\"
:services (concat infra-services app-services)
:networks (list
(network :name \"internal\" :driver :overlay :internal true)
(network :name \"public\" :driver :overlay))
:volumes (list
(volume :name \"pg-data\" :driver :local)
(volume :name \"redis-data\" :driver :local))))" "lisp"))
(~doc-section :title "Deploy as SX Pipeline" :id "deploy"
(p "Deployment is an sx-ci pipeline, not a shell script:")
(highlight "(define deploy-pipeline
(pipeline :name \"deploy\"
(stage :build
(map (fn (svc)
(docker-build
:context (str \"./\" (get svc \"name\"))
:tag (str \"rose-ash/\" (get svc \"name\") \":\" (git-sha))))
(changed-services)))
(stage :push
(map (fn (svc)
(docker-push (service-image svc)))
(changed-services)))
(stage :deploy
(swarm-deploy rose-ash-stack
:target (current-deploy-target)
:rolling true
:health-check-interval \"5s\"))))" "lisp"))
(~doc-section :title "Swarm Operations as IO" :id "swarm-ops"
(p "Swarm management exposed as SX IO primitives:")
(highlight ";; boundary.sx additions
(io swarm-deploy (stack &key target rolling) dict)
(io swarm-status (&key stack service) list)
(io swarm-scale (service replicas) dict)
(io swarm-rollback (service) dict)
(io swarm-logs (service &key since tail follow) list)
(io swarm-inspect (service) dict)
(io swarm-nodes () list)
(io swarm-secrets (&key create delete) dict)
(io docker-build (&key context tag dockerfile) dict)
(io docker-push (image) dict)" "lisp")
(p "These compose with sx-ci and the forge — push to forge triggers CI, "
"CI runs tests, tests pass, deploy pipeline executes, swarm updates."))
(~doc-section :title "Health and Monitoring" :id "monitoring"
(p "Service health as live SX components:")
(ul :class "space-y-2 text-stone-600 list-disc pl-5"
(li (strong "Dashboard") " — defcomp rendering swarm-status, auto-refreshing via sx-get polling")
(li (strong "Log viewer") " — swarm-logs streamed via SSE, rendered as SX")
(li (strong "Alerts") " — SX predicates on service state, notifications via sx-activity")
(li (strong "Rollback") " — one-click rollback via swarm-rollback IO primitive"))
(p "The monitoring dashboard is an SX page like any other. "
"No Grafana, no separate monitoring stack. Same language, same renderer, same platform."))
(~doc-section :title "Implementation Path" :id "implementation"
(ol :class "space-y-3 text-stone-600 list-decimal pl-5"
(li (strong "Phase 1: Stack definition") " — SX data structures for services, networks, volumes. "
"Compiler to docker-compose.yml / docker stack deploy format.")
(li (strong "Phase 2: Environment macros") " — defmacro for env-for, dev-volumes, prod-volumes. "
"Single source file, multiple targets.")
(li (strong "Phase 3: Deploy pipeline") " — sx-ci integration. Build, push, deploy as SX stages.")
(li (strong "Phase 4: Swarm IO") " — boundary primitives wrapping Docker API. "
"Dashboard and log viewer components.")
(li (strong "Phase 5: Native orchestration") " — replace Docker Swarm entirely. "
"SX-native container scheduling, networking, service discovery. "
"The ultimate goal: no Docker, no Swarm — just SX managing containers directly."))
(p "Phase 1-3 are pragmatic — compile SX to existing Docker tooling. "
"Phase 5 is ambitious — replace Docker tooling with SX. "
"The architecture supports both because the abstraction boundary is clean."))))