Kubernetes operator for ephemeral service with auto-ingress.
kind: Ephemeron
apiVersion: ephemerons.qualified.io/v1alpha1
metadata:
name: "foo"
spec:
service:
# The name of the image to use.
image: "nginx"
# The exposed port to route to.
port: 80
# When to kill
expirationTime: "2021-03-01T00:00:00Z"With EPHEMERON_DOMAIN=example.com, creating the above resource makes the service available at foo.example.com until 2021-03-01T00:00:00Z.
The controller is configured with the following environment variables:
EPHEMERON_DOMAIN(required): The main domain to use.
PodReady:TruewhenPodisReady(not necessarily serving).Available:TruewhenServicehas endpoints associated.
.
├── k8s/ Kubernetes manifests
│ ├── ephemerons.yaml - Ephemeron CRD
│ └── example.yaml - Example resource
└── src/
├── api/ Implements the Web API
├── bin/ Executables
│ ├── api.rs - Start Web API
│ ├── crd.rs - Output CRD YAML
│ └── run.rs - Run controller (default-run)
├── controller/ Implements the Controller
├── resource/ Implements the Custom Resource
└── lib.rs
Dev Commands
cargo run: Run controllercargo run --bin crd: Output CRDcargo run --bin api: Start Web API server
Add CRD and wait for Established condition:
kubectl apply -f k8s/ephemerons.yaml
kubectl wait --for=condition=Established crd/ephemerons.qualified.ioRun controller:
EPHEMERON_DOMAIN=example.com cargo runsslip.io can be used for local development
k3d/k3s example:
LB_IP=$(kubectl get svc -o=jsonpath='{.status.loadBalancer.ingress[0].ip}' -n kube-system traefik)
EPHEMERON_DOMAIN="$LB_IP.sslip.io" cargo run
*.10.0.0.1.sslip.ioresolves to10.0.0.1
Add Ephemeron:
# Set environment variable `EXPIRES` and apply `k8s/example.yaml` with it.
# The following example will expire tomorrow.
export EXPIRES=$(date -d "+1 days" -Iseconds --utc)
envsubst < k8s/example.yaml | kubectl apply -f -
# Wait for the `Available` condition
kubectl wait --for=condition=Available ephemeron/exampleCheck that the example is deployed:
host=$(kubectl get eph example -o jsonpath='{.metadata.annotations.host}')
curl $host | grep "<h1>Welcome to nginx!</h1>"Routes
POST /: Create a new service based onpresetspecified in config that lives forlifetimeMinutes.- Request
{preset: String, lifetimeMinutes: u32}. - Response
{id: String, expirationTime: DateTime<Utc>}. Use thisidto control the resource.
- Request
GET /{id}: Get the hostname of the service if available.- Response
{host: Option<String>, expirationTime: DateTime<Utc>, tls: bool}.hostis a string{id}.{domain}when available. Otherwise,null.expirationTimeis when the service is destroyed.tlsis true if TLS is configured.
- Response
PATCH /{id}: Update the expiration time.- Request
{lifetimeMinutes: u32}. - Response
{expirationTime: DateTime<Utc>}. The new expiration date time.
- Request
DELETE /{id}: Delete the resource and any resources it owns.POST /auth: Authenticate with credentials set in config to get token. Other routes requiresAuthorization: Bearer $TOKEN.- Designed to be used by some backend service to authenticate on behalf of its user.
keyshould be kept secret. - Request
{app: String, key: String, uid: String, gid?: String}.uidmust be unique withinapp.gidis an optional id of the group user belongs to. - Response
{token: String}.tokenis a JWT withsubset to{uid}.{app}.
- Designed to be used by some backend service to authenticate on behalf of its user.
Start the server:
EPHEMERON_CONFIG=k8s/api/config.yaml JWT_SECRET=secret cargo run --bin apiGet token using app and key set in config:
curl \
-X POST \
http://localhost:3030/auth \
-H 'Content-Type: application/json' \
-d "{\"app\": \"example\", \"key\": \"apikey\", \"uid\": \"user\"}"Create some service:
curl \
-X POST \
http://localhost:3030/ \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d "{\"preset\": \"nginx\", \"lifetimeMinutes\": 30}"
# {"id": "c0nddh7s3ok4clog56n0"}Get the host. (There's no convenient way to wait until it's ready at the moment and host is null when it's not ready.)
curl -H "Authorization: Bearer $TOKEN" http://localhost:3030/c0nddh7s3ok4clog56n0
# {"host": "c0nddh7s3ok4clog56n0.example.com"}See if it's working:
curl c0nddh7s3ok4clog56n0.example.com | grep "<h1>Welcome to nginx!</h1>"
# <h1>Welcome to nginx!</h1># Create local registry first
k3d registry create dev.localhost
# Find the port
PORT=$(docker port k3d-dev.localhost 5000/tcp | cut -d ':' -f 2)
# Create a new cluster with the registry
k3d cluster create dev --registry-use k3d-dev.localhost:$PORTdocker buildx build --tag ghcr.io/qualified/ephemeron-controller:latest --file ./k8s/controller/Dockerfile .
docker tag ghcr.io/qualified/ephemeron-controller:latest k3d-dev.localhost:$PORT/ephemeron-controller:latest
docker push k3d-dev.localhost:$PORT/ephemeron-controller:latestdocker buildx build --tag ghcr.io/qualified/ephemeron-api:latest --file ./k8s/api/Dockerfile .
docker tag ghcr.io/qualified/ephemeron-api:latest k3d-dev.localhost:$PORT/ephemeron-api:latest
docker push k3d-dev.localhost:$PORT/ephemeron-api:latestkubectl apply -f k8s/controller/sa.yaml
kubectl apply -f k8s/api/sa.yamlLB_IP="$(kubectl get svc -o=jsonpath='{.status.loadBalancer.ingress[0].ip}' -n kube-system traefik)"
export DOMAIN="$LB_IP.sslip.io"
export IMAGE=k3d-dev.localhost:$PORT/ephemeron-controller:latest
envsubst < k8s/controller/deployment.yaml | kubectl apply -f -
export IMAGE=k3d-dev.localhost:$PORT/ephemeron-api:latest
export HOST="api.$DOMAIN"
export JWT_SECRET=$(echo $RANDOM | md5sum | head -c 10)
envsubst < k8s/api/deployment.yaml | kubectl apply -f -Delete all Ephemerons. All the resources owned by them are deleted as well:
kubectl delete ephs --all- Ingress: Name based virtual hosting