pepr 0.13.4 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/commitlint.config.js +1 -0
- package/dist/cli.js +375 -204
- package/dist/controller.js +1 -1
- package/dist/lib/assets/deploy.d.ts.map +1 -1
- package/dist/lib/assets/destroy.d.ts +2 -0
- package/dist/lib/assets/destroy.d.ts.map +1 -0
- package/dist/lib/assets/index.d.ts +6 -5
- package/dist/lib/assets/index.d.ts.map +1 -1
- package/dist/lib/assets/networking.d.ts +6 -5
- package/dist/lib/assets/networking.d.ts.map +1 -1
- package/dist/lib/assets/pods.d.ts +84 -4
- package/dist/lib/assets/pods.d.ts.map +1 -1
- package/dist/lib/assets/rbac.d.ts +6 -4
- package/dist/lib/assets/rbac.d.ts.map +1 -1
- package/dist/lib/assets/store.d.ts +7 -0
- package/dist/lib/assets/store.d.ts.map +1 -0
- package/dist/lib/assets/webhooks.d.ts +2 -2
- package/dist/lib/assets/webhooks.d.ts.map +1 -1
- package/dist/lib/assets/yaml.d.ts.map +1 -1
- package/dist/lib/capability.d.ts +21 -4
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/controller/index.d.ts +10 -0
- package/dist/lib/controller/index.d.ts.map +1 -0
- package/dist/lib/controller/store.d.ts +7 -0
- package/dist/lib/controller/store.d.ts.map +1 -0
- package/dist/lib/filter.d.ts +2 -2
- package/dist/lib/filter.d.ts.map +1 -1
- package/dist/lib/{k8s/types.d.ts → k8s.d.ts} +14 -25
- package/dist/lib/k8s.d.ts.map +1 -0
- package/dist/lib/metrics.d.ts +12 -12
- package/dist/lib/metrics.d.ts.map +1 -1
- package/dist/lib/module.d.ts +25 -4
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/mutate-processor.d.ts +3 -3
- package/dist/lib/mutate-processor.d.ts.map +1 -1
- package/dist/lib/mutate-request.d.ts +11 -10
- package/dist/lib/mutate-request.d.ts.map +1 -1
- package/dist/lib/storage.d.ts +56 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/tls.d.ts.map +1 -0
- package/dist/lib/types.d.ts +28 -48
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/validate-processor.d.ts +2 -2
- package/dist/lib/validate-processor.d.ts.map +1 -1
- package/dist/lib/validate-request.d.ts +9 -8
- package/dist/lib/validate-request.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts +3 -0
- package/dist/lib/watch-processor.d.ts.map +1 -0
- package/dist/lib.d.ts +3 -7
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +484 -807
- package/dist/lib.js.map +4 -4
- package/package.json +20 -22
- package/src/lib/assets/deploy.ts +69 -127
- package/src/lib/assets/destroy.ts +33 -0
- package/src/lib/assets/index.ts +8 -14
- package/src/lib/assets/networking.ts +28 -5
- package/src/lib/assets/pods.ts +130 -11
- package/src/lib/assets/rbac.ts +42 -4
- package/src/lib/assets/store.ts +49 -0
- package/src/lib/assets/webhooks.ts +2 -2
- package/src/lib/assets/yaml.ts +13 -3
- package/src/lib/capability.ts +69 -14
- package/src/lib/{controller.ts → controller/index.ts} +25 -23
- package/src/lib/controller/store.ts +197 -0
- package/src/lib/filter.ts +2 -2
- package/src/lib/{k8s/types.ts → k8s.ts} +15 -26
- package/src/lib/metrics.ts +22 -38
- package/src/lib/module.ts +47 -10
- package/src/lib/mutate-processor.ts +6 -6
- package/src/lib/mutate-request.ts +18 -26
- package/src/lib/storage.ts +128 -0
- package/src/lib/types.ts +30 -53
- package/src/lib/validate-processor.ts +5 -4
- package/src/lib/validate-request.ts +15 -19
- package/src/lib/watch-processor.ts +55 -0
- package/src/lib.ts +4 -8
- package/src/templates/capabilities/hello-pepr.ts +54 -5
- package/src/templates/package.json +1 -0
- package/dist/lib/controller.d.ts +0 -10
- package/dist/lib/controller.d.ts.map +0 -1
- package/dist/lib/fetch.d.ts +0 -23
- package/dist/lib/fetch.d.ts.map +0 -1
- package/dist/lib/k8s/index.d.ts +0 -7
- package/dist/lib/k8s/index.d.ts.map +0 -1
- package/dist/lib/k8s/kinds.d.ts +0 -12
- package/dist/lib/k8s/kinds.d.ts.map +0 -1
- package/dist/lib/k8s/tls.d.ts.map +0 -1
- package/dist/lib/k8s/types.d.ts.map +0 -1
- package/dist/lib/k8s/upstream.d.ts +0 -4
- package/dist/lib/k8s/upstream.d.ts.map +0 -1
- package/jest.config.json +0 -4
- package/journey/before.ts +0 -21
- package/journey/k8s.ts +0 -100
- package/journey/pepr-build.ts +0 -69
- package/journey/pepr-deploy.ts +0 -174
- package/journey/pepr-dev.ts +0 -155
- package/journey/pepr-format.ts +0 -13
- package/journey/pepr-init.ts +0 -12
- package/src/lib/fetch.ts +0 -76
- package/src/lib/k8s/index.ts +0 -14
- package/src/lib/k8s/kinds.ts +0 -531
- package/src/lib/k8s/upstream.ts +0 -53
- /package/dist/lib/{k8s/tls.d.ts → tls.d.ts} +0 -0
- /package/src/lib/{k8s/tls.ts → tls.ts} +0 -0
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
V1LabelSelectorRequirement,
|
|
7
7
|
V1RuleWithOperations,
|
|
8
8
|
} from "@kubernetes/client-node";
|
|
9
|
+
import { kind } from "kubernetes-fluent-client";
|
|
9
10
|
import { concat, equals, uniqWith } from "ramda";
|
|
10
11
|
|
|
11
12
|
import { Assets } from ".";
|
|
12
|
-
import { MutatingWebhookConfiguration, ValidatingWebhookConfiguration } from "../k8s/upstream";
|
|
13
13
|
import { Event } from "../types";
|
|
14
14
|
|
|
15
15
|
const peprIgnoreLabel: V1LabelSelectorRequirement = {
|
|
@@ -70,7 +70,7 @@ export async function webhookConfig(
|
|
|
70
70
|
assets: Assets,
|
|
71
71
|
mutateOrValidate: "mutate" | "validate",
|
|
72
72
|
timeoutSeconds = 10,
|
|
73
|
-
): Promise<MutatingWebhookConfiguration | ValidatingWebhookConfiguration | null> {
|
|
73
|
+
): Promise<kind.MutatingWebhookConfiguration | kind.ValidatingWebhookConfiguration | null> {
|
|
74
74
|
const ignore = [peprIgnoreLabel];
|
|
75
75
|
|
|
76
76
|
const { name, tls, config, apiToken, host } = assets;
|
package/src/lib/assets/yaml.ts
CHANGED
|
@@ -6,10 +6,11 @@ import crypto from "crypto";
|
|
|
6
6
|
import { promises as fs } from "fs";
|
|
7
7
|
|
|
8
8
|
import { Assets } from ".";
|
|
9
|
-
import { apiTokenSecret, service, tlsSecret } from "./networking";
|
|
10
|
-
import { deployment, moduleSecret, namespace } from "./pods";
|
|
11
|
-
import { clusterRole, clusterRoleBinding, serviceAccount } from "./rbac";
|
|
9
|
+
import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking";
|
|
10
|
+
import { deployment, moduleSecret, namespace, watcher } from "./pods";
|
|
11
|
+
import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
|
|
12
12
|
import { webhookConfig } from "./webhooks";
|
|
13
|
+
import { peprStoreCRD } from "./store";
|
|
13
14
|
|
|
14
15
|
export function zarfYaml({ name, image, config }: Assets, path: string) {
|
|
15
16
|
const zarfCfg = {
|
|
@@ -49,6 +50,7 @@ export async function allYaml(assets: Assets) {
|
|
|
49
50
|
|
|
50
51
|
const mutateWebhook = await webhookConfig(assets, "mutate");
|
|
51
52
|
const validateWebhook = await webhookConfig(assets, "validate");
|
|
53
|
+
const watchDeployment = watcher(assets, hash);
|
|
52
54
|
|
|
53
55
|
const resources = [
|
|
54
56
|
namespace,
|
|
@@ -59,7 +61,11 @@ export async function allYaml(assets: Assets) {
|
|
|
59
61
|
tlsSecret(name, tls),
|
|
60
62
|
deployment(assets, hash),
|
|
61
63
|
service(name),
|
|
64
|
+
watcherService(name),
|
|
62
65
|
moduleSecret(name, code, hash),
|
|
66
|
+
peprStoreCRD,
|
|
67
|
+
storeRole(name),
|
|
68
|
+
storeRoleBinding(name),
|
|
63
69
|
];
|
|
64
70
|
|
|
65
71
|
if (mutateWebhook) {
|
|
@@ -70,6 +76,10 @@ export async function allYaml(assets: Assets) {
|
|
|
70
76
|
resources.push(validateWebhook);
|
|
71
77
|
}
|
|
72
78
|
|
|
79
|
+
if (watchDeployment) {
|
|
80
|
+
resources.push(watchDeployment);
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
// Convert the resources to a single YAML string
|
|
74
84
|
return resources.map(r => dumpYaml(r, { noRefs: true })).join("---\n");
|
|
75
85
|
}
|
package/src/lib/capability.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
+
import { GenericClass, GroupVersionKind, modelToGroupVersionKind } from "kubernetes-fluent-client";
|
|
5
|
+
import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types";
|
|
4
6
|
import { pickBy } from "ramda";
|
|
5
7
|
|
|
6
|
-
import { isWatchMode, modelToGroupVersionKind } from "./k8s/index";
|
|
7
|
-
import { GroupVersionKind } from "./k8s/types";
|
|
8
8
|
import Log from "./logger";
|
|
9
|
+
import { isBuildMode, isDevMode, isWatchMode } from "./module";
|
|
10
|
+
import { PeprStore, Storage } from "./storage";
|
|
9
11
|
import {
|
|
10
12
|
Binding,
|
|
11
13
|
BindingFilter,
|
|
@@ -13,13 +15,16 @@ import {
|
|
|
13
15
|
CapabilityCfg,
|
|
14
16
|
CapabilityExport,
|
|
15
17
|
Event,
|
|
16
|
-
GenericClass,
|
|
17
18
|
MutateAction,
|
|
18
19
|
MutateActionChain,
|
|
19
20
|
ValidateAction,
|
|
21
|
+
ValidateActionChain,
|
|
20
22
|
WhenSelector,
|
|
21
23
|
} from "./types";
|
|
22
24
|
|
|
25
|
+
const registerAdmission = isBuildMode() || !isWatchMode();
|
|
26
|
+
const registerWatch = isBuildMode() || isWatchMode() || isDevMode();
|
|
27
|
+
|
|
23
28
|
/**
|
|
24
29
|
* A capability is a unit of functionality that can be registered with the Pepr runtime.
|
|
25
30
|
*/
|
|
@@ -28,6 +33,24 @@ export class Capability implements CapabilityExport {
|
|
|
28
33
|
#description: string;
|
|
29
34
|
#namespaces?: string[] | undefined;
|
|
30
35
|
#bindings: Binding[] = [];
|
|
36
|
+
#store = new Storage();
|
|
37
|
+
#registered = false;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Store is a key-value data store that can be used to persist data that should be shared
|
|
41
|
+
* between requests. Each capability has its own store, and the data is persisted in Kubernetes
|
|
42
|
+
* in the `pepr-system` namespace.
|
|
43
|
+
*
|
|
44
|
+
* Note: You should only access the store from within an action.
|
|
45
|
+
*/
|
|
46
|
+
Store: PeprStore = {
|
|
47
|
+
clear: this.#store.clear,
|
|
48
|
+
getItem: this.#store.getItem,
|
|
49
|
+
removeItem: this.#store.removeItem,
|
|
50
|
+
setItem: this.#store.setItem,
|
|
51
|
+
subscribe: this.#store.subscribe,
|
|
52
|
+
onReady: this.#store.onReady,
|
|
53
|
+
};
|
|
31
54
|
|
|
32
55
|
get bindings() {
|
|
33
56
|
return this.#bindings;
|
|
@@ -50,15 +73,32 @@ export class Capability implements CapabilityExport {
|
|
|
50
73
|
this.#description = cfg.description;
|
|
51
74
|
this.#namespaces = cfg.namespaces;
|
|
52
75
|
|
|
53
|
-
// Bind When() to this instance
|
|
54
|
-
this.When = this.When.bind(this);
|
|
55
|
-
|
|
56
76
|
Log.info(`Capability ${this.#name} registered`);
|
|
57
77
|
Log.debug(cfg);
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
/**
|
|
61
|
-
*
|
|
81
|
+
* Register the store with the capability. This is called automatically by the Pepr controller.
|
|
82
|
+
*
|
|
83
|
+
* @param store
|
|
84
|
+
*/
|
|
85
|
+
registerStore = () => {
|
|
86
|
+
Log.info(`Registering store for ${this.#name}`);
|
|
87
|
+
|
|
88
|
+
if (this.#registered) {
|
|
89
|
+
throw new Error(`Store already registered for ${this.#name}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.#registered = true;
|
|
93
|
+
|
|
94
|
+
// Pass back any ready callback to the controller
|
|
95
|
+
return {
|
|
96
|
+
store: this.#store,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The When method is used to register a action to be executed when a Kubernetes resource is
|
|
62
102
|
* processed by Pepr. The action will be executed if the resource matches the specified kind and any
|
|
63
103
|
* filters that are applied.
|
|
64
104
|
*
|
|
@@ -66,7 +106,7 @@ export class Capability implements CapabilityExport {
|
|
|
66
106
|
* @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
|
|
67
107
|
* @returns
|
|
68
108
|
*/
|
|
69
|
-
When<T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> {
|
|
109
|
+
When = <T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> => {
|
|
70
110
|
const matchedKind = modelToGroupVersionKind(model.name);
|
|
71
111
|
|
|
72
112
|
// If the kind is not specified and the model is not a KubernetesObject, throw an error
|
|
@@ -75,6 +115,7 @@ export class Capability implements CapabilityExport {
|
|
|
75
115
|
}
|
|
76
116
|
|
|
77
117
|
const binding: Binding = {
|
|
118
|
+
model,
|
|
78
119
|
// If the kind is not specified, use the matched kind from the model
|
|
79
120
|
kind: kind || matchedKind,
|
|
80
121
|
event: Event.Any,
|
|
@@ -88,7 +129,7 @@ export class Capability implements CapabilityExport {
|
|
|
88
129
|
|
|
89
130
|
const bindings = this.#bindings;
|
|
90
131
|
const prefix = `${this.#name}: ${model.name}`;
|
|
91
|
-
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate };
|
|
132
|
+
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate, Watch };
|
|
92
133
|
const isNotEmpty = (value: object) => Object.keys(value).length > 0;
|
|
93
134
|
const log = (message: string, cbString: string) => {
|
|
94
135
|
const filteredObj = pickBy(isNotEmpty, binding.filters);
|
|
@@ -98,8 +139,8 @@ export class Capability implements CapabilityExport {
|
|
|
98
139
|
Log.debug(cbString, prefix);
|
|
99
140
|
};
|
|
100
141
|
|
|
101
|
-
function Validate(validateCallback: ValidateAction<T>):
|
|
102
|
-
if (
|
|
142
|
+
function Validate(validateCallback: ValidateAction<T>): ValidateActionChain<T> {
|
|
143
|
+
if (registerAdmission) {
|
|
103
144
|
log("Validate Action", validateCallback.toString());
|
|
104
145
|
|
|
105
146
|
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
@@ -110,10 +151,12 @@ export class Capability implements CapabilityExport {
|
|
|
110
151
|
validateCallback,
|
|
111
152
|
});
|
|
112
153
|
}
|
|
154
|
+
|
|
155
|
+
return { Watch };
|
|
113
156
|
}
|
|
114
157
|
|
|
115
158
|
function Mutate(mutateCallback: MutateAction<T>): MutateActionChain<T> {
|
|
116
|
-
if (
|
|
159
|
+
if (registerAdmission) {
|
|
117
160
|
log("Mutate Action", mutateCallback.toString());
|
|
118
161
|
|
|
119
162
|
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
@@ -126,7 +169,19 @@ export class Capability implements CapabilityExport {
|
|
|
126
169
|
}
|
|
127
170
|
|
|
128
171
|
// Now only allow adding actions to the same binding
|
|
129
|
-
return { Validate };
|
|
172
|
+
return { Watch, Validate };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function Watch(watchCallback: WatchAction<T>) {
|
|
176
|
+
if (registerWatch) {
|
|
177
|
+
log("Watch Action", watchCallback.toString());
|
|
178
|
+
|
|
179
|
+
bindings.push({
|
|
180
|
+
...binding,
|
|
181
|
+
isWatch: true,
|
|
182
|
+
watchCallback,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
130
185
|
}
|
|
131
186
|
|
|
132
187
|
function InNamespace(...namespaces: string[]): BindingWithName<T> {
|
|
@@ -168,5 +223,5 @@ export class Capability implements CapabilityExport {
|
|
|
168
223
|
IsUpdated: () => bindEvent(Event.Update),
|
|
169
224
|
IsDeleted: () => bindEvent(Event.Delete),
|
|
170
225
|
};
|
|
171
|
-
}
|
|
226
|
+
};
|
|
172
227
|
}
|
|
@@ -5,14 +5,14 @@ import express, { NextFunction } from "express";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import https from "https";
|
|
7
7
|
|
|
8
|
-
import { Capability } from "
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import { mutateProcessor } from "
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
8
|
+
import { Capability } from "../capability";
|
|
9
|
+
import { MutateResponse, AdmissionRequest, ValidateResponse } from "../k8s";
|
|
10
|
+
import Log from "../logger";
|
|
11
|
+
import { MetricsCollector } from "../metrics";
|
|
12
|
+
import { ModuleConfig, isWatchMode } from "../module";
|
|
13
|
+
import { mutateProcessor } from "../mutate-processor";
|
|
14
|
+
import { validateProcessor } from "../validate-processor";
|
|
15
|
+
import { PeprControllerStore } from "./store";
|
|
16
16
|
|
|
17
17
|
export class Controller {
|
|
18
18
|
// Track whether the server is running
|
|
@@ -30,22 +30,25 @@ export class Controller {
|
|
|
30
30
|
// Initialized with the constructor
|
|
31
31
|
readonly #config: ModuleConfig;
|
|
32
32
|
readonly #capabilities: Capability[];
|
|
33
|
-
readonly #beforeHook?: (req:
|
|
33
|
+
readonly #beforeHook?: (req: AdmissionRequest) => void;
|
|
34
34
|
readonly #afterHook?: (res: MutateResponse) => void;
|
|
35
35
|
|
|
36
36
|
constructor(
|
|
37
37
|
config: ModuleConfig,
|
|
38
38
|
capabilities: Capability[],
|
|
39
|
-
beforeHook?: (req:
|
|
39
|
+
beforeHook?: (req: AdmissionRequest) => void,
|
|
40
40
|
afterHook?: (res: MutateResponse) => void,
|
|
41
|
+
onReady?: () => void,
|
|
41
42
|
) {
|
|
42
43
|
this.#config = config;
|
|
43
44
|
this.#capabilities = capabilities;
|
|
44
|
-
this.#beforeHook = beforeHook;
|
|
45
|
-
this.#afterHook = afterHook;
|
|
46
45
|
|
|
47
|
-
//
|
|
48
|
-
|
|
46
|
+
// Initialize the Pepr store for each capability
|
|
47
|
+
new PeprControllerStore(config, capabilities, () => {
|
|
48
|
+
this.#bindEndpoints();
|
|
49
|
+
onReady && onReady();
|
|
50
|
+
Log.info("✅ Controller startup complete");
|
|
51
|
+
});
|
|
49
52
|
|
|
50
53
|
// Middleware for logging requests
|
|
51
54
|
this.#app.use(Controller.#logger);
|
|
@@ -55,18 +58,17 @@ export class Controller {
|
|
|
55
58
|
|
|
56
59
|
if (beforeHook) {
|
|
57
60
|
Log.info(`Using beforeHook: ${beforeHook}`);
|
|
61
|
+
this.#beforeHook = beforeHook;
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
if (afterHook) {
|
|
61
65
|
Log.info(`Using afterHook: ${afterHook}`);
|
|
66
|
+
this.#afterHook = afterHook;
|
|
62
67
|
}
|
|
63
|
-
|
|
64
|
-
// Bind endpoints
|
|
65
|
-
this.#bindEndpoints();
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/** Start the webhook server */
|
|
69
|
-
startServer(port: number) {
|
|
71
|
+
startServer = (port: number) => {
|
|
70
72
|
if (this.#running) {
|
|
71
73
|
throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
|
|
72
74
|
}
|
|
@@ -78,7 +80,7 @@ export class Controller {
|
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
// Get the API token if not in watch mode
|
|
81
|
-
if (!isWatchMode) {
|
|
83
|
+
if (!isWatchMode()) {
|
|
82
84
|
// Get the API token from the environment variable or the mounted secret
|
|
83
85
|
this.#token = process.env.PEPR_API_TOKEN || fs.readFileSync("/app/api-token/value").toString().trim();
|
|
84
86
|
Log.info(`Using API token: ${this.#token}`);
|
|
@@ -119,7 +121,7 @@ export class Controller {
|
|
|
119
121
|
process.exit(0);
|
|
120
122
|
});
|
|
121
123
|
});
|
|
122
|
-
}
|
|
124
|
+
};
|
|
123
125
|
|
|
124
126
|
#bindEndpoints = () => {
|
|
125
127
|
// Health check endpoint
|
|
@@ -128,7 +130,7 @@ export class Controller {
|
|
|
128
130
|
// Metrics endpoint
|
|
129
131
|
this.#app.get("/metrics", this.#metrics);
|
|
130
132
|
|
|
131
|
-
if (isWatchMode) {
|
|
133
|
+
if (isWatchMode()) {
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
134
136
|
|
|
@@ -190,11 +192,11 @@ export class Controller {
|
|
|
190
192
|
// Create the admission request handler
|
|
191
193
|
return async (req: express.Request, res: express.Response) => {
|
|
192
194
|
// Start the metrics timer
|
|
193
|
-
const startTime =
|
|
195
|
+
const startTime = MetricsCollector.observeStart();
|
|
194
196
|
|
|
195
197
|
try {
|
|
196
198
|
// Get the request from the body or create an empty request
|
|
197
|
-
const request:
|
|
199
|
+
const request: AdmissionRequest = req.body?.request || ({} as AdmissionRequest);
|
|
198
200
|
|
|
199
201
|
// Run the before hook if it exists
|
|
200
202
|
this.#beforeHook && this.#beforeHook(request || {});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { Operation } from "fast-json-patch";
|
|
5
|
+
import { K8s } from "kubernetes-fluent-client";
|
|
6
|
+
import { startsWith } from "ramda";
|
|
7
|
+
|
|
8
|
+
import { Capability } from "../capability";
|
|
9
|
+
import { PeprStore } from "../k8s";
|
|
10
|
+
import Log from "../logger";
|
|
11
|
+
import { ModuleConfig } from "../module";
|
|
12
|
+
import { DataOp, DataSender, DataStore, Storage } from "../storage";
|
|
13
|
+
|
|
14
|
+
const namespace = "pepr-system";
|
|
15
|
+
const debounceBackoff = 5000;
|
|
16
|
+
|
|
17
|
+
export class PeprControllerStore {
|
|
18
|
+
#name: string;
|
|
19
|
+
#stores: Record<string, Storage> = {};
|
|
20
|
+
#sendDebounce: NodeJS.Timeout | undefined;
|
|
21
|
+
#onReady?: () => void;
|
|
22
|
+
|
|
23
|
+
constructor(config: ModuleConfig, capabilities: Capability[], onReady?: () => void) {
|
|
24
|
+
this.#onReady = onReady;
|
|
25
|
+
|
|
26
|
+
// Setup Pepr State bindings
|
|
27
|
+
this.#name = `pepr-${config.uuid}-store`;
|
|
28
|
+
|
|
29
|
+
// Establish the store for each capability
|
|
30
|
+
for (const { name, registerStore } of capabilities) {
|
|
31
|
+
// Register the store with the capability
|
|
32
|
+
const { store } = registerStore();
|
|
33
|
+
|
|
34
|
+
// Bind the store sender to the capability
|
|
35
|
+
store.registerSender(this.#send(name));
|
|
36
|
+
|
|
37
|
+
// Store the storage instance
|
|
38
|
+
this.#stores[name] = store;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Add a jitter to the Store creation to avoid collisions
|
|
42
|
+
setTimeout(
|
|
43
|
+
() =>
|
|
44
|
+
K8s(PeprStore)
|
|
45
|
+
.InNamespace(namespace)
|
|
46
|
+
.Get(this.#name)
|
|
47
|
+
// If the get succeeds, setup the watch
|
|
48
|
+
.then(this.#setupWatch)
|
|
49
|
+
// Otherwise, create the resource
|
|
50
|
+
.catch(this.#createStoreResource),
|
|
51
|
+
Math.random() * 3000,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#setupWatch = () => {
|
|
56
|
+
void K8s(PeprStore, { name: this.#name, namespace }).Watch(this.#receive);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
#receive = (store: PeprStore) => {
|
|
60
|
+
Log.debug(store, "Pepr Store update");
|
|
61
|
+
|
|
62
|
+
// Wrap the update in a debounced function
|
|
63
|
+
const debounced = () => {
|
|
64
|
+
// Base64 decode the data
|
|
65
|
+
const data: DataStore = store.data || {};
|
|
66
|
+
|
|
67
|
+
// Loop over each stored capability
|
|
68
|
+
for (const name of Object.keys(this.#stores)) {
|
|
69
|
+
// Get the prefix offset for the keys
|
|
70
|
+
const offset = `${name}-`.length;
|
|
71
|
+
|
|
72
|
+
// Get any keys that match the capability name prefix
|
|
73
|
+
const filtered: DataStore = {};
|
|
74
|
+
|
|
75
|
+
// Loop over each key in the secret
|
|
76
|
+
for (const key of Object.keys(data)) {
|
|
77
|
+
// Match on the capability name as a prefix
|
|
78
|
+
if (startsWith(name, key)) {
|
|
79
|
+
// Strip the prefix and store the value
|
|
80
|
+
filtered[key.slice(offset)] = data[key];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Send the data to the receiver callback
|
|
85
|
+
this.#stores[name].receive(filtered);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Call the onReady callback if this is the first time the secret has been read
|
|
89
|
+
if (this.#onReady) {
|
|
90
|
+
this.#onReady();
|
|
91
|
+
this.#onReady = undefined;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Debounce the update to 1 second to avoid multiple rapid calls
|
|
96
|
+
clearTimeout(this.#sendDebounce);
|
|
97
|
+
this.#sendDebounce = setTimeout(debounced, debounceBackoff);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
#send = (capabilityName: string) => {
|
|
101
|
+
const sendCache: Record<string, Operation> = {};
|
|
102
|
+
|
|
103
|
+
// Load the sendCache with patch operations
|
|
104
|
+
const fillCache = (op: DataOp, key: string[], val?: string) => {
|
|
105
|
+
if (op === "add") {
|
|
106
|
+
const path = `/data/${capabilityName}-${key}`;
|
|
107
|
+
const value = val || "";
|
|
108
|
+
const cacheIdx = [op, path, value].join(":");
|
|
109
|
+
|
|
110
|
+
// Add the operation to the cache
|
|
111
|
+
sendCache[cacheIdx] = { op, path, value };
|
|
112
|
+
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (op === "remove") {
|
|
117
|
+
if (key.length < 1) {
|
|
118
|
+
throw new Error(`Key is required for REMOVE operation`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const k of key) {
|
|
122
|
+
const path = `/data/${capabilityName}-${k}`;
|
|
123
|
+
const cacheIdx = [op, path].join(":");
|
|
124
|
+
|
|
125
|
+
// Add the operation to the cache
|
|
126
|
+
sendCache[cacheIdx] = { op, path };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// If we get here, the operation is not supported
|
|
133
|
+
throw new Error(`Unsupported operation: ${op}`);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Send the cached updates to the cluster
|
|
137
|
+
const flushCache = async () => {
|
|
138
|
+
const indexes = Object.keys(sendCache);
|
|
139
|
+
const payload = Object.values(sendCache);
|
|
140
|
+
|
|
141
|
+
// Loop over each key in the cache and delete it to avoid collisions with other sender calls
|
|
142
|
+
for (const idx of indexes) {
|
|
143
|
+
delete sendCache[idx];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Send the patch to the cluster
|
|
148
|
+
await K8s(PeprStore, { namespace, name: this.#name }).Patch(payload);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
Log.error(err, "Pepr store update failure");
|
|
151
|
+
|
|
152
|
+
// On failure to update, re-add the operations to the cache to be retried
|
|
153
|
+
for (const idx of indexes) {
|
|
154
|
+
sendCache[idx] = payload[Number(idx)];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Create a sender function for the capability to add/remove data from the store
|
|
160
|
+
const sender: DataSender = async (op: DataOp, key: string[], val?: string) => {
|
|
161
|
+
fillCache(op, key, val);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Send any cached updates every debounceBackoff milliseconds
|
|
165
|
+
setInterval(() => {
|
|
166
|
+
if (Object.keys(sendCache).length > 0) {
|
|
167
|
+
Log.debug(sendCache, "Sending updates to Pepr store");
|
|
168
|
+
void flushCache();
|
|
169
|
+
}
|
|
170
|
+
}, debounceBackoff);
|
|
171
|
+
|
|
172
|
+
return sender;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
#createStoreResource = async (e: unknown) => {
|
|
176
|
+
Log.info(`Pepr store not found, creating...`);
|
|
177
|
+
Log.debug(e);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await K8s(PeprStore).Apply({
|
|
181
|
+
metadata: {
|
|
182
|
+
name: this.#name,
|
|
183
|
+
namespace,
|
|
184
|
+
},
|
|
185
|
+
data: {
|
|
186
|
+
// JSON Patch will die if the data is empty, so we need to add a placeholder
|
|
187
|
+
__pepr_do_not_delete__: "k-thx-bye",
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Now that the resource exists, setup the watch
|
|
192
|
+
this.#setupWatch();
|
|
193
|
+
} catch (err) {
|
|
194
|
+
Log.error(err, "Failed to create Pepr store");
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
package/src/lib/filter.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { AdmissionRequest, Operation } from "./k8s";
|
|
5
5
|
import logger from "./logger";
|
|
6
6
|
import { Binding, Event } from "./types";
|
|
7
7
|
|
|
@@ -12,7 +12,7 @@ import { Binding, Event } from "./types";
|
|
|
12
12
|
* @param req the incoming request
|
|
13
13
|
* @returns
|
|
14
14
|
*/
|
|
15
|
-
export function shouldSkipRequest(binding: Binding, req:
|
|
15
|
+
export function shouldSkipRequest(binding: Binding, req: AdmissionRequest, capabilityNamespaces: string[]) {
|
|
16
16
|
const { group, kind, version } = binding.kind || {};
|
|
17
17
|
const { namespaces, labels, annotations, name } = binding.filters || {};
|
|
18
18
|
const operation = req.operation.toUpperCase();
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export { KubernetesListObject, KubernetesObject };
|
|
4
|
+
import { GenericKind, GroupVersionKind, KubernetesObject, RegisterKind } from "kubernetes-fluent-client";
|
|
7
5
|
|
|
8
6
|
export enum Operation {
|
|
9
7
|
CREATE = "CREATE",
|
|
@@ -13,30 +11,21 @@ export enum Operation {
|
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
|
-
*
|
|
17
|
-
* that is not explicitly supported by Pepr. This can be used on its own or as a base class for
|
|
18
|
-
* other types. See the examples in `HelloPepr.ts` for more information.
|
|
14
|
+
* PeprStore for internal use by Pepr. This is used to store arbitrary data in the cluster.
|
|
19
15
|
*/
|
|
20
|
-
export class GenericKind {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
-
[key: string]: any;
|
|
16
|
+
export class PeprStore extends GenericKind {
|
|
17
|
+
declare data: {
|
|
18
|
+
[key: string]: string;
|
|
19
|
+
};
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
readonly group: string;
|
|
36
|
-
readonly version?: string;
|
|
37
|
-
/** Optional, override the plural name for use in Webhook rules generation */
|
|
38
|
-
readonly plural?: string;
|
|
39
|
-
}
|
|
22
|
+
export const peprStoreGVK = {
|
|
23
|
+
kind: "PeprStore",
|
|
24
|
+
version: "v1",
|
|
25
|
+
group: "pepr.dev",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
RegisterKind(PeprStore, peprStoreGVK);
|
|
40
29
|
|
|
41
30
|
/**
|
|
42
31
|
* GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
|
|
@@ -51,7 +40,7 @@ export interface GroupVersionResource {
|
|
|
51
40
|
/**
|
|
52
41
|
* A Kubernetes admission request to be processed by a capability.
|
|
53
42
|
*/
|
|
54
|
-
export interface
|
|
43
|
+
export interface AdmissionRequest<T = KubernetesObject> {
|
|
55
44
|
/** UID is an identifier for the individual request/response. */
|
|
56
45
|
readonly uid: string;
|
|
57
46
|
|
|
@@ -161,7 +150,7 @@ export interface ValidateResponse extends MutateResponse {
|
|
|
161
150
|
/** Status contains extra details into why an admission request was denied. This field IS NOT consulted in any way if "Allowed" is "true". */
|
|
162
151
|
status?: {
|
|
163
152
|
/** A machine-readable description of why this operation is in the
|
|
164
|
-
|
|
153
|
+
"Failure" status. If this value is empty there is no information available. */
|
|
165
154
|
code: number;
|
|
166
155
|
|
|
167
156
|
/** A human-readable description of the status of this operation. */
|