The NodeJS operator framework for Kubernetes is implemented in TypeScript, but can be called from either Javascript or TypeScript.
The operator framework is implemented for server-side use with node
using the @kubernetes/client-node
library.
npm install linode/apl-k8s-operator-node
To implement your operator and watch one or more resources, create a sub-class from Operator
.
import Operator from 'linode/apl-k8s-operator-node';
export default class MyOperator extends Operator {
protected async init() {
// ...
}
}
You can add as many watches as you want from your init()
method, both on standard or custom resources.
Create the singleton instance of your operator in your main()
at startup time and start()
it. Before exiting call stop()
.
const operator = new MyOperator();
await operator.start();
const exit = (reason: string) => {
operator.stop();
process.exit(0);
};
process.on('SIGTERM', () => exit('SIGTERM'))
.on('SIGINT', () => exit('SIGINT'));
You can pass on optional logger to the constructor. It must implement this interface:
interface OperatorLogger {
info(message: string): void;
debug(message: string): void;
warn(message: string): void;
error(message: string): void;
}
protected abstract async init(): Promise<void>
Implement this method on your own operator class to initialize one or more resource watches. Call watchResource()
on as many resources as you need.
NOTE: if you need to initialize other things, place your watches at the end of the init()
method to avoid running the risk of accessing uninitialized dependencies.
protected async watchResource(group: string, version: string, plural: string,
onEvent: (event: ResourceEvent) => Promise<void>, namespace?: string): Promise<void>
Start watching a Kubernetes resource. Pass in the resource's group, version and plural name. For "core" resources group
must be set to an empty string. The last parameter is optional and allows you to limit the watch to the given namespace.
The onEvent
callback will be called for each resource event that comes in from the Kubernetes API.
A resource event is defined as follows:
interface ResourceEvent {
meta: ResourceMeta;
type: ResourceEventType;
object: any;
}
interface ResourceMeta {
name: string;
namespace: string;
id: string;
resourceVersion: string;
apiVersion: string;
kind: string;
}
enum ResourceEventType {
Added = 'ADDED',
Modified = 'MODIFIED',
Deleted = 'DELETED'
}
object
will contain the actual resource object as received from the Kubernetes API.
protected async setResourceStatus(meta: ResourceMeta, status: any): Promise<void>
If your custom resource definition contains a status section you can set the status of your resources using setResourceStatus()
. The resource object to set the status on is identified by passing in the meta
field from the event you received.
protected async patchResourceStatus(meta: ResourceMeta, status: any): Promise<void>
If your custom resource definition contains a status section you can patch the status of your resources using patchResourceStatus()
. The resource object to set the status on is identified by passing in the meta
field from the event you received. status
is a JSON Merge patch object as described in RFC 7386 (https://tools.ietf.org/html/rfc7386).
protected async handleResourceFinalizer(event: ResourceEvent, finalizer: string,
deleteAction: (event: ResourceEvent) => Promise<void>): Promise<boolean>
Handle deletion of your resource using your unique finalizer.
If the resource doesn't have your finalizer set yet, it will be added. If the finalizer is set and the resource is marked for deletion by Kubernetes your deleteAction
action will be called and the finalizer will be removed (so Kubernetes will actually delete it).
If this method returns true
the event is fully handled, if it returns false
you still need to process the added or modified event.
protected async setResourceFinalizers(meta: ResourceMeta, finalizers: string[]): Promise<void>
Set the finalizers on the Kubernetes resource defined by meta
. Typically you will not use this method, but use handleResourceFinalizer
to handle the complete delete logic.
protected async registerCustomResourceDefinition(crdFile: string): Promise<{
group: string;
versions: any;
plural: string;
}>
You can optionally register a custom resource definition from code, to auto-create it when the operator is deployed and first run.
import { CoreV1Api } from '@kubernetes/client-node';
export default class MyOperator extends Operator {
protected async init() {
const coreV1Api = this.kubeConfig.makeApiClient(CoreV1Api)
const listFnBody = coreV1Api.listNamespacedPod('default');
// Call the watchResourceWithInformer with the listFn
await this.informResource(
'v1',
'pods',
async (event) => {
console.log('Received event:', event.type, event.meta.name);
console.log('Object:', event.object);
console.log(new Date().toISOString());
},
listFnBody,
)
}
}
const operator = new MyOperator();
async function main() {
await operator.start()
const exit = (reason: string) => {
console.log(reason)
operator.stop()
process.exit(0)
}
process.on('SIGTERM', () => exit('SIGTERM')).on('SIGINT', () => exit('SIGINT'))
}
main()
export default class MyOperator extends Operator {
protected async init() {
const customObjectsApi = this.kubeConfig.makeApiClient(CustomObjectsApi)
const listFnBody = async () => {
const res = await customObjectsApi.listClusterCustomObject('group.company.com', 'v1alpha1', 'resources');
return {
response: res.response,
body: res.body as KubernetesListObject<KubernetesObject>,
};
};
// Call the watchResourceWithInformer with the listFn
await this.informResource(
'v1alpha1',
'resources',
async (event) => {
console.log('Received event:', event.type, event.meta.name);
console.log('Object:', event.object);
console.log(new Date().toISOString());
},
listFnBody,
'group.company.com',
)
}
}
const operator = new MyOperator();
async function main() {
await operator.start()
const exit = (reason: string) => {
console.log(reason)
operator.stop()
process.exit(0)
}
process.on('SIGTERM', () => exit('SIGTERM')).on('SIGINT', () => exit('SIGINT'))
}
main()
import Operator, { ResourceEventType, ResourceEvent } from 'linode/apl-k8s-operator-node';
export default class MyOperator extends Operator {
protected async init() {
await this.watchResource('', 'v1', 'namespaces', async (e) => {
const object = e.object;
const metadata = object.metadata;
switch (e.type) {
case ResourceEventType.Added:
// do something useful here
break;
case ResourceEventType.Modified:
// do something useful here
break;
case ResourceEventType.Deleted:
// do something useful here
break;
}
});
}
}
You will typically create an interface to define your custom resource:
export interface MyCustomResource extends KubernetesObject {
spec: MyCustomResourceSpec;
status: MyCustomResourceStatus;
}
export interface MyCustomResourceSpec {
foo: string;
bar?: number;
}
export interface MyCustomResourceStatus {
observedGeneration?: number;
}
Your operator can then watch your resource like this:
import Operator, { ResourceEventType, ResourceEvent } from 'linode/apl-k8s-operator-node';
export default class MyOperator extends Operator {
constructor() {
super(/* pass in optional logger*/);
}
protected async init() {
// NOTE: we pass the plural name of the resource
await this.watchResource('groupToWatch', 'v1', 'mycustomresources', async (e) => {
try {
if (e.type === ResourceEventType.Added || e.type === ResourceEventType.Modified) {
if (!await this.handleResourceFinalizer(e, 'mycustomresources', (event) => this.resourceDeleted(event))) {
await this.resourceModified(e);
}
}
} catch (err) {
// Log here...
}
});
}
private async resourceModified(e: ResourceEvent) {
const object = e.object as MyCustomResource;
const metadata = object.metadata;
if (!object.status || object.status.observedGeneration !== metadata.generation) {
// TODO: handle resource modification here
await this.setResourceStatus(e.meta, {
observedGeneration: metadata.generation
});
}
}
private async resourceDeleted(e: ResourceEvent) {
// TODO: handle resource deletion here
}
}
It is possible to register a custom resource definition directly from the operator code, from your init()
method.
Be aware your operator will need the required roles to be able do this. It's recommended to create the CRD as part of the installation of your operator.
import * as Path from 'path';
export default class MyCustomResourceOperator extends Operator {
protected async init() {
const crdFile = Path.resolve(__dirname, '..', 'your-crd.yaml');
const { group, versions, plural } = await this.registerCustomResourceDefinition(crdFile);
await this.watchResource(group, versions[0].name, plural, async (e) => {
// ...
});
}
}
All dependencies of this project are expressed in its package.json
file. Before you start developing, ensure
that you have NPM installed, then run:
npm install
Install an editor plugin like https://github.com/prettier/prettier-vscode and https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig.
Run npm run lint
or install an editor plugin like https://github.com/Microsoft/vscode-typescript-tslint-plugin.