Skip to content
GitHub

Custom resources

Kuben’s custom resources are in the kuben.dev/v1alpha1 API group. The manifests ship in the Helm chart (charts/kuben/crds/kuben.dev_all.yaml); the binary embeds the same file and server-side-applies it at every start. Validation is done with the schemas and CEL rules inside the CRDs, so there is no admission webhook to run.

Kuben’s database is the source of desired state (ADR-032). The console, the API and the CLI write to the database; the materializer writes the Project, Environment and App objects from it with server-side apply (field manager kuben-materializer), or, for an app the cluster’s agent delivers, an ApplicationRuntime. The controllers turn those objects into Deployments, Services, CronJobs, volumes and HTTPRoutes.

Read them with kubectl as much as you like. A change made with kubectl or a GitOps tool to an object the materializer wrote is drift: it is recorded on the app, and the object is written again from the database. Change apps through the console, the API or the CLI. KubenConfig is the exception: it belongs to the operator, and is written by the Helm chart from platform.*, by kuben setup, or by hand.

Kind Short name Scope Written by Purpose
KubenConfig kcfg cluster the operator platform-wide settings (the singleton kuben)
Project kproj cluster the materializer a group of environments
Environment kenv cluster the materializer one namespace with quota, network policy and protection
App kapp namespaced (the environment’s) the materializer a deployable unit, reconciled by the App controller
BuildRun kbr namespaced (the build namespace) the build worker one build attempt; owns its Job and source Secret
Release krel namespaced nobody kept in the schema for compatibility; releases are rows in the database
ApplicationRuntime kbrt namespaced the hub, over AgentLink the execution envelope an agent applies (internal)
ExecutionTask kbet namespaced nobody yet reserved for tasks an agent runs (internal)
apiVersion: kuben.dev/v1alpha1
kind: KubenConfig
metadata:
name: kuben
spec:
baseDomain: apps.example.com
gatewayClassName: traefik # Kuben creates and owns kuben-system/kuben
gateway: kuben-system/kuben # or a Gateway you dedicate (label kuben.dev/gateway-owner=kuben)
gatewayPorts: { http: 8000, https: 8443 } # default 80 and 443; Traefik matches its entry points
clusterIssuer: letsencrypt
wildcardTlsSecret: apps-wildcard # optional: one listener for *.<baseDomain>
registry: # optional
prefix: ghcr.io/acme
credentialsSecret: registry-credentials
sizes: # compute presets; these are the defaults
- { name: nano, cpuRequest: 50m, memoryRequest: 64Mi, memoryLimit: 128Mi }
- { name: small, cpuRequest: 100m, memoryRequest: 128Mi, memoryLimit: 256Mi }
- { name: medium, cpuRequest: 250m, memoryRequest: 512Mi, memoryLimit: 1Gi }
- { name: large, cpuRequest: 1, memoryRequest: 2Gi, memoryLimit: 4Gi }

Size presets have no CPU limit by default (cpuLimit is optional), which avoids throttling latency-sensitive apps. Add your own presets or change the defaults; apps reference them by name. The status has a Gateway condition that says whether Kuben can use its Gateway; see Exposing apps and HTTPS.

The Helm chart sets baseDomain, gateway, gatewayClassName, gatewayPorts and clusterIssuer; set the other fields on the resource itself.

apiVersion: kuben.dev/v1alpha1
kind: Project
metadata:
name: shop
spec:
displayName: Online Shop
description: Storefront and its services
previews:
max: 10 # the default
status:
environments: 2

Named <project>-<environment>. It owns one namespace, kb-<project>-<environment>, with Pod Security Admission (baseline enforced, restricted warned), a NetworkPolicy and, with quota, a ResourceQuota.

apiVersion: kuben.dev/v1alpha1
kind: Environment
metadata:
name: shop-production
spec:
project: shop
type: production # standard | production | preview
deletionPolicy: Delete # Retain | Delete; the materializer writes Delete
protection:
requireApprovals: 0
deletionGrace: 168h # soft-delete grace period (production environments)
quota:
cpu: "8"
memory: 16Gi
pods: 50
status:
namespace: kb-shop-production
phase: Ready # Pending | Ready | Terminating | Degraded

type: production marks the environment as protected: deleting it needs the env-delete-protected permission (owners), and the namespace goes only after the grace period. type: preview environments are the preview environments of pull requests; ttl (idle, max) is part of the schema.

apiVersion: kuben.dev/v1alpha1
kind: App
metadata:
name: api
namespace: kb-shop-production
spec:
source:
image: ghcr.io/acme/shop-api@sha256:…
# or git: { repo, branch: main, path, build: { strategy: auto, dockerfile } }
runtime:
processes:
web:
command: [] # image default
port: 8080
size: small
replicas: { min: 2, max: 4 } # max > min enables CPU autoscaling
protocol: http # http | tcp (cluster-internal only)
nightly:
command: ["report", "--yesterday"]
schedule: "0 3 * * *" # runs as a CronJob; no port
timeZone: Europe/Berlin
healthCheck:
path: /healthz
port: 8080
fsGroup: 1000 # for non-root images writing to volumes
env:
- name: LOG_LEVEL
value: info
- name: DATABASE_URL
fromSecret: { name: db-credentials, key: url }
imagePullSecrets: [registry-login]
domains:
- host: api.shop.example.com
tls: auto # auto | none | the name of a Secret
volumes:
- name: data
mountPath: /data
size: 5Gi
storageClass: "" # cluster default
status:
url: https://api.shop.example.com
conditions:
- type: Ready
status: "True"
reason: Available
- type: Exposed
status: "True"
reason: RouteApplied

Notes:

  • source names exactly one of image or git (otherwise Ready is false with InvalidSource). A git source runs once a build has produced an image (AwaitingBuild until then).
  • git.build.strategy is dockerfile, railpack or auto (the default).
  • An environment variable takes one of value, fromSecret or fromService.
  • At most one process may have a port, and a process with schedule may not have one.
  • An app with volumes runs one process with at most one replica, and rolls with Recreate.
  • idle (mode: off | zero | throttle, after; defaults off and 15m) is reserved for scale-to-zero and has no effect yet.
  • An App with the annotation kuben.dev/handover has been handed to the cluster’s agent; the App controller leaves it alone.

The Ready and Exposed reasons are listed under Troubleshooting.

One per build attempt, named kbuild-<attempt> in the build namespace (build.namespace). It owns the attempt’s Job and the Secret with its repository-scoped fetch token, so deleting it removes both. The build worker creates it and follows the Job; the attempt’s state and history are in the database, behind GET …/apps/{app}/builds.

apiVersion: kuben.dev/v1alpha1
kind: BuildRun
metadata:
name: kbuild-0190f3c2-…
spec:
app: 0190f3a8-… # the application target's id
repo: https://github.com/acme/shop-api
gitRef: 3f9c1e… # the commit
path: "" # the build context in the repository
strategy: auto # dockerfile | railpack | auto
image: ghcr.io/acme/shop-api # where the image is pushed

The schema is kept so existing manifests and tools keep validating. Kuben does not create Release objects: releases (the image digest, the configuration revision and the secret revisions an app runs with) are rows in the database, and each deployment run renders from them. See Rollbacks.

Internal resources of the cluster agent (ADR-027). The hub writes one ApplicationRuntime per application target that an agent delivers; it holds the frozen render plan with its generation and input hash, and the agent reports what it applied (effectiveGeneration, inventory) in the status. CEL rules keep the envelope ordered: generation and controlEpoch never decrease, and the same generation always carries the same plan. ExecutionTask (builds, backups, restores, migrations and platform changes run by an agent) is defined but not written by this version. Do not edit either by hand.