A Kubernetes Aggregated API Server where CRUD operations are implemented as shell scripts. No etcd, no in-memory store — scripts read from and write to reality directly.
kubectl get dockercontainer my-app
↓
shell-apiserver
↓
read.sh my-app
↓
docker inspect my-app
↓
JSON response
- No storage backend — reality is the source of truth
- Shell-based handlers — CRUD operations are plain shell scripts
- Standard kubectl — get, describe, delete, watch, -o json/yaml
- Full RBAC — standard ClusterRole/RoleBinding
- OpenAPI/SSA — generated from CRD schemas
- Watch support — streaming events via long-running scripts
- Go 1.23+
- Docker (for the example provider)
# Build and start the server
./hack/run-local.sh
# In another terminal, use the generated kubeconfig
export KUBECONFIG=$(pwd)/.kubeconfig
# List Docker containers as Kubernetes resources
kubectl get dockercontainers
# Get a specific container
kubectl get dockercontainer <container-name> -o yaml$ kubectl get dockercontainers
NAME CREATED AT
my-nginx 2025-01-15T10:30:00Z
my-postgres 2025-01-15T09:00:00Z
$ kubectl get dockercontainer my-nginx -o yaml
apiVersion: shell.io/v1
kind: DockerContainer
metadata:
name: my-nginx
namespace: default
spec:
image: nginx:latest
command: ["nginx", "-g", "daemon off;"]
status:
phase: Running
containerId: abc123...┌─────────────────────────────────────────────────┐
│ kube-apiserver │
│ (aggregation layer) │
└──────────────────┬──────────────────────────────┘
│ APIService registration
▼
┌─────────────────────────────────────────────────┐
│ shell-apiserver (Go) │
│ │
│ HTTP verb → shell executor → runtime.Object │
│ │
│ GET /resource/:name → read.sh <name> │
│ GET /resource → list.sh │
│ POST /resource → create.sh (stdin) │
│ PUT /resource/:name → update.sh (stdin) │
│ DELETE /resource/:name → delete.sh <name> │
│ GET ?watch=true → watch.sh │
└──────────────────┬──────────────────────────────┘
│
┌──────────▼──────────┐
│ provider scripts │
└─────────────────────┘
Each provider is a directory containing a CRD schema and shell scripts:
providers/myresource/
├── schema.yaml # CRD manifest (defines the resource)
├── create.sh # stdin: JSON → stdout: JSON
├── read.sh # args: <name> → stdout: JSON
├── list.sh # stdout: JSON list
├── update.sh # args: <name>, stdin: JSON → stdout: JSON
├── delete.sh # args: <name> → exit 0 or error
└── watch.sh # stdout: stream of JSON events (optional)
Standard Kubernetes CRD format:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myresources.example.io
spec:
group: example.io
names:
kind: MyResource
plural: myresources
singular: myresource
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
field1:
type: string
status:
type: object
properties:
phase:
type: string| Code | HTTP Status | Meaning |
|---|---|---|
| 0 | 200/201 | Success |
| 1 | 500 | Generic error |
| 2 | 404 | Not found |
| 3 | 409 | Conflict |
| 4 | 422 | Invalid input |
Scripts receive these environment variables:
| Variable | Description |
|---|---|
NAMESPACE |
Kubernetes namespace (for namespaced resources) |
#!/bin/bash
NAME="$1"
docker inspect "$NAME" --format '{{json .}}' | jq '{
apiVersion: "shell.io/v1",
kind: "DockerContainer",
metadata: {
name: .Name[1:],
namespace: "default",
creationTimestamp: .Created
},
spec: {
image: .Config.Image,
command: .Config.Cmd,
env: (.Config.Env | to_entries | map({key: .value | split("=")[0], value: .value | split("=")[1:] | join("=")}))
},
status: {
phase: (if .State.Running then "Running" else "Stopped" end),
containerId: .Id
}
}'# Run all tests
go test ./...
# Run with verbose output
go test ./... -v| Flag | Default | Description |
|---|---|---|
--providers-dir |
/providers |
Directory containing shell providers |
--secure-port |
443 |
HTTPS port |
--bind-address |
0.0.0.0 |
Listen address |
| shell-apiserver | shell-operator | Crossplane Provider | |
|---|---|---|---|
| State backend | None (reality) | etcd (CRs) | etcd (CRs) |
| Language | Shell | Shell | Go |
| Owns API | Yes (aggregated) | No (CRD) | Yes (aggregated) |
| Reconcile loop | No | Yes | Yes |
| Complexity | Minimal | Low | High |
- No storage — scripts may use their own persistence, apiserver does not
- No reconcile loop — use shell-operator if you need eventual consistency
- No GitOps — this is imperative, not declarative
- No auto-generated CRDs — schema defined manually
Apache License 2.0. See LICENSE for details.