# @morten-olsen/k8s-operator A TypeScript library for building Kubernetes operators with type-safe custom resource definitions (CRDs) and automatic reconciliation. ## Features - **Type-safe CRDs** with Zod schema validation - **Automatic reconciliation** with configurable intervals - **Resource watching** with event-driven updates - **Built-in resource management** for both native and custom Kubernetes resources - **Status management** with conditions and ready states - **Derived resource handling** with automatic dependencies ## Installation ```bash pnpm add @morten-olsen/k8s-operator @kubernetes/client-node zod ``` ## Quick Start ```typescript // start.ts import { K8sOperator, Resource, CustomResource, type CustomResourceOptions } from '@morten-olsen/k8s-operator'; import type { V1Secret } from '@kubernetes/client-node'; import { z } from 'zod'; // We create a subscribable resouce for the built in secret type class Secret extends Resource { public static readonly apiVersion = 'v1'; public static readonly kind = 'Secret'; } // We create a schema for the spec of our custom Kubernetes definition const specSchema = z.object({ value: z.string(), }); const SECRET_VALUE = Buffer.from('TEST').toString('base64'); class MyCustomResource extends CustomResource { public static readonly apiVersion = 'myoperator.example.com/v1'; public static readonly kind = 'MyCustomResource'; public static readonly spec = specSchema; public static readonly scope = 'Namespaced'; // We create a resource dependency for a derived secret #secret: Resource; constructor(options: CustomResourceOptions) { super(options); // We get a resource reference for our derived secret this.#secret = this.resources.get(Secret, `${this.name}-secret`, this.namespace); // And subscribe the reconciler to any changes to this resource this.#secret.on('changed', this.queueReconcile); } // When the resource or #secret change we run a reconcile // to ensure the secret exists and has the expected value. public reconcile = async () => { // If the derived secret doesn't exist or has incorrect values we update it await this.#secret.ensure({ data: { mysecret: SECRET_VALUE, }, }); }; } const operator = new K8sOperator(); // We install our new CRD into the server await operator.resources.install(MyCustomResource); // We register our resources to ensure all changes are observed await operator.resources.register(Secret, MyCustomResource); ``` ## Core Concepts ### Resources Resources represent Kubernetes objects. They can be built-in types (like `Secret`, `ConfigMap`) or custom resources. ```typescript import type { V1ConfigMap } from '@kubernetes/client-node'; class ConfigMap extends Resource { public static readonly apiVersion = 'v1'; public static readonly kind = 'ConfigMap'; } ``` ### Custom Resources Custom resources extend the `CustomResource` class and define their spec with Zod schemas: ```typescript const mySchema = z.object({ replicas: z.number().min(1), image: z.string(), }); class MyResource extends CustomResource { public static readonly apiVersion = 'example.com/v1'; public static readonly kind = 'MyResource'; public static readonly spec = mySchema; public static readonly scope = 'Namespaced'; // or 'Cluster' } ``` ### Reconciliation The `reconcile` method is called automatically when: - The resource changes - A watched dependency changes - The configured cron interval triggers (default: every 2 minutes) ```typescript public reconcile = async () => { // Your reconciliation logic here }; ``` ### Resource Dependencies Resources can depend on other resources and react to their changes: ```typescript constructor(options: CustomResourceOptions) { super(options); this.#deployment = this.resources.get(Deployment, this.name, this.namespace); this.#deployment.on('changed', this.queueReconcile); } ``` ### Status Management Custom resources have built-in status management: ```typescript // Mark resource as ready await this.markReady(); // Mark resource as not ready with reason await this.markNotReady('ConfigError', 'Missing required configuration'); // Update custom status fields await this.patchStatus({ observedGeneration: this.metadata?.generation, conditions: [ { type: 'Ready', status: 'True', }, ], }); ``` ### Resource Operations ```typescript // Create or update a resource await resource.patch({ metadata: { labels: { app: 'myapp' } }, spec: { replicas: 3 }, }); // Ensure a resource matches desired state (only patches if different) const changed = await resource.ensure({ data: { key: 'value' }, }); // Check if resource exists if (resource.exists) { // Access resource properties const spec = resource.spec; const status = resource.status; } ``` ## API Reference ### K8sOperator Main operator class that manages resources. ```typescript const operator = new K8sOperator(); ``` #### `operator.resources` Access to the `ResourceService` for managing resources. ### ResourceService ```typescript // Install a CRD await operator.resources.install(MyCustomResource); // Register resources for watching await operator.resources.register(MyResource, Secret, ConfigMap); // Get a resource reference const resource = operator.resources.get(MyResource, 'my-name', 'my-namespace'); ``` ### Resource Base class for Kubernetes resources. #### Properties - `manifest`: The Kubernetes object - `exists`: Whether the resource exists - `ready`: Whether the resource is ready - `name`: Resource name - `namespace`: Resource namespace - `apiVersion`: Resource API version - `kind`: Resource kind - `spec`: Resource spec (if applicable) - `status`: Resource status (if applicable) - `metadata`: Resource metadata #### Methods - `patch(manifest)`: Create or update the resource - `ensure(manifest)`: Update only if different from current state - `on(event, handler)`: Listen to resource events ### CustomResource Extends `Resource` with CRD-specific functionality. #### Static Properties - `apiVersion`: CRD API version (e.g., `'example.com/v1'`) - `kind`: CRD kind - `spec`: Zod schema for the spec - `scope`: `'Namespaced'` or `'Cluster'` - `labels`: Default labels for the resource #### Properties - `isSeen`: Whether the current generation has been observed - `reconcileTime`: Cron pattern for reconciliation #### Methods - `reconcile()`: Override this method with your reconciliation logic - `queueReconcile()`: Queue a reconciliation - `markReady()`: Mark resource as ready - `markNotReady(reason, message)`: Mark resource as not ready - `patchStatus(status)`: Update the status subresource ## Error Handling Use `NotReadyError` to signal that a resource is not ready during reconciliation: ```typescript import { NotReadyError } from '@morten-olsen/k8s-operator'; public reconcile = async () => { if (!this.dependency.ready) { throw new NotReadyError('DependencyNotReady', 'Waiting for dependency'); } // Continue reconciliation... }; ``` ## Development ```bash # Install dependencies pnpm install # Build pnpm run build # Run tests pnpm test # Lint pnpm run test:lint ``` ## License AGPL-3.0