293 lines
7.2 KiB
Markdown
293 lines
7.2 KiB
Markdown
# @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
|