Application deployment
Application brings resource definitions, controller RBAC, admission hosting, and the
controller manager into one application definition. Use app.main() instead
of writing argument parsing, signal handling, or client cleanup for each operator.
A complete controller-only application needs one resource group and one handler:
from cloudcoil.models.kubernetes.core.v1 import ConfigMap
from cloudcoil.application import Application
app = Application("settings", leader_election=True)
configs = app.controller(ConfigMap, label_selector="example.com/manage=true")
@configs.reconcile()
async def reconcile(config: ConfigMap) -> ConfigMap:
config.data = {**(config.data or {}), "managed-by": "cloudcoil"}
return config
if __name__ == "__main__":
app.main()
For reusable modules, create a Controller(Model) group and include it with
app.include(group). Definitions register without I/O; import modules explicitly
before generating manifests or starting the runtime.
The Widget operator adds a CRD, ordered stages, three owned child kinds, status and scoped admission. The local demo builds an image, creates TLS credentials and tests the generated deployment.
Install cloudcoil[operator,kubernetes] to host admission with the optional Uvicorn
server. Configure webhook=WebhookServer(tls_secret="widgets-tls") on the
Application and register admission decorators. Controller-only
applications and offline manifest generation do not require an HTTP server.
Use the checkout setup for repository examples.
Generate, install, run
The same executable has three commands:
# Offline: no kubeconfig, discovery, or API requests.
python app.py manifests --image example/widgets:v1 > operator.yaml
# Apply with your installation credentials; wait for CRDs and Deployment rollout.
python app.py install --image example/widgets:v1
# Run with the Pod's ServiceAccount or your local kubeconfig.
python app.py run
The container image must contain your application and dependencies, with its
entry point set to execute the module (for example ENTRYPOINT ["python", "app.py"]).
The generated Deployment adds run as its arguments. Alternatively pass
--command "python app.py" alongside --image. Cloudcoil does not build or publish
the image. --replicas 2 generates two replicas; configure leader election when
only one reconciliation manager should be active.
The namespace defaults to an explicitly supplied Config's namespace, then
CLOUDCOIL_NAMESPACE, then default. An explicit namespace= overrides that
choice and must match a supplied Config. Generated Pods receive their namespace
through the downward API. For the runnable example:
export CLOUDCOIL_NAMESPACE=operators
export CLOUDCOIL_WEBHOOK_CA_FILE=/certs/ca.crt
python examples/widget_operator.py manifests --image example/widgets:v1
python examples/widget_operator.py install --image example/widgets:v1
Create the namespace and widgets-tls Secret first. The Secret contains tls.crt
and tls.key; the certificate must cover widgets.operators.svc. The shared CLI accepts --ca-file (or CLOUDCOIL_WEBHOOK_CA_FILE) for manifests
and installation, so it can start in the Pod using
the mounted certificate and key without that environment variable. TLS defaults
to /var/run/cloudcoil/tls/tls.crt and tls.key, port 9443, behind Service port 443.
Certificate issuance and rotation remain with your certificate/deployment tooling;
restart Pods after replacing serving certificates. Private keys never appear in
generated manifests. Kubernetes webhook TLS requirements.
install uses server-side apply with a named field manager. It waits for each
CRD's Established condition, applies runtime RBAC and the Service/Deployment,
waits for the current Deployment revision to be available, then enables admission
registrations and refreshes discovery. Existing field ownership conflicts fail;
--force explicitly takes ownership. --timeout bounds the whole installation.
Failures leave already applied objects for inspection and retry; installation
does not delete or roll back resources.
For CRD/RBAC setup without webhook registration or a Deployment:
python app.py manifests --without-webhooks
python app.py install --without-webhooks
Installing webhook registration requires an image so the installer can wait for the serving Deployment. For an externally managed server, export manifests and apply them through your deployment system in the same order. During removal, remove admission registrations before removing their server.
Resources and permissions
Primary models annotated with @custom_resource are included in generated CRD
manifests; install applies them. Ordinary generated resources do not imply CRD
installation. Add other owned
definitions through resources=(OtherResource, CRD(...)). Watched dependencies
are not automatically installed: they may belong to another operator. Repeated
definitions of the same CRD name fail, including competing single-version models.
RBAC inference covers the framework's own operations:
| Access | Generated permissions |
|---|---|
| Primary resource | get, list, watch, patch |
| Enabled primary status | patch on /status |
Owned children (owns) |
get, list, watch, create, patch |
Referenced dependencies (watch) |
get, list, watch |
| Enabled Events | create on events.k8s.io/events |
| Leader election | create Leases; get/update the named Lease |
| Arbitrary reconcile/webhook client calls | Declare with RBACRule |
CRDs and newly generated models provide exact plurals and scope. Older generated
models need these declared once in an RBACRule, or can be regenerated.
This avoids guessing irregular plurals or accidentally granting cluster-wide
access. Namespaced rules default to the operator namespace. Use namespace= for
another namespace or all_namespaces=True explicitly; scope="Cluster" describes
cluster-scoped resources. Controller namespace/all_namespaces settings drive
watch permissions, and cluster-scoped owners may watch children across namespaces.
Namespaced admission registrations follow the primary controller namespaces unless a route supplies an explicit
namespace_selector;
webhook-only resources default to the operator namespace. Cluster resources and
all-namespace controllers retain cluster-wide admission matching.
Owned-child write permissions follow the controller watch scope. A mapped
watch only grants read access and does not expand separate write permissions. subresources=("status",) targets only those endpoints;
resource_names=("settings",) restricts named operations where Kubernetes permits it.
Runtime ServiceAccounts receive no implicit permission to install CRDs, edit RBAC,
or register webhooks. Run install using an identity permitted to perform setup;
the deployed application runs run with its generated ServiceAccount. The client
does not inspect Python function bodies to infer arbitrary API access.
Kubernetes RBAC rules.
Running and embedding
await app.run(stop=event) embeds the runtime without replacing signal
handlers. app.main() supplies SIGINT/SIGTERM handling. Owned clients close
after workers and webhook requests have stopped; a supplied Config stays open.
Fatal component errors stop sibling components and propagate. Each Application and
controller runs once; use a new instance for a restart.
Webhook serving runs on every replica independently of manager leadership.
HTTPS /readyz measures admission availability, and /controllers/readyz
measures controller readiness; standby replicas continue receiving admission
traffic. /healthz and /metrics are available on the same listener. For an
operator without webhooks, pass health=HealthServer(...) to expose the manager's
health server. app.manager becomes available during startup for direct
manager readiness and metrics access.
Controllers use await ctx.client(ResourceType) and admission uses
await request.client(ResourceType) for a live client of any kind, defaulting to the request namespace and sharing the operator
connection. Callbacks do not manage connections or their lifetime.
All managed controllers share the operator Config. For controllers targeting
different clusters, use separate operators or the lower-level Manager API.
For a standalone client, webhook server or controller manager, the lower-level
CRD, AdmissionWebhook and Manager remain usable independently.
Common patterns
The pattern examples cover informer get/list,
shared dependencies, existing-resource aggregation, child pruning, finalizers,
multiple controllers, and admission on built-in or externally defined resources.
Use ctx.cached(Kind) for controller snapshots and await ctx.client(Kind) for
live API access. Register standalone policies with @app.validate(Model) and
@app.mutate(Model) without adding a CRD or controller.
Lifespans
Register process resources with @app.lifespan() and leader-only resources with
@app.lifespan(scope="leader"). Each hook pairs setup and cleanup around yield.
An optional LifecycleEvent distinguishes normal shutdown, leadership loss and
failure. See lifespans and leadership for a complete example,
event fields and cleanup ordering.
Definition validation
An initially empty Application is valid while decorators register components. Manifest generation validates the completed registry offline; run validates and freezes it before network startup. Include reusable groups with app.include(group), or create/include them together with app.controller(Model, ...). Duplicate inclusion, ambiguous stage/case order, missing fallbacks, conflicting routes and missing webhook hosting configuration fail explicitly. Registration does not execute handler code.