This commit is contained in:
Morten Olsen
2025-10-23 13:47:07 +02:00
commit b851dc3006
91 changed files with 7578 additions and 0 deletions

292
packages/k8s/README.md Normal file
View File

@@ -0,0 +1,292 @@
# @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<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.
```typescript
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:
```typescript
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)
```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<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:
```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