@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
pnpm add @morten-olsen/k8s-operator @kubernetes/client-node zod
Quick Start
// 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<V1Secret> {
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<typeof specSchema> {
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<typeof Secret>;
constructor(options: CustomResourceOptions<typeof specSchema>) {
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.
import type { V1ConfigMap } from '@kubernetes/client-node';
class ConfigMap extends Resource<V1ConfigMap> {
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:
const mySchema = z.object({
replicas: z.number().min(1),
image: z.string(),
});
class MyResource extends CustomResource<typeof mySchema> {
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)
public reconcile = async () => {
// Your reconciliation logic here
};
Resource Dependencies
Resources can depend on other resources and react to their changes:
constructor(options: CustomResourceOptions<typeof specSchema>) {
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:
// 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
// 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.
const operator = new K8sOperator();
operator.resources
Access to the ResourceService for managing resources.
ResourceService
// 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 objectexists: Whether the resource existsready: Whether the resource is readyname: Resource namenamespace: Resource namespaceapiVersion: Resource API versionkind: Resource kindspec: Resource spec (if applicable)status: Resource status (if applicable)metadata: Resource metadata
Methods
patch(manifest): Create or update the resourceensure(manifest): Update only if different from current stateon(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 kindspec: Zod schema for the specscope:'Namespaced'or'Cluster'labels: Default labels for the resource
Properties
isSeen: Whether the current generation has been observedreconcileTime: Cron pattern for reconciliation
Methods
reconcile(): Override this method with your reconciliation logicqueueReconcile(): Queue a reconciliationmarkReady(): Mark resource as readymarkNotReady(reason, message): Mark resource as not readypatchStatus(status): Update the status subresource
Error Handling
Use NotReadyError to signal that a resource is not ready during reconciliation:
import { NotReadyError } from '@morten-olsen/k8s-operator';
public reconcile = async () => {
if (!this.dependency.ready) {
throw new NotReadyError('DependencyNotReady', 'Waiting for dependency');
}
// Continue reconciliation...
};
Development
# Install dependencies
pnpm install
# Build
pnpm run build
# Run tests
pnpm test
# Lint
pnpm run test:lint
License
AGPL-3.0