From 1c01c5be0295f8f1b4861f4b6e6fcb6daa4caea0 Mon Sep 17 00:00:00 2001 From: Zahar Pecherichny Date: Mon, 3 Jun 2024 09:10:16 +0300 Subject: [PATCH] add watch using genericApi Signed-off-by: Zahar Pecherichny --- package-lock.json | 14 +++++++ package.json | 1 + src/generic_api.ts | 51 +++++++++++++++++++++++++ src/generic_api_test.ts | 84 +++++++++++++++++++++++++++++++++++++++++ src/watch.ts | 13 +++++++ src/watch_test.ts | 29 +++++++++++--- 6 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 src/generic_api.ts create mode 100644 src/generic_api_test.ts diff --git a/package-lock.json b/package-lock.json index c088005144..7c2dc3a3f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "isomorphic-ws": "^5.0.0", "js-yaml": "^4.1.0", "jsonpath-plus": "^8.0.0", + "pluralize": "^8.0.0", "request": "^2.88.0", "rfc4648": "^1.3.0", "stream-buffers": "^3.0.2", @@ -3209,6 +3210,14 @@ "node": ">=8" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "engines": { + "node": ">=4" + } + }, "node_modules/prettier": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", @@ -6767,6 +6776,11 @@ } } }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==" + }, "prettier": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", diff --git a/package.json b/package.json index b5df7e0042..d1f2709dbf 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "isomorphic-ws": "^5.0.0", "js-yaml": "^4.1.0", "jsonpath-plus": "^8.0.0", + "pluralize": "^8.0.0", "request": "^2.88.0", "rfc4648": "^1.3.0", "stream-buffers": "^3.0.2", diff --git a/src/generic_api.ts b/src/generic_api.ts new file mode 100644 index 0000000000..06dc7a1845 --- /dev/null +++ b/src/generic_api.ts @@ -0,0 +1,51 @@ +import querystring = require('querystring'); +import pluralize = require('pluralize'); + +export interface K8sObject { + apiVersion: string; + kind: string; + metadata?: { + name?: string; + namespace?: string; + labels?: object; + }; +} + +export function CreatePath(object: K8sObject, include_name: boolean = true) { + if (!object.apiVersion) { + throw new Error('The object passed must contain apiVersion field') + } + if (!object.kind) { + throw new Error('The object passed must contain kind field') + } + + let path = "" + + // add apiVersion + if (object.apiVersion == "v1") { + path += "/api/v1" + } else { + path += "/apis/" + object.apiVersion + } + + // add namespace if object namespaced + if (object.metadata?.namespace) { + path += "/namespaces/" + object.metadata.namespace + } + + // add object kind + path += "/" + pluralize(object.kind.toLowerCase()) + + // add object name + if (include_name && object.metadata?.name) { + path += "/" + object.metadata.name + } + + // add label selector + const labels = object.metadata?.labels + if (labels) { + path += "?" + querystring.stringify({labelSelector: Object.keys(labels).map(label => label + "=" + labels[label]).join(",")}) + } + + return path +} diff --git a/src/generic_api_test.ts b/src/generic_api_test.ts new file mode 100644 index 0000000000..6d45af49c0 --- /dev/null +++ b/src/generic_api_test.ts @@ -0,0 +1,84 @@ +import { expect } from 'chai'; +import { CreatePath } from './generic_api'; + + + +describe('CreatePath', () => { + it('should convert namespace to path', async () => { + const obj1 = { + apiVersion: "v1", + kind: "Namespace" + }; + + const path1 = CreatePath(obj1); + expect(path1).to.equal("/api/v1/namespaces"); + + const obj2 = { + apiVersion: "v1", + kind: "Namespace", + metadata: { + name: "test" + } + }; + + const path2 = CreatePath(obj2); + expect(path2).to.equal("/api/v1/namespaces/test"); + }); + + it('should convert custom resource to path', async () => { + const obj1 = { + apiVersion: "fake.crd.io/v1", + kind: "fakekind", + metadata: { + name: "fake-name", + namespace: "fake-namespace" + } + }; + + const path1 = CreatePath(obj1); + expect(path1).to.equal("/apis/fake.crd.io/v1/namespaces/fake-namespace/fakekinds/fake-name"); + }); + + it('should convert objects with labels to path with labels selector', async () => { + const obj1 = { + apiVersion: "v1", + kind: "pod", + metadata: { + labels: { + label1: "value1", + label2: "value2" + } + } + }; + + const path1 = CreatePath(obj1); + expect(path1).to.equal("/api/v1/pods?labelSelector=label1%3Dvalue1%2Clabel2%3Dvalue2"); + }); + + it('should convert cluster wide object to path', async () => { + const obj1 = { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "ClusterRoleBinding" + }; + + const path1 = CreatePath(obj1); + expect(path1).to.equal("/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"); + }); + + it('should convert object to path while skipping the name', async () => { + const obj1 = { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "RoleBinding", + metadata: { + name: "fake-name", + namespace: "fake-namespace" + } + }; + + const path1 = CreatePath(obj1); + expect(path1).to.equal("/apis/rbac.authorization.k8s.io/v1/namespaces/fake-namespace/rolebindings/fake-name"); + + const path2 = CreatePath(obj1, false); + expect(path2).to.equal("/apis/rbac.authorization.k8s.io/v1/namespaces/fake-namespace/rolebindings"); + }); +}); diff --git a/src/watch.ts b/src/watch.ts index 99bc665dc3..b297a33dba 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -1,7 +1,9 @@ import byline = require('byline'); +import querystring = require('querystring'); import request = require('request'); import { Duplex } from 'stream'; import { KubeConfig } from './config'; +import { K8sObject, CreatePath } from './generic_api' export interface WatchUpdate { type: string; @@ -137,4 +139,15 @@ export class Watch { req.pipe(stream); return req; } + + // watch by object instead of url path + public async genericWatch( + object: K8sObject, + queryParams: any, + callback: (phase: string, apiObj: any, watchObj?: any) => void, + done: (err: any) => void, + ): Promise { + let path = CreatePath(object, false) + return this.watch(path, queryParams, callback, done) + } } diff --git a/src/watch_test.ts b/src/watch_test.ts index 55cd9011a3..77fed043ff 100644 --- a/src/watch_test.ts +++ b/src/watch_test.ts @@ -108,7 +108,7 @@ describe('Watch', () => { }, }); - const path = '/some/path/to/object'; + const path = '/api/v1/pods'; let doneCalled = false; let doneErr: any; @@ -125,6 +125,23 @@ describe('Watch', () => { expect(doneCalled).to.equal(true); expect(doneErr.toString()).to.equal('Error: some error'); expect(aborted).to.equal(true); + + doneCalled = false; + doneErr = null; + await watch.genericWatch( + { + apiVersion: "v1", + kind: "pod" + }, + {}, + (phase: string, obj: string) => {}, + (err: any) => { + doneCalled = true; + doneErr = err; + }, + ); + expect(doneCalled).to.equal(true); + expect(doneErr.toString()).to.equal('Error: some error'); }); it('should not call watch done callback more than once', async () => { @@ -157,7 +174,7 @@ describe('Watch', () => { when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest); - const path = '/some/path/to/object'; + const path = '/api/v1/pods'; const receivedTypes: string[] = []; const receivedObjects: string[] = []; @@ -223,7 +240,7 @@ describe('Watch', () => { when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest); - const path = '/some/path/to/object'; + const path = '/api/v1/pods'; const receivedTypes: string[] = []; const receivedObjects: string[] = []; @@ -281,7 +298,7 @@ describe('Watch', () => { when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest); - const path = '/some/path/to/object'; + const path = '/api/v1/pods'; const receivedTypes: string[] = []; const receivedObjects: string[] = []; @@ -338,7 +355,7 @@ describe('Watch', () => { when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest); - const path = '/some/path/to/object'; + const path = '/api/v1/pods'; const receivedTypes: string[] = []; const receivedObjects: string[] = []; @@ -385,7 +402,7 @@ describe('Watch', () => { when(fakeRequestor.webRequest(anything())).thenReturn(fakeRequest); - const path = '/some/path/to/object'; + const path = '/api/v1/pods'; const receivedTypes: string[] = []; const receivedObjects: string[] = [];