386 lines
12 KiB
Markdown
386 lines
12 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.
|
|
|
|
## Next.js Example
|
|
|
|
[`example/`](example/) contains a documented deployment template for adding kuber to an existing Bun-powered Next.js project without initializing or bundling an application in this repository. It includes a standalone-output Dockerfile, `.dockerignore`, `compose.yml`, and the required Next.js configuration.
|
|
|
|
## Running
|
|
|
|
During development:
|
|
|
|
```bash
|
|
bun run index.ts up
|
|
```
|
|
|
|
Run the dedicated unit suite and type checks:
|
|
|
|
```bash
|
|
bun run test
|
|
bun run typecheck
|
|
```
|
|
|
|
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
|
|
bun run index.ts s3 ls
|
|
bun run index.ts s3 creds app
|
|
bun run index.ts s3 ui app
|
|
```
|
|
|
|
All commands accept `--config` to use a configuration file other than
|
|
`.kuberrc.ts`:
|
|
|
|
```bash
|
|
kuber --config deploy/production.kuberrc.ts up
|
|
kuber up --config deploy/production.kuberrc.ts
|
|
```
|
|
|
|
### Shell Completion
|
|
|
|
Generate and load completions for your shell:
|
|
|
|
```bash
|
|
source <(kuber complete zsh)
|
|
source <(kuber complete bash)
|
|
```
|
|
|
|
For a permanent setup, write the generated script to a file and source it from your shell configuration. Fish and PowerShell are also supported through `kuber complete fish` and `kuber complete powershell`.
|
|
|
|
## Commands
|
|
|
|
- `up`: build images if needed, render manifests, apply them, restart deployments whose image content changed, 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
|
|
- `s3 ls`: list managed S3 claims declared in the current Compose file
|
|
- `s3 creds <service>`: print all generated S3 environment variables for a service
|
|
- `s3 ui <service>`: print the Garage UI object-browser URL for a service bucket
|
|
|
|
## Configuration
|
|
|
|
Kuber optionally loads `.kuberrc.ts` from the working directory. The file must
|
|
default export an object satisfying the published `KuberConfig` type:
|
|
|
|
```ts
|
|
import type { KuberConfig } from "@dmgnr/kuber";
|
|
|
|
export default {
|
|
project: "my-app",
|
|
composeFile: "compose.production.yml",
|
|
registry: "registry.example.com",
|
|
builders: {
|
|
amd64: "kuber@amd-builder",
|
|
arm64: "kuber@arm-builder",
|
|
remoteRoot: "kuber-build",
|
|
},
|
|
rolloutTimeoutMs: 10 * 60_000,
|
|
|
|
async compose(compose) {
|
|
const app = compose.services?.app;
|
|
if (app && !Array.isArray(app.environment)) {
|
|
app.environment ??= {};
|
|
app.environment.NEXT_PUBLIC_BUILD_ID =
|
|
await Bun.$`git rev-parse --short HEAD`
|
|
.text()
|
|
.then((value) => value.trim());
|
|
}
|
|
},
|
|
} satisfies KuberConfig;
|
|
```
|
|
|
|
Operational defaults:
|
|
|
|
- `project`: current working directory name
|
|
- `composeFile`: the first recognized Compose filename in the working directory
|
|
- `registry`: `registry.neko-piranha.ts.net`
|
|
- `builders.amd64`: `kuber@astral-th`
|
|
- `builders.arm64`: `kuber@astral`
|
|
- `builders.remoteRoot`: `kuber-build`
|
|
- `rolloutTimeoutMs`: `300000`
|
|
|
|
Configuration hooks can be synchronous or asynchronous and receive mutable
|
|
values:
|
|
|
|
- `compose(compose, context)`: once after parsing and validation; affects every command that reads Compose
|
|
- `preBuild(compose, context)`: before build eligibility is evaluated when builds are enabled
|
|
- `postBuild(result, context)`: after images are built; `result` contains `built` and `changed` service names
|
|
- `postRender(resources, context)`: after rendering and before reconciliation planning; also runs for `export`
|
|
- `postApply(resources, context)`: after desired resources are successfully applied
|
|
|
|
Hook context contains the resolved `cwd`, `project`, `composeFile`, and optional
|
|
`configFile`. A hook error aborts the command and is reported by the normal CLI
|
|
error handler.
|
|
|
|
## 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`.
|
|
|
|
Single-level wildcard subdomains are supported. Quote wildcard entries so YAML does not treat the leading `*` as an alias:
|
|
|
|
```yml
|
|
services:
|
|
app:
|
|
ports:
|
|
- "*.astrxl.dev:3000"
|
|
- "*.secure.astrxl.dev:3001:protected"
|
|
```
|
|
|
|
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`.
|
|
|
|
Inspect a claim, print its generated credentials, or get its Garage UI URL:
|
|
|
|
```bash
|
|
kuber s3 ls
|
|
kuber s3 creds app
|
|
kuber s3 ui app
|
|
```
|
|
|
|
`s3 creds` prints `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
|
|
`AWS_ENDPOINT_URL_S3`, `AWS_REGION`, and `S3_BUCKET` as shell-style environment
|
|
assignments. `s3 ui` only prints the URL; it does not open a browser.
|
|
|
|
### 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
|
|
|
|
Image builds and image-digest checks run through the selected SSH builder. If a pushed image has the same content fingerprint as the existing registry image, `up` does not restart that deployment.
|
|
|
|
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.
|