Keep Kubernetes generation simple and composable.
The core rule:
Manifests()runs the World Engine game. It does not automatically provision the whole surrounding platform.
That means:
world.toml
+
deployment options
↓
pkg/k8s.Manifests(...)
↓
game workload Kubernetes objects
NATS and Postgres remain optional building blocks that callers can compose around the game.
Proposed API shape:
func Manifests(
world toml.World,
opts ManifestOptions,
) ([]client.Object, error)Exact types can change, but the responsibility should stay narrow.
Likely inputs include:
type ManifestOptions struct {
Namespace string
Images map[string]ImageRef
NATSURL string
DBDSN string
AuthMode string
ArgusAuthURL string
// Optional routing/deployment details.
Ingress *IngressOptions
}This is illustrative, not final API design.
Manifests() should produce the Kubernetes resources needed to run the game's World Engine workloads.
Likely responsibilities:
- shard
Deployments - shard
Services - pool-size / instance expansion
- workload names
- labels and selectors
- container image references
- container ports
- resource requests and limits
CARDINAL_*runtime environment variablesNATS_URLDB_DSNfor DB-backed shards- authentication-related environment
- optional
Ingress, if public routing is considered part of generic World Engine deployment
Conceptually:
world.toml
+
images
NATS URL
DB DSN
auth config
namespace
↓
Manifests()
↓
Deployment
Deployment
Service
Service
Ingress?
...
Do not put these inside Manifests():
- creating Kubernetes namespaces
- installing NATS
- installing Postgres
- installing an OpenTelemetry Collector
- ArgoCD configuration
- Git operations
- GitHub Actions behavior
- ECR queries
- image-tag selection policy
- branch names
- ephemeral-environment TTL
- environment owner
- automatic reaping
- concurrency limits
- AWS-specific deployment policy
The function should not need access to:
GitHub
AWS
ArgoCD
a Kubernetes cluster
a container registry
Git
It should only transform inputs into Kubernetes objects.
NATS and Postgres can still live in pkg/k8s.
They should just be separate composition helpers.
For example:
func NATSManifests(opts NATSOptions) ([]client.Object, error)
func PostgresManifests(opts PostgresOptions) ([]client.Object, error)Possible structure:
pkg/k8s/
manifests.go
deployment.go
service.go
ingress.go
env.go
nats.go
postgres.go
Avoid creating subpackages until there is enough complexity to justify them.
Local development should remain batteries-included.
world start should compose the helpers automatically.
Conceptually:
objects := []client.Object{}
objects = append(
objects,
k8s.NATSManifests(k8s.NATSOptions{
Namespace: localNamespace,
})...,
)
if world.NeedsDB() {
objects = append(
objects,
k8s.PostgresManifests(k8s.PostgresOptions{
Namespace: localNamespace,
})...,
)
}
objects = append(
objects,
k8s.Manifests(world, k8s.ManifestOptions{
Namespace: localNamespace,
Images: images,
NATSURL: localNATSURL,
DBDSN: localDBDSN,
AuthMode: "DEV",
})...,
)
apply(objects)From the user's perspective:
world startstill just works.
The fact that NATS/Postgres are separate helpers is an implementation detail.
Remote deployment should default to using infrastructure supplied by the environment.
Example:
managed/existing NATS
│
└── NATS_URL
managed/existing Postgres
│
└── DB_DSN
↓
k8s.Manifests()
↓
game workloads
This keeps World Engine compatible with:
- an existing NATS installation
- managed Postgres
- platform-owned databases
- an organisation's own observability stack
- any Kubernetes distribution
World Engine does not need to install infrastructure that the organisation already has.
Rampage may still want per-environment NATS and Postgres.
That is fine.
ephemeralgen can compose the same helpers:
objects := []client.Object{}
objects = append(
objects,
k8s.NATSManifests(... )...,
)
if world.NeedsDB() {
objects = append(
objects,
k8s.PostgresManifests(... )...,
)
}
objects = append(
objects,
k8s.Manifests(world, k8s.ManifestOptions{
Namespace: namespace,
Images: selectedImages,
NATSURL: generatedNATSURL,
DBDSN: generatedDBDSN,
AuthMode: selectedAuthMode,
})...,
)Rampage-specific deployment logic remains outside pkg/k8s:
branch → env slug
TTL
owner
image selection
ECR lookup
ArgoCD registration
ephemeral-envs branch
environment reaping
Avoid this:
Manifests()
├─ create Namespace
├─ create NATS
├─ create Postgres
├─ create OTel
├─ create secrets
├─ create workloads
├─ decide ingress
└─ effectively become a platform installer
Prefer:
NATSManifests() ───────┐
│
PostgresManifests() ───┼─ caller composes
│
Manifests() ───────────┘
This gives:
- simple local DX
- flexible remote deployment
- no hidden platform assumptions
- reusable infrastructure pieces
- easier testing
- easier future removal/replacement of individual dependencies
Manifests() should accept a namespace but should probably not create the Namespace object itself.
For example:
ManifestOptions{
Namespace: "rampage-eph-feature-x",
}Why:
- local
world startalready knows when to create the local namespace - Rampage ephemeral tooling owns ephemeral namespace lifecycle
- platform teams may pre-create namespaces with RBAC, quotas, policies, secrets, etc.
- ArgoCD may manage namespace creation separately
Namespace lifecycle is deployment policy, not game workload generation.
If useful, a separate helper could exist:
func NamespaceManifest(name string) client.Objectbut it should not be implicit.
NATSManifests() should be a convenience helper, not a requirement.
Possible generated resources:
Deployment / StatefulSet
Service
ConfigMap
Secret, if required
The output should give the caller the URL to pass to the game:
nats://nats:4222
or the caller derives it from stable naming.
Manifests() only needs:
NATSURL: "nats://nats:4222"It should not care whether that NATS came from:
NATSManifests()- a Helm chart
- a platform-wide NATS cluster
- managed infrastructure
- another namespace
Same principle.
PostgresManifests() is an optional convenience helper.
Possible generated resources:
StatefulSet / Deployment
Service
Secret
PVC
It can expose or return the DSN:
postgres://...
Manifests() only receives:
DBDSN: "postgres://..."and injects it into shards that require a database.
It should not know how the database was provisioned.
Follow the same pattern.
Prefer:
Manifests()
→ receives OTEL_EXPORTER_OTLP_ENDPOINT
Do not automatically create an OTel Collector unless there is an explicit convenience helper:
func OTelCollectorManifests(opts OTelOptions) ([]client.Object, error)Local or Rampage environments can choose to compose it.
External deployments can point directly at their existing collector.
world.toml
│
▼
pkg/k8s.Manifests()
│
▼
game K8s workloads
/ \
/ \
world start remote deploy
│ │
│ │
NATSManifests() optional platform NATS or
PostgresManifests() NATSManifests()
│ │
▼ ▼
local k3d Kubernetes
The important rule:
Manifests()knows how to run the game. The caller decides what infrastructure surrounds it.