From f04cb8803bbc323529aaff06403ce11c99d4fef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Wed, 7 Feb 2024 17:00:36 +0200 Subject: [PATCH 1/9] feat: generate kube-client library to act as kubernetes proxy --- admission-controller/server/package-lock.json | 152 +++++++++++++++++- admission-controller/server/package.json | 1 + admission-controller/server/src/index.ts | 10 +- .../server/src/utils/get-informer.ts | 61 ------- .../server/src/utils/kube-client.ts | 96 +++++++++++ .../server/src/utils/namespace-getter.ts | 25 --- .../server/src/utils/policy-manager.ts | 2 +- .../server/src/utils/validation-server.ts | 14 +- 8 files changed, 262 insertions(+), 99 deletions(-) delete mode 100644 admission-controller/server/src/utils/get-informer.ts create mode 100644 admission-controller/server/src/utils/kube-client.ts delete mode 100644 admission-controller/server/src/utils/namespace-getter.ts diff --git a/admission-controller/server/package-lock.json b/admission-controller/server/package-lock.json index c783d17..607c573 100644 --- a/admission-controller/server/package-lock.json +++ b/admission-controller/server/package-lock.json @@ -16,6 +16,7 @@ "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.3.2", "pino": "^8.16.1", + "ts-node": "^10.9.2", "type-fest": "^4.5.0" }, "devDependencies": { @@ -23,6 +24,17 @@ "typescript": "^5.2.2" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@fastify/ajv-compiler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", @@ -36,6 +48,28 @@ "resolved": "https://registry.npmjs.org/@fastify/error/-/error-2.0.0.tgz", "integrity": "sha512-wI3fpfDT0t7p8E6dA2eTECzzOd+bZsZCJ2Hcv+Onn2b7ZwK3RwD27uW2QDaMtQhAfWQQP+WNK7nKf0twLsBf9w==" }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@kubernetes/client-node": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.19.0.tgz", @@ -147,6 +181,26 @@ } } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@types/caseless": { "version": "0.12.3", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.3.tgz", @@ -205,6 +259,25 @@ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -225,6 +298,11 @@ "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -426,6 +504,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -477,6 +560,14 @@ "node": ">=0.4.0" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -1026,6 +1117,11 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1667,6 +1763,48 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -1703,7 +1841,6 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1749,6 +1886,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -1822,6 +1964,14 @@ "node": ">= 14" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/zod": { "version": "3.19.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz", diff --git a/admission-controller/server/package.json b/admission-controller/server/package.json index 73f43b8..7520f43 100644 --- a/admission-controller/server/package.json +++ b/admission-controller/server/package.json @@ -17,6 +17,7 @@ "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.3.2", "pino": "^8.16.1", + "ts-node": "^10.9.2", "type-fest": "^4.5.0" }, "devDependencies": { diff --git a/admission-controller/server/src/index.ts b/admission-controller/server/src/index.ts index bf58224..92e9b68 100644 --- a/admission-controller/server/src/index.ts +++ b/admission-controller/server/src/index.ts @@ -1,8 +1,8 @@ import pino from 'pino'; -import {getInformer} from './utils/get-informer.js'; import {MonoklePolicy, MonoklePolicyBinding, PolicyManager} from './utils/policy-manager.js'; import {ValidatorManager} from './utils/validator-manager.js'; import {ValidationServer} from './utils/validation-server.js'; +import KubeClient from "./utils/kube-client.js"; const LOG_LEVEL = (process.env.MONOKLE_LOG_LEVEL || 'warn').toLowerCase(); const IGNORED_NAMESPACES = (process.env.MONOKLE_IGNORE_NAMESPACES || '').split(',').filter(Boolean); @@ -13,7 +13,11 @@ const logger = pino({ }); (async() => { - const policyInformer = await getInformer( + + + KubeClient.buildKubeConfig(); + + const policyInformer = await KubeClient.getInformer( 'monokle.io', 'v1alpha1', 'policies', @@ -22,7 +26,7 @@ const logger = pino({ } ); - const bindingsInformer = await getInformer( + const bindingsInformer = await KubeClient.getInformer( 'monokle.io', 'v1alpha1', 'policybindings', diff --git a/admission-controller/server/src/utils/get-informer.ts b/admission-controller/server/src/utils/get-informer.ts deleted file mode 100644 index 5fb4142..0000000 --- a/admission-controller/server/src/utils/get-informer.ts +++ /dev/null @@ -1,61 +0,0 @@ -import k8s from '@kubernetes/client-node'; - -export type Informer = k8s.Informer & k8s.ObjectCache; - -export type InformerWrapper = { - informer: Informer, - start: () => Promise -} - -const ERROR_RESTART_INTERVAL = 500; - -export async function getInformer( - group: string, version: string, plural: string, onError?: k8s.ErrorCallback -): Promise> { - const informer = await createInformer(group, version, plural, onError); - const start = createInformerStarter(informer, onError); - - return {informer, start} -} - -function createInformerStarter(informer: Informer, onError?: k8s.ErrorCallback) { - return async () => { - let tries = 0; - let started = false; - - while (!started) { - try { - tries++; - await informer.start(); - started = true; - } catch (err: any) { - if (onError) { - onError(err); - } - - await new Promise((resolve) => setTimeout(resolve, ERROR_RESTART_INTERVAL)); - } - } - } -} - -async function createInformer(group: string, version: string, plural: string, onError?: k8s.ErrorCallback) { - const kc = new k8s.KubeConfig(); - kc.loadFromCluster(); - - const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi) - const listFn = () => k8sApi.listClusterCustomObject(group, version, plural); - const informer = k8s.makeInformer(kc, `/apis/${group}/${version}/${plural}`, listFn as any); - - informer.on('error', (err) => { - if (onError) { - onError(err); - } - - setTimeout(async () => { - await informer.start(); - }, ERROR_RESTART_INTERVAL); - }); - - return informer; -} diff --git a/admission-controller/server/src/utils/kube-client.ts b/admission-controller/server/src/utils/kube-client.ts new file mode 100644 index 0000000..444958e --- /dev/null +++ b/admission-controller/server/src/utils/kube-client.ts @@ -0,0 +1,96 @@ +import k8s, {V1Namespace} from "@kubernetes/client-node"; + +class KubeClient { + private static readonly ERROR_RESTART_INTERVAL = 500; + + private readonly kc: k8s.KubeConfig; + + constructor() { + this.kc = new k8s.KubeConfig(); + } + + buildKubeConfig() { + try { + if (process.env.KUBE_CONTEXT) { + this.kc.loadFromDefault(); + this.kc.setCurrentContext(process.env.KUBE_CONTEXT); + } + else { + this.kc.loadFromCluster(); + } + } catch (e) { + console.warn("Failed to load kubeconfig from file"); + } + } + + async getNamespace(name: string): Promise { + return this.kc.makeApiClient(k8s.CoreV1Api).readNamespace(name) + .then(res => res.body) + .catch(err => { + // todo: replace with logger + console.error({msg: 'NamespaceGetter: Failed to get namespace', name, err}); + return undefined; + }) + } + + private createInformer(group: string, version: string, plural: string, onError?: k8s.ErrorCallback) { + const listFn = () => this.kc.makeApiClient(k8s.CustomObjectsApi).listClusterCustomObject(group, version, plural); + const cr = `${group}/${version}/${plural}`; + const informer = k8s.makeInformer(this.kc, `/apis/${cr}`, listFn as any); + + informer.on('error', (err) => { + if (onError) { + onError(err); + } + + setTimeout(async () => { + await informer.start(); + }, KubeClient.ERROR_RESTART_INTERVAL); + }); + + return informer; + } + + private startInformer(informer: Informer, onError?: k8s.ErrorCallback) { + return async () => { + let tries = 0; + let started = false; + + while (!started) { + try { + tries++; + await informer.start(); + started = true; + } catch (err: any) { + if (err.statusCode === 404) { + console.error(`Not found, CRD might not be installed`); + } + if (onError) { + onError(err); + } + + await new Promise((resolve) => setTimeout(resolve, KubeClient.ERROR_RESTART_INTERVAL)); + } + } + } + } + + + async getInformer( + group: string, version: string, plural: string, onError?: k8s.ErrorCallback + ): Promise> { + const informer = await this.createInformer(group, version, plural, onError); + const start = this.startInformer(informer, onError); + + return {informer, start} + } +} + +export type Informer = k8s.Informer & k8s.ObjectCache; + +export type InformerWrapper = { + informer: Informer, + start: () => Promise +} + +export default new KubeClient(); diff --git a/admission-controller/server/src/utils/namespace-getter.ts b/admission-controller/server/src/utils/namespace-getter.ts deleted file mode 100644 index 6167c1c..0000000 --- a/admission-controller/server/src/utils/namespace-getter.ts +++ /dev/null @@ -1,25 +0,0 @@ -import pino from 'pino'; -import k8s, { V1Namespace } from '@kubernetes/client-node'; - -export class NamespaceGetter { - private _k8sApi: k8s.CoreV1Api; - - constructor( - private readonly _logger: ReturnType, - ) { - const kc = new k8s.KubeConfig(); - kc.loadFromCluster(); - - this._k8sApi = kc.makeApiClient(k8s.CoreV1Api); - } - - async getNamespace(name: string): Promise { - try { - const result = await this._k8sApi.readNamespace(name); - return result.body; - } catch (err: any) { - this._logger.error({msg: 'NamespaceGetter: Failed to get namespace', name, err}); - return undefined; - } - } -} diff --git a/admission-controller/server/src/utils/policy-manager.ts b/admission-controller/server/src/utils/policy-manager.ts index f3a5309..0fac272 100644 --- a/admission-controller/server/src/utils/policy-manager.ts +++ b/admission-controller/server/src/utils/policy-manager.ts @@ -2,9 +2,9 @@ import {EventEmitter} from 'events'; import pino from 'pino'; import {KubernetesObject, V1Namespace} from '@kubernetes/client-node'; import {Config} from '@monokle/validation'; -import {InformerWrapper} from './get-informer.js'; import {AdmissionRequestObject} from './validation-server.js'; import {postprocess} from './policy-postprocessor.js'; +import {InformerWrapper} from "./kube-client"; export type MonoklePolicy = KubernetesObject & { spec: Config diff --git a/admission-controller/server/src/utils/validation-server.ts b/admission-controller/server/src/utils/validation-server.ts index e9d3ae3..6033efa 100644 --- a/admission-controller/server/src/utils/validation-server.ts +++ b/admission-controller/server/src/utils/validation-server.ts @@ -5,7 +5,7 @@ import {readFileSync} from 'fs'; import {Message, Resource, RuleLevel, ValidationResult} from '@monokle/validation'; import {V1ObjectMeta} from '@kubernetes/client-node'; import {ValidatorManager} from './validator-manager.js'; -import { NamespaceGetter } from './namespace-getter.js'; +import KubeClient from "./kube-client.js"; export type ValidationServerOptions = { port: number @@ -57,7 +57,6 @@ export type Violation = { export class ValidationServer { private _server: ReturnType; - private _namespaceGetter: NamespaceGetter; constructor( private readonly _validators: ValidatorManager, @@ -70,17 +69,16 @@ export class ValidationServer { ) { try { this._server = fastify({ - https: { - key: readFileSync(path.join('/run/secrets/tls', 'tls.key')), - cert: readFileSync(path.join('/run/secrets/tls', 'tls.crt')) - } + // https: { + // key: readFileSync(path.join('/run/secrets/tls', 'tls.key')), + // cert: readFileSync(path.join('/run/secrets/tls', 'tls.crt')) + // } }); } catch (err) { this._logger.error({msg: 'Failed to read TLS certificate', err}); process.exit(1); } - this._namespaceGetter = new NamespaceGetter(this._logger); this._initRouting(); } @@ -136,7 +134,7 @@ export class ValidationServer { return response; } - const namespaceObject = namespace ? await this._namespaceGetter.getNamespace(namespace) : undefined; + const namespaceObject = namespace ? await KubeClient.getNamespace(namespace) : undefined; this._logger.debug({request: req, namespaceObject}); From a4cb54828df070b2ee4ddcfbe93b3b9db46c5039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Wed, 7 Feb 2024 17:17:26 +0200 Subject: [PATCH 2/9] feat: use kube-client in the init container --- admission-controller/init/src/index.ts | 14 +- .../init/src/utils/kube-client.ts | 163 ++++++++++++++++++ .../init/src/utils/kubernetes.ts | 139 --------------- 3 files changed, 169 insertions(+), 147 deletions(-) create mode 100644 admission-controller/init/src/utils/kube-client.ts delete mode 100644 admission-controller/init/src/utils/kubernetes.ts diff --git a/admission-controller/init/src/index.ts b/admission-controller/init/src/index.ts index aba8271..47ec2f6 100644 --- a/admission-controller/init/src/index.ts +++ b/admission-controller/init/src/index.ts @@ -1,8 +1,7 @@ -import k8s from '@kubernetes/client-node'; import retry from 'async-retry'; import logger, { formatLog } from './utils/logger.js'; import { generateCertificates, isCertExpiring, isCertValid } from './utils/certificates.js'; -import { getSecretCertificate, applySecretCertificate, getWebhookConfiguration, patchWebhookCertificate } from './utils/kubernetes.js'; +import KubeClient from "./utils/kube-client.js"; const NAMESPACE = (process.env.MONOKLE_NAMESPACE || 'monokle-admission-controller'); @@ -33,15 +32,14 @@ const WEBHOOK_NAME = 'monokle-admission-controller-webhook'; // Such order of actions prevents from cases where secret (with cert) is updated but webhook is not. // At the same time, entire process is treated as atomic one, if something goes wrong, retry from the beginning. async function run(_bail: (e: Error) => void, _attempt: number) { - const kc = new k8s.KubeConfig(); - kc.loadFromCluster(); + KubeClient.buildKubeConfig(); - const webhookConfig = await getWebhookConfiguration(NAMESPACE, WEBHOOK_NAME, kc); + const webhookConfig = await KubeClient.getWebhookConfiguration(NAMESPACE, WEBHOOK_NAME); if (!webhookConfig) { throw new Error(`Webhook ${NAMESPACE}/${WEBHOOK_NAME} does not exist.`); } - const existingCert = await getSecretCertificate(NAMESPACE, SECRET_NAME, kc); + const existingCert = await KubeClient.getSecretCertificate(NAMESPACE, SECRET_NAME); if (existingCert && isCertValid(existingCert.certificate) && !isCertExpiring(existingCert.certificate, 90)) { logger.info('Valid cert already exists.'); return; @@ -49,14 +47,14 @@ async function run(_bail: (e: Error) => void, _attempt: number) { const certs = generateCertificates(NAMESPACE, 6); - const webhookPatched = patchWebhookCertificate(NAMESPACE, WEBHOOK_NAME, webhookConfig, certs.caCert, kc); + const webhookPatched = KubeClient.patchWebhookCertificate(NAMESPACE, WEBHOOK_NAME, webhookConfig, certs.caCert); if (!webhookPatched) { throw new Error('Failed to update webhook.'); } logger.info('Webhook patched successfully.'); - const certCreated = await applySecretCertificate(NAMESPACE, SECRET_NAME, certs.serverKey, certs.serverCert, kc); + const certCreated = await KubeClient.applySecretCertificate(NAMESPACE, SECRET_NAME, certs.serverKey, certs.serverCert); if (!certCreated) { throw new Error('Failed to create secret.'); } diff --git a/admission-controller/init/src/utils/kube-client.ts b/admission-controller/init/src/utils/kube-client.ts new file mode 100644 index 0000000..26f794c --- /dev/null +++ b/admission-controller/init/src/utils/kube-client.ts @@ -0,0 +1,163 @@ +import k8s from "@kubernetes/client-node"; +import logger, {formatLog} from "./logger"; +import forge from 'node-forge'; + +export type SecretCertificate = { + secret: k8s.V1Secret; + certificate: forge.pki.Certificate; +} + +class KubeClient { + private readonly kc: k8s.KubeConfig; + + constructor() { + this.kc = new k8s.KubeConfig(); + } + + buildKubeConfig() { + try { + if (process.env.KUBE_CONTEXT) { + this.kc.loadFromDefault(); + this.kc.setCurrentContext(process.env.KUBE_CONTEXT); + } + else { + this.kc.loadFromCluster(); + } + } catch (e) { + console.warn("Failed to load kubeconfig from file"); + } + } + + async getSecretCertificate(namespace: string, name: string): Promise { + const k8sApi = this.kc.makeApiClient(k8s.CoreV1Api); + + let res; + try { + res = await k8sApi.readNamespacedSecret(name, namespace); + + const secret = res.body; + + // Should not happen, means secret data is invalid/corrupted. + if (!secret.data?.['tls.crt']) { + throw new Error(`Secret ${namespace}/${name} does not contain a tls.crt.`); + } + + const certificate = forge.pki.certificateFromPem(Buffer.from(secret.data['tls.crt'], 'base64').toString('utf8')); + + return { + secret, + certificate + } + } catch (err: any) { + if (err.body?.code === 404) { + logger.info(formatLog(`No existing secret ${namespace}/${name}`, err, res)); + } else { + logger.error(formatLog(`Failed to read secret ${namespace}/${name}`, err, res)); + } + + return null; + } + } + + async applySecretCertificate(namespace: string, name: string, pk: forge.pki.PrivateKey, certificate: forge.pki.Certificate) { + const k8sApi = this.kc.makeApiClient(k8s.CoreV1Api); + + const pemPK = forge.pki.privateKeyToPem(pk); + const pemCert = forge.pki.certificateToPem(certificate); + + try { + await k8sApi.deleteNamespacedSecret(name, namespace); + } catch (err: any) { + logger.debug(formatLog(`Failed to delete secret ${namespace}/${name}`), err); + } + + let res; + try { + res = await k8sApi.createNamespacedSecret(namespace, { + apiVersion: 'v1', + kind: 'Secret', + type: 'kubernetes.io/tls', + metadata: { + name, + }, + data: { + 'tls.crt': Buffer.from(pemCert).toString('base64'), + 'tls.key': Buffer.from(pemPK).toString('base64'), + }, + }); + + if (res.response.statusCode !== 201) { + throw new Error(`Failed to apply secret ${namespace}/${name} (non 201 status code)`); + } + + return true; + } catch (err: any) { + logger.error(formatLog(`Failed to apply secret ${namespace}/${name}`, err, res)); + return false; + } + } + + async getWebhookConfiguration(namespace: string, name: string): Promise { + const client = this.kc.makeApiClient(k8s.KubernetesObjectApi); + + let res; + try { + res = await client.read({ + apiVersion: 'admissionregistration.k8s.io/v1', + kind: 'ValidatingWebhookConfiguration', + metadata: { + name, + namespace, + }, + }); + + if (res.response.statusCode !== 200) { + throw new Error(`Failed to get webhook ${namespace}/${name} (non 200 status code)`); + } + + return res.body; + } catch (err: any) { + logger.error(formatLog(`Failed to get webhook ${namespace}/${name}`, err, res)); + return null; + } + } + + async patchWebhookCertificate(namespace: string, name: string, webhook: k8s.V1ValidatingWebhookConfiguration, certificate: forge.pki.Certificate) { + const k8sApi = this.kc.makeApiClient(k8s.AdmissionregistrationV1Api); + + let res; + try { + const webhookConfig = (webhook.webhooks || [])[0]; + if (!webhookConfig) { + throw new Error(`Webhook ${namespace}/${name} does not exist`); + } + + webhookConfig.clientConfig.caBundle = Buffer.from(forge.pki.certificateToPem(certificate)).toString('base64'); + + const resPatch = res = await k8sApi.patchValidatingWebhookConfiguration(name, { + webhooks: [webhookConfig], + metadata: { + labels: { + 'monokle.io/updated': Date.now().toString(), + }, + } + }, + undefined, undefined, 'Monokle', undefined, undefined, { + headers: { + 'Content-Type': 'application/merge-patch+json', + }, + }); + + if (resPatch.response.statusCode !== 200) { + throw new Error(`Failed to patch webhook ${namespace}/${name} (non 200 status code)`); + } + + return true; + } catch (err: any) { + logger.error(formatLog(`Failed to patch webhook ${namespace}/${name}`, err, res)); + return false; + } + } +} + +export default new KubeClient(); diff --git a/admission-controller/init/src/utils/kubernetes.ts b/admission-controller/init/src/utils/kubernetes.ts deleted file mode 100644 index 73c8856..0000000 --- a/admission-controller/init/src/utils/kubernetes.ts +++ /dev/null @@ -1,139 +0,0 @@ -import k8s from '@kubernetes/client-node'; -import forge from 'node-forge'; -import logger, {formatLog} from './logger.js'; - -export type SecretCertificate = { - secret: k8s.V1Secret; - certificate: forge.pki.Certificate; -} - -export async function getSecretCertificate(namespace: string, name: string, config: k8s.KubeConfig): Promise { - const k8sApi = config.makeApiClient(k8s.CoreV1Api); - - let res; - try { - res = await k8sApi.readNamespacedSecret(name, namespace); - - const secret = res.body; - - // Should not happen, means secret data is invalid/corrupted. - if (!secret.data?.['tls.crt']) { - throw new Error(`Secret ${namespace}/${name} does not contain a tls.crt.`); - } - - const certificate = forge.pki.certificateFromPem(Buffer.from(secret.data['tls.crt'], 'base64').toString('utf8')); - - return { - secret, - certificate - } - } catch (err: any) { - if (err.body?.code === 404) { - logger.info(formatLog(`No existing secret ${namespace}/${name}`, err, res)); - } else { - logger.error(formatLog(`Failed to read secret ${namespace}/${name}`, err, res)); - } - - return null; - } -} - -export async function applySecretCertificate(namespace: string, name: string, pk: forge.pki.PrivateKey, certificate: forge.pki.Certificate, config: k8s.KubeConfig) { - const k8sApi = config.makeApiClient(k8s.CoreV1Api); - - const pemPK = forge.pki.privateKeyToPem(pk); - const pemCert = forge.pki.certificateToPem(certificate); - - try { - await k8sApi.deleteNamespacedSecret(name, namespace); - } catch (err: any) { - logger.debug(formatLog(`Failed to delete secret ${namespace}/${name}`), err); - } - - let res; - try { - res = await k8sApi.createNamespacedSecret(namespace, { - apiVersion: 'v1', - kind: 'Secret', - type: 'kubernetes.io/tls', - metadata: { - name, - }, - data: { - 'tls.crt': Buffer.from(pemCert).toString('base64'), - 'tls.key': Buffer.from(pemPK).toString('base64'), - }, - }); - - if (res.response.statusCode !== 201) { - throw new Error(`Failed to apply secret ${namespace}/${name} (non 201 status code)`); - } - - return true; - } catch (err: any) { - logger.error(formatLog(`Failed to apply secret ${namespace}/${name}`, err, res)); - return false; - } -} - -export async function getWebhookConfiguration(namespace: string, name: string, config: k8s.KubeConfig): Promise { - const client = k8s.KubernetesObjectApi.makeApiClient(config); - - let res; - try { - res = await client.read({ - apiVersion: 'admissionregistration.k8s.io/v1', - kind: 'ValidatingWebhookConfiguration', - metadata: { - name, - namespace, - }, - }); - - if (res.response.statusCode !== 200) { - throw new Error(`Failed to get webhook ${namespace}/${name} (non 200 status code)`); - } - - return res.body; - } catch (err: any) { - logger.error(formatLog(`Failed to get webhook ${namespace}/${name}`, err, res)); - return null; - } -} - -export async function patchWebhookCertificate(namespace: string, name: string, webhook: k8s.V1ValidatingWebhookConfiguration, certificate: forge.pki.Certificate, config: k8s.KubeConfig) { - const k8sApi = config.makeApiClient(k8s.AdmissionregistrationV1Api); - - let res; - try { - const webhookConfig = (webhook.webhooks || [])[0]; - if (!webhookConfig) { - throw new Error(`Webhook ${namespace}/${name} does not exist`); - } - - webhookConfig.clientConfig.caBundle = Buffer.from(forge.pki.certificateToPem(certificate)).toString('base64'); - - const resPatch = res = await k8sApi.patchValidatingWebhookConfiguration(name, { - webhooks: [webhookConfig], - metadata: { - labels: { - 'monokle.io/updated': Date.now().toString(), - }, - } - }, - undefined, undefined, 'Monokle', undefined, undefined, { - headers: { - 'Content-Type': 'application/merge-patch+json', - }, - }); - - if (resPatch.response.statusCode !== 200) { - throw new Error(`Failed to patch webhook ${namespace}/${name} (non 200 status code)`); - } - - return true; - } catch (err: any) { - logger.error(formatLog(`Failed to patch webhook ${namespace}/${name}`, err, res)); - return false; - } -} From 0886b908c22b4b046f69e048459ed8b29dfb1790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Thu, 8 Feb 2024 11:27:01 +0200 Subject: [PATCH 3/9] chore: dev scripts & helpers --- scripts/certgen.sh | 15 ++ scripts/cluster.yaml | 7 + scripts/dev-static-install.yaml | 244 ++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 scripts/certgen.sh create mode 100644 scripts/cluster.yaml create mode 100644 scripts/dev-static-install.yaml diff --git a/scripts/certgen.sh b/scripts/certgen.sh new file mode 100644 index 0000000..ab99482 --- /dev/null +++ b/scripts/certgen.sh @@ -0,0 +1,15 @@ +mkdir certs + +openssl genrsa -out certs/ca.key 2048 +openssl req -x509 -new -nodes -key certs/ca.key -days 100000 -out certs/ca.crt -subj "/CN=admission_ca" + +echo CA B64 Bundle: $(cat certs/ca.crt | base64 | tr -d '\n') + +openssl genrsa -out certs/server.key 2048 +openssl req -new -key certs/server.key -out certs/server.csr -subj "/CN=host.docker.internal" -addext "subjectAltName = DNS:host.docker.internal" +openssl x509 -req -in certs/server.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial -out certs/server.crt -days 100000 -extfile <(echo subjectAltName = DNS:host.docker.internal) + +echo Server Key: ../../../scripts/certs/server.key +echo Server Crt: ../../../scripts/certs/server.crt + +echo CA Bundle: kubectl patch validatingwebhookconfigurations monokle-admission-controller-webhook --type=json -p=\'[\{\"op\":\"replace\",\"path\":\"/webhooks/0/clientConfig/caBundle\",\"value\":\"$(cat certs/ca.crt | base64 | tr -d '\n')\"\}]\' diff --git a/scripts/cluster.yaml b/scripts/cluster.yaml new file mode 100644 index 0000000..eed38d3 --- /dev/null +++ b/scripts/cluster.yaml @@ -0,0 +1,7 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +nodes: + - role: control-plane + image: kindest/node:v1.28.6 + - role: worker + image: kindest/node:v1.28.6 diff --git a/scripts/dev-static-install.yaml b/scripts/dev-static-install.yaml new file mode 100644 index 0000000..b92c45a --- /dev/null +++ b/scripts/dev-static-install.yaml @@ -0,0 +1,244 @@ +--- +# Source: monokle-admission-controller/templates/service-account.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: monokle-policies-sa + namespace: monokle + labels: + helm.sh/chart: monokle-admission-controller-0.2.7 + app.kubernetes.io/name: monokle-admission-controller + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: "0.2.7" + app.kubernetes.io/managed-by: Helm +--- +# Source: monokle-admission-controller/templates/monokle-policy-binding-crd.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: policybindings.monokle.io +spec: + group: monokle.io + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + required: + - policyName + - validationActions + properties: + policyName: + type: string + validationActions: + type: array + items: + type: string + enum: [Warn, Deny] + matchResources: + type: object + properties: + namespaceSelector: + type: object + properties: + matchLabels: + type: object + additionalProperties: + type: string + matchExpressions: + type: array + items: + type: object + required: + - key + - operator + - values + properties: + key: + type: string + operator: + type: string + enum: [In, NotIn] + values: + type: array + items: + type: string + scope: Cluster + names: + plural: policybindings + singular: policybinding + kind: MonoklePolicyBinding + shortNames: + - mpb +--- +# Source: monokle-admission-controller/templates/monokle-policy-crd.yaml +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: policies.monokle.io +spec: + group: monokle.io + versions: + - name: v1alpha1 + served: true + storage: true + schema: + # For schema see: + # - maps/dicts - https://swagger.io/docs/specification/data-models/dictionaries/ + # - structural schema - https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema + # + # Object values as multitypes: + # Even though it's supported by OpenAPI spec, e.g. https://stackoverflow.com/a/46475776, + # Kubernetes requires "structural" definition # https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema + # which seems to be in opposite to it "does not set description, type, default, additionalProperties, nullable + # within an allOf, anyOf, oneOf or not, with the exception of the two pattern for x-kubernetes-int-or-string: true (see below)." + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + required: + - plugins + properties: + plugins: + type: object + additionalProperties: + type: boolean + rules: + type: object + additionalProperties: true + settings: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + scope: Cluster + names: + plural: policies + singular: policy + kind: MonoklePolicy + shortNames: + - mp +--- +# Source: monokle-admission-controller/templates/service-account.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: monokle-policies-cluster + labels: + helm.sh/chart: monokle-admission-controller-0.2.7 + app.kubernetes.io/name: monokle-admission-controller + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: "0.2.7" + app.kubernetes.io/managed-by: Helm +rules: + - apiGroups: ["monokle.io"] + resources: ["policies", "policybindings"] + verbs: ["list", "watch"] + - apiGroups: ["admissionregistration.k8s.io"] + resources: ["validatingwebhookconfigurations"] + verbs: ["get", "patch"] + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get"] +--- +# Source: monokle-admission-controller/templates/service-account.yaml +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: monokle-policies-cluster + labels: + helm.sh/chart: monokle-admission-controller-0.2.7 + app.kubernetes.io/name: monokle-admission-controller + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: "0.2.7" + app.kubernetes.io/managed-by: Helm +subjects: + - kind: ServiceAccount + name: monokle-policies-sa + namespace: monokle +roleRef: + kind: ClusterRole + name: monokle-policies-cluster + apiGroup: rbac.authorization.k8s.io +--- +# Source: monokle-admission-controller/templates/service-account.yaml +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: monokle-policies-namespace + namespace: monokle + labels: + helm.sh/chart: monokle-admission-controller-0.2.7 + app.kubernetes.io/name: monokle-admission-controller + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: "0.2.7" + app.kubernetes.io/managed-by: Helm +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "delete"] +--- +# Source: monokle-admission-controller/templates/service-account.yaml +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: monokle-policies-namespace + namespace: monokle + labels: + helm.sh/chart: monokle-admission-controller-0.2.7 + app.kubernetes.io/name: monokle-admission-controller + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: "0.2.7" + app.kubernetes.io/managed-by: Helm +subjects: + - kind: ServiceAccount + name: monokle-policies-sa + namespace: monokle +roleRef: + kind: Role + name: monokle-policies-namespace + apiGroup: rbac.authorization.k8s.io +--- +# Source: monokle-admission-controller/templates/webhook.yaml +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: monokle-admission-controller-webhook + labels: + helm.sh/chart: monokle-admission-controller-0.2.7 + app.kubernetes.io/name: monokle-admission-controller + app.kubernetes.io/instance: release-name + app.kubernetes.io/version: "0.2.7" + app.kubernetes.io/managed-by: Helm +webhooks: + - name: monokle-admission-controller-server.monokle.svc + sideEffects: None + admissionReviewVersions: ["v1", "v1beta1"] + clientConfig: + url: https://host.docker.internal:8443/validate + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-node-lease + - kube-public + - kube-system + - monokle + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["*"] + apiVersions: ["*"] + resources: ["*"] + scope: "*" From 6fa68cde970ae6e93a440d3541ff984e856d3fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Thu, 8 Feb 2024 12:55:12 +0200 Subject: [PATCH 4/9] chore: add linters --- admission-controller/server/.eslintrc.js | 31 ++++++++++++++++++++++++ admission-controller/server/.prettierrc | 4 +++ admission-controller/server/package.json | 6 +++++ 3 files changed, 41 insertions(+) create mode 100644 admission-controller/server/.eslintrc.js create mode 100644 admission-controller/server/.prettierrc diff --git a/admission-controller/server/.eslintrc.js b/admission-controller/server/.eslintrc.js new file mode 100644 index 0000000..6ce4b1e --- /dev/null +++ b/admission-controller/server/.eslintrc.js @@ -0,0 +1,31 @@ +const { schema } = require('@octokit/graphql-schema'); + +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js', 'src/__generated__/**', 'src / schema.gql'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + + '@typescript-eslint/no-unused-vars': 'warn', + + 'eol-last': 1, + }, +}; diff --git a/admission-controller/server/.prettierrc b/admission-controller/server/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/admission-controller/server/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/admission-controller/server/package.json b/admission-controller/server/package.json index 7520f43..f5e9cd8 100644 --- a/admission-controller/server/package.json +++ b/admission-controller/server/package.json @@ -22,6 +22,12 @@ }, "devDependencies": { "@types/node": "^20.8.8", + "@typescript-eslint/eslint-plugin": "6.17.0", + "@typescript-eslint/parser": "6.17.0", + "eslint": "8.56.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-prettier": "5.1.2", + "prettier": "3.1.1", "typescript": "^5.2.2" } } From 9cff68189072337604ad2113927a46c025677216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Thu, 8 Feb 2024 12:56:54 +0200 Subject: [PATCH 5/9] feat(wip): added NestJS and started refactor --- admission-controller/server/package-lock.json | 2215 ++++++++++++++++- admission-controller/server/package.json | 6 +- .../src/admission/admission.controller.ts | 184 ++ .../server/src/admission/admission.models.ts | 50 + .../server/src/admission/admission.module.ts | 9 + .../server/src/config.service.ts | 27 + admission-controller/server/src/config.ts | 27 + admission-controller/server/src/index.ts | 95 +- .../src/policies/policies.manager.service.ts | 201 ++ .../server/src/policies/policies.models.ts | 35 + .../server/src/policies/policies.module.ts | 8 + .../server/src/policies/validation.service.ts | 86 + .../server/src/server.module.ts | 9 + .../server/src/utils/policy-manager.ts | 203 -- .../server/src/utils/policy-postprocessor.ts | 30 - .../server/src/utils/validation-server.ts | 279 --- .../server/src/utils/validator-manager.ts | 71 - .../synchronizer/tsconfig.json | 5 +- 18 files changed, 2841 insertions(+), 699 deletions(-) create mode 100644 admission-controller/server/src/admission/admission.controller.ts create mode 100644 admission-controller/server/src/admission/admission.models.ts create mode 100644 admission-controller/server/src/admission/admission.module.ts create mode 100644 admission-controller/server/src/config.service.ts create mode 100644 admission-controller/server/src/config.ts create mode 100644 admission-controller/server/src/policies/policies.manager.service.ts create mode 100644 admission-controller/server/src/policies/policies.models.ts create mode 100644 admission-controller/server/src/policies/policies.module.ts create mode 100644 admission-controller/server/src/policies/validation.service.ts create mode 100644 admission-controller/server/src/server.module.ts delete mode 100644 admission-controller/server/src/utils/policy-manager.ts delete mode 100644 admission-controller/server/src/utils/policy-postprocessor.ts delete mode 100644 admission-controller/server/src/utils/validation-server.ts delete mode 100644 admission-controller/server/src/utils/validator-manager.ts diff --git a/admission-controller/server/package-lock.json b/admission-controller/server/package-lock.json index 607c573..82d277d 100644 --- a/admission-controller/server/package-lock.json +++ b/admission-controller/server/package-lock.json @@ -12,6 +12,10 @@ "@kubernetes/client-node": "^0.19.0", "@monokle/parser": "0.2.0", "@monokle/validation": "^0.31.5", + "@nestjs/common": "^10.3.2", + "@nestjs/config": "^3.2.0", + "@nestjs/core": "^10.3.2", + "@nestjs/platform-fastify": "^10.3.2", "fastify": "^3.28.0", "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.3.2", @@ -21,9 +25,24 @@ }, "devDependencies": { "@types/node": "^20.8.8", + "@typescript-eslint/eslint-plugin": "6.17.0", + "@typescript-eslint/parser": "6.17.0", + "eslint": "8.56.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-prettier": "5.1.2", + "prettier": "3.1.1", "typescript": "^5.2.2" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -35,6 +54,84 @@ "node": ">=12" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@fastify/ajv-compiler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", @@ -43,11 +140,155 @@ "ajv": "^6.12.6" } }, + "node_modules/@fastify/cors": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-9.0.1.tgz", + "integrity": "sha512-YY9Ho3ovI+QHIL2hW+9X4XqQjXLjJqsU+sMV/xFsxZkE8p3GNnYVFpoOxF7SsP5ZL76gwvbo3V9L+FIekBGU4Q==", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, "node_modules/@fastify/error": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-2.0.0.tgz", "integrity": "sha512-wI3fpfDT0t7p8E6dA2eTECzzOd+bZsZCJ2Hcv+Onn2b7ZwK3RwD27uW2QDaMtQhAfWQQP+WNK7nKf0twLsBf9w==" }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/fast-json-stringify-compiler/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/fast-json-stringify-compiler/node_modules/fast-json-stringify": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", + "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/@fastify/fast-json-stringify-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/@fastify/formbody": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-7.4.0.tgz", + "integrity": "sha512-H3C6h1GN56/SMrZS8N2vCT2cZr7mIHzBHzOBa5OPpjfB/D6FzP9mMpE02ZzrFX0ANeh0BAJdoXKOF2e7IbV+Og==", + "dependencies": { + "fast-querystring": "^1.0.0", + "fastify-plugin": "^4.0.0" + } + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", + "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@fastify/middie": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/middie/-/middie-8.3.0.tgz", + "integrity": "sha512-h+zBxCzMlkEkh4fM7pZaSGzqS7P9M0Z6rXnWPdUEPfe7x1BCj++wEk/pQ5jpyYY4pF8AknFqb77n7uwh8HdxEA==", + "dependencies": { + "@fastify/error": "^3.2.0", + "fastify-plugin": "^4.0.0", + "path-to-regexp": "^6.1.0", + "reusify": "^1.0.4" + } + }, + "node_modules/@fastify/middie/node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/middie/node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", @@ -94,6 +335,14 @@ "openid-client": "^5.3.0" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "engines": { + "node": ">=8" + } + }, "node_modules/@monokle/parser": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@monokle/parser/-/parser-0.2.0.tgz", @@ -148,6 +397,322 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/@nestjs/common": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.2.tgz", + "integrity": "sha512-yrtohmQlN5J/gFS/ui86SO+KIfUKeL39JPR3f/3AWgpz+duIfc9cGkfh7FGZQMfG9ZqXf7Zw+PRO9G+D4iEbPw==", + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/config": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", + "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", + "dependencies": { + "dotenv": "16.4.1", + "dotenv-expand": "10.0.0", + "lodash": "4.17.21", + "uuid": "9.0.1" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@nestjs/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.2.tgz", + "integrity": "sha512-JW3bQvDFY1gB+xXR6E5DzCdKftRszyWtd0YyDkdlKh1+44e2IGybFhSa5HcQBOiRqdVgPqAM5Vqc81rmhgeBnQ==", + "hasInstallScript": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-fastify": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-10.3.2.tgz", + "integrity": "sha512-VWxsB2Ew40Tb7Za/5Eo5Fc9vHT/Md34uCuehI3Si5ZU1iDG8ZzSjJuKzpXQiN9B9syyANHHJbZHnY+XXUSpleQ==", + "dependencies": { + "@fastify/cors": "9.0.1", + "@fastify/formbody": "7.4.0", + "@fastify/middie": "8.3.0", + "fastify": "4.26.0", + "light-my-request": "5.11.0", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@fastify/view": "^7.0.0 || ^8.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "@fastify/view": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@nestjs/platform-fastify/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/avvio": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", + "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "dependencies": { + "@fastify/error": "^3.3.0", + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/fast-json-stringify": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.12.0.tgz", + "integrity": "sha512-7Nnm9UPa7SfHRbHVA1kJQrGXCRzB7LMlAAqHXQFkEQqueJm1V8owm0FsE/2Do55/4CcdhwiLQERaKomOnKQkyA==", + "dependencies": { + "@fastify/merge-json-schemas": "^0.1.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/fastify": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.0.tgz", + "integrity": "sha512-Fq/7ziWKc6pYLYLIlCRaqJqEVTIZ5tZYfcW/mDK2AQ9v/sqjGFpj0On0/7hU50kbPVjLO4de+larPA1WwPZSfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.2.1", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^8.0.0", + "light-my-request": "^5.11.0", + "pino": "^8.17.0", + "process-warning": "^3.0.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/fastify/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/@nestjs/platform-fastify/node_modules/find-my-way": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", + "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/@nestjs/platform-fastify/node_modules/light-my-request": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", + "integrity": "sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==", + "dependencies": { + "cookie": "^0.5.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/@nuxtjs/opencollective/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@open-policy-agent/opa-wasm": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@open-policy-agent/opa-wasm/-/opa-wasm-1.8.0.tgz", @@ -165,6 +730,18 @@ "node": ">= 6" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@rollup/plugin-virtual": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.1.tgz", @@ -211,6 +788,12 @@ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz", "integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.8.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.8.tgz", @@ -219,30 +802,232 @@ "undici-types": "~5.25.1" } }, - "node_modules/@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "node_modules/@types/request": { + "version": "2.48.8", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", + "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==" + }, + "node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz", + "integrity": "sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/type-utils": "6.17.0", + "@typescript-eslint/utils": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz", + "integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz", + "integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.17.0.tgz", + "integrity": "sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==", + "dev": true, "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/utils": "6.17.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", - "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==" + "node_modules/@typescript-eslint/types": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz", + "integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz", + "integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, - "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "node_modules/@typescript-eslint/utils": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.17.0.tgz", + "integrity": "sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==", + "dev": true, "dependencies": { - "@types/node": "*" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz", + "integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.17.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -270,6 +1055,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", @@ -293,6 +1087,65 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", @@ -308,6 +1161,15 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -361,6 +1223,12 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -388,6 +1256,27 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -419,6 +1308,15 @@ "node": ">=0.10.0" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/camel-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", @@ -443,6 +1341,21 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/change-case": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", @@ -470,6 +1383,22 @@ "node": ">=10" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -481,6 +1410,17 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" + }, "node_modules/constant-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", @@ -509,6 +1449,20 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -528,67 +1482,334 @@ "node": ">= 12" } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "engines": { + "node": ">=12" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.2.tgz", + "integrity": "sha512-dhlpWc9vOwohcWmClFcA+HjlvUpuyynYs0Rf+L/P6/0iQE6vlHW9l5bkfzN62/Stm9fbq8ku46qzde76T1xlSg==", + "dev": true, "dependencies": { - "ms": "2.1.2" + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.6" }, "engines": { - "node": ">=6.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { "optional": true } } }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "engines": { - "node": ">=0.4.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=0.3.1" + "node": "*" } }, - "node_modules/dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" } }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, "node_modules/event-target-shim": { @@ -635,6 +1856,40 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -654,6 +1909,20 @@ "node": ">= 10.0.0" } }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, "node_modules/fast-redact": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", @@ -667,6 +1936,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, "node_modules/fastify": { "version": "3.29.5", "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.29.5.tgz", @@ -690,6 +1964,11 @@ "tiny-lru": "^8.0.1" } }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/fastify/node_modules/pino": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", @@ -727,9 +2006,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dependencies": { "reusify": "^1.0.4" } @@ -756,6 +2035,30 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-my-way": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz", @@ -770,11 +2073,47 @@ "node": ">=10" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/flatstr": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -837,6 +2176,12 @@ "node": ">=8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -869,6 +2214,60 @@ "assert-plus": "^1.0.0" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/global": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", @@ -878,6 +2277,59 @@ "process": "^0.11.10" } }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -899,6 +2351,14 @@ "node": ">=6" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/header-case": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", @@ -941,6 +2401,56 @@ } ] }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -949,11 +2459,56 @@ "node": ">= 0.10" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/isomorphic-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", @@ -995,6 +2550,14 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/jose": { "version": "4.14.6", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.6.tgz", @@ -1020,16 +2583,36 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -1048,13 +2631,35 @@ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=0.6.0" + "node": ">= 0.8.0" } }, "node_modules/light-my-request": { @@ -1093,11 +2698,32 @@ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -1122,6 +2748,28 @@ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1149,6 +2797,21 @@ "dom-walk": "^0.1.0" } }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -1191,11 +2854,25 @@ "node": ">=10" } }, + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -1257,6 +2934,11 @@ "node": ">= 6" } }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -1271,6 +2953,15 @@ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, "node_modules/openid-client": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.5.0.tgz", @@ -1286,6 +2977,53 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pako": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", @@ -1300,6 +3038,18 @@ "tslib": "^2.0.3" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -1323,22 +3073,75 @@ "tslib": "^2.0.3" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pino": { - "version": "8.16.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", - "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.18.0.tgz", + "integrity": "sha512-Mz/gKiRyuXu4HnpHgi1YWdHQCoWMufapzooisvFn78zl4dZciAxS+YeRkUxXl1ee/SzU80YCz1zpECCh4oC6Aw==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", - "process-warning": "^2.0.0", + "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", @@ -1363,6 +3166,47 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, + "node_modules/pino/node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1456,6 +3300,12 @@ "node": ">= 12.13.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==", + "peer": true + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -1517,6 +3367,15 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/ret": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", @@ -1544,6 +3403,21 @@ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "3.18.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.18.0.tgz", @@ -1559,6 +3433,38 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1638,6 +3544,36 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -1714,6 +3650,57 @@ "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info." }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", + "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -1730,6 +3717,12 @@ "node": ">=10" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, "node_modules/thread-stream": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.0.tgz", @@ -1746,6 +3739,26 @@ "node": ">=6" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -1763,6 +3776,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-api-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -1826,6 +3851,18 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-fest": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.5.0.tgz", @@ -1849,6 +3886,17 @@ "node": ">=14.17" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/undici-types": { "version": "5.25.3", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", @@ -1931,6 +3979,27 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, "node_modules/ws": { "version": "8.14.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", @@ -1972,6 +4041,18 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.19.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz", diff --git a/admission-controller/server/package.json b/admission-controller/server/package.json index f5e9cd8..9ec3938 100644 --- a/admission-controller/server/package.json +++ b/admission-controller/server/package.json @@ -13,6 +13,10 @@ "@kubernetes/client-node": "^0.19.0", "@monokle/parser": "0.2.0", "@monokle/validation": "^0.31.5", + "@nestjs/common": "^10.3.2", + "@nestjs/config": "^3.2.0", + "@nestjs/core": "^10.3.2", + "@nestjs/platform-fastify": "^10.3.2", "fastify": "^3.28.0", "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.3.2", @@ -30,4 +34,4 @@ "prettier": "3.1.1", "typescript": "^5.2.2" } -} +} \ No newline at end of file diff --git a/admission-controller/server/src/admission/admission.controller.ts b/admission-controller/server/src/admission/admission.controller.ts new file mode 100644 index 0000000..b0d0c27 --- /dev/null +++ b/admission-controller/server/src/admission/admission.controller.ts @@ -0,0 +1,184 @@ +import {Body, Controller, Logger, Post} from "@nestjs/common"; +import {AdmissionRequest, AdmissionResponse, Violation} from "./admission.models"; +import KubeClient from "../utils/kube-client"; +import {Resource, ValidationResult} from "@monokle/validation"; +import {ConfigService} from "@nestjs/config"; +import {ValidationService} from "../policies/validation.service"; + +@Controller('validate') +export class AdmissionController { + private readonly log = new Logger(AdmissionController.name); + private readonly ignoredNamespaces: string[]; + + constructor(private readonly $config: ConfigService, private readonly $validation: ValidationService) { + this.ignoredNamespaces = this.$config.get('ignoredNamespaces') ?? []; + } + + @Post() + async validate(@Body() body: AdmissionRequest): Promise { + this.log.verbose({body}); + const namespace = body.request?.namespace || body.request?.object?.metadata?.namespace; + + const response = { + kind: body?.kind || '', + apiVersion: body?.apiVersion || '', + response: { + uid: body?.request?.uid || "", + allowed: true, + status: { + message: "OK" + } + } + } + + if (namespace && this.ignoredNamespaces.includes(namespace)) { + this.log.error({msg: 'Namespace ignored', namespace}); + return response; + } + + const resource = body.request?.object; + if (!resource) { + this.log.error({msg: 'No resource found', metadata: body.request}); + return response; + } + + const namespaceObject = namespace ? await KubeClient.getNamespace(namespace) : undefined; + this.log.debug({ namespaceObject }); + + const validators = this.$validation.getMatchingValidators(resource, namespaceObject); + + this.log.debug({msg: 'Matching validators', count: validators.length}); + + if (validators.length === 0) { + return response; + } + + const resourceForValidation = AdmissionController.createResourceForValidation(body); + const validationResponses = await Promise.all(validators.map(async (validator) => { + return { + result: await validator.validator.validate({ resources: [resourceForValidation] }), + policy: validator.policy + }; + } + )); + + const violations: Violation[] = []; + for (const validationResponse of validationResponses) { + const actions = validationResponse.policy.binding.validationActions; + + for (const result of validationResponse.result.runs) { + for (const item of result.results) { + violations.push({ + ruleId: item.ruleId, + message: item.message, + level: item.level, + actions: actions, + name: AdmissionController.getFullyQualifiedName(item) ?? resourceForValidation.name + }); + } + } + } + + this.log.verbose({resourceForValidation, validationResponses}); + + if (violations.length === 0) { + this.log.debug({msg: 'No violations', response}); + return response; + } + + const violationsByAction = violations.reduce((acc: Record, violation: Violation) => { + const actions = violation.actions; + + for (const action of actions) { + if (!acc[action]) { + acc[action] = []; + } + + acc[action].push(violation); + } + + return acc; + }, {}); + + const responseFull = AdmissionController.handleViolationsByAction(violationsByAction, resourceForValidation, response); + + this.log.debug({response}); + return responseFull; + } + + private static createResourceForValidation(admissionResource: AdmissionRequest): Resource { + const resource = { + id: admissionResource.request?.uid || '', + fileId: '', + filePath: '', + fileOffset: 0, + name: admissionResource.request?.name || '', + apiVersion: admissionResource.request?.object?.apiVersion || '', + kind: admissionResource.request?.object?.kind || '', + namespace: admissionResource.request?.namespace || '', + content: admissionResource.request?.object || {}, + text: '' + }; + + return resource; + } + + private static handleViolationsByAction(violationsByAction: Record, resource: Resource, response: AdmissionResponse) { + for (const action of Object.keys(violationsByAction)) { + // 'Warn' action should be mapped to warnings, see: + // - https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#validation-actions + // - https://kubernetes.io/blog/2020/09/03/warnings/ + if (action.toLowerCase() === 'warn') { + response = this.handleViolationsAsWarn(violationsByAction[action], resource, response); + } else if (action.toLowerCase() === 'deny') { + const violationMessages = this.getViolationsMessages(violationsByAction[action], resource); + + response.response.allowed = false; + response.response.status.message = violationMessages.join("\n"); + } + } + + return response; + } + + private static handleViolationsAsWarn(violations: Violation[], resource: Resource, response: AdmissionResponse) { + const violationMessages = AdmissionController.getViolationsMessages(violations, resource); + if (violationMessages.length > 0) { + response.response.warnings = violationMessages; + } + + return response; + } + + private static getViolationsMessages(violations: Violation[], resource: Resource): string[] { + const errors = violations + .filter((v) => v.level === 'error') + .map((e) => AdmissionController.formatViolationMessage(e, resource)); + + const warnings = violations + .filter((v) => v.level === 'warning') + .map((e) => AdmissionController.formatViolationMessage(e, resource)); + + if (errors.length > 0 || warnings.length > 0) { + return [ + `Monokle Admission Controller found ${errors.length} errors and ${warnings.length} warnings:`, + ...errors, + ...warnings, + 'You can use Monokle Cloud (https://monokle.io/) to fix those errors easily.', + ]; + } + + return []; + } + + private static getFullyQualifiedName(result: ValidationResult) { + const locations = result.locations; + const locationWithName = locations.find((l) => l.logicalLocations?.length && l.logicalLocations.length > 0 && l.logicalLocations[0].fullyQualifiedName); + + return locationWithName ? (locationWithName.logicalLocations || [])[0].fullyQualifiedName?.replace(/\./g, '/').replace('@', '').trim() : null; + } + + private static formatViolationMessage(violation: Violation, resource: Resource) { + return `${violation.ruleId} (${violation.level}): ${violation.message.text.replace(/\.$/, '')}, in kind "${resource.kind}" with name "${violation.name}".`; + } +} diff --git a/admission-controller/server/src/admission/admission.models.ts b/admission-controller/server/src/admission/admission.models.ts new file mode 100644 index 0000000..4909675 --- /dev/null +++ b/admission-controller/server/src/admission/admission.models.ts @@ -0,0 +1,50 @@ +import {V1ObjectMeta} from "@kubernetes/client-node"; +import {Message, RuleLevel} from "@monokle/validation"; + +export type ValidationServerOptions = { + port: number + host: string +}; + +export type AdmissionRequestObject = { + apiVersion: string + kind: string + metadata: V1ObjectMeta + spec: any + status: any +}; + +export type AdmissionRequest = { + apiVersion: string + kind: string + request: { + name: string + namespace: string + uid: string + object: AdmissionRequestObject + } +}; + +// See +// https://pkg.go.dev/k8s.io/api/admission/v1#AdmissionResponse +// https://kubernetes.io/docs/reference/config-api/apiserver-admission.v1/#admission-k8s-io-v1-AdmissionReview +export type AdmissionResponse = { + kind: string + apiVersion: string + response: { + uid: string + allowed: boolean + warnings?: string[] + status: { + message: string + } + } +}; + +export type Violation = { + ruleId: string + message: Message + level?: RuleLevel + actions: string[] + name: string +} diff --git a/admission-controller/server/src/admission/admission.module.ts b/admission-controller/server/src/admission/admission.module.ts new file mode 100644 index 0000000..c72b02b --- /dev/null +++ b/admission-controller/server/src/admission/admission.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import {AdmissionController} from "./admission.controller"; + +@Module({ + imports: [], + controllers: [AdmissionController], + providers: [], +}) +export class AdmissionModule {} diff --git a/admission-controller/server/src/config.service.ts b/admission-controller/server/src/config.service.ts new file mode 100644 index 0000000..108c304 --- /dev/null +++ b/admission-controller/server/src/config.service.ts @@ -0,0 +1,27 @@ +import { ConfigService as BaseConfigService } from '@nestjs/config'; +import Configuration from './config'; + +type NestedKeyOf = { + [Key in keyof ObjectType & + (string | number | boolean)]: ObjectType[Key] extends object + ? `${Key}` | `${Key}.${NestedKeyOf}` + : `${Key}`; +}[keyof ObjectType & (string | number | boolean)]; + +export class ConfigService extends BaseConfigService { + private readonly config: typeof Configuration; + + constructor() { + super(); + this.config = Configuration; + } + + get(key: NestedKeyOf): any { + const keys = key.split('.'); + let result: any = this.config; + for (const key of keys) { + result = result[key]; + } + return result; + } +} diff --git a/admission-controller/server/src/config.ts b/admission-controller/server/src/config.ts new file mode 100644 index 0000000..ac1f438 --- /dev/null +++ b/admission-controller/server/src/config.ts @@ -0,0 +1,27 @@ +import * as FS from 'node:fs/promises'; + +const Configuration = { + logLevel: process.env.MONOKLE_LOG_LEVEL ?? 'warn', + ignoredNamespaces: (process.env.MONOKLE_IGNORE_NAMESPACES ?? '') + .split(',') + .filter(Boolean), + + server: { + host: '0.0.0.0', + port: 8443, + tls: { + key: process.env.TLS_KEY ?? '/run/secrets/tls/tls.key', + cert: process.env.TLS_CERT ?? '/run/secrets/tls/tls.crt', + }, + }, +}; + +Configuration.server.tls.key = await FS.readFile( + Configuration.server.tls.key, +).then((buffer) => buffer.toString()); + +Configuration.server.tls.cert = await FS.readFile( + Configuration.server.tls.cert, +).then((buffer) => buffer.toString()); + +export default Configuration; diff --git a/admission-controller/server/src/index.ts b/admission-controller/server/src/index.ts index 92e9b68..5fd95e9 100644 --- a/admission-controller/server/src/index.ts +++ b/admission-controller/server/src/index.ts @@ -1,46 +1,49 @@ -import pino from 'pino'; -import {MonoklePolicy, MonoklePolicyBinding, PolicyManager} from './utils/policy-manager.js'; -import {ValidatorManager} from './utils/validator-manager.js'; -import {ValidationServer} from './utils/validation-server.js'; -import KubeClient from "./utils/kube-client.js"; - -const LOG_LEVEL = (process.env.MONOKLE_LOG_LEVEL || 'warn').toLowerCase(); -const IGNORED_NAMESPACES = (process.env.MONOKLE_IGNORE_NAMESPACES || '').split(',').filter(Boolean); - -const logger = pino({ - name: 'Monokle', - level: LOG_LEVEL, -}); - -(async() => { - - - KubeClient.buildKubeConfig(); - - const policyInformer = await KubeClient.getInformer( - 'monokle.io', - 'v1alpha1', - 'policies', - (err: any) => { - logger.error({msg: 'Informer: Policies: Error', err: err.message, body: err.body}); - } - ); - - const bindingsInformer = await KubeClient.getInformer( - 'monokle.io', - 'v1alpha1', - 'policybindings', - (err: any) => { - logger.error({msg: 'Informer: Bindings: Error', err: err.message, body: err.body}); - } - ); - - const policyManager = new PolicyManager(policyInformer, bindingsInformer, logger); - const validatorManager = new ValidatorManager(policyManager, logger); - - await policyManager.start(); - - const server = new ValidationServer(validatorManager, IGNORED_NAMESPACES, logger); - - await server.start(); -})(); +import KubeClient from './utils/kube-client.js'; +import { NestFactory } from '@nestjs/core'; +import { ServerModule } from './server.module'; +import { + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { ConfigService } from './config.service'; +import Configuration from './config'; + +const app = await NestFactory.create( + ServerModule, + new FastifyAdapter({ + https: Configuration.server.tls, + }), +); + +const config = app.get(ConfigService); +app.useLogger(config.get('logLevel') as any); + +KubeClient.buildKubeConfig(); + +const policyInformer = await KubeClient.getInformer( + 'monokle.io', + 'v1alpha1', + 'policies', + (err: any) => { + logger.error({ + msg: 'Informer: Policies: Error', + err: err.message, + body: err.body, + }); + }, +); + +const bindingsInformer = await KubeClient.getInformer( + 'monokle.io', + 'v1alpha1', + 'policybindings', + (err: any) => { + logger.error({ + msg: 'Informer: Bindings: Error', + err: err.message, + body: err.body, + }); + }, +); + +await app.listen(config.get('server.port'), config.get('server.host')); diff --git a/admission-controller/server/src/policies/policies.manager.service.ts b/admission-controller/server/src/policies/policies.manager.service.ts new file mode 100644 index 0000000..0e7422a --- /dev/null +++ b/admission-controller/server/src/policies/policies.manager.service.ts @@ -0,0 +1,201 @@ +import {Injectable, Logger} from "@nestjs/common"; +import {InformerWrapper} from "../utils/kube-client"; +import {EventEmitter} from "events"; +import {AdmissionRequestObject} from "../utils/validation-server"; +import {V1Namespace} from "@kubernetes/client-node"; +import {Config} from "@monokle/validation"; +import {MonokleApplicablePolicy, MonoklePolicy, MonoklePolicyBinding} from "./policies.models"; + +@Injectable() +export class PoliciesManagerService extends EventEmitter { + private static readonly PLUGIN_BLOCKLIST = [ + 'resource-links', + ]; + + private readonly _logger = new Logger(PoliciesManagerService.name); + + private readonly _policies = new Map(); // Map + private readonly _bindings = new Map(); // Map + + constructor( + private readonly _policyInformer: InformerWrapper, + private readonly _bindingInformer: InformerWrapper, + ) { + super(); + + this._policyInformer.informer.on('add', this.onPolicy.bind(this)); + this._policyInformer.informer.on('update', this.onPolicy.bind(this)); + this._policyInformer.informer.on('delete', this.onPolicyRemoval.bind(this)); + + this._bindingInformer.informer.on('add', this.onBinding.bind(this)); + this._bindingInformer.informer.on('update', this.onBinding.bind(this)); + this._bindingInformer.informer.on('delete', this.onBindingRemoval.bind(this)); + } + + private static postprocess(policy: MonoklePolicy) { + const newPolicy = { ...policy }; + newPolicy.spec = PoliciesManagerService.blockPlugins(newPolicy.spec); + return newPolicy; + } + + private static blockPlugins(policySpec: Config): Config { + if (policySpec.plugins === undefined) { + return policySpec; + } + + const newPlugins = { ...policySpec.plugins }; + for (const blockedPlugin of PoliciesManagerService.PLUGIN_BLOCKLIST) { + if (newPlugins[blockedPlugin] === true) { + newPlugins[blockedPlugin] = false; + } + } + + return { + ...policySpec, + plugins: newPlugins, + }; + } + + + getMatchingPolicies(resource: AdmissionRequestObject, resourceNamespace?: V1Namespace): MonokleApplicablePolicy[] { + this._logger.debug({policies: this._policies.size, bindings: this._bindings.size}); + + if (this._bindings.size === 0) { + return []; + } + + return Array.from(this._bindings.values()) + .map((binding) => { + const policy = this._policies.get(binding.spec.policyName); + + if (!policy) { + this._logger.error({msg: 'Binding is pointing to missing policy', binding}); + return null; + } + + if (binding.spec.matchResources && !this.isResourceMatching(binding, resource, resourceNamespace)) { + return null; + } + + return { + policy: policy.spec, + binding: binding.spec + } + }) + .filter((policy) => policy !== null) as MonokleApplicablePolicy[]; + } + + private onPolicy(rawPolicy: MonoklePolicy) { + const policy = PoliciesManagerService.postprocess(rawPolicy); + + this._logger.debug({msg: 'Policy updated', rawPolicy, policy}); + + this._policies.set(rawPolicy.metadata!.name!, policy); + + this.emit('policyUpdated', policy); + } + + private onPolicyRemoval(rawPolicy: MonoklePolicy) { + const policy = PoliciesManagerService.postprocess(rawPolicy); + + this._logger.debug({msg: 'Policy removed', rawPolicy, policy}); + + this._policies.delete(rawPolicy.metadata!.name!); + + this.emit('policyRemoved', policy); + } + + private onBinding(rawBinding: MonoklePolicyBinding) { + this._logger.debug({msg: 'Binding updated', rawBinding}); + + this._bindings.set(rawBinding.metadata!.name!, rawBinding); + + this.emit('bindingUpdated', rawBinding); + } + + private onBindingRemoval(rawBinding: MonoklePolicyBinding) { + this._logger.debug({msg: 'Binding removed', rawBinding}); + + this._bindings.delete(rawBinding.metadata!.name!); + + this.emit('bindingRemoved', rawBinding); + } + + // Based on K8s docs here - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchresources-v1beta1-admissionregistration-k8s-io: + private isResourceMatching(binding: MonoklePolicyBinding, resource: AdmissionRequestObject, resourceNamespace?: V1Namespace): boolean { + const namespaceMatchLabels = binding.spec.matchResources?.namespaceSelector?.matchLabels; + const namespaceMatchExpressions = binding.spec.matchResources?.namespaceSelector?.matchExpressions ?? []; + const kind = resource.kind.toLowerCase(); + const isClusterWide = ((resource as any).namespace || resource.metadata.namespace) === undefined; + + this._logger.verbose({ + msg: 'Checking if resource matches binding', + namespaceMatchLabels, + namespaceMatchExpressions, + kind, + resourceMetadata: resource.metadata.labels + }); + + // If non of the matchers are specified, then the resource matches, both cluster wide and namespaced ones. + // So this is global policy. As in docs: + // > Default to the empty LabelSelector, which matches everything. + if (!namespaceMatchLabels && !namespaceMatchExpressions?.length) { + return true; + } + + // Skip cluster-wide resources if namespaceSelector defined. + // This is different from the K8s docs which says: + // > If the object itself is a namespace (...) If the object is another cluster scoped resource, it never skips the policy. + if (isClusterWide && kind !== 'namespace') { + return false; + } + + // If resource is Namespace use it, if not get resource owning namespace. + // > If the object itself is a namespace, the matching is performed on object.metadata.labels + const namespaceObject = kind !== 'namespace' ? resourceNamespace : resource; + if (!namespaceObject) { + return false; + } + + const namespaceObjectLabels = namespaceObject?.metadata?.labels || {}; + + // Convert matchLabels to matchExpressions to have single matching logic. As in docs: + // > matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + // > map is equivalent to an element of matchExpressions, whose key field is "key", the operator + // > is "In", and the values array contains only "value". The requirements are ANDed. + if (namespaceMatchLabels) { + Object.entries(namespaceMatchLabels).forEach(entry => { + namespaceMatchExpressions.push({ + key: entry[0], + operator: 'In', + values: [entry[1]] + }); + }); + } + + let isMatching = true; + if (namespaceMatchExpressions.length) { + for (const expression of namespaceMatchExpressions) { + let labelValue = namespaceObjectLabels[expression.key]; + + // Try default K8s labels for specific keys if there is no value. + if (!labelValue && expression.key === 'name') { + labelValue = namespaceObjectLabels[`kubernetes.io/metadata.${expression.key}`] + } + + if (expression.operator === 'In' && !expression.values.includes(labelValue)) { + isMatching = false; + break; + } + + // If label is not there it fits into 'NotIn' scenario. + if (expression.operator === 'NotIn' && expression.values.includes(labelValue) ) { + isMatching = false; + break; + } + } + } + + return isMatching; + } +} diff --git a/admission-controller/server/src/policies/policies.models.ts b/admission-controller/server/src/policies/policies.models.ts new file mode 100644 index 0000000..f13c1b6 --- /dev/null +++ b/admission-controller/server/src/policies/policies.models.ts @@ -0,0 +1,35 @@ +import {KubernetesObject} from "@kubernetes/client-node"; +import {Config, MonokleValidator} from "@monokle/validation"; + +export type MonoklePolicy = KubernetesObject & { + spec: Config +} + +export type MonoklePolicyBindingConfiguration = { + policyName: string + validationActions: ['Warn'] + matchResources?: { + namespaceSelector?: { + matchLabels?: Record, + matchExpressions?: { + key: string, + operator: 'In' | 'NotIn', + values: string[] + }[] + } + } +} + +export type MonoklePolicyBinding = KubernetesObject & { + spec: MonoklePolicyBindingConfiguration +} + +export type MonokleApplicablePolicy = { + policy: Config, + binding: MonoklePolicyBindingConfiguration +} + +export type MonokleApplicableValidator = { + validator: MonokleValidator, + policy: MonokleApplicablePolicy +} diff --git a/admission-controller/server/src/policies/policies.module.ts b/admission-controller/server/src/policies/policies.module.ts new file mode 100644 index 0000000..30d5729 --- /dev/null +++ b/admission-controller/server/src/policies/policies.module.ts @@ -0,0 +1,8 @@ +import {Module} from "@nestjs/common"; + +@Module({ + imports: [], + controllers: [], + providers: [], +}) +export class PoliciesModule {} diff --git a/admission-controller/server/src/policies/validation.service.ts b/admission-controller/server/src/policies/validation.service.ts new file mode 100644 index 0000000..b398b9f --- /dev/null +++ b/admission-controller/server/src/policies/validation.service.ts @@ -0,0 +1,86 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { + AnnotationSuppressor, + Config, + DisabledFixer, + FingerprintSuppressor, + MonokleValidator, + RemotePluginLoader, + ResourceParser, + SchemaLoader, +} from '@monokle/validation'; +import { PoliciesManagerService } from './policies.manager.service'; +import { V1Namespace } from '@kubernetes/client-node'; +import { MonokleApplicableValidator } from './policies.models'; +import { AdmissionRequestObject } from '../admission/admission.models'; + +@Injectable() +export class ValidationService { + private _validators = new Map(); // Map + private readonly _logger = new Logger(ValidationService.name); + + constructor(private readonly $manager: PoliciesManagerService) { + this.$manager.on('policyUpdated', async (policy) => { + await this.setupValidator(policy.metadata!.name!, policy.spec); + }); + + this.$manager.on('policyRemoved', async (policy) => { + await this._validators.delete(policy.metadata!.name!); + }); + } + + getMatchingValidators( + resource: AdmissionRequestObject, + resourceNamespace?: V1Namespace, + ): MonokleApplicableValidator[] { + const matchingPolicies = this.$manager.getMatchingPolicies( + resource, + resourceNamespace, + ); + + if (matchingPolicies.length === 0) { + return []; + } + + return matchingPolicies + .map((policy) => { + if (!this._validators.has(policy.binding.policyName)) { + // This should not happen and means there is a bug in other place in the code. Raise warning and skip. + // Do not create validator instance here to keep this function sync and to keep processing time low. + this._logger.warn({ + msg: 'ValidatorManager: Validator not found', + policyName: policy.binding.policyName, + }); + return null; + } + + return { + validator: this._validators.get(policy.binding.policyName)!, + policy, + }; + }) + .filter( + (validator) => validator !== null, + ) as MonokleApplicableValidator[]; + } + + private async setupValidator(policyName: string, policy: Config) { + if (this._validators.has(policyName)) { + await this._validators.get(policyName)!.preload(policy); + } else { + const validator = new MonokleValidator({ + loader: new RemotePluginLoader(), + parser: new ResourceParser(), + schemaLoader: new SchemaLoader(), + suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], + fixer: new DisabledFixer(), + }); + + // Run separately (instead of passing config to constructor) to make sure that validator + // is ready when 'setupValidator' function call fulfills. + await validator.preload(policy); + + this._validators.set(policyName, validator); + } + } +} diff --git a/admission-controller/server/src/server.module.ts b/admission-controller/server/src/server.module.ts new file mode 100644 index 0000000..91020f8 --- /dev/null +++ b/admission-controller/server/src/server.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import {AdmissionModule} from "./admission/admission.module"; + +@Module({ + imports: [AdmissionModule], + controllers: [], + providers: [], +}) +export class ServerModule {} diff --git a/admission-controller/server/src/utils/policy-manager.ts b/admission-controller/server/src/utils/policy-manager.ts deleted file mode 100644 index 0fac272..0000000 --- a/admission-controller/server/src/utils/policy-manager.ts +++ /dev/null @@ -1,203 +0,0 @@ -import {EventEmitter} from 'events'; -import pino from 'pino'; -import {KubernetesObject, V1Namespace} from '@kubernetes/client-node'; -import {Config} from '@monokle/validation'; -import {AdmissionRequestObject} from './validation-server.js'; -import {postprocess} from './policy-postprocessor.js'; -import {InformerWrapper} from "./kube-client"; - -export type MonoklePolicy = KubernetesObject & { - spec: Config -} - -export type MonoklePolicyBindingConfiguration = { - policyName: string - validationActions: ['Warn'] - matchResources?: { - namespaceSelector?: { - matchLabels?: Record, - matchExpressions?: { - key: string, - operator: 'In' | 'NotIn', - values: string[] - }[] - } - } -} - -export type MonoklePolicyBinding = KubernetesObject & { - spec: MonoklePolicyBindingConfiguration -} - -export type MonokleApplicablePolicy = { - policy: Config, - binding: MonoklePolicyBindingConfiguration -} - -export class PolicyManager extends EventEmitter{ - private _policies = new Map(); // Map - private _bindings = new Map(); // Map - - constructor( - private readonly _policyInformer: InformerWrapper, - private readonly _bindingInformer: InformerWrapper, - private readonly _logger: ReturnType, - ) { - super(); - - this._policyInformer.informer.on('add', this.onPolicy.bind(this)); - this._policyInformer.informer.on('update', this.onPolicy.bind(this)); - this._policyInformer.informer.on('delete', this.onPolicyRemoval.bind(this)); - - this._bindingInformer.informer.on('add', this.onBinding.bind(this)); - this._bindingInformer.informer.on('update', this.onBinding.bind(this)); - this._bindingInformer.informer.on('delete', this.onBindingRemoval.bind(this)); - } - - async start() { - await this._policyInformer.start(); - await this._bindingInformer.start(); - } - - getMatchingPolicies(resource: AdmissionRequestObject, resourceNamespace?: V1Namespace): MonokleApplicablePolicy[] { - this._logger.debug({policies: this._policies.size, bindings: this._bindings.size}); - - if (this._bindings.size === 0) { - return []; - } - - return Array.from(this._bindings.values()) - .map((binding) => { - const policy = this._policies.get(binding.spec.policyName); - - if (!policy) { - this._logger.error({msg: 'Binding is pointing to missing policy', binding}); - return null; - } - - if (binding.spec.matchResources && !this.isResourceMatching(binding, resource, resourceNamespace)) { - return null; - } - - return { - policy: policy.spec, - binding: binding.spec - } - }) - .filter((policy) => policy !== null) as MonokleApplicablePolicy[]; - } - - private onPolicy(rawPolicy: MonoklePolicy) { - const policy = postprocess(rawPolicy); - - this._logger.debug({msg: 'Policy updated', rawPolicy, policy}); - - this._policies.set(rawPolicy.metadata!.name!, policy); - - this.emit('policyUpdated', policy); - } - - private onPolicyRemoval(rawPolicy: MonoklePolicy) { - const policy = postprocess(rawPolicy); - - this._logger.debug({msg: 'Policy removed', rawPolicy, policy}); - - this._policies.delete(rawPolicy.metadata!.name!); - - this.emit('policyRemoved', policy); - } - - private onBinding(rawBinding: MonoklePolicyBinding) { - this._logger.debug({msg: 'Binding updated', rawBinding}); - - this._bindings.set(rawBinding.metadata!.name!, rawBinding); - - this.emit('bindingUpdated', rawBinding); - } - - private onBindingRemoval(rawBinding: MonoklePolicyBinding) { - this._logger.debug({msg: 'Binding removed', rawBinding}); - - this._bindings.delete(rawBinding.metadata!.name!); - - this.emit('bindingRemoved', rawBinding); - } - - // Based on K8s docs here - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchresources-v1beta1-admissionregistration-k8s-io: - private isResourceMatching(binding: MonoklePolicyBinding, resource: AdmissionRequestObject, resourceNamespace?: V1Namespace): boolean { - const namespaceMatchLabels = binding.spec.matchResources?.namespaceSelector?.matchLabels; - const namespaceMatchExpressions = binding.spec.matchResources?.namespaceSelector?.matchExpressions ?? []; - const kind = resource.kind.toLowerCase(); - const isClusterWide = ((resource as any).namespace || resource.metadata.namespace) === undefined; - - this._logger.trace({ - msg: 'Checking if resource matches binding', - namespaceMatchLabels, - namespaceMatchExpressions, - kind, - resourceMetadata: resource.metadata.labels - }); - - // If non of the matchers are specified, then the resource matches, both cluster wide and namespaced ones. - // So this is global policy. As in docs: - // > Default to the empty LabelSelector, which matches everything. - if (!namespaceMatchLabels && !namespaceMatchExpressions?.length) { - return true; - } - - // Skip cluster-wide resources if namespaceSelector defined. - // This is different from the K8s docs which says: - // > If the object itself is a namespace (...) If the object is another cluster scoped resource, it never skips the policy. - if (isClusterWide && kind !== 'namespace') { - return false; - } - - // If resource is Namespace use it, if not get resource owning namespace. - // > If the object itself is a namespace, the matching is performed on object.metadata.labels - const namespaceObject = kind !== 'namespace' ? resourceNamespace : resource; - if (!namespaceObject) { - return false; - } - - const namespaceObjectLabels = namespaceObject?.metadata?.labels || {}; - - // Convert matchLabels to matchExpressions to have single matching logic. As in docs: - // > matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - // > map is equivalent to an element of matchExpressions, whose key field is "key", the operator - // > is "In", and the values array contains only "value". The requirements are ANDed. - if (namespaceMatchLabels) { - Object.entries(namespaceMatchLabels).forEach(entry => { - namespaceMatchExpressions.push({ - key: entry[0], - operator: 'In', - values: [entry[1]] - }); - }); - } - - let isMatching = true; - if (namespaceMatchExpressions.length) { - for (const expression of namespaceMatchExpressions) { - let labelValue = namespaceObjectLabels[expression.key]; - - // Try default K8s labels for specific keys if there is no value. - if (!labelValue && expression.key === 'name') { - labelValue = namespaceObjectLabels[`kubernetes.io/metadata.${expression.key}`] - } - - if (expression.operator === 'In' && !expression.values.includes(labelValue)) { - isMatching = false; - break; - } - - // If label is not there it fits into 'NotIn' scenario. - if (expression.operator === 'NotIn' && expression.values.includes(labelValue) ) { - isMatching = false; - break; - } - } - } - - return isMatching; - } -} diff --git a/admission-controller/server/src/utils/policy-postprocessor.ts b/admission-controller/server/src/utils/policy-postprocessor.ts deleted file mode 100644 index c4063bc..0000000 --- a/admission-controller/server/src/utils/policy-postprocessor.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {Config} from '@monokle/validation'; -import {MonoklePolicy} from './policy-manager.js'; - -const PLUGIN_BLOCKLIST = [ - 'resource-links', -]; - -export function postprocess(policy: MonoklePolicy) { - const newPolicy = { ...policy }; - newPolicy.spec = blockPlugins(newPolicy.spec); - return newPolicy; -} - -function blockPlugins(policySpec: Config): Config { - if (policySpec.plugins === undefined) { - return policySpec; - } - - const newPlugins = { ...policySpec.plugins }; - for (const blockedPlugin of PLUGIN_BLOCKLIST) { - if (newPlugins[blockedPlugin] === true) { - newPlugins[blockedPlugin] = false; - } - } - - return { - ...policySpec, - plugins: newPlugins, - }; -} diff --git a/admission-controller/server/src/utils/validation-server.ts b/admission-controller/server/src/utils/validation-server.ts deleted file mode 100644 index 6033efa..0000000 --- a/admission-controller/server/src/utils/validation-server.ts +++ /dev/null @@ -1,279 +0,0 @@ -import fastify from 'fastify'; -import pino from 'pino'; -import path from 'path'; -import {readFileSync} from 'fs'; -import {Message, Resource, RuleLevel, ValidationResult} from '@monokle/validation'; -import {V1ObjectMeta} from '@kubernetes/client-node'; -import {ValidatorManager} from './validator-manager.js'; -import KubeClient from "./kube-client.js"; - -export type ValidationServerOptions = { - port: number - host: string -}; - -export type AdmissionRequestObject = { - apiVersion: string - kind: string - metadata: V1ObjectMeta - spec: any - status: any -}; - -export type AdmissionRequest = { - apiVersion: string - kind: string - request: { - name: string - namespace: string - uid: string - object: AdmissionRequestObject - } -}; - -// See -// https://pkg.go.dev/k8s.io/api/admission/v1#AdmissionResponse -// https://kubernetes.io/docs/reference/config-api/apiserver-admission.v1/#admission-k8s-io-v1-AdmissionReview -export type AdmissionResponse = { - kind: string - apiVersion: string - response: { - uid: string - allowed: boolean - warnings?: string[] - status: { - message: string - } - } -}; - -export type Violation = { - ruleId: string - message: Message - level?: RuleLevel - actions: string[] - name: string -} - -export class ValidationServer { - private _server: ReturnType; - - constructor( - private readonly _validators: ValidatorManager, - private readonly _ignoredNamespaces: string[], - private readonly _logger: ReturnType, - private readonly _options: ValidationServerOptions = { - port: 8443, - host: '0.0.0.0' - } - ) { - try { - this._server = fastify({ - // https: { - // key: readFileSync(path.join('/run/secrets/tls', 'tls.key')), - // cert: readFileSync(path.join('/run/secrets/tls', 'tls.crt')) - // } - }); - } catch (err) { - this._logger.error({msg: 'Failed to read TLS certificate', err}); - process.exit(1); - } - - this._initRouting(); - } - - async start() { - return new Promise((resolve, reject) => { - this._server.listen({port: this._options.port, host: this._options.host}, (err, address) => { - if (err) { - reject(err); - } - - this._logger.info(`Server listening at ${address}`); - - resolve(address); - }); - - }); - } - - async stop() { - if (this._server) { - await this._server.close(); - } - } - - private _initRouting() { - this._server.post("/validate", async (req, _res): Promise => { - - this._logger.trace({requestBody: req.body}); - - const body = req.body as AdmissionRequest; - const namespace = body.request?.namespace || body.request?.object?.metadata?.namespace; - - const response = { - kind: body?.kind || '', - apiVersion: body?.apiVersion || '', - response: { - uid: body?.request?.uid || "", - allowed: true, - status: { - message: "OK" - } - } - } - - if (namespace && this._ignoredNamespaces.includes(namespace)) { - this._logger.error({msg: 'Namespace ignored', namespace}); - return response; - } - - const resource = body.request?.object; - if (!resource) { - this._logger.error({msg: 'No resource found', metadata: body.request}); - return response; - } - - const namespaceObject = namespace ? await KubeClient.getNamespace(namespace) : undefined; - - this._logger.debug({request: req, namespaceObject}); - - const validators = this._validators.getMatchingValidators(resource, namespaceObject); - - this._logger.debug({msg: 'Matching validators', count: validators.length}); - - if (validators.length === 0) { - return response; - } - - const resourceForValidation = this._createResourceForValidation(body); - const validationResponses = await Promise.all(validators.map(async (validator) => { - return { - result: await validator.validator.validate({ resources: [resourceForValidation] }), - policy: validator.policy - }; - } - )); - - const violations: Violation[] = []; - for (const validationResponse of validationResponses) { - const actions = validationResponse.policy.binding.validationActions; - - for (const result of validationResponse.result.runs) { - for (const item of result.results) { - violations.push({ - ruleId: item.ruleId, - message: item.message, - level: item.level, - actions: actions, - name: this._getFullyQualifiedName(item) ?? resourceForValidation.name - }); - } - } - } - - this._logger.trace({resourceForValidation, validationResponses}); - - if (violations.length === 0) { - this._logger.debug({msg: 'No violations', response}); - return response; - } - - const violationsByAction = violations.reduce((acc: Record, violation: Violation) => { - const actions = violation.actions; - - for (const action of actions) { - if (!acc[action]) { - acc[action] = []; - } - - acc[action].push(violation); - } - - return acc; - }, {}); - - const responseFull = this._handleViolationsByAction(violationsByAction, resourceForValidation, response); - - this._logger.debug({response}); - - return responseFull; - }); - } - - private _createResourceForValidation(admissionResource: AdmissionRequest): Resource { - const resource = { - id: admissionResource.request?.uid || '', - fileId: '', - filePath: '', - fileOffset: 0, - name: admissionResource.request?.name || '', - apiVersion: admissionResource.request?.object?.apiVersion || '', - kind: admissionResource.request?.object?.kind || '', - namespace: admissionResource.request?.namespace || '', - content: admissionResource.request?.object || {}, - text: '' - }; - - return resource; - } - - private _handleViolationsByAction(violationsByAction: Record, resource: Resource, response: AdmissionResponse) { - for (const action of Object.keys(violationsByAction)) { - // 'Warn' action should be mapped to warnings, see: - // - https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#validation-actions - // - https://kubernetes.io/blog/2020/09/03/warnings/ - if (action.toLowerCase() === 'warn') { - response = this._handleViolationsAsWarn(violationsByAction[action], resource, response); - } else if (action.toLowerCase() === 'deny') { - const violationMessages = this._getViolationsMessages(violationsByAction[action], resource); - - response.response.allowed = false; - response.response.status.message = violationMessages.join("\n"); - } - } - - return response; - } - - private _handleViolationsAsWarn(violations: Violation[], resource: Resource, response: AdmissionResponse) { - const violationMessages = this._getViolationsMessages(violations, resource); - if (violationMessages.length > 0) { - response.response.warnings = violationMessages; - } - - return response; - } - - private _getViolationsMessages(violations: Violation[], resource: Resource): string[] { - const errors = violations - .filter((v) => v.level === 'error') - .map((e) => this._formatViolationMessage(e, resource)); - - const warnings = violations - .filter((v) => v.level === 'warning') - .map((e) => this._formatViolationMessage(e, resource)); - - if (errors.length > 0 || warnings.length > 0) { - return [ - `Monokle Admission Controller found ${errors.length} errors and ${warnings.length} warnings:`, - ...errors, - ...warnings, - 'You can use Monokle Cloud (https://monokle.io/) to fix those errors easily.', - ]; - } - - return []; - } - - private _getFullyQualifiedName(result: ValidationResult) { - const locations = result.locations; - const locationWithName = locations.find((l) => l.logicalLocations?.length && l.logicalLocations.length > 0 && l.logicalLocations[0].fullyQualifiedName); - - return locationWithName ? (locationWithName.logicalLocations || [])[0].fullyQualifiedName?.replace(/\./g, '/').replace('@', '').trim() : null; - } - - private _formatViolationMessage(violation: Violation, resource: Resource) { - return `${violation.ruleId} (${violation.level}): ${violation.message.text.replace(/\.$/, '')}, in kind "${resource.kind}" with name "${violation.name}".`; - } -} diff --git a/admission-controller/server/src/utils/validator-manager.ts b/admission-controller/server/src/utils/validator-manager.ts deleted file mode 100644 index d4851b5..0000000 --- a/admission-controller/server/src/utils/validator-manager.ts +++ /dev/null @@ -1,71 +0,0 @@ -import pino from 'pino'; -import {AnnotationSuppressor, Config, DisabledFixer, FingerprintSuppressor, MonokleValidator, RemotePluginLoader, ResourceParser, SchemaLoader} from '@monokle/validation'; -import {MonokleApplicablePolicy, MonoklePolicy, PolicyManager} from './policy-manager.js'; -import {AdmissionRequestObject} from './validation-server.js'; -import { V1Namespace } from '@kubernetes/client-node'; - -export type MonokleApplicableValidator = { - validator: MonokleValidator, - policy: MonokleApplicablePolicy -} - -export class ValidatorManager { - private _validators = new Map(); // Map - - constructor( - private readonly _policyManager: PolicyManager, - private readonly _logger: ReturnType, - ) { - this._policyManager.on('policyUpdated', async (policy: MonoklePolicy) => { - await this.setupValidator(policy.metadata!.name!, policy.spec); - }); - - this._policyManager.on('policyRemoved', async (policy: MonoklePolicy) => { - await this._validators.delete(policy.metadata!.name!); - }); - } - - getMatchingValidators(resource: AdmissionRequestObject, resourceNamespace?: V1Namespace): MonokleApplicableValidator[] { - const matchingPolicies = this._policyManager.getMatchingPolicies(resource, resourceNamespace); - - if (matchingPolicies.length === 0) { - return []; - } - - return matchingPolicies.map((policy) => { - if (!this._validators.has(policy.binding.policyName)) { - // This should not happen and means there is a bug in other place in the code. Raise warning and skip. - // Do not create validator instance here to keep this function sync and to keep processing time low. - this._logger.warn({msg: 'ValidatorManager: Validator not found', policyName: policy.binding.policyName}); - return null; - } - - return { - validator: this._validators.get(policy.binding.policyName)!, - policy - } - }).filter((validator) => validator !== null) as MonokleApplicableValidator[]; - } - - private async setupValidator(policyName: string, policy: Config) { - if (this._validators.has(policyName)) { - await this._validators.get(policyName)!.preload(policy); - } else { - const validator = new MonokleValidator( - { - loader: new RemotePluginLoader(), - parser: new ResourceParser(), - schemaLoader: new SchemaLoader(), - suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], - fixer: new DisabledFixer(), - } - ); - - // Run separately (instead of passing config to constructor) to make sure that validator - // is ready when 'setupValidator' function call fulfills. - await validator.preload(policy); - - this._validators.set(policyName, validator); - } - } -} diff --git a/admission-controller/synchronizer/tsconfig.json b/admission-controller/synchronizer/tsconfig.json index bf8c6f0..37cb71d 100644 --- a/admission-controller/synchronizer/tsconfig.json +++ b/admission-controller/synchronizer/tsconfig.json @@ -7,8 +7,9 @@ "outDir": "./dist" /* Specify an output folder for all emitted files. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - "strict": true /* Enable all strict type-checking options. */, + "experimentalDecorators": true + "strict": true /* Enable all strict type-checking options. */ }, "exclude": ["node_modules", "dist"], "include": ["src"] -} \ No newline at end of file +} From ee6f253b975a85428472c7f66b277589c44dbf53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Fri, 9 Feb 2024 14:11:55 +0200 Subject: [PATCH 6/9] feat: rework server --- admission-controller/server/.eslintrc.js | 31 -- admission-controller/server/.eslintrc.json | 28 ++ admission-controller/server/package-lock.json | 265 +++--------- admission-controller/server/package.json | 11 +- .../src/admission/admission.controller.ts | 381 ++++++++++-------- .../server/src/admission/admission.models.ts | 66 +-- .../server/src/admission/admission.module.ts | 13 +- admission-controller/server/src/config.ts | 4 +- admission-controller/server/src/index.ts | 49 +-- .../server/src/kubernetes/client.service.ts | 71 ++++ .../src/kubernetes/kubernetes.module.ts | 12 + .../server/src/kubernetes/resource.service.ts | 22 + .../server/src/kubernetes/watcher.service.ts | 65 +++ .../src/policies/policies.manager.service.ts | 201 --------- .../server/src/policies/policies.models.ts | 50 +-- .../server/src/policies/policies.module.ts | 11 +- .../server/src/policies/policies.service.ts | 311 ++++++++++++++ .../server/src/policies/validation.service.ts | 86 ---- .../server/src/server.module.ts | 5 +- .../server/src/{ => shared}/config.service.ts | 4 +- .../server/src/shared/shared.module.ts | 11 + .../server/src/utils/kube-client.ts | 96 ----- admission-controller/server/tsconfig.json | 8 +- 23 files changed, 903 insertions(+), 898 deletions(-) delete mode 100644 admission-controller/server/.eslintrc.js create mode 100644 admission-controller/server/.eslintrc.json create mode 100644 admission-controller/server/src/kubernetes/client.service.ts create mode 100644 admission-controller/server/src/kubernetes/kubernetes.module.ts create mode 100644 admission-controller/server/src/kubernetes/resource.service.ts create mode 100644 admission-controller/server/src/kubernetes/watcher.service.ts delete mode 100644 admission-controller/server/src/policies/policies.manager.service.ts create mode 100644 admission-controller/server/src/policies/policies.service.ts delete mode 100644 admission-controller/server/src/policies/validation.service.ts rename admission-controller/server/src/{ => shared}/config.service.ts (88%) create mode 100644 admission-controller/server/src/shared/shared.module.ts delete mode 100644 admission-controller/server/src/utils/kube-client.ts diff --git a/admission-controller/server/.eslintrc.js b/admission-controller/server/.eslintrc.js deleted file mode 100644 index 6ce4b1e..0000000 --- a/admission-controller/server/.eslintrc.js +++ /dev/null @@ -1,31 +0,0 @@ -const { schema } = require('@octokit/graphql-schema'); - -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - tsconfigRootDir: __dirname, - sourceType: 'module', - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', - ], - root: true, - env: { - node: true, - jest: true, - }, - ignorePatterns: ['.eslintrc.js', 'src/__generated__/**', 'src / schema.gql'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - - '@typescript-eslint/no-unused-vars': 'warn', - - 'eol-last': 1, - }, -}; diff --git a/admission-controller/server/.eslintrc.json b/admission-controller/server/.eslintrc.json new file mode 100644 index 0000000..2f54a3c --- /dev/null +++ b/admission-controller/server/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint/eslint-plugin"], + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended" + ], + "root": true, + "env": { + "node": true, + "jest": true + }, + "ignorePatterns": ["dist/", "node_modules/"], + "rules": { + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + + "@typescript-eslint/no-unused-vars": "warn", + + "eol-last": 1 + } +} diff --git a/admission-controller/server/package-lock.json b/admission-controller/server/package-lock.json index 82d277d..5a8c35f 100644 --- a/admission-controller/server/package-lock.json +++ b/admission-controller/server/package-lock.json @@ -15,13 +15,9 @@ "@nestjs/common": "^10.3.2", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.3.2", + "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-fastify": "^10.3.2", - "fastify": "^3.28.0", - "isomorphic-fetch": "^3.0.0", - "node-fetch": "^3.3.2", - "pino": "^8.16.1", - "ts-node": "^10.9.2", - "type-fest": "^4.5.0" + "type-fest": "^4.10.2" }, "devDependencies": { "@types/node": "^20.8.8", @@ -31,6 +27,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.1.2", "prettier": "3.1.1", + "ts-node": "^10.9.2", "typescript": "^5.2.2" } }, @@ -47,6 +44,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -132,14 +130,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fastify/ajv-compiler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", - "integrity": "sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg==", - "dependencies": { - "ajv": "^6.12.6" - } - }, "node_modules/@fastify/cors": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-9.0.1.tgz", @@ -149,11 +139,6 @@ "mnemonist": "0.39.6" } }, - "node_modules/@fastify/error": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-2.0.0.tgz", - "integrity": "sha512-wI3fpfDT0t7p8E6dA2eTECzzOd+bZsZCJ2Hcv+Onn2b7ZwK3RwD27uW2QDaMtQhAfWQQP+WNK7nKf0twLsBf9w==" - }, "node_modules/@fastify/fast-json-stringify-compiler": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", @@ -293,6 +278,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -300,12 +286,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -489,6 +477,18 @@ } } }, + "node_modules/@nestjs/event-emitter": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz", + "integrity": "sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/@nestjs/platform-fastify": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-10.3.2.tgz", @@ -761,22 +761,26 @@ "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true }, "node_modules/@types/caseless": { "version": "0.12.3", @@ -1048,6 +1052,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -1068,6 +1073,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -1154,7 +1160,8 @@ "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -1199,17 +1206,6 @@ "node": ">=8.0.0" } }, - "node_modules/avvio": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.5.tgz", - "integrity": "sha512-AOhBxyLVdpOad3TujtC9kL/9r3HnTkxwQ5ggOsYrvvZP1cCFvzHWJd5XxZDFuTn+IN8vkKSG5SEJrd27vCSbeA==", - "dependencies": { - "archy": "^1.0.0", - "debug": "^4.0.0", - "fastq": "^1.6.1", - "queue-microtask": "^1.1.2" - } - }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -1447,7 +1443,8 @@ "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true }, "node_modules/cross-spawn": { "version": "7.0.3", @@ -1504,14 +1501,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1524,6 +1513,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, "engines": { "node": ">=0.3.1" } @@ -1820,6 +1810,11 @@ "node": ">=6" } }, + "node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -1895,20 +1890,6 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, - "node_modules/fast-json-stringify": { - "version": "2.7.13", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz", - "integrity": "sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==", - "dependencies": { - "ajv": "^6.11.0", - "deepmerge": "^4.2.2", - "rfdc": "^1.2.0", - "string-similarity": "^4.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -1941,70 +1922,11 @@ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" }, - "node_modules/fastify": { - "version": "3.29.5", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.29.5.tgz", - "integrity": "sha512-FBDgb1gkenZxxh4sTD6AdI6mFnZnsgckpjIXzIvfLSYCa4isfQeD8QWGPib63dxq6btnY0l1j8I0xYhMvUb+sw==", - "dependencies": { - "@fastify/ajv-compiler": "^1.0.0", - "@fastify/error": "^2.0.0", - "abstract-logging": "^2.0.0", - "avvio": "^7.1.2", - "fast-content-type-parse": "^1.0.0", - "fast-json-stringify": "^2.5.2", - "find-my-way": "^4.5.0", - "flatstr": "^1.0.12", - "light-my-request": "^4.2.0", - "pino": "^6.13.0", - "process-warning": "^1.0.0", - "proxy-addr": "^2.0.7", - "rfdc": "^1.1.4", - "secure-json-parse": "^2.0.0", - "semver": "^7.3.2", - "tiny-lru": "^8.0.1" - } - }, "node_modules/fastify-plugin": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" }, - "node_modules/fastify/node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", - "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", - "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/fastify/node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" - }, - "node_modules/fastify/node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" - }, - "node_modules/fastify/node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" - } - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2059,20 +1981,6 @@ "node": ">=8" } }, - "node_modules/find-my-way": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz", - "integrity": "sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg==", - "dependencies": { - "fast-decode-uri-component": "^1.0.1", - "fast-deep-equal": "^3.1.3", - "safe-regex2": "^2.0.0", - "semver-store": "^0.3.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2103,11 +2011,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" - }, "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", @@ -2662,42 +2565,6 @@ "node": ">= 0.8.0" } }, - "node_modules/light-my-request": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.12.0.tgz", - "integrity": "sha512-0y+9VIfJEsPVzK5ArSIJ8Dkxp8QMP7/aCuxCUtG/tr9a2NoOf/snATE/OUc05XUplJCEnRh6gTkH7xh9POt1DQ==", - "dependencies": { - "ajv": "^8.1.0", - "cookie": "^0.5.0", - "process-warning": "^1.0.0", - "set-cookie-parser": "^2.4.1" - } - }, - "node_modules/light-my-request/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/light-my-request/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2746,7 +2613,8 @@ "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -2900,23 +2768,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -3257,6 +3108,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -3524,11 +3376,6 @@ "node": ">=10" } }, - "node_modules/semver-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", - "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==" - }, "node_modules/sentence-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", @@ -3644,12 +3491,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-similarity": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", - "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info." - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3731,14 +3572,6 @@ "real-require": "^0.2.0" } }, - "node_modules/tiny-lru": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-8.0.2.tgz", - "integrity": "sha512-ApGvZ6vVvTNdsmt676grvCkUCGwzG9IqXma5Z07xJgiC5L7akUMof5U8G2JTI9Rz/ovtVhJBlY6mNhEvtjzOIg==", - "engines": { - "node": ">=6" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3792,6 +3625,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -3864,9 +3698,9 @@ } }, "node_modules/type-fest": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.5.0.tgz", - "integrity": "sha512-diLQivFzddJl4ylL3jxSkEc39Tpw7o1QeEHIPxVwryDK2lpB7Nqhzhuo6v5/Ls08Z0yPSAhsyAWlv1/H0ciNmw==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.2.tgz", + "integrity": "sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==", "engines": { "node": ">=16" }, @@ -3878,6 +3712,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3937,7 +3772,8 @@ "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true }, "node_modules/verror": { "version": "1.10.0", @@ -4037,6 +3873,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, "engines": { "node": ">=6" } diff --git a/admission-controller/server/package.json b/admission-controller/server/package.json index 9ec3938..dab6165 100644 --- a/admission-controller/server/package.json +++ b/admission-controller/server/package.json @@ -16,13 +16,9 @@ "@nestjs/common": "^10.3.2", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.3.2", + "@nestjs/event-emitter": "^2.0.4", "@nestjs/platform-fastify": "^10.3.2", - "fastify": "^3.28.0", - "isomorphic-fetch": "^3.0.0", - "node-fetch": "^3.3.2", - "pino": "^8.16.1", - "ts-node": "^10.9.2", - "type-fest": "^4.5.0" + "type-fest": "^4.10.2" }, "devDependencies": { "@types/node": "^20.8.8", @@ -32,6 +28,7 @@ "eslint-config-prettier": "9.1.0", "eslint-plugin-prettier": "5.1.2", "prettier": "3.1.1", + "ts-node": "^10.9.2", "typescript": "^5.2.2" } -} \ No newline at end of file +} diff --git a/admission-controller/server/src/admission/admission.controller.ts b/admission-controller/server/src/admission/admission.controller.ts index b0d0c27..3683246 100644 --- a/admission-controller/server/src/admission/admission.controller.ts +++ b/admission-controller/server/src/admission/admission.controller.ts @@ -1,184 +1,251 @@ -import {Body, Controller, Logger, Post} from "@nestjs/common"; -import {AdmissionRequest, AdmissionResponse, Violation} from "./admission.models"; -import KubeClient from "../utils/kube-client"; -import {Resource, ValidationResult} from "@monokle/validation"; -import {ConfigService} from "@nestjs/config"; -import {ValidationService} from "../policies/validation.service"; +import { Body, Controller, Logger, Post } from '@nestjs/common'; +import { + AdmissionRequest, + AdmissionResponse, + Violation, +} from './admission.models'; +import { Resource, ValidationResult } from '@monokle/validation'; +import { ConfigService } from '../shared/config.service'; +import { PoliciesService } from '../policies/policies.service'; +import { ResourceService } from '../kubernetes/resource.service'; @Controller('validate') export class AdmissionController { - private readonly log = new Logger(AdmissionController.name); - private readonly ignoredNamespaces: string[]; - - constructor(private readonly $config: ConfigService, private readonly $validation: ValidationService) { - this.ignoredNamespaces = this.$config.get('ignoredNamespaces') ?? []; + private readonly log = new Logger(AdmissionController.name); + private readonly ignoredNamespaces: string[]; + + constructor( + private readonly $config: ConfigService, + private readonly $policies: PoliciesService, + private readonly $kubernetes: ResourceService, + ) { + this.ignoredNamespaces = this.$config.get('ignoredNamespaces') ?? []; + } + + private static createResourceForValidation( + admissionResource: AdmissionRequest, + ): Resource { + const resource = { + id: admissionResource.request?.uid || '', + fileId: '', + filePath: '', + fileOffset: 0, + name: admissionResource.request?.name || '', + apiVersion: admissionResource.request?.object?.apiVersion || '', + kind: admissionResource.request?.object?.kind || '', + namespace: admissionResource.request?.namespace || '', + content: admissionResource.request?.object || {}, + text: '', + }; + + return resource; + } + + private static handleViolationsByAction( + violationsByAction: Record, + resource: Resource, + response: AdmissionResponse, + ) { + for (const action of Object.keys(violationsByAction)) { + // 'Warn' action should be mapped to warnings, see: + // - https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#validation-actions + // - https://kubernetes.io/blog/2020/09/03/warnings/ + if (action.toLowerCase() === 'warn') { + response = this.handleViolationsAsWarn( + violationsByAction[action], + resource, + response, + ); + } else if (action.toLowerCase() === 'deny') { + const violationMessages = this.getViolationsMessages( + violationsByAction[action], + resource, + ); + + response.response.allowed = false; + response.response.status.message = violationMessages.join('\n'); + } } - @Post() - async validate(@Body() body: AdmissionRequest): Promise { - this.log.verbose({body}); - const namespace = body.request?.namespace || body.request?.object?.metadata?.namespace; - - const response = { - kind: body?.kind || '', - apiVersion: body?.apiVersion || '', - response: { - uid: body?.request?.uid || "", - allowed: true, - status: { - message: "OK" - } - } - } - - if (namespace && this.ignoredNamespaces.includes(namespace)) { - this.log.error({msg: 'Namespace ignored', namespace}); - return response; - } - - const resource = body.request?.object; - if (!resource) { - this.log.error({msg: 'No resource found', metadata: body.request}); - return response; - } - - const namespaceObject = namespace ? await KubeClient.getNamespace(namespace) : undefined; - this.log.debug({ namespaceObject }); - - const validators = this.$validation.getMatchingValidators(resource, namespaceObject); - - this.log.debug({msg: 'Matching validators', count: validators.length}); - - if (validators.length === 0) { - return response; - } - - const resourceForValidation = AdmissionController.createResourceForValidation(body); - const validationResponses = await Promise.all(validators.map(async (validator) => { - return { - result: await validator.validator.validate({ resources: [resourceForValidation] }), - policy: validator.policy - }; - } - )); - - const violations: Violation[] = []; - for (const validationResponse of validationResponses) { - const actions = validationResponse.policy.binding.validationActions; - - for (const result of validationResponse.result.runs) { - for (const item of result.results) { - violations.push({ - ruleId: item.ruleId, - message: item.message, - level: item.level, - actions: actions, - name: AdmissionController.getFullyQualifiedName(item) ?? resourceForValidation.name - }); - } - } - } - - this.log.verbose({resourceForValidation, validationResponses}); + return response; + } + + private static handleViolationsAsWarn( + violations: Violation[], + resource: Resource, + response: AdmissionResponse, + ) { + const violationMessages = AdmissionController.getViolationsMessages( + violations, + resource, + ); + if (violationMessages.length > 0) { + response.response.warnings = violationMessages; + } - if (violations.length === 0) { - this.log.debug({msg: 'No violations', response}); - return response; - } + return response; + } + + private static getViolationsMessages( + violations: Violation[], + resource: Resource, + ): string[] { + const errors = violations + .filter((v) => v.level === 'error') + .map((e) => AdmissionController.formatViolationMessage(e, resource)); + + const warnings = violations + .filter((v) => v.level === 'warning') + .map((e) => AdmissionController.formatViolationMessage(e, resource)); + + if (errors.length > 0 || warnings.length > 0) { + return [ + `Monokle Admission Controller found ${errors.length} errors and ${warnings.length} warnings:`, + ...errors, + ...warnings, + 'You can use Monokle Cloud (https://monokle.io/) to fix those errors easily.', + ]; + } - const violationsByAction = violations.reduce((acc: Record, violation: Violation) => { - const actions = violation.actions; + return []; + } + + private static getFullyQualifiedName(result: ValidationResult) { + const locations = result.locations; + const locationWithName = locations.find( + (l) => + l.logicalLocations?.length && + l.logicalLocations.length > 0 && + l.logicalLocations[0].fullyQualifiedName, + ); + + return locationWithName + ? (locationWithName.logicalLocations || [])[0].fullyQualifiedName + ?.replace(/\./g, '/') + .replace('@', '') + .trim() + : null; + } + + private static formatViolationMessage( + violation: Violation, + resource: Resource, + ) { + return `${violation.ruleId} (${ + violation.level + }): ${violation.message.text.replace(/\.$/, '')}, in kind "${ + resource.kind + }" with name "${violation.name}".`; + } + + @Post() + async validate(@Body() body: AdmissionRequest): Promise { + this.log.verbose({ body }); + const namespace = + body.request?.namespace || body.request?.object?.metadata?.namespace; + + const response = { + kind: body?.kind || '', + apiVersion: body?.apiVersion || '', + response: { + uid: body?.request?.uid || '', + allowed: true, + status: { + message: 'OK', + }, + }, + }; + + if (namespace && this.ignoredNamespaces.includes(namespace)) { + this.log.error({ msg: 'Namespace ignored', namespace }); + return response; + } - for (const action of actions) { - if (!acc[action]) { - acc[action] = []; - } + const resource = body.request?.object; + if (!resource) { + this.log.error({ msg: 'No resource found', metadata: body.request }); + return response; + } - acc[action].push(violation); - } + const namespaceObject = namespace + ? await this.$kubernetes.getNamespace(namespace) + : undefined; + this.log.debug({ namespaceObject }); - return acc; - }, {}); + const validators = this.$policies.getMatchingValidators( + resource, + namespaceObject, + ); - const responseFull = AdmissionController.handleViolationsByAction(violationsByAction, resourceForValidation, response); + this.log.debug({ msg: 'Matching validators', count: validators.length }); - this.log.debug({response}); - return responseFull; + if (validators.length === 0) { + return response; } - private static createResourceForValidation(admissionResource: AdmissionRequest): Resource { - const resource = { - id: admissionResource.request?.uid || '', - fileId: '', - filePath: '', - fileOffset: 0, - name: admissionResource.request?.name || '', - apiVersion: admissionResource.request?.object?.apiVersion || '', - kind: admissionResource.request?.object?.kind || '', - namespace: admissionResource.request?.namespace || '', - content: admissionResource.request?.object || {}, - text: '' + const resourceForValidation = + AdmissionController.createResourceForValidation(body); + const validationResponses = await Promise.all( + validators.map(async (validator) => { + return { + result: await validator.validator.validate({ + resources: [resourceForValidation], + }), + policy: validator.policy, }; - - return resource; + }), + ); + + const violations: Violation[] = []; + for (const validationResponse of validationResponses) { + const actions = validationResponse.policy.binding.validationActions; + + for (const result of validationResponse.result.runs) { + for (const item of result.results) { + violations.push({ + ruleId: item.ruleId, + message: item.message, + level: item.level, + actions: actions, + name: + AdmissionController.getFullyQualifiedName(item) ?? + resourceForValidation.name, + }); + } + } } - private static handleViolationsByAction(violationsByAction: Record, resource: Resource, response: AdmissionResponse) { - for (const action of Object.keys(violationsByAction)) { - // 'Warn' action should be mapped to warnings, see: - // - https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#validation-actions - // - https://kubernetes.io/blog/2020/09/03/warnings/ - if (action.toLowerCase() === 'warn') { - response = this.handleViolationsAsWarn(violationsByAction[action], resource, response); - } else if (action.toLowerCase() === 'deny') { - const violationMessages = this.getViolationsMessages(violationsByAction[action], resource); - - response.response.allowed = false; - response.response.status.message = violationMessages.join("\n"); - } - } + this.log.verbose({ resourceForValidation, validationResponses }); - return response; + if (violations.length === 0) { + this.log.debug({ msg: 'No violations', response }); + return response; } - private static handleViolationsAsWarn(violations: Violation[], resource: Resource, response: AdmissionResponse) { - const violationMessages = AdmissionController.getViolationsMessages(violations, resource); - if (violationMessages.length > 0) { - response.response.warnings = violationMessages; - } + const violationsByAction = violations.reduce( + (acc: Record, violation: Violation) => { + const actions = violation.actions; - return response; - } + for (const action of actions) { + if (!acc[action]) { + acc[action] = []; + } - private static getViolationsMessages(violations: Violation[], resource: Resource): string[] { - const errors = violations - .filter((v) => v.level === 'error') - .map((e) => AdmissionController.formatViolationMessage(e, resource)); - - const warnings = violations - .filter((v) => v.level === 'warning') - .map((e) => AdmissionController.formatViolationMessage(e, resource)); - - if (errors.length > 0 || warnings.length > 0) { - return [ - `Monokle Admission Controller found ${errors.length} errors and ${warnings.length} warnings:`, - ...errors, - ...warnings, - 'You can use Monokle Cloud (https://monokle.io/) to fix those errors easily.', - ]; + acc[action].push(violation); } - return []; - } + return acc; + }, + {}, + ); - private static getFullyQualifiedName(result: ValidationResult) { - const locations = result.locations; - const locationWithName = locations.find((l) => l.logicalLocations?.length && l.logicalLocations.length > 0 && l.logicalLocations[0].fullyQualifiedName); + const responseFull = AdmissionController.handleViolationsByAction( + violationsByAction, + resourceForValidation, + response, + ); - return locationWithName ? (locationWithName.logicalLocations || [])[0].fullyQualifiedName?.replace(/\./g, '/').replace('@', '').trim() : null; - } - - private static formatViolationMessage(violation: Violation, resource: Resource) { - return `${violation.ruleId} (${violation.level}): ${violation.message.text.replace(/\.$/, '')}, in kind "${resource.kind}" with name "${violation.name}".`; - } + this.log.debug({ response }); + return responseFull; + } } diff --git a/admission-controller/server/src/admission/admission.models.ts b/admission-controller/server/src/admission/admission.models.ts index 4909675..98c8e80 100644 --- a/admission-controller/server/src/admission/admission.models.ts +++ b/admission-controller/server/src/admission/admission.models.ts @@ -1,50 +1,50 @@ -import {V1ObjectMeta} from "@kubernetes/client-node"; -import {Message, RuleLevel} from "@monokle/validation"; +import { V1ObjectMeta } from '@kubernetes/client-node'; +import { Message, RuleLevel } from '@monokle/validation'; export type ValidationServerOptions = { - port: number - host: string + port: number; + host: string; }; export type AdmissionRequestObject = { - apiVersion: string - kind: string - metadata: V1ObjectMeta - spec: any - status: any + apiVersion: string; + kind: string; + metadata: V1ObjectMeta; + spec: any; + status: any; }; export type AdmissionRequest = { - apiVersion: string - kind: string - request: { - name: string - namespace: string - uid: string - object: AdmissionRequestObject - } + apiVersion: string; + kind: string; + request: { + name: string; + namespace: string; + uid: string; + object: AdmissionRequestObject; + }; }; // See // https://pkg.go.dev/k8s.io/api/admission/v1#AdmissionResponse // https://kubernetes.io/docs/reference/config-api/apiserver-admission.v1/#admission-k8s-io-v1-AdmissionReview export type AdmissionResponse = { - kind: string - apiVersion: string - response: { - uid: string - allowed: boolean - warnings?: string[] - status: { - message: string - } - } + kind: string; + apiVersion: string; + response: { + uid: string; + allowed: boolean; + warnings?: string[]; + status: { + message: string; + }; + }; }; export type Violation = { - ruleId: string - message: Message - level?: RuleLevel - actions: string[] - name: string -} + ruleId: string; + message: Message; + level?: RuleLevel; + actions: string[]; + name: string; +}; diff --git a/admission-controller/server/src/admission/admission.module.ts b/admission-controller/server/src/admission/admission.module.ts index c72b02b..a425ba1 100644 --- a/admission-controller/server/src/admission/admission.module.ts +++ b/admission-controller/server/src/admission/admission.module.ts @@ -1,9 +1,12 @@ -import { Module } from "@nestjs/common"; -import {AdmissionController} from "./admission.controller"; +import { Module } from '@nestjs/common'; +import { AdmissionController } from './admission.controller'; +import { SharedModule } from '../shared/shared.module'; +import { PoliciesModule } from '../policies/policies.module'; +import { KubernetesModule } from '../kubernetes/kubernetes.module'; @Module({ - imports: [], - controllers: [AdmissionController], - providers: [], + imports: [SharedModule, KubernetesModule, PoliciesModule], + controllers: [AdmissionController], + providers: [], }) export class AdmissionModule {} diff --git a/admission-controller/server/src/config.ts b/admission-controller/server/src/config.ts index ac1f438..5098ed5 100644 --- a/admission-controller/server/src/config.ts +++ b/admission-controller/server/src/config.ts @@ -1,7 +1,7 @@ import * as FS from 'node:fs/promises'; const Configuration = { - logLevel: process.env.MONOKLE_LOG_LEVEL ?? 'warn', + logLevel: process.env.MONOKLE_LOG_LEVEL ?? 'log', ignoredNamespaces: (process.env.MONOKLE_IGNORE_NAMESPACES ?? '') .split(',') .filter(Boolean), @@ -11,7 +11,7 @@ const Configuration = { port: 8443, tls: { key: process.env.TLS_KEY ?? '/run/secrets/tls/tls.key', - cert: process.env.TLS_CERT ?? '/run/secrets/tls/tls.crt', + cert: process.env.TLS_CRT ?? '/run/secrets/tls/tls.crt', }, }, }; diff --git a/admission-controller/server/src/index.ts b/admission-controller/server/src/index.ts index 5fd95e9..b1f886b 100644 --- a/admission-controller/server/src/index.ts +++ b/admission-controller/server/src/index.ts @@ -1,12 +1,21 @@ -import KubeClient from './utils/kube-client.js'; import { NestFactory } from '@nestjs/core'; import { ServerModule } from './server.module'; import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; -import { ConfigService } from './config.service'; import Configuration from './config'; +import { ConfigService } from './shared/config.service'; +import { Logger, LogLevel } from '@nestjs/common'; + +const LOG_LEVELS = [ + 'fatal', + 'error', + 'warn', + 'log', + 'debug', + 'verbose', +] as LogLevel[]; const app = await NestFactory.create( ServerModule, @@ -16,34 +25,12 @@ const app = await NestFactory.create( ); const config = app.get(ConfigService); -app.useLogger(config.get('logLevel') as any); - -KubeClient.buildKubeConfig(); - -const policyInformer = await KubeClient.getInformer( - 'monokle.io', - 'v1alpha1', - 'policies', - (err: any) => { - logger.error({ - msg: 'Informer: Policies: Error', - err: err.message, - body: err.body, - }); - }, -); - -const bindingsInformer = await KubeClient.getInformer( - 'monokle.io', - 'v1alpha1', - 'policybindings', - (err: any) => { - logger.error({ - msg: 'Informer: Bindings: Error', - err: err.message, - body: err.body, - }); - }, -); +const levels = LOG_LEVELS.slice(LOG_LEVELS.indexOf(config.get('logLevel'))); +app.useLogger([config.get('logLevel')]); await app.listen(config.get('server.port'), config.get('server.host')); +new Logger('Entrypoint').log( + `Server listening on ${config.get('server.host')}:${config.get( + 'server.port', + )}`, +); diff --git a/admission-controller/server/src/kubernetes/client.service.ts b/admission-controller/server/src/kubernetes/client.service.ts new file mode 100644 index 0000000..4dbaa12 --- /dev/null +++ b/admission-controller/server/src/kubernetes/client.service.ts @@ -0,0 +1,71 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import k8s from '@kubernetes/client-node'; + +@Injectable() +export class ClientService implements OnModuleInit { + private readonly log = new Logger(ClientService.name); + private readonly _kubeconfig: k8s.KubeConfig; + + constructor() { + this._kubeconfig = new k8s.KubeConfig(); + } + + async onModuleInit(): Promise { + try { + if (process.env.KUBE_CONTEXT) { + this._kubeconfig.loadFromDefault(); + this._kubeconfig.setCurrentContext(process.env.KUBE_CONTEXT); + } else { + this._kubeconfig.loadFromCluster(); + } + } catch (e) { + console.warn('Failed to load kubeconfig from file'); + } + + await this._kubeconfig + .makeApiClient(k8s.CoreV1Api) + .listNamespace() + .then(() => this.log.log(`Connected to k8s Api Server`)) + .catch((err) => { + throw new Error(`Failed bootstrap check, list namespaces: ${err}`); + }); + } + + api(api: new () => T): T { + return this._kubeconfig.makeApiClient(api); + } + + watch( + group: string, + version: string, + plural: string, + ) { + const cr = `${group}/${version}/${plural}`; + const api = this.api(k8s.CustomObjectsApi); + + const informer = k8s.makeInformer( + this._kubeconfig, + `/apis/${cr}`, + async () => { + this.log.debug(`Building initial inventory of ${cr}`); + const result = (await api.listClusterCustomObject( + group, + version, + plural, + )) as any; + return result; + }, + ); + + this.log.log(`Watching for changes in ${cr}`); + + informer.on('error', (err) => { + setTimeout(async () => { + this.log.warn(err); + await informer.start(); + }, 500); + }); + + return informer; + } +} diff --git a/admission-controller/server/src/kubernetes/kubernetes.module.ts b/admission-controller/server/src/kubernetes/kubernetes.module.ts new file mode 100644 index 0000000..fea8720 --- /dev/null +++ b/admission-controller/server/src/kubernetes/kubernetes.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ClientService } from './client.service'; +import { ResourceService } from './resource.service'; +import { WatcherService } from './watcher.service'; + +@Module({ + imports: [], + controllers: [], + providers: [ClientService, ResourceService, WatcherService], + exports: [ResourceService, WatcherService], +}) +export class KubernetesModule {} diff --git a/admission-controller/server/src/kubernetes/resource.service.ts b/admission-controller/server/src/kubernetes/resource.service.ts new file mode 100644 index 0000000..1ee68b4 --- /dev/null +++ b/admission-controller/server/src/kubernetes/resource.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { ClientService } from './client.service'; +import k8s from '@kubernetes/client-node'; + +@Injectable() +export class ResourceService { + constructor(private readonly $client: ClientService) {} + + listNamespaces() { + return this.$client + .api(k8s.CoreV1Api) + .listNamespace() + .then((res) => res.body.items); + } + + getNamespace(name: string) { + return this.$client + .api(k8s.CoreV1Api) + .readNamespace(name) + .then((res) => res.body); + } +} diff --git a/admission-controller/server/src/kubernetes/watcher.service.ts b/admission-controller/server/src/kubernetes/watcher.service.ts new file mode 100644 index 0000000..4ccb816 --- /dev/null +++ b/admission-controller/server/src/kubernetes/watcher.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; +import { ClientService } from './client.service'; +import k8s from '@kubernetes/client-node'; +import { EventEmitter2 } from '@nestjs/event-emitter'; + +@Injectable() +export class WatcherService { + public static WATCH_PREFIX = 'kubernetes-client:watch'; + + constructor( + private readonly $client: ClientService, + private readonly $events: EventEmitter2, + ) {} + + public static getEventsKey(group: string, version: string, plural: string) { + return `${WatcherService.WATCH_PREFIX}:${group}:${version}:${plural}`; + } + + async watch( + group: string, + version: string, + plural: string, + ) { + const informer = this.$client.watch(group, version, plural); + + informer.on( + 'connect', + this.$events.emit.bind( + this.$events, + `${WatcherService.getEventsKey(group, version, plural)}:connect`, + ), + ); + informer.on( + 'change', + this.$events.emit.bind( + this.$events, + `${WatcherService.getEventsKey(group, version, plural)}:change`, + ), + ); + informer.on( + 'add', + this.$events.emit.bind( + this.$events, + `${WatcherService.getEventsKey(group, version, plural)}:add`, + ), + ); + informer.on( + 'update', + this.$events.emit.bind( + this.$events, + `${WatcherService.getEventsKey(group, version, plural)}:update`, + ), + ); + informer.on( + 'delete', + this.$events.emit.bind( + this.$events, + `${WatcherService.getEventsKey(group, version, plural)}:delete`, + ), + ); + + await informer.start(); + return informer; + } +} diff --git a/admission-controller/server/src/policies/policies.manager.service.ts b/admission-controller/server/src/policies/policies.manager.service.ts deleted file mode 100644 index 0e7422a..0000000 --- a/admission-controller/server/src/policies/policies.manager.service.ts +++ /dev/null @@ -1,201 +0,0 @@ -import {Injectable, Logger} from "@nestjs/common"; -import {InformerWrapper} from "../utils/kube-client"; -import {EventEmitter} from "events"; -import {AdmissionRequestObject} from "../utils/validation-server"; -import {V1Namespace} from "@kubernetes/client-node"; -import {Config} from "@monokle/validation"; -import {MonokleApplicablePolicy, MonoklePolicy, MonoklePolicyBinding} from "./policies.models"; - -@Injectable() -export class PoliciesManagerService extends EventEmitter { - private static readonly PLUGIN_BLOCKLIST = [ - 'resource-links', - ]; - - private readonly _logger = new Logger(PoliciesManagerService.name); - - private readonly _policies = new Map(); // Map - private readonly _bindings = new Map(); // Map - - constructor( - private readonly _policyInformer: InformerWrapper, - private readonly _bindingInformer: InformerWrapper, - ) { - super(); - - this._policyInformer.informer.on('add', this.onPolicy.bind(this)); - this._policyInformer.informer.on('update', this.onPolicy.bind(this)); - this._policyInformer.informer.on('delete', this.onPolicyRemoval.bind(this)); - - this._bindingInformer.informer.on('add', this.onBinding.bind(this)); - this._bindingInformer.informer.on('update', this.onBinding.bind(this)); - this._bindingInformer.informer.on('delete', this.onBindingRemoval.bind(this)); - } - - private static postprocess(policy: MonoklePolicy) { - const newPolicy = { ...policy }; - newPolicy.spec = PoliciesManagerService.blockPlugins(newPolicy.spec); - return newPolicy; - } - - private static blockPlugins(policySpec: Config): Config { - if (policySpec.plugins === undefined) { - return policySpec; - } - - const newPlugins = { ...policySpec.plugins }; - for (const blockedPlugin of PoliciesManagerService.PLUGIN_BLOCKLIST) { - if (newPlugins[blockedPlugin] === true) { - newPlugins[blockedPlugin] = false; - } - } - - return { - ...policySpec, - plugins: newPlugins, - }; - } - - - getMatchingPolicies(resource: AdmissionRequestObject, resourceNamespace?: V1Namespace): MonokleApplicablePolicy[] { - this._logger.debug({policies: this._policies.size, bindings: this._bindings.size}); - - if (this._bindings.size === 0) { - return []; - } - - return Array.from(this._bindings.values()) - .map((binding) => { - const policy = this._policies.get(binding.spec.policyName); - - if (!policy) { - this._logger.error({msg: 'Binding is pointing to missing policy', binding}); - return null; - } - - if (binding.spec.matchResources && !this.isResourceMatching(binding, resource, resourceNamespace)) { - return null; - } - - return { - policy: policy.spec, - binding: binding.spec - } - }) - .filter((policy) => policy !== null) as MonokleApplicablePolicy[]; - } - - private onPolicy(rawPolicy: MonoklePolicy) { - const policy = PoliciesManagerService.postprocess(rawPolicy); - - this._logger.debug({msg: 'Policy updated', rawPolicy, policy}); - - this._policies.set(rawPolicy.metadata!.name!, policy); - - this.emit('policyUpdated', policy); - } - - private onPolicyRemoval(rawPolicy: MonoklePolicy) { - const policy = PoliciesManagerService.postprocess(rawPolicy); - - this._logger.debug({msg: 'Policy removed', rawPolicy, policy}); - - this._policies.delete(rawPolicy.metadata!.name!); - - this.emit('policyRemoved', policy); - } - - private onBinding(rawBinding: MonoklePolicyBinding) { - this._logger.debug({msg: 'Binding updated', rawBinding}); - - this._bindings.set(rawBinding.metadata!.name!, rawBinding); - - this.emit('bindingUpdated', rawBinding); - } - - private onBindingRemoval(rawBinding: MonoklePolicyBinding) { - this._logger.debug({msg: 'Binding removed', rawBinding}); - - this._bindings.delete(rawBinding.metadata!.name!); - - this.emit('bindingRemoved', rawBinding); - } - - // Based on K8s docs here - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchresources-v1beta1-admissionregistration-k8s-io: - private isResourceMatching(binding: MonoklePolicyBinding, resource: AdmissionRequestObject, resourceNamespace?: V1Namespace): boolean { - const namespaceMatchLabels = binding.spec.matchResources?.namespaceSelector?.matchLabels; - const namespaceMatchExpressions = binding.spec.matchResources?.namespaceSelector?.matchExpressions ?? []; - const kind = resource.kind.toLowerCase(); - const isClusterWide = ((resource as any).namespace || resource.metadata.namespace) === undefined; - - this._logger.verbose({ - msg: 'Checking if resource matches binding', - namespaceMatchLabels, - namespaceMatchExpressions, - kind, - resourceMetadata: resource.metadata.labels - }); - - // If non of the matchers are specified, then the resource matches, both cluster wide and namespaced ones. - // So this is global policy. As in docs: - // > Default to the empty LabelSelector, which matches everything. - if (!namespaceMatchLabels && !namespaceMatchExpressions?.length) { - return true; - } - - // Skip cluster-wide resources if namespaceSelector defined. - // This is different from the K8s docs which says: - // > If the object itself is a namespace (...) If the object is another cluster scoped resource, it never skips the policy. - if (isClusterWide && kind !== 'namespace') { - return false; - } - - // If resource is Namespace use it, if not get resource owning namespace. - // > If the object itself is a namespace, the matching is performed on object.metadata.labels - const namespaceObject = kind !== 'namespace' ? resourceNamespace : resource; - if (!namespaceObject) { - return false; - } - - const namespaceObjectLabels = namespaceObject?.metadata?.labels || {}; - - // Convert matchLabels to matchExpressions to have single matching logic. As in docs: - // > matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - // > map is equivalent to an element of matchExpressions, whose key field is "key", the operator - // > is "In", and the values array contains only "value". The requirements are ANDed. - if (namespaceMatchLabels) { - Object.entries(namespaceMatchLabels).forEach(entry => { - namespaceMatchExpressions.push({ - key: entry[0], - operator: 'In', - values: [entry[1]] - }); - }); - } - - let isMatching = true; - if (namespaceMatchExpressions.length) { - for (const expression of namespaceMatchExpressions) { - let labelValue = namespaceObjectLabels[expression.key]; - - // Try default K8s labels for specific keys if there is no value. - if (!labelValue && expression.key === 'name') { - labelValue = namespaceObjectLabels[`kubernetes.io/metadata.${expression.key}`] - } - - if (expression.operator === 'In' && !expression.values.includes(labelValue)) { - isMatching = false; - break; - } - - // If label is not there it fits into 'NotIn' scenario. - if (expression.operator === 'NotIn' && expression.values.includes(labelValue) ) { - isMatching = false; - break; - } - } - } - - return isMatching; - } -} diff --git a/admission-controller/server/src/policies/policies.models.ts b/admission-controller/server/src/policies/policies.models.ts index f13c1b6..360225c 100644 --- a/admission-controller/server/src/policies/policies.models.ts +++ b/admission-controller/server/src/policies/policies.models.ts @@ -1,35 +1,35 @@ -import {KubernetesObject} from "@kubernetes/client-node"; -import {Config, MonokleValidator} from "@monokle/validation"; +import { KubernetesObject } from '@kubernetes/client-node'; +import { Config, MonokleValidator } from '@monokle/validation'; export type MonoklePolicy = KubernetesObject & { - spec: Config -} + spec: Config; +}; export type MonoklePolicyBindingConfiguration = { - policyName: string - validationActions: ['Warn'] - matchResources?: { - namespaceSelector?: { - matchLabels?: Record, - matchExpressions?: { - key: string, - operator: 'In' | 'NotIn', - values: string[] - }[] - } - } -} + policyName: string; + validationActions: ['Warn']; + matchResources?: { + namespaceSelector?: { + matchLabels?: Record; + matchExpressions?: { + key: string; + operator: 'In' | 'NotIn'; + values: string[]; + }[]; + }; + }; +}; export type MonoklePolicyBinding = KubernetesObject & { - spec: MonoklePolicyBindingConfiguration -} + spec: MonoklePolicyBindingConfiguration; +}; export type MonokleApplicablePolicy = { - policy: Config, - binding: MonoklePolicyBindingConfiguration -} + policy: Config; + binding: MonoklePolicyBindingConfiguration; +}; export type MonokleApplicableValidator = { - validator: MonokleValidator, - policy: MonokleApplicablePolicy -} + validator: MonokleValidator; + policy: MonokleApplicablePolicy; +}; diff --git a/admission-controller/server/src/policies/policies.module.ts b/admission-controller/server/src/policies/policies.module.ts index 30d5729..9967855 100644 --- a/admission-controller/server/src/policies/policies.module.ts +++ b/admission-controller/server/src/policies/policies.module.ts @@ -1,8 +1,11 @@ -import {Module} from "@nestjs/common"; +import { Module } from '@nestjs/common'; +import { PoliciesService } from './policies.service'; +import { KubernetesModule } from '../kubernetes/kubernetes.module'; @Module({ - imports: [], - controllers: [], - providers: [], + imports: [KubernetesModule], + controllers: [], + providers: [PoliciesService], + exports: [PoliciesService], }) export class PoliciesModule {} diff --git a/admission-controller/server/src/policies/policies.service.ts b/admission-controller/server/src/policies/policies.service.ts new file mode 100644 index 0000000..8df5e43 --- /dev/null +++ b/admission-controller/server/src/policies/policies.service.ts @@ -0,0 +1,311 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { V1Namespace } from '@kubernetes/client-node'; +import { + AnnotationSuppressor, + Config, + DisabledFixer, + FingerprintSuppressor, + MonokleValidator, + RemotePluginLoader, + ResourceParser, + SchemaLoader, +} from '@monokle/validation'; +import { + MonokleApplicablePolicy, + MonokleApplicableValidator, + MonoklePolicy, + MonoklePolicyBinding, +} from './policies.models'; +import { AdmissionRequestObject } from '../admission/admission.models'; +import { OnEvent } from '@nestjs/event-emitter'; +import { WatcherService } from '../kubernetes/watcher.service'; + +@Injectable() +export class PoliciesService implements OnModuleInit { + private static readonly PLUGIN_BLOCKLIST = ['resource-links']; + + private readonly _logger = new Logger(PoliciesService.name); + + private readonly policyStore = new Map(); // Map + private readonly bindingStore = new Map(); // Map + private readonly validatorStore = new Map(); // Map + + constructor(private readonly $watcher: WatcherService) {} + + private static postprocess(policy: MonoklePolicy) { + const newPolicy = { ...policy }; + newPolicy.spec = PoliciesService.blockPlugins(newPolicy.spec); + return newPolicy; + } + + private static blockPlugins(policySpec: Config): Config { + if (policySpec.plugins === undefined) { + return policySpec; + } + + const newPlugins = { ...policySpec.plugins }; + for (const blockedPlugin of PoliciesService.PLUGIN_BLOCKLIST) { + if (newPlugins[blockedPlugin] === true) { + newPlugins[blockedPlugin] = false; + } + } + + return { + ...policySpec, + plugins: newPlugins, + }; + } + + async onModuleInit() { + await this.$watcher + .watch('monokle.io', 'v1alpha1', 'policies') + .then((watch) => + watch.list().map((policy) => this.onPolicy(policy as any)), + ); + await this.$watcher + .watch('monokle.io', 'v1alpha1', 'policybindings') + .then((watch) => + watch.list().map((binding) => this.onBinding(binding as any)), + ); + } + + getMatchingValidators( + resource: AdmissionRequestObject, + resourceNamespace?: V1Namespace, + ): MonokleApplicableValidator[] { + const matchingPolicies = this.getMatchingPolicies( + resource, + resourceNamespace, + ); + + if (matchingPolicies.length === 0) { + return []; + } + + return matchingPolicies + .map((policy) => { + if (!this.validatorStore.has(policy.binding.policyName)) { + // This should not happen and means there is a bug in other place in the code. Raise warning and skip. + // Do not create validator instance here to keep this function sync and to keep processing time low. + this._logger.warn( + `Validator not found for policy: ${policy.binding.policyName}`, + ); + return null; + } + + return { + validator: this.validatorStore.get(policy.binding.policyName)!, + policy, + }; + }) + .filter( + (validator) => validator !== null, + ) as MonokleApplicableValidator[]; + } + + private getMatchingPolicies( + resource: AdmissionRequestObject, + resourceNamespace?: V1Namespace, + ): MonokleApplicablePolicy[] { + this._logger.debug({ + policies: this.policyStore.size, + bindings: this.bindingStore.size, + }); + + if (this.bindingStore.size === 0) { + return []; + } + + return Array.from(this.bindingStore.values()) + .map((binding) => { + const policy = this.policyStore.get(binding.spec.policyName); + + if (!policy) { + this._logger.error('Binding is pointing to missing policy', binding); + return null; + } + + if ( + binding.spec.matchResources && + !this.isResourceMatching(binding, resource, resourceNamespace) + ) { + return null; + } + + return { + policy: policy.spec, + binding: binding.spec, + }; + }) + .filter((policy) => policy !== null) as MonokleApplicablePolicy[]; + } + + @OnEvent( + WatcherService.getEventsKey('monokle.io', 'v1alpha1', 'policies') + ':add', + ) + @OnEvent( + WatcherService.getEventsKey('monokle.io', 'v1alpha1', 'policies') + + ':update', + ) + private async onPolicy(rawPolicy: MonoklePolicy) { + const policy = PoliciesService.postprocess(rawPolicy); + + this._logger.log(`Policy change received: ${rawPolicy.metadata!.name}`); + this._logger.verbose({ rawPolicy, policy }); + + this.policyStore.set(rawPolicy.metadata!.name!, policy); + + if (this.validatorStore.has(policy.metadata!.name!)) { + return await this.validatorStore + .get(policy.metadata!.name!)! + .preload(policy.spec); + } + + const validator = new MonokleValidator({ + loader: new RemotePluginLoader(), + parser: new ResourceParser(), + schemaLoader: new SchemaLoader(), + suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], + fixer: new DisabledFixer(), + }); + + // Run separately (instead of passing config to constructor) to make sure that validator + // is ready when 'setupValidator' function call fulfills. + await validator.preload(policy.spec); + this._logger.log(`Policy reconciled: ${rawPolicy.metadata!.name}`); + + this.validatorStore.set(policy.metadata!.name!, validator); + } + + @OnEvent( + WatcherService.getEventsKey('monokle.io', 'v1alpha1', 'policies') + + ':delete', + ) + private onPolicyRemoval(rawPolicy: MonoklePolicy) { + const policy = PoliciesService.postprocess(rawPolicy); + + this._logger.log(`Policy removed: ${rawPolicy.metadata!.name}`); + this._logger.verbose({ rawPolicy, policy }); + + this.policyStore.delete(rawPolicy.metadata!.name!); + this.validatorStore.delete(policy.metadata!.name!); + } + + @OnEvent( + WatcherService.getEventsKey('monokle.io', 'v1alpha1', 'policybindings') + + ':add', + ) + @OnEvent( + WatcherService.getEventsKey('monokle.io', 'v1alpha1', 'policybindings') + + ':update', + ) + private onBinding(rawBinding: MonoklePolicyBinding) { + this._logger.log(`Binding updated: ${rawBinding.metadata!.name}`); + this._logger.verbose({ rawBinding }); + + this.bindingStore.set(rawBinding.metadata!.name!, rawBinding); + } + + @OnEvent( + WatcherService.getEventsKey('monokle.io', 'v1alpha1', 'policybindings') + + ':delete', + ) + private onBindingRemoval(rawBinding: MonoklePolicyBinding) { + this._logger.log(`Binding removed: ${rawBinding.metadata!.name}`); + this._logger.verbose({ rawBinding }); + + this.bindingStore.delete(rawBinding.metadata!.name!); + } + + // Based on K8s docs here - https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#matchresources-v1beta1-admissionregistration-k8s-io: + private isResourceMatching( + binding: MonoklePolicyBinding, + resource: AdmissionRequestObject, + resourceNamespace?: V1Namespace, + ): boolean { + const namespaceMatchLabels = + binding.spec.matchResources?.namespaceSelector?.matchLabels; + const namespaceMatchExpressions = + binding.spec.matchResources?.namespaceSelector?.matchExpressions ?? []; + const kind = resource.kind.toLowerCase(); + const isClusterWide = + ((resource as any).namespace || resource.metadata.namespace) === + undefined; + + this._logger.verbose('Checking if resource matches binding', { + namespaceMatchLabels, + namespaceMatchExpressions, + kind, + resourceMetadata: resource.metadata.labels, + }); + + // If non of the matchers are specified, then the resource matches, both cluster wide and namespaced ones. + // So this is global policy. As in docs: + // > Default to the empty LabelSelector, which matches everything. + if (!namespaceMatchLabels && !namespaceMatchExpressions?.length) { + return true; + } + + // Skip cluster-wide resources if namespaceSelector defined. + // This is different from the K8s docs which says: + // > If the object itself is a namespace (...) If the object is another cluster scoped resource, it never skips the policy. + if (isClusterWide && kind !== 'namespace') { + return false; + } + + // If resource is Namespace use it, if not get resource owning namespace. + // > If the object itself is a namespace, the matching is performed on object.metadata.labels + const namespaceObject = kind !== 'namespace' ? resourceNamespace : resource; + if (!namespaceObject) { + return false; + } + + const namespaceObjectLabels = namespaceObject?.metadata?.labels || {}; + + // Convert matchLabels to matchExpressions to have single matching logic. As in docs: + // > matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + // > map is equivalent to an element of matchExpressions, whose key field is "key", the operator + // > is "In", and the values array contains only "value". The requirements are ANDed. + if (namespaceMatchLabels) { + Object.entries(namespaceMatchLabels).forEach((entry) => { + namespaceMatchExpressions.push({ + key: entry[0], + operator: 'In', + values: [entry[1]], + }); + }); + } + + let isMatching = true; + if (namespaceMatchExpressions.length) { + for (const expression of namespaceMatchExpressions) { + let labelValue = namespaceObjectLabels[expression.key]; + + // Try default K8s labels for specific keys if there is no value. + if (!labelValue && expression.key === 'name') { + labelValue = + namespaceObjectLabels[`kubernetes.io/metadata.${expression.key}`]; + } + + if ( + expression.operator === 'In' && + !expression.values.includes(labelValue) + ) { + isMatching = false; + break; + } + + // If label is not there it fits into 'NotIn' scenario. + if ( + expression.operator === 'NotIn' && + expression.values.includes(labelValue) + ) { + isMatching = false; + break; + } + } + } + + return isMatching; + } +} diff --git a/admission-controller/server/src/policies/validation.service.ts b/admission-controller/server/src/policies/validation.service.ts deleted file mode 100644 index b398b9f..0000000 --- a/admission-controller/server/src/policies/validation.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { - AnnotationSuppressor, - Config, - DisabledFixer, - FingerprintSuppressor, - MonokleValidator, - RemotePluginLoader, - ResourceParser, - SchemaLoader, -} from '@monokle/validation'; -import { PoliciesManagerService } from './policies.manager.service'; -import { V1Namespace } from '@kubernetes/client-node'; -import { MonokleApplicableValidator } from './policies.models'; -import { AdmissionRequestObject } from '../admission/admission.models'; - -@Injectable() -export class ValidationService { - private _validators = new Map(); // Map - private readonly _logger = new Logger(ValidationService.name); - - constructor(private readonly $manager: PoliciesManagerService) { - this.$manager.on('policyUpdated', async (policy) => { - await this.setupValidator(policy.metadata!.name!, policy.spec); - }); - - this.$manager.on('policyRemoved', async (policy) => { - await this._validators.delete(policy.metadata!.name!); - }); - } - - getMatchingValidators( - resource: AdmissionRequestObject, - resourceNamespace?: V1Namespace, - ): MonokleApplicableValidator[] { - const matchingPolicies = this.$manager.getMatchingPolicies( - resource, - resourceNamespace, - ); - - if (matchingPolicies.length === 0) { - return []; - } - - return matchingPolicies - .map((policy) => { - if (!this._validators.has(policy.binding.policyName)) { - // This should not happen and means there is a bug in other place in the code. Raise warning and skip. - // Do not create validator instance here to keep this function sync and to keep processing time low. - this._logger.warn({ - msg: 'ValidatorManager: Validator not found', - policyName: policy.binding.policyName, - }); - return null; - } - - return { - validator: this._validators.get(policy.binding.policyName)!, - policy, - }; - }) - .filter( - (validator) => validator !== null, - ) as MonokleApplicableValidator[]; - } - - private async setupValidator(policyName: string, policy: Config) { - if (this._validators.has(policyName)) { - await this._validators.get(policyName)!.preload(policy); - } else { - const validator = new MonokleValidator({ - loader: new RemotePluginLoader(), - parser: new ResourceParser(), - schemaLoader: new SchemaLoader(), - suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], - fixer: new DisabledFixer(), - }); - - // Run separately (instead of passing config to constructor) to make sure that validator - // is ready when 'setupValidator' function call fulfills. - await validator.preload(policy); - - this._validators.set(policyName, validator); - } - } -} diff --git a/admission-controller/server/src/server.module.ts b/admission-controller/server/src/server.module.ts index 91020f8..21bcf02 100644 --- a/admission-controller/server/src/server.module.ts +++ b/admission-controller/server/src/server.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; -import {AdmissionModule} from "./admission/admission.module"; +import { AdmissionModule } from './admission/admission.module'; +import { EventEmitterModule } from '@nestjs/event-emitter'; @Module({ - imports: [AdmissionModule], + imports: [EventEmitterModule.forRoot(), AdmissionModule], controllers: [], providers: [], }) diff --git a/admission-controller/server/src/config.service.ts b/admission-controller/server/src/shared/config.service.ts similarity index 88% rename from admission-controller/server/src/config.service.ts rename to admission-controller/server/src/shared/config.service.ts index 108c304..6c72c23 100644 --- a/admission-controller/server/src/config.service.ts +++ b/admission-controller/server/src/shared/config.service.ts @@ -1,5 +1,6 @@ import { ConfigService as BaseConfigService } from '@nestjs/config'; -import Configuration from './config'; +import Configuration from '../config'; +import { Injectable } from '@nestjs/common'; type NestedKeyOf = { [Key in keyof ObjectType & @@ -8,6 +9,7 @@ type NestedKeyOf = { : `${Key}`; }[keyof ObjectType & (string | number | boolean)]; +@Injectable() export class ConfigService extends BaseConfigService { private readonly config: typeof Configuration; diff --git a/admission-controller/server/src/shared/shared.module.ts b/admission-controller/server/src/shared/shared.module.ts new file mode 100644 index 0000000..0f215b4 --- /dev/null +++ b/admission-controller/server/src/shared/shared.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ConfigService } from './config.service'; + +@Module({ + imports: [ConfigModule.forRoot()], + controllers: [], + providers: [ConfigService], + exports: [ConfigService], +}) +export class SharedModule {} diff --git a/admission-controller/server/src/utils/kube-client.ts b/admission-controller/server/src/utils/kube-client.ts deleted file mode 100644 index 444958e..0000000 --- a/admission-controller/server/src/utils/kube-client.ts +++ /dev/null @@ -1,96 +0,0 @@ -import k8s, {V1Namespace} from "@kubernetes/client-node"; - -class KubeClient { - private static readonly ERROR_RESTART_INTERVAL = 500; - - private readonly kc: k8s.KubeConfig; - - constructor() { - this.kc = new k8s.KubeConfig(); - } - - buildKubeConfig() { - try { - if (process.env.KUBE_CONTEXT) { - this.kc.loadFromDefault(); - this.kc.setCurrentContext(process.env.KUBE_CONTEXT); - } - else { - this.kc.loadFromCluster(); - } - } catch (e) { - console.warn("Failed to load kubeconfig from file"); - } - } - - async getNamespace(name: string): Promise { - return this.kc.makeApiClient(k8s.CoreV1Api).readNamespace(name) - .then(res => res.body) - .catch(err => { - // todo: replace with logger - console.error({msg: 'NamespaceGetter: Failed to get namespace', name, err}); - return undefined; - }) - } - - private createInformer(group: string, version: string, plural: string, onError?: k8s.ErrorCallback) { - const listFn = () => this.kc.makeApiClient(k8s.CustomObjectsApi).listClusterCustomObject(group, version, plural); - const cr = `${group}/${version}/${plural}`; - const informer = k8s.makeInformer(this.kc, `/apis/${cr}`, listFn as any); - - informer.on('error', (err) => { - if (onError) { - onError(err); - } - - setTimeout(async () => { - await informer.start(); - }, KubeClient.ERROR_RESTART_INTERVAL); - }); - - return informer; - } - - private startInformer(informer: Informer, onError?: k8s.ErrorCallback) { - return async () => { - let tries = 0; - let started = false; - - while (!started) { - try { - tries++; - await informer.start(); - started = true; - } catch (err: any) { - if (err.statusCode === 404) { - console.error(`Not found, CRD might not be installed`); - } - if (onError) { - onError(err); - } - - await new Promise((resolve) => setTimeout(resolve, KubeClient.ERROR_RESTART_INTERVAL)); - } - } - } - } - - - async getInformer( - group: string, version: string, plural: string, onError?: k8s.ErrorCallback - ): Promise> { - const informer = await this.createInformer(group, version, plural, onError); - const start = this.startInformer(informer, onError); - - return {informer, start} - } -} - -export type Informer = k8s.Informer & k8s.ObjectCache; - -export type InformerWrapper = { - informer: Informer, - start: () => Promise -} - -export default new KubeClient(); diff --git a/admission-controller/server/tsconfig.json b/admission-controller/server/tsconfig.json index bf8c6f0..8183228 100644 --- a/admission-controller/server/tsconfig.json +++ b/admission-controller/server/tsconfig.json @@ -6,9 +6,11 @@ "moduleResolution": "node", "outDir": "./dist" /* Specify an output folder for all emitted files. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - "strict": true /* Enable all strict type-checking options. */, + "strict": true }, "exclude": ["node_modules", "dist"], - "include": ["src"] -} \ No newline at end of file + "include": ["src"], +} From 0cd647b7e686d415292bb8320aa512926513ba56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Fri, 9 Feb 2024 16:12:38 +0200 Subject: [PATCH 7/9] feat: improved logging and admissionrequest type --- .../src/admission/admission.controller.ts | 22 +++++-- .../server/src/admission/admission.models.ts | 59 ++++++++++++++----- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/admission-controller/server/src/admission/admission.controller.ts b/admission-controller/server/src/admission/admission.controller.ts index 3683246..bb78a0e 100644 --- a/admission-controller/server/src/admission/admission.controller.ts +++ b/admission-controller/server/src/admission/admission.controller.ts @@ -140,7 +140,16 @@ export class AdmissionController { @Post() async validate(@Body() body: AdmissionRequest): Promise { + this.log.log( + `Received admission request: ${body.request.resource?.group}/${body + .request.resource?.version}/${body.request.resource?.resource} - ${ + body.request.operation + } ${body.request.namespace ? body.request.namespace + '/' : ''}${ + body.request.name + }`, + ); this.log.verbose({ body }); + const namespace = body.request?.namespace || body.request?.object?.metadata?.namespace; @@ -157,27 +166,26 @@ export class AdmissionController { }; if (namespace && this.ignoredNamespaces.includes(namespace)) { - this.log.error({ msg: 'Namespace ignored', namespace }); + this.log.error(`Namespace ignored: ${namespace}`); return response; } const resource = body.request?.object; if (!resource) { - this.log.error({ msg: 'No resource found', metadata: body.request }); + this.log.error(`No resource found: ${body.request}`); return response; } const namespaceObject = namespace ? await this.$kubernetes.getNamespace(namespace) : undefined; - this.log.debug({ namespaceObject }); const validators = this.$policies.getMatchingValidators( resource, namespaceObject, ); - this.log.debug({ msg: 'Matching validators', count: validators.length }); + this.log.debug(`Matching validators: ${validators.length}`); if (validators.length === 0) { return response; @@ -218,7 +226,9 @@ export class AdmissionController { this.log.verbose({ resourceForValidation, validationResponses }); if (violations.length === 0) { - this.log.debug({ msg: 'No violations', response }); + this.log.log( + `No violations: ${resource.apiVersion}/${resource.kind} - ${resource.metadata?.name}`, + ); return response; } @@ -245,7 +255,7 @@ export class AdmissionController { response, ); - this.log.debug({ response }); + this.log.verbose(response); return responseFull; } } diff --git a/admission-controller/server/src/admission/admission.models.ts b/admission-controller/server/src/admission/admission.models.ts index 98c8e80..213bc37 100644 --- a/admission-controller/server/src/admission/admission.models.ts +++ b/admission-controller/server/src/admission/admission.models.ts @@ -1,10 +1,50 @@ import { V1ObjectMeta } from '@kubernetes/client-node'; import { Message, RuleLevel } from '@monokle/validation'; -export type ValidationServerOptions = { - port: number; - host: string; -}; +export interface AdmissionRequest { + kind: string; + apiVersion: string; + request: { + uid: string; + kind: ResourceApi; + resource: Resource; + requestKind: RequestKind; + requestResource: Resource; + name: string; + namespace: string; + operation: string; + userInfo: UserInfo; + object: any; + oldObject: any; + dryRun: boolean; + options: { + kind: string; + apiVersion: string; + }; + }; +} + +export interface ResourceVersion { + group: string; + version: string; +} + +export interface ResourceApi extends ResourceVersion { + kind: string; +} + +export interface Resource extends ResourceVersion { + resource: string; +} + +export interface RequestKind extends ResourceVersion { + kind: string; +} + +export interface UserInfo { + username: string; + groups: string[]; +} export type AdmissionRequestObject = { apiVersion: string; @@ -14,17 +54,6 @@ export type AdmissionRequestObject = { status: any; }; -export type AdmissionRequest = { - apiVersion: string; - kind: string; - request: { - name: string; - namespace: string; - uid: string; - object: AdmissionRequestObject; - }; -}; - // See // https://pkg.go.dev/k8s.io/api/admission/v1#AdmissionResponse // https://kubernetes.io/docs/reference/config-api/apiserver-admission.v1/#admission-k8s-io-v1-AdmissionReview From 3ff4f975149b289d49599eef9b47d598cf818c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Mon, 12 Feb 2024 14:47:33 +0200 Subject: [PATCH 8/9] feat(server): initial implementation of scan over kubernetes-native resources --- .../server/src/kubernetes/resource.service.ts | 23 ++-- .../server/src/policies/policies.service.ts | 34 +++-- .../server/src/reporting/reporting.models.ts | 4 + .../server/src/reporting/reporting.module.ts | 10 ++ .../server/src/reporting/reporting.service.ts | 119 ++++++++++++++++++ .../server/src/server.module.ts | 3 +- .../synchronizer/tsconfig.json | 2 +- 7 files changed, 165 insertions(+), 30 deletions(-) create mode 100644 admission-controller/server/src/reporting/reporting.models.ts create mode 100644 admission-controller/server/src/reporting/reporting.module.ts create mode 100644 admission-controller/server/src/reporting/reporting.service.ts diff --git a/admission-controller/server/src/kubernetes/resource.service.ts b/admission-controller/server/src/kubernetes/resource.service.ts index 1ee68b4..702fda3 100644 --- a/admission-controller/server/src/kubernetes/resource.service.ts +++ b/admission-controller/server/src/kubernetes/resource.service.ts @@ -6,17 +6,20 @@ import k8s from '@kubernetes/client-node'; export class ResourceService { constructor(private readonly $client: ClientService) {} - listNamespaces() { - return this.$client - .api(k8s.CoreV1Api) - .listNamespace() - .then((res) => res.body.items); + async listNamespaces() { + const res = await this.$client.api(k8s.CoreV1Api).listNamespace(); + return res.body.items; } - getNamespace(name: string) { - return this.$client - .api(k8s.CoreV1Api) - .readNamespace(name) - .then((res) => res.body); + async getNamespace(name: string) { + const res = await this.$client.api(k8s.CoreV1Api).readNamespace(name); + return res.body; + } + + async list(apiVersion: string, kind: string, namespace?: string) { + const res = await this.$client + .api(k8s.KubernetesObjectApi) + .list(apiVersion, kind, namespace); + return res.body.items; } } diff --git a/admission-controller/server/src/policies/policies.service.ts b/admission-controller/server/src/policies/policies.service.ts index 8df5e43..e511ab6 100644 --- a/admission-controller/server/src/policies/policies.service.ts +++ b/admission-controller/server/src/policies/policies.service.ts @@ -24,7 +24,7 @@ import { WatcherService } from '../kubernetes/watcher.service'; export class PoliciesService implements OnModuleInit { private static readonly PLUGIN_BLOCKLIST = ['resource-links']; - private readonly _logger = new Logger(PoliciesService.name); + private readonly log = new Logger(PoliciesService.name); private readonly policyStore = new Map(); // Map private readonly bindingStore = new Map(); // Map @@ -87,7 +87,7 @@ export class PoliciesService implements OnModuleInit { if (!this.validatorStore.has(policy.binding.policyName)) { // This should not happen and means there is a bug in other place in the code. Raise warning and skip. // Do not create validator instance here to keep this function sync and to keep processing time low. - this._logger.warn( + this.log.warn( `Validator not found for policy: ${policy.binding.policyName}`, ); return null; @@ -107,11 +107,9 @@ export class PoliciesService implements OnModuleInit { resource: AdmissionRequestObject, resourceNamespace?: V1Namespace, ): MonokleApplicablePolicy[] { - this._logger.debug({ - policies: this.policyStore.size, - bindings: this.bindingStore.size, - }); - + this.log.debug( + `policies: ${this.policyStore.size}, bindings: ${this.bindingStore.size}`, + ); if (this.bindingStore.size === 0) { return []; } @@ -121,7 +119,7 @@ export class PoliciesService implements OnModuleInit { const policy = this.policyStore.get(binding.spec.policyName); if (!policy) { - this._logger.error('Binding is pointing to missing policy', binding); + this.log.error('Binding is pointing to missing policy', binding); return null; } @@ -150,8 +148,8 @@ export class PoliciesService implements OnModuleInit { private async onPolicy(rawPolicy: MonoklePolicy) { const policy = PoliciesService.postprocess(rawPolicy); - this._logger.log(`Policy change received: ${rawPolicy.metadata!.name}`); - this._logger.verbose({ rawPolicy, policy }); + this.log.log(`Policy change received: ${rawPolicy.metadata!.name}`); + this.log.verbose({ rawPolicy, policy }); this.policyStore.set(rawPolicy.metadata!.name!, policy); @@ -172,7 +170,7 @@ export class PoliciesService implements OnModuleInit { // Run separately (instead of passing config to constructor) to make sure that validator // is ready when 'setupValidator' function call fulfills. await validator.preload(policy.spec); - this._logger.log(`Policy reconciled: ${rawPolicy.metadata!.name}`); + this.log.log(`Policy reconciled: ${rawPolicy.metadata!.name}`); this.validatorStore.set(policy.metadata!.name!, validator); } @@ -184,8 +182,8 @@ export class PoliciesService implements OnModuleInit { private onPolicyRemoval(rawPolicy: MonoklePolicy) { const policy = PoliciesService.postprocess(rawPolicy); - this._logger.log(`Policy removed: ${rawPolicy.metadata!.name}`); - this._logger.verbose({ rawPolicy, policy }); + this.log.log(`Policy removed: ${rawPolicy.metadata!.name}`); + this.log.verbose({ rawPolicy, policy }); this.policyStore.delete(rawPolicy.metadata!.name!); this.validatorStore.delete(policy.metadata!.name!); @@ -200,8 +198,8 @@ export class PoliciesService implements OnModuleInit { ':update', ) private onBinding(rawBinding: MonoklePolicyBinding) { - this._logger.log(`Binding updated: ${rawBinding.metadata!.name}`); - this._logger.verbose({ rawBinding }); + this.log.log(`Binding updated: ${rawBinding.metadata!.name}`); + this.log.verbose({ rawBinding }); this.bindingStore.set(rawBinding.metadata!.name!, rawBinding); } @@ -211,8 +209,8 @@ export class PoliciesService implements OnModuleInit { ':delete', ) private onBindingRemoval(rawBinding: MonoklePolicyBinding) { - this._logger.log(`Binding removed: ${rawBinding.metadata!.name}`); - this._logger.verbose({ rawBinding }); + this.log.log(`Binding removed: ${rawBinding.metadata!.name}`); + this.log.verbose({ rawBinding }); this.bindingStore.delete(rawBinding.metadata!.name!); } @@ -232,7 +230,7 @@ export class PoliciesService implements OnModuleInit { ((resource as any).namespace || resource.metadata.namespace) === undefined; - this._logger.verbose('Checking if resource matches binding', { + this.log.verbose('Checking if resource matches binding', { namespaceMatchLabels, namespaceMatchExpressions, kind, diff --git a/admission-controller/server/src/reporting/reporting.models.ts b/admission-controller/server/src/reporting/reporting.models.ts new file mode 100644 index 0000000..1a37cb2 --- /dev/null +++ b/admission-controller/server/src/reporting/reporting.models.ts @@ -0,0 +1,4 @@ +export type ResourceIdentifier = { + kind: string; + apiVersion: string; +}; diff --git a/admission-controller/server/src/reporting/reporting.module.ts b/admission-controller/server/src/reporting/reporting.module.ts new file mode 100644 index 0000000..5e51563 --- /dev/null +++ b/admission-controller/server/src/reporting/reporting.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { KubernetesModule } from '../kubernetes/kubernetes.module'; +import { ReportingService } from './reporting.service'; +import { PoliciesModule } from '../policies/policies.module'; + +@Module({ + imports: [KubernetesModule, PoliciesModule], + providers: [ReportingService], +}) +export class ReportingModule {} diff --git a/admission-controller/server/src/reporting/reporting.service.ts b/admission-controller/server/src/reporting/reporting.service.ts new file mode 100644 index 0000000..fe90b06 --- /dev/null +++ b/admission-controller/server/src/reporting/reporting.service.ts @@ -0,0 +1,119 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ResourceService } from '../kubernetes/resource.service'; +import { PoliciesService } from '../policies/policies.service'; +import { ResourceIdentifier } from './reporting.models'; +import { Resource } from '@monokle/validation'; +import { V1Namespace } from '@kubernetes/client-node'; + +type ScannedResourceKind = ResourceIdentifier; + +@Injectable() +export class ReportingService implements OnModuleInit { + private static readonly VALIDATOR_RESOURCE_DEFAULTS = { + id: '', + fileId: '', + filePath: '', + fileOffset: 0, + text: '', + }; + + private SCANNED_RESOURCES: ScannedResourceKind[] = [ + { apiVersion: 'apps/v1', kind: 'Deployment' }, + { apiVersion: 'apps/v1', kind: 'StatefulSet' }, + { apiVersion: 'apps/v1', kind: 'DaemonSet' }, + { apiVersion: 'batch/v1', kind: 'CronJob' }, + { apiVersion: 'batch/v1', kind: 'Job' }, + { apiVersion: 'autoscaling/v2', kind: 'HorizontalPodAutoscaler' }, + { apiVersion: 'autoscaling/v1', kind: 'HorizontalPodAutoscaler' }, + { apiVersion: 'v1', kind: 'Pod' }, + { apiVersion: 'v1', kind: 'Service' }, + { apiVersion: 'v1', kind: 'ConfigMap' }, + { apiVersion: 'v1', kind: 'Secret' }, + { apiVersion: 'networking.k8s.io/v1', kind: 'Ingress' }, + { apiVersion: 'networking.k8s.io/v1', kind: 'NetworkPolicy' }, + { apiVersion: 'policy/v1beta1', kind: 'PodSecurityPolicy' }, + { apiVersion: 'rbac.authorization.k8s.io/v1', kind: 'Role' }, + { apiVersion: 'rbac.authorization.k8s.io/v1', kind: 'RoleBinding' }, + { apiVersion: 'v1', kind: 'ServiceAccount' }, + // { apiVersion: 'apiextensions.k8s.io/v1', kind: 'customresourcedefinitions' }, + ]; + + private readonly log = new Logger(ReportingService.name); + + constructor( + private readonly $client: ResourceService, + private readonly $policies: PoliciesService, + ) {} + + // todo: replace with correct trigger / entrypoint + async onModuleInit() { + this.log.log('Starting cluster report.'); + setTimeout( + () => + this.$client.listNamespaces().then(async (namespaces) => { + for (const namespace of namespaces) { + const response = await this.validate(namespace); + this.log.log( + `Namespace ${namespace.metadata!.name} has ${ + response!.runs[0].results.length ?? 'no' + } violations`, + ); + } + }), + 5000, + ); + } + + public async validate(namespace: V1Namespace) { + this.log.debug(`Running scan on namespace ${namespace.metadata!.name}`); + + const validator = this.$policies + .getMatchingValidators(namespace as any) + .at(0); + if (!validator) { + this.log.log( + `No validator found for namespace ${namespace.metadata!.name}`, + ); + return; + } + + const resources = await this.buildInventory(namespace.metadata!.name!); + return await validator.validator.validate({ resources }); + } + + private async buildInventory(namespace: string) { + const inventory = new Set(); + + for (const { apiVersion, kind } of this.SCANNED_RESOURCES) { + const resources = await this.$client + .list(apiVersion, kind, namespace) + .catch((err) => { + // todo: sentry should handle this and report the available API versions for the given kind + // ie: HPA is not available in v2, but in v2beta2 + this.log.warn( + `Failed to list resources for ${apiVersion}/${kind} in namespace ${namespace}`, + ); + return []; + }); + resources.forEach((resource) => { + resource.apiVersion ??= apiVersion; + resource.kind ??= kind; + + inventory.add( + Object.assign( + { + name: resource.metadata!.name ?? '', + apiVersion: resource.apiVersion, + kind: resource.kind, + namespace: resource.metadata?.namespace, + content: resource, + }, + ReportingService.VALIDATOR_RESOURCE_DEFAULTS, + ), + ); + }); + } + + return [...inventory]; + } +} diff --git a/admission-controller/server/src/server.module.ts b/admission-controller/server/src/server.module.ts index 21bcf02..e8b7a81 100644 --- a/admission-controller/server/src/server.module.ts +++ b/admission-controller/server/src/server.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { AdmissionModule } from './admission/admission.module'; import { EventEmitterModule } from '@nestjs/event-emitter'; +import { ReportingModule } from './reporting/reporting.module'; @Module({ - imports: [EventEmitterModule.forRoot(), AdmissionModule], + imports: [EventEmitterModule.forRoot(), AdmissionModule, ReportingModule], controllers: [], providers: [], }) diff --git a/admission-controller/synchronizer/tsconfig.json b/admission-controller/synchronizer/tsconfig.json index 37cb71d..83ac392 100644 --- a/admission-controller/synchronizer/tsconfig.json +++ b/admission-controller/synchronizer/tsconfig.json @@ -7,7 +7,7 @@ "outDir": "./dist" /* Specify an output folder for all emitted files. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - "experimentalDecorators": true + "experimentalDecorators": true, "strict": true /* Enable all strict type-checking options. */ }, "exclude": ["node_modules", "dist"], From d2c09a4f7c166c6101c78c866024651451f464b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20MURARU?= Date: Tue, 13 Feb 2024 11:44:56 +0200 Subject: [PATCH 9/9] feat(server): build native and custom resource api versions --- .../server/src/kubernetes/resource.service.ts | 12 ++ .../server/src/reporting/reporting.models.ts | 4 +- .../server/src/reporting/reporting.service.ts | 151 ++++++++++++++---- 3 files changed, 135 insertions(+), 32 deletions(-) diff --git a/admission-controller/server/src/kubernetes/resource.service.ts b/admission-controller/server/src/kubernetes/resource.service.ts index 702fda3..f723f93 100644 --- a/admission-controller/server/src/kubernetes/resource.service.ts +++ b/admission-controller/server/src/kubernetes/resource.service.ts @@ -22,4 +22,16 @@ export class ResourceService { .list(apiVersion, kind, namespace); return res.body.items; } + + async listCRDs() { + const res = await this.$client + .api(k8s.ApiextensionsV1Api) + .listCustomResourceDefinition(); + return res.body.items; + } + + async listAPIs() { + const res = await this.$client.api(k8s.ApisApi).getAPIVersions(); + return res.body; + } } diff --git a/admission-controller/server/src/reporting/reporting.models.ts b/admission-controller/server/src/reporting/reporting.models.ts index 1a37cb2..7f81305 100644 --- a/admission-controller/server/src/reporting/reporting.models.ts +++ b/admission-controller/server/src/reporting/reporting.models.ts @@ -1,4 +1,6 @@ export type ResourceIdentifier = { + apiVersion?: string; + group?: string; kind: string; - apiVersion: string; + version?: string; }; diff --git a/admission-controller/server/src/reporting/reporting.service.ts b/admission-controller/server/src/reporting/reporting.service.ts index fe90b06..25ddd8f 100644 --- a/admission-controller/server/src/reporting/reporting.service.ts +++ b/admission-controller/server/src/reporting/reporting.service.ts @@ -2,8 +2,12 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ResourceService } from '../kubernetes/resource.service'; import { PoliciesService } from '../policies/policies.service'; import { ResourceIdentifier } from './reporting.models'; +import { + V1APIGroup, + V1CustomResourceDefinition, + V1Namespace, +} from '@kubernetes/client-node'; import { Resource } from '@monokle/validation'; -import { V1Namespace } from '@kubernetes/client-node'; type ScannedResourceKind = ResourceIdentifier; @@ -17,26 +21,24 @@ export class ReportingService implements OnModuleInit { text: '', }; - private SCANNED_RESOURCES: ScannedResourceKind[] = [ - { apiVersion: 'apps/v1', kind: 'Deployment' }, - { apiVersion: 'apps/v1', kind: 'StatefulSet' }, - { apiVersion: 'apps/v1', kind: 'DaemonSet' }, - { apiVersion: 'batch/v1', kind: 'CronJob' }, - { apiVersion: 'batch/v1', kind: 'Job' }, - { apiVersion: 'autoscaling/v2', kind: 'HorizontalPodAutoscaler' }, - { apiVersion: 'autoscaling/v1', kind: 'HorizontalPodAutoscaler' }, - { apiVersion: 'v1', kind: 'Pod' }, - { apiVersion: 'v1', kind: 'Service' }, - { apiVersion: 'v1', kind: 'ConfigMap' }, - { apiVersion: 'v1', kind: 'Secret' }, - { apiVersion: 'networking.k8s.io/v1', kind: 'Ingress' }, - { apiVersion: 'networking.k8s.io/v1', kind: 'NetworkPolicy' }, - { apiVersion: 'policy/v1beta1', kind: 'PodSecurityPolicy' }, - { apiVersion: 'rbac.authorization.k8s.io/v1', kind: 'Role' }, - { apiVersion: 'rbac.authorization.k8s.io/v1', kind: 'RoleBinding' }, - { apiVersion: 'v1', kind: 'ServiceAccount' }, - // { apiVersion: 'apiextensions.k8s.io/v1', kind: 'customresourcedefinitions' }, + private SCANNED_NATIVE_RESOURCES: ScannedResourceKind[] = [ + { version: 'v1', kind: 'Service' }, + { version: 'v1', kind: 'ConfigMap' }, + { version: 'v1', kind: 'Secret' }, + { version: 'v1', kind: 'ServiceAccount' }, + { version: 'v1', kind: 'PersistentVolumeClaim' }, + { group: 'apps', kind: 'Deployment' }, + { group: 'apps', kind: 'StatefulSet' }, + { group: 'apps', kind: 'DaemonSet' }, + { group: 'batch', kind: 'CronJob' }, + { group: 'batch', kind: 'Job' }, + { group: 'autoscaling', kind: 'HorizontalPodAutoscaler' }, + { group: 'networking.k8s.io', kind: 'Ingress' }, + { group: 'networking.k8s.io', kind: 'NetworkPolicy' }, + { group: 'rbac.authorization.k8s.io', kind: 'Role' }, + { group: 'rbac.authorization.k8s.io', kind: 'RoleBinding' }, ]; + private SCANNED_CUSTOM_RESOURCES: Array = []; private readonly log = new Logger(ReportingService.name); @@ -45,19 +47,39 @@ export class ReportingService implements OnModuleInit { private readonly $policies: PoliciesService, ) {} + private static getApiVersion(group: string | undefined, version: string) { + if (group) { + return `${group}/${version}`; + } + return version; + } + // todo: replace with correct trigger / entrypoint async onModuleInit() { - this.log.log('Starting cluster report.'); + this.log.log('Starting cluster reporting module'); + + this.SCANNED_NATIVE_RESOURCES = this.buildNativeApiVersions( + await this.$client.listAPIs().then((apis) => apis.groups), + ); + this.SCANNED_CUSTOM_RESOURCES = this.buildCustomApiVersions( + await this.$client.listCRDs(), + ); + + // todo: replace with correct trigger / entrypoint + // running in setTimeout so the PoliciesService has time to preload the validators. + // when running in k8s, this should be removed as will be called trough apicalls / events setTimeout( () => this.$client.listNamespaces().then(async (namespaces) => { for (const namespace of namespaces) { const response = await this.validate(namespace); - this.log.log( - `Namespace ${namespace.metadata!.name} has ${ - response!.runs[0].results.length ?? 'no' - } violations`, - ); + if (response?.runs) { + this.log.log( + `Namespace ${namespace.metadata!.name} has ${ + response.runs[0].results.length ?? 'no' + } violations`, + ); + } } }), 5000, @@ -65,13 +87,13 @@ export class ReportingService implements OnModuleInit { } public async validate(namespace: V1Namespace) { - this.log.debug(`Running scan on namespace ${namespace.metadata!.name}`); + this.log.log(`Running scan on namespace ${namespace.metadata!.name}`); const validator = this.$policies .getMatchingValidators(namespace as any) .at(0); if (!validator) { - this.log.log( + this.log.debug( `No validator found for namespace ${namespace.metadata!.name}`, ); return; @@ -81,12 +103,79 @@ export class ReportingService implements OnModuleInit { return await validator.validator.validate({ resources }); } + private buildNativeApiVersions(groups: V1APIGroup[]) { + return this.SCANNED_NATIVE_RESOURCES.map((resource) => { + const group = groups.find((group) => group.name === resource.group); + if (!group) { + const apiVersion = ReportingService.getApiVersion( + resource.group, + resource.version!, + ); + + if (resource.group) { + // resource had group defined yet could not be inferred from the API + this.log.warn( + `Could not find API group for resource ${apiVersion}/${resource.kind}`, + ); + } else { + this.log.debug( + `API Discovery using default ${apiVersion}/${resource.kind}`, + ); + } + return { + ...resource, + apiVersion, + }; + } + + return group.versions.map((version) => { + const res = { + ...resource, + version: version.version, + apiVersion: ReportingService.getApiVersion( + resource.group!, + version.version, + ), + }; + this.log.debug( + `API Discovery using found ${res.apiVersion}/${res.kind}`, + ); + return res; + }); + }).flat(); + } + + private buildCustomApiVersions(crds: V1CustomResourceDefinition[]) { + const namespaced = crds.filter((crd) => crd.spec.scope === 'Namespaced'); + + return namespaced + .map((crd) => + crd.spec.versions.map((version) => { + const res = { + group: crd.spec.group, + kind: crd.spec.names.kind, + version: version.name, + apiVersion: ReportingService.getApiVersion( + crd.spec.group, + version.name, + ), + }; + this.log.debug(`CRD Discovery found ${res.apiVersion}/${res.kind}`); + return res; + }), + ) + .flat(); + } + private async buildInventory(namespace: string) { const inventory = new Set(); - for (const { apiVersion, kind } of this.SCANNED_RESOURCES) { + for (const { apiVersion, kind } of [ + ...this.SCANNED_NATIVE_RESOURCES, + ...this.SCANNED_CUSTOM_RESOURCES, + ]) { const resources = await this.$client - .list(apiVersion, kind, namespace) + .list(apiVersion!, kind, namespace) .catch((err) => { // todo: sentry should handle this and report the available API versions for the given kind // ie: HPA is not available in v2, but in v2beta2 @@ -103,7 +192,7 @@ export class ReportingService implements OnModuleInit { Object.assign( { name: resource.metadata!.name ?? '', - apiVersion: resource.apiVersion, + apiVersion: resource.apiVersion!, kind: resource.kind, namespace: resource.metadata?.namespace, content: resource,