271 lines
7.6 KiB
Markdown
271 lines
7.6 KiB
Markdown
# kuber
|
|
|
|
`kuber` is Astral's internal Docker Compose to Kubernetes translation layer.
|
|
|
|
It reads a local Compose file, renders Kubernetes resources, applies them to the cluster, and can build images remotely from your working tree without requiring Docker on your machine.
|
|
|
|
## What It Does
|
|
|
|
- Reads `compose.yml` or `docker-compose.yml`
|
|
- Converts supported Compose services into Kubernetes resources
|
|
- Applies those resources into a namespace named after the current directory
|
|
- Turns `env_file` entries into Kubernetes Secrets and mounts them through `envFrom`
|
|
- Translates file mounts into ConfigMaps and directory/volume mounts into PVC-backed volumes
|
|
- Builds images on the remote builder by syncing:
|
|
- committed git state
|
|
- tracked local diffs
|
|
- untracked files
|
|
- ignored `.env*` files
|
|
- Supports host-based `ports` syntax that renders Kubernetes `Ingress` rules
|
|
- Supports managed Postgres claims through special pseudo-volumes such as `postgresql:app`
|
|
- Supports managed S3 buckets and credentials through pseudo-volumes such as `s3:app`
|
|
|
|
## Environment Assumptions
|
|
|
|
This tool is built for Astral's cluster and workstation setup. It is not intended to work unchanged outside that environment.
|
|
|
|
Expected local setup:
|
|
|
|
- Tailscale access to the remote builder and Kubernetes network
|
|
- a working `~/.kube/config`
|
|
- `ssh` available locally
|
|
|
|
Not required locally:
|
|
|
|
- Docker
|
|
- `kubectl`
|
|
|
|
Docker is not required because builds happen on the remote builder after `kuber` syncs your repo state there. `kubectl` is not required because cluster access is handled through the bundled Kubernetes client.
|
|
|
|
## Running
|
|
|
|
During development:
|
|
|
|
```bash
|
|
bun run index.ts up
|
|
```
|
|
|
|
Other useful commands:
|
|
|
|
```bash
|
|
bun run index.ts ps
|
|
bun run index.ts logs
|
|
bun run index.ts logs -f
|
|
bun run index.ts exec app sh
|
|
bun run index.ts start
|
|
bun run index.ts stop
|
|
bun run index.ts restart
|
|
bun run index.ts db ls
|
|
```
|
|
|
|
## Commands
|
|
|
|
- `up`: build images if needed, render manifests, apply them, restart deployments, and wait for rollout
|
|
- `start`: same as `up` but skips image builds
|
|
- `stop`: scale managed deployments to zero
|
|
- `restart`: roll out a restart across managed deployments
|
|
- `down`: delete managed resources while keeping ingress, PVCs, managed databases, and managed S3 storage
|
|
- `down -f`: also delete ingress, PVCs, managed database and S3 resources, and the namespace
|
|
- `ps`: list deployments in the current project namespace
|
|
- `logs [deployment]`: print logs for one deployment or all managed deployments
|
|
- `logs -f [deployment]`: follow logs continuously
|
|
- `exec <deployment> <command...>`: execute a command inside a running deployment pod
|
|
- `db ls`: list managed Postgres claims declared in the current Compose file
|
|
- `db creds <service>`: print the generated connection details for a managed Postgres claim
|
|
|
|
## Compose Conventions
|
|
|
|
`kuber` supports a few project-specific Compose conventions on top of normal service translation.
|
|
|
|
### Host-Based Ports
|
|
|
|
If a `ports` entry uses a hostname instead of a numeric published port, `kuber` treats it as an ingress host and routes traffic to the target container port.
|
|
|
|
Example:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
ports:
|
|
- somedomain.astrxl.dev:3000
|
|
```
|
|
|
|
That produces a Kubernetes `Ingress` rule for `somedomain.astrxl.dev` pointing at the service port for container port `3000`.
|
|
|
|
Protected routes use the kuber dialect and render Traefik `IngressRoute` resources instead of plain Kubernetes `Ingress`:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
ports:
|
|
- db.astrxl.dev:4984:protected
|
|
- status.astrxl.dev:3001:protected(/dashboard,/socket.io)
|
|
```
|
|
|
|
Translation rules:
|
|
|
|
- `host:port` -> Kubernetes `Ingress`
|
|
- `host:port:protected` -> Traefik `IngressRoute` with middleware `routing/cf-auth` and host-wide matching
|
|
- `host:port:protected(path1,path2,...)` -> Traefik `IngressRoute` with middleware `routing/cf-auth` and explicit `PathPrefix(...)` matches only
|
|
|
|
### Managed Postgres
|
|
|
|
You can declare a managed Postgres database with a pseudo-volume:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
volumes:
|
|
- postgresql:app
|
|
```
|
|
|
|
Or with an explicit username and database name:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
volumes:
|
|
- postgresql:user/database
|
|
```
|
|
|
|
This creates or reuses the managed CNPG role secret, reconciles the database resource, and injects `DATABASE_URL` into the generated app secret in Kubernetes.
|
|
|
|
### Managed S3
|
|
|
|
Declare a Garage S3 bucket and access key with a pseudo-volume:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
volumes:
|
|
- s3:app
|
|
```
|
|
|
|
This creates `GarageBucket/app` and `GarageKey/app` in `garage-system`. To use different key and bucket names:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
volumes:
|
|
- s3:app-key/shared-assets
|
|
```
|
|
|
|
The Garage operator generates the credentials. `kuber` reads its generated Secret and injects these values into the service's `<service>-env` Secret:
|
|
|
|
- `AWS_ACCESS_KEY_ID`
|
|
- `AWS_SECRET_ACCESS_KEY`
|
|
- `AWS_ENDPOINT_URL_S3`
|
|
- `AWS_REGION`
|
|
- `S3_BUCKET`
|
|
|
|
One service can declare both `postgresql:...` and `s3:...`; all generated values are merged into the same service Secret. Managed Garage buckets and keys are retained by normal `down` and deleted by `down -f`.
|
|
|
|
### Environment Files
|
|
|
|
`env_file` entries are read locally and turned into a Kubernetes `Secret` named `<service>-env`. Deployments then consume that secret through `envFrom`.
|
|
|
|
This is also where generated values such as `DATABASE_URL` and the managed S3 environment are injected.
|
|
|
|
### Volumes
|
|
|
|
`kuber` treats different volume shapes differently:
|
|
|
|
- file bind mounts become ConfigMaps
|
|
- directory bind mounts become PVC-backed mounts
|
|
- named volumes become PVC-backed mounts
|
|
- `tmpfs` becomes `emptyDir` with memory backing
|
|
- `postgresql:...` is treated as a managed database claim, not as a filesystem mount
|
|
- `s3:...` is treated as a managed object-storage claim, not as a filesystem mount
|
|
|
|
Named volumes also support kuber-specific storage hints.
|
|
|
|
Default behavior:
|
|
|
|
```yml
|
|
# compose
|
|
services:
|
|
app:
|
|
volumes:
|
|
- myvolume:/data
|
|
|
|
# effective kuber interpretation
|
|
services:
|
|
app:
|
|
volumes:
|
|
- myvolume(1Gi on 2 fast):/data
|
|
```
|
|
|
|
Short syntax:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
volumes:
|
|
- data(20Gi):/data
|
|
- archive(200Gi on archive):/archive
|
|
- cache(10Gi on 1 fast):/cache
|
|
```
|
|
|
|
Meaning:
|
|
|
|
- `name(20Gi):/path` -> PVC size `20Gi`
|
|
- `name(20Gi on archive):/path` -> PVC size `20Gi`, `diskTag: ["archive"]`, `dataLocality: "none"`
|
|
- `name(20Gi on 1 archive):/path` -> PVC size `20Gi`, `replicaCount: 1`, `diskTag: ["archive"]`, `dataLocality: "none"`
|
|
|
|
Explicit extensions are also supported.
|
|
|
|
Top-level named volume:
|
|
|
|
```yml
|
|
volumes:
|
|
data:
|
|
x-size: 20Gi
|
|
x-diskTag: [archive]
|
|
x-replicaCount: 1
|
|
x-dataLocality: none
|
|
```
|
|
|
|
Long-form service mount:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
volumes:
|
|
- type: volume
|
|
source: data
|
|
target: /data
|
|
volume:
|
|
x-size: 20Gi
|
|
x-diskTag: [archive]
|
|
x-replicaCount: 1
|
|
x-dataLocality: none
|
|
```
|
|
|
|
Precedence:
|
|
|
|
- short syntax like `data(20Gi on 1 archive):/data`
|
|
- long-form `volume.x-*`
|
|
- top-level `volumes.<name>.x-*`
|
|
- fallback default `1Gi on 2 fast`
|
|
|
|
## Building
|
|
|
|
To build distributable binaries:
|
|
|
|
```bash
|
|
bun run build.ts
|
|
```
|
|
|
|
If you only want a plain JavaScript bundle for quick local use in another workspace, build `index.ts` directly:
|
|
|
|
```bash
|
|
bun build index.ts --target bun --minify --sourcemap --outdir dist
|
|
```
|
|
|
|
## Notes
|
|
|
|
- Resource names and namespaces are derived from the current working directory.
|
|
- The remote build flow is optimized for local iteration, not for producing a perfectly clean export of the repository.
|
|
- Managed database support is Kubernetes-only. It injects `DATABASE_URL` into the generated app secret and does not rewrite local `.env` files.
|
|
- `kuber` operates on managed resources in the namespace matching the current directory name.
|