Files
box/packages/k8s
Morten Olsen b851dc3006 init
2025-10-23 14:01:06 +02:00
..
2025-10-23 14:01:06 +02:00
2025-10-23 14:01:06 +02:00
2025-10-23 14:01:06 +02:00
2025-10-23 14:01:06 +02:00
2025-10-23 14:01:06 +02:00
2025-10-23 14:01:06 +02:00

@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 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:

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