All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 12m0s
172 lines
8.3 KiB
Plaintext
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."))))
|