Giter Club home page Giter Club logo

k8s-operator-node's Introduction

NodeJS Kubernetes operator framework

Build Status Version node

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.

Installation

npm install @dot-i/k8s-operator

Basic usage

Operator class

To implement your operator and watch one or more resources, create a sub-class from Operator.

import Operator from '@dot-i/k8s-operator';

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'));

Operator methods

constructor

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;
}

init

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.

watchResource

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.

setResourceStatus

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.

patchResourceStatus

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).

handleResourceFinalizer

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.

setResourceFinalizers

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.

registerCustomResourceDefinition

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.

Examples

Operator that watches namespaces

import Operator, { ResourceEventType, ResourceEvent } from '@dot-i/k8s-operator';

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;
            }
        });
    }
}

Operator that watches a custom resource

You will typicall 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 '@dot-i/k8s-operator';

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('dot-i.com', 'v1', 'mycustomresources', async (e) => {
            try {
                if (e.type === ResourceEventType.Added || e.type === ResourceEventType.Modified) {
                    if (!await this.handleResourceFinalizer(e, 'mycustomresources.dot-i.com', (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
    }
}

Register a custom resource definition from the operator

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) => {
            // ...
        });
    }
}

Development

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

Formatting

Install an editor plugin like https://github.com/prettier/prettier-vscode and https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig.

Linting

Run npm run lint or install an editor plugin like https://github.com/Microsoft/vscode-typescript-tslint-plugin.

k8s-operator-node's People

Contributors

dependabot[bot] avatar dot-i avatar nico-francois avatar sbesselsen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

k8s-operator-node's Issues

Import Error

This used to work a couple of months ago ..

I have a file index.mjs contains

import TenantOperator from './modules/operator/tenantoperator.mjs';
var tenantOperator = new TenantOperator();

./modules/operator/tenantoperator.mjs contains this

import Operator from '@dot-i/k8s-operator';
export default class TenantOperator extends Operator {

    constructor() {}
    
    async init() { ... code ... }
}

Node.js V16.18.0 (Same as I had earlier) is now giving this error @dot-i/k8s-operator": "^1.3.5 installed

Uncaught TypeError TypeError: Class extends value #<Object> is not a constructor or null

At first I thought I had a circular reference, but dpdm says I am good

AL that is changed is that I want form a index.js to index.mjs, Im happily importing other commonJS packages

I'm actually stumped at the moment .. Amny suggestions?

Watch resource with 403 Forbidden error

Hi,

there is some kind of incompatibility between k8s versions for the watchResource() observer?

I released an operator that monitors the events of some custom resources and in cluster 1.21.x it works, while in cluster 1.18.x it doesn't.

I get a generic 403 Forbidden error when I try to watch the resources.

The service account with which the operator runs has all verbs enabled and if queried directly, using the same service account, the k8s API respond correctly without any kind of permission problem.

[info][2021-09-03T14:58:27.563Z] [K8S Main Operator] Operator start - undefined
[info][2021-09-03T14:58:27.765Z] watching resource lambdas.company.org/v1 - undefined
[error][2021-09-03T14:58:29.142Z] watch on resource lambdas.company.org/v1 failed: {"name":"Error","message":"Forbidden","stack":"Error: Forbidden\n    at Request.<anonymous> (/operator/main.js:159289:35)\n    at Request.emit (events.js:400:28)\n    at Request../node_modules/request/request.js.Request.onRequestResponse (/operator/main.js:253512:10)\n    at ClientRequest.emit (events.js:400:28)\n    at HTTPParser.parserOnIncomingClient [as onIncoming] (_http_client.js:647:27)\n    at HTTPParser.parserOnHeadersComplete (_http_common.js:126:17)\n    at TLSSocket.socketOnData (_http_client.js:515:22)\n    at TLSSocket.emit (events.js:400:28)\n    at addChunk (internal/streams/readable.js:290:12)\n    at readableAddChunk (internal/streams/readable.js:265:9)"} - undefined
> kubectl auth can-i --list -n devel --as system:serviceaccount:devel:default
Resources                                       Non-Resource URLs   Resource Names         Verbs
routes.company.org                              []                  []                     [*]
functions.company.org                           []                  []                     [*]
lambdas.company.org                             []                  []                     [*]
selfsubjectaccessreviews.authorization.k8s.io   []                  []                     [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  []                     [create]
                                                [/api/*]            []                     [get]
                                                [/api]              []                     [get]
                                                [/apis/*]           []                     [get]
                                                [/apis]             []                     [get]
                                                [/healthz]          []                     [get]
                                                [/healthz]          []                     [get]
                                                [/livez]            []                     [get]
                                                [/livez]            []                     [get]
                                                [/openapi/*]        []                     [get]
                                                [/openapi]          []                     [get]
                                                [/readyz]           []                     [get]
                                                [/readyz]           []                     [get]
                                                [/version/]         []                     [get]
                                                [/version/]         []                     [get]
                                                [/version]          []                     [get]
                                                [/version]          []                     [get]

Some idea?

Thanks!

Support for apiextensions.k8s.io/v1

First of all, thank you for this! This has been really helpful for building an operator in JS.

I was wondering if you can add support for apiextensions.k8s.io/v1 instead of apiextensions.k8s.io/v1beta. I pulled and tried to modify the Operator class, but when I made that change I was unable to receive any events after the first event.

Include source for KubernetesObject typing in README example

In the README, it is not annotated where the KubernetesObject interface comes from in the example where a CRD is declared in TypeScript. After a little bit of searching I realized there is an assumption that developers are also importing @kubernetes/client-node.

I recommend updating the example code block to include the import statement, like so:

// ADD THIS
import { KubernetesObject } from '@kubernetes/client-node';

export interface MyCustomResource extends KubernetesObject {
    spec: MyCustomResourceSpec;
    status: MyCustomResourceStatus;
}

export interface MyCustomResourceSpec {
    foo: string;
    bar?: number;
}

export interface MyCustomResourceStatus {
    observedGeneration?: number;
}

references to verdaccio.qover.io in lockfile

whoever last committed the lock file committed references to a private verdaccio instance. I am unable to npm i on my fork until I deleted the lockfile.

npm i
npm ERR! code ENOTFOUND
npm ERR! syscall getaddrinfo
npm ERR! errno ENOTFOUND
npm ERR! network request to https://verdaccio.qover.io/ws/-/ws-8.13.0.tgz failed, reason: getaddrinfo ENOTFOUND verdaccio.qover.io
npm ERR! network This is a problem related to network connectivity.
npm ERR! network In most cases you are behind a proxy or have bad network settings.
npm ERR! network 
npm ERR! network If you are behind a proxy, please make sure that the
npm ERR! network 'proxy' config is set properly.  See: 'npm help config'

Support for filtering watchResource with labels

As the title says, it would be a great feature to be able to filter watchResource by labels. Perhaps just expose a few of the options available for the watch query params?
e.g.

export type WatchOptions = {
  labelSelector: { [key: string]: string; };
}

protected async watchResource(
  group: string,
  version: string,
  plural: string,
  onEvent: (event: ResourceEvent) => Promise<void>,
  namespace?: string,
  options: WatchOptions,
): Promise<void>

I would be happy to work on this and open a PR.

Btw, I'm curious if your implementation is inspired by any existing solution like the Operator SDK?

Property 'status' does not exist on type 'KubernetesObject'

I have this issue in TS with the current latest version and the example from the README:

import Operator, { ResourceEventType, ResourceEvent } from '@dot-i/k8s-operator';

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('dot-i.com', 'v1', 'mycustomresources', async (e) => {
            try {
                if (e.type === ResourceEventType.Added || e.type === ResourceEventType.Modified) {
                    if (!await this.handleResourceFinalizer(e, 'mycustomresources.dot-i.com', (event) => this.resourceDeleted(event))) {
                        await this.resourceModified(e);
                    }
                }
            } catch (err) {
                // Log here...
            }
        });
    }

    private async resourceModified(e: ResourceEvent) {
        const object = e.object;
        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
    }
}

Error: Property 'status' does not exist on type 'KubernetesObject'
Code: if (!object.status || object.status.observedGeneration !== metadata.generation)

[Info/Feature] Watcher vs Informer

Hi,

I have noticed that the watchResource does not contain any cache which helps while re-listing and re-watching k8s resources. This functionality already exists in the underlying kubernetes@client-node but was surprised to see not being used in this project.

Any idea as to why?

Private logger property

It would be nice if the logger property would be protected instead of private; that would allow its use in the init function.

example app

can you please provide a sample application? :)
that would help me a lot

setResourceStatus and patchResourceStatus throw exceptions and do not work at the moment

Version Info:

Component Version
Kubernetes v1.25.3
k8s-operator-node 1.3.5

I was unable to update the status of my Custom resource and was alwys presented with the error

{"name":"Error","message":"Request failed with status code 404","stack":"Error: Request failed with status code 404\n at Gaxios._request (/path-to-project/node_modules/gaxios/build/src/gaxios.js:130:23)

I checked debugged and it comes down to the path to the resource that is calling kubernetes api

v1/namespaces/<group>/<crd-plural>/<resource-name>/status

When I rebuilt the request in Insomnia and left out the status at the end of the path the update worked but if you dont patches kills all the content but status in your object as well. I don't know if there has been a change in the kubernetes API but this seems to be a bug at least with the current kubernetes version

TypeError: Converting circular structure to JSON

When I attempt to update the status on my custom resource, I get the following error:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'TLSSocket'
    |     property '_httpMessage' -> object with constructor 'ClientRequest'
    --- property 'socket' closes the circle
    at JSON.stringify (<anonymous>)
    at CompletionOperator.errorToJson (/home/oleg/Programming/main/openshift-copilot-poc/operator-in-JavaScript/node_modules/@dot-i/k8s-operator/dist/operator.js:341:21)
    at CompletionOperator.resourceStatusRequest (/home/oleg/Programming/main/openshift-copilot-poc/operator-in-JavaScript/node_modules/@dot-i/k8s-operator/dist/operator.js:332:36)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async CompletionOperator.setResourceStatus (/home/oleg/Programming/main/openshift-copilot-poc/operator-in-JavaScript/node_modules/@dot-i/k8s-operator/dist/operator.js:203:16)
    at async CompletionOperator.resourceModified (/home/oleg/Programming/main/openshift-copilot-poc/operator-in-JavaScript/dist/operator.js:82:13)
    at async Object.onEvent (/home/oleg/Programming/main/openshift-copilot-poc/operator-in-JavaScript/dist/operator.js:40:21)
    at async /home/oleg/Programming/main/openshift-copilot-poc/operator-in-JavaScript/node_modules/@dot-i/k8s-operator/dist/operator.js:96:55

The code throwing this error is the call to this.setResourceStatus in my resourceModified method:

await this.setResourceStatus(e.meta, {
  observedGeneration: object.metadata.generation,
  completion: completion,
}).catch((err) => {
  console.log(err);
});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.