Skip to content

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.