pepr 0.42.0 → 0.42.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/dist/cli/build.d.ts +1 -0
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/build.helpers.d.ts +66 -0
- package/dist/cli/build.helpers.d.ts.map +1 -1
- package/dist/cli/monitor.d.ts +23 -0
- package/dist/cli/monitor.d.ts.map +1 -1
- package/dist/cli.js +341 -283
- package/dist/controller.js +1 -1
- package/dist/lib/assets/destroy.d.ts.map +1 -1
- package/dist/lib/assets/helm.d.ts +1 -1
- package/dist/lib/assets/helm.d.ts.map +1 -1
- package/dist/lib/assets/index.d.ts.map +1 -1
- package/dist/lib/assets/pods.d.ts +5 -19
- package/dist/lib/assets/pods.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/lib/controller/index.d.ts.map +1 -1
- package/dist/lib/controller/store.d.ts +0 -1
- package/dist/lib/controller/store.d.ts.map +1 -1
- package/dist/lib/controller/storeCache.d.ts +1 -0
- package/dist/lib/controller/storeCache.d.ts.map +1 -1
- package/dist/lib/mutate-request.d.ts +2 -2
- package/dist/lib/mutate-request.d.ts.map +1 -1
- package/dist/lib/queue.d.ts.map +1 -1
- package/dist/lib/storage.d.ts +4 -4
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/validate-processor.d.ts +4 -1
- package/dist/lib/validate-processor.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts.map +1 -1
- package/dist/lib.js +136 -109
- package/dist/lib.js.map +3 -3
- package/package.json +1 -1
- package/src/cli/build.helpers.ts +180 -0
- package/src/cli/build.ts +85 -133
- package/src/cli/monitor.ts +108 -65
- package/src/lib/assets/deploy.ts +6 -6
- package/src/lib/assets/destroy.ts +1 -1
- package/src/lib/assets/helm.ts +6 -6
- package/src/lib/assets/index.ts +22 -22
- package/src/lib/assets/pods.ts +10 -5
- package/src/lib/assets/webhooks.ts +1 -1
- package/src/lib/assets/yaml.ts +12 -9
- package/src/lib/capability.ts +21 -10
- package/src/lib/controller/index.ts +9 -7
- package/src/lib/controller/store.ts +23 -10
- package/src/lib/controller/storeCache.ts +10 -1
- package/src/lib/mutate-request.ts +11 -11
- package/src/lib/queue.ts +12 -4
- package/src/lib/storage.ts +33 -24
- package/src/lib/utils.ts +5 -5
- package/src/lib/validate-processor.ts +47 -39
- package/src/lib/watch-processor.ts +11 -7
- package/src/sdk/cosign.ts +4 -4
|
@@ -78,7 +78,7 @@ export class Controller {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** Start the webhook server */
|
|
81
|
-
startServer = (port: number) => {
|
|
81
|
+
startServer = (port: number): void => {
|
|
82
82
|
if (this.#running) {
|
|
83
83
|
throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
|
|
84
84
|
}
|
|
@@ -133,7 +133,7 @@ export class Controller {
|
|
|
133
133
|
});
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
-
#bindEndpoints = () => {
|
|
136
|
+
#bindEndpoints = (): void => {
|
|
137
137
|
// Health check endpoint
|
|
138
138
|
this.#app.get("/healthz", Controller.#healthz);
|
|
139
139
|
|
|
@@ -162,7 +162,7 @@ export class Controller {
|
|
|
162
162
|
* @param next The next middleware function
|
|
163
163
|
* @returns
|
|
164
164
|
*/
|
|
165
|
-
#validateToken = (req: express.Request, res: express.Response, next: NextFunction) => {
|
|
165
|
+
#validateToken = (req: express.Request, res: express.Response, next: NextFunction): void => {
|
|
166
166
|
// Validate the token
|
|
167
167
|
const { token } = req.params;
|
|
168
168
|
if (token !== this.#token) {
|
|
@@ -183,7 +183,7 @@ export class Controller {
|
|
|
183
183
|
* @param req the incoming request
|
|
184
184
|
* @param res the outgoing response
|
|
185
185
|
*/
|
|
186
|
-
#metrics = async (req: express.Request, res: express.Response) => {
|
|
186
|
+
#metrics = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
187
187
|
try {
|
|
188
188
|
// https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#basic-info
|
|
189
189
|
res.set("Content-Type", "text/plain; version=0.0.4");
|
|
@@ -200,7 +200,9 @@ export class Controller {
|
|
|
200
200
|
* @param admissionKind the type of admission request
|
|
201
201
|
* @returns the request handler
|
|
202
202
|
*/
|
|
203
|
-
#admissionReq = (
|
|
203
|
+
#admissionReq = (
|
|
204
|
+
admissionKind: "Mutate" | "Validate",
|
|
205
|
+
): ((req: express.Request, res: express.Response) => Promise<void>) => {
|
|
204
206
|
// Create the admission request handler
|
|
205
207
|
return async (req: express.Request, res: express.Response) => {
|
|
206
208
|
// Start the metrics timer
|
|
@@ -259,7 +261,7 @@ export class Controller {
|
|
|
259
261
|
* @param res the outgoing response
|
|
260
262
|
* @param next the next middleware function
|
|
261
263
|
*/
|
|
262
|
-
static #logger(req: express.Request, res: express.Response, next: express.NextFunction) {
|
|
264
|
+
static #logger(req: express.Request, res: express.Response, next: express.NextFunction): void {
|
|
263
265
|
const startTime = Date.now();
|
|
264
266
|
|
|
265
267
|
res.on("finish", () => {
|
|
@@ -288,7 +290,7 @@ export class Controller {
|
|
|
288
290
|
* @param req the incoming request
|
|
289
291
|
* @param res the outgoing response
|
|
290
292
|
*/
|
|
291
|
-
static #healthz(req: express.Request, res: express.Response) {
|
|
293
|
+
static #healthz(req: express.Request, res: express.Response): void {
|
|
292
294
|
try {
|
|
293
295
|
res.send("OK");
|
|
294
296
|
} catch (err) {
|
|
@@ -12,7 +12,8 @@ import { DataOp, DataSender, DataStore, Storage } from "../storage";
|
|
|
12
12
|
import { fillStoreCache, sendUpdatesAndFlushCache } from "./storeCache";
|
|
13
13
|
|
|
14
14
|
const namespace = "pepr-system";
|
|
15
|
-
|
|
15
|
+
const debounceBackoffReceive = 1000;
|
|
16
|
+
const debounceBackoffSend = 4000;
|
|
16
17
|
|
|
17
18
|
export class StoreController {
|
|
18
19
|
#name: string;
|
|
@@ -25,7 +26,7 @@ export class StoreController {
|
|
|
25
26
|
|
|
26
27
|
this.#name = name;
|
|
27
28
|
|
|
28
|
-
const setStorageInstance = (registrationFunction: () => Storage, name: string) => {
|
|
29
|
+
const setStorageInstance = (registrationFunction: () => Storage, name: string): void => {
|
|
29
30
|
const scheduleStore = registrationFunction();
|
|
30
31
|
|
|
31
32
|
// Bind the store sender to the capability
|
|
@@ -61,13 +62,22 @@ export class StoreController {
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
#setupWatch = () => {
|
|
65
|
+
#setupWatch = (): void => {
|
|
65
66
|
const watcher = K8s(Store, { name: this.#name, namespace }).Watch(this.#receive);
|
|
66
67
|
watcher.start().catch(e => Log.error(e, "Error starting Pepr store watch"));
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
#migrateAndSetupWatch = async (store: Store) => {
|
|
70
|
+
#migrateAndSetupWatch = async (store: Store): Promise<void> => {
|
|
70
71
|
Log.debug(redactedStore(store), "Pepr Store migration");
|
|
72
|
+
// Add cacheID label to store
|
|
73
|
+
await K8s(Store, { namespace, name: this.#name }).Patch([
|
|
74
|
+
{
|
|
75
|
+
op: "add",
|
|
76
|
+
path: "/metadata/labels/pepr.dev-cacheID",
|
|
77
|
+
value: `${Date.now()}`,
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
|
|
71
81
|
const data: DataStore = store.data || {};
|
|
72
82
|
let storeCache: Record<string, Operation> = {};
|
|
73
83
|
|
|
@@ -96,11 +106,11 @@ export class StoreController {
|
|
|
96
106
|
this.#setupWatch();
|
|
97
107
|
};
|
|
98
108
|
|
|
99
|
-
#receive = (store: Store) => {
|
|
109
|
+
#receive = (store: Store): void => {
|
|
100
110
|
Log.debug(redactedStore(store), "Pepr Store update");
|
|
101
111
|
|
|
102
112
|
// Wrap the update in a debounced function
|
|
103
|
-
const debounced = () => {
|
|
113
|
+
const debounced = (): void => {
|
|
104
114
|
// Base64 decode the data
|
|
105
115
|
const data: DataStore = store.data || {};
|
|
106
116
|
|
|
@@ -134,10 +144,10 @@ export class StoreController {
|
|
|
134
144
|
|
|
135
145
|
// Debounce the update to 1 second to avoid multiple rapid calls
|
|
136
146
|
clearTimeout(this.#sendDebounce);
|
|
137
|
-
this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 :
|
|
147
|
+
this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoffReceive);
|
|
138
148
|
};
|
|
139
149
|
|
|
140
|
-
#send = (capabilityName: string) => {
|
|
150
|
+
#send = (capabilityName: string): DataSender => {
|
|
141
151
|
let storeCache: Record<string, Operation> = {};
|
|
142
152
|
|
|
143
153
|
// Create a sender function for the capability to add/remove data from the store
|
|
@@ -151,12 +161,12 @@ export class StoreController {
|
|
|
151
161
|
Log.debug(redactedPatch(storeCache), "Sending updates to Pepr store");
|
|
152
162
|
void sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
|
|
153
163
|
}
|
|
154
|
-
},
|
|
164
|
+
}, debounceBackoffSend);
|
|
155
165
|
|
|
156
166
|
return sender;
|
|
157
167
|
};
|
|
158
168
|
|
|
159
|
-
#createStoreResource = async (e: unknown) => {
|
|
169
|
+
#createStoreResource = async (e: unknown): Promise<void> => {
|
|
160
170
|
Log.info(`Pepr store not found, creating...`);
|
|
161
171
|
Log.debug(e);
|
|
162
172
|
|
|
@@ -165,6 +175,9 @@ export class StoreController {
|
|
|
165
175
|
metadata: {
|
|
166
176
|
name: this.#name,
|
|
167
177
|
namespace,
|
|
178
|
+
labels: {
|
|
179
|
+
"pepr.dev-cacheID": `${Date.now()}`,
|
|
180
|
+
},
|
|
168
181
|
},
|
|
169
182
|
data: {
|
|
170
183
|
// JSON Patch will die if the data is empty, so we need to add a placeholder
|
|
@@ -11,7 +11,7 @@ export const sendUpdatesAndFlushCache = async (cache: Record<string, Operation>,
|
|
|
11
11
|
|
|
12
12
|
try {
|
|
13
13
|
if (payload.length > 0) {
|
|
14
|
-
await K8s(Store, { namespace, name }).Patch(payload); // Send patch to cluster
|
|
14
|
+
await K8s(Store, { namespace, name }).Patch(updateCacheID(payload)); // Send patch to cluster
|
|
15
15
|
Object.keys(cache).forEach(key => delete cache[key]);
|
|
16
16
|
}
|
|
17
17
|
} catch (err) {
|
|
@@ -61,3 +61,12 @@ export const fillStoreCache = (
|
|
|
61
61
|
}
|
|
62
62
|
return cache;
|
|
63
63
|
};
|
|
64
|
+
|
|
65
|
+
export function updateCacheID(payload: Operation[]): Operation[] {
|
|
66
|
+
payload.push({
|
|
67
|
+
op: "replace",
|
|
68
|
+
path: "/metadata/labels/pepr.dev-cacheID",
|
|
69
|
+
value: `${Date.now()}`,
|
|
70
|
+
});
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
@@ -11,19 +11,19 @@ export class PeprMutateRequest<T extends KubernetesObject> {
|
|
|
11
11
|
Raw: T;
|
|
12
12
|
#input: AdmissionRequest<T>;
|
|
13
13
|
|
|
14
|
-
get PermitSideEffects() {
|
|
14
|
+
get PermitSideEffects(): boolean {
|
|
15
15
|
return !this.#input.dryRun;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
get IsDryRun() {
|
|
18
|
+
get IsDryRun(): boolean | undefined {
|
|
19
19
|
return this.#input.dryRun;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
get OldResource() {
|
|
22
|
+
get OldResource(): KubernetesObject | undefined {
|
|
23
23
|
return this.#input.oldObject;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
get Request() {
|
|
26
|
+
get Request(): AdmissionRequest<KubernetesObject> {
|
|
27
27
|
return this.#input;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -42,11 +42,11 @@ export class PeprMutateRequest<T extends KubernetesObject> {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
Merge = (obj: DeepPartial<T>) => {
|
|
45
|
+
Merge = (obj: DeepPartial<T>): void => {
|
|
46
46
|
this.Raw = mergeDeepRight(this.Raw, obj) as unknown as T;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
SetLabel = (key: string, value: string) => {
|
|
49
|
+
SetLabel = (key: string, value: string): this => {
|
|
50
50
|
const ref = this.Raw;
|
|
51
51
|
ref.metadata = ref.metadata ?? {};
|
|
52
52
|
ref.metadata.labels = ref.metadata.labels ?? {};
|
|
@@ -54,7 +54,7 @@ export class PeprMutateRequest<T extends KubernetesObject> {
|
|
|
54
54
|
return this;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
SetAnnotation = (key: string, value: string) => {
|
|
57
|
+
SetAnnotation = (key: string, value: string): this => {
|
|
58
58
|
const ref = this.Raw;
|
|
59
59
|
ref.metadata = ref.metadata ?? {};
|
|
60
60
|
ref.metadata.annotations = ref.metadata.annotations ?? {};
|
|
@@ -62,25 +62,25 @@ export class PeprMutateRequest<T extends KubernetesObject> {
|
|
|
62
62
|
return this;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
RemoveLabel = (key: string) => {
|
|
65
|
+
RemoveLabel = (key: string): this => {
|
|
66
66
|
if (this.Raw.metadata?.labels?.[key]) {
|
|
67
67
|
delete this.Raw.metadata.labels[key];
|
|
68
68
|
}
|
|
69
69
|
return this;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
RemoveAnnotation = (key: string) => {
|
|
72
|
+
RemoveAnnotation = (key: string): this => {
|
|
73
73
|
if (this.Raw.metadata?.annotations?.[key]) {
|
|
74
74
|
delete this.Raw.metadata.annotations[key];
|
|
75
75
|
}
|
|
76
76
|
return this;
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
HasLabel = (key: string) => {
|
|
79
|
+
HasLabel = (key: string): boolean => {
|
|
80
80
|
return this.Raw.metadata?.labels?.[key] !== undefined;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
HasAnnotation = (key: string) => {
|
|
83
|
+
HasAnnotation = (key: string): boolean => {
|
|
84
84
|
return this.Raw.metadata?.annotations?.[key] !== undefined;
|
|
85
85
|
};
|
|
86
86
|
}
|
package/src/lib/queue.ts
CHANGED
|
@@ -29,11 +29,19 @@ export class Queue<K extends KubernetesObject> {
|
|
|
29
29
|
this.#uid = `${Date.now()}-${randomBytes(2).toString("hex")}`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
label() {
|
|
32
|
+
label(): { name: string; uid: string } {
|
|
33
33
|
return { name: this.#name, uid: this.#uid };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
stats() {
|
|
36
|
+
stats(): {
|
|
37
|
+
queue: {
|
|
38
|
+
name: string;
|
|
39
|
+
uid: string;
|
|
40
|
+
};
|
|
41
|
+
stats: {
|
|
42
|
+
length: number;
|
|
43
|
+
};
|
|
44
|
+
} {
|
|
37
45
|
return {
|
|
38
46
|
queue: this.label(),
|
|
39
47
|
stats: {
|
|
@@ -51,7 +59,7 @@ export class Queue<K extends KubernetesObject> {
|
|
|
51
59
|
* @param reconcile The callback to enqueue for reconcile
|
|
52
60
|
* @returns A promise that resolves when the object is reconciled
|
|
53
61
|
*/
|
|
54
|
-
enqueue(item: K, phase: WatchPhase, reconcile: WatchCallback) {
|
|
62
|
+
enqueue(item: K, phase: WatchPhase, reconcile: WatchCallback): Promise<void> {
|
|
55
63
|
const note = {
|
|
56
64
|
queue: this.label(),
|
|
57
65
|
item: {
|
|
@@ -73,7 +81,7 @@ export class Queue<K extends KubernetesObject> {
|
|
|
73
81
|
*
|
|
74
82
|
* @returns A promise that resolves when the webapp is reconciled
|
|
75
83
|
*/
|
|
76
|
-
async #dequeue() {
|
|
84
|
+
async #dequeue(): Promise<false | undefined> {
|
|
77
85
|
// If there is a pending promise, do nothing
|
|
78
86
|
if (this.#pendingPromise) {
|
|
79
87
|
Log.debug("Pending promise, not dequeuing");
|
package/src/lib/storage.ts
CHANGED
|
@@ -12,6 +12,11 @@ export type Unsubscribe = () => void;
|
|
|
12
12
|
const MAX_WAIT_TIME = 15000;
|
|
13
13
|
const STORE_VERSION_PREFIX = "v2";
|
|
14
14
|
|
|
15
|
+
interface WaitRecord {
|
|
16
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
17
|
+
unsubscribe?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
export function v2StoreKey(key: string): string {
|
|
16
21
|
return `${STORE_VERSION_PREFIX}-${pointer.escape(key)}`;
|
|
17
22
|
}
|
|
@@ -58,13 +63,13 @@ export interface PeprStore {
|
|
|
58
63
|
* Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
|
|
59
64
|
* Resolves when the key/value show up in the store.
|
|
60
65
|
*/
|
|
61
|
-
setItemAndWait(key: string, value: string): Promise<
|
|
66
|
+
setItemAndWait(key: string, value: string): Promise<string>;
|
|
62
67
|
|
|
63
68
|
/**
|
|
64
69
|
* Remove the value of the key.
|
|
65
70
|
* Resolves when the key does not show up in the store.
|
|
66
71
|
*/
|
|
67
|
-
removeItemAndWait(key: string): Promise<
|
|
72
|
+
removeItemAndWait(key: string): Promise<string>;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
/**
|
|
@@ -128,22 +133,24 @@ export class Storage implements PeprStore {
|
|
|
128
133
|
* @param value - The value of the key
|
|
129
134
|
* @returns
|
|
130
135
|
*/
|
|
131
|
-
setItemAndWait = (key: string, value: string): Promise<
|
|
136
|
+
setItemAndWait = (key: string, value: string): Promise<string> => {
|
|
132
137
|
this.#dispatchUpdate("add", [v2StoreKey(key)], value);
|
|
138
|
+
const record: WaitRecord = {};
|
|
133
139
|
|
|
134
|
-
return new Promise<
|
|
135
|
-
|
|
140
|
+
return new Promise<string>((resolve, reject) => {
|
|
141
|
+
// If promise has not resolved before MAX_WAIT_TIME reject
|
|
142
|
+
record.timeout = setTimeout(() => {
|
|
143
|
+
record.unsubscribe!();
|
|
144
|
+
return reject(`MAX_WAIT_TIME elapsed: Key ${key} not seen in ${MAX_WAIT_TIME / 1000}s`);
|
|
145
|
+
}, MAX_WAIT_TIME);
|
|
146
|
+
|
|
147
|
+
record.unsubscribe = this.subscribe(data => {
|
|
136
148
|
if (data[`${v2UnescapedStoreKey(key)}`] === value) {
|
|
137
|
-
unsubscribe();
|
|
138
|
-
|
|
149
|
+
record.unsubscribe!();
|
|
150
|
+
clearTimeout(record.timeout);
|
|
151
|
+
resolve("ok");
|
|
139
152
|
}
|
|
140
153
|
});
|
|
141
|
-
|
|
142
|
-
// If promise has not resolved before MAX_WAIT_TIME reject
|
|
143
|
-
setTimeout(() => {
|
|
144
|
-
unsubscribe();
|
|
145
|
-
return reject();
|
|
146
|
-
}, MAX_WAIT_TIME);
|
|
147
154
|
});
|
|
148
155
|
};
|
|
149
156
|
|
|
@@ -154,21 +161,23 @@ export class Storage implements PeprStore {
|
|
|
154
161
|
* @param key - The key to add into the store
|
|
155
162
|
* @returns
|
|
156
163
|
*/
|
|
157
|
-
removeItemAndWait = (key: string): Promise<
|
|
164
|
+
removeItemAndWait = (key: string): Promise<string> => {
|
|
158
165
|
this.#dispatchUpdate("remove", [v2StoreKey(key)]);
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
const record: WaitRecord = {};
|
|
167
|
+
return new Promise<string>((resolve, reject) => {
|
|
168
|
+
// If promise has not resolved before MAX_WAIT_TIME reject
|
|
169
|
+
record.timeout = setTimeout(() => {
|
|
170
|
+
record.unsubscribe!();
|
|
171
|
+
return reject(`MAX_WAIT_TIME elapsed: Key ${key} still seen after ${MAX_WAIT_TIME / 1000}s`);
|
|
172
|
+
}, MAX_WAIT_TIME);
|
|
173
|
+
|
|
174
|
+
record.unsubscribe = this.subscribe(data => {
|
|
161
175
|
if (!Object.hasOwn(data, `${v2UnescapedStoreKey(key)}`)) {
|
|
162
|
-
unsubscribe();
|
|
163
|
-
|
|
176
|
+
record.unsubscribe!();
|
|
177
|
+
clearTimeout(record.timeout);
|
|
178
|
+
resolve("ok");
|
|
164
179
|
}
|
|
165
180
|
});
|
|
166
|
-
|
|
167
|
-
// If promise has not resolved before MAX_WAIT_TIME reject
|
|
168
|
-
setTimeout(() => {
|
|
169
|
-
unsubscribe();
|
|
170
|
-
return reject();
|
|
171
|
-
}, MAX_WAIT_TIME);
|
|
172
181
|
});
|
|
173
182
|
};
|
|
174
183
|
|
package/src/lib/utils.ts
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
import Log from "./telemetry/logger";
|
|
5
5
|
|
|
6
6
|
/** Test if a string is ascii or not */
|
|
7
|
-
export const isAscii = /^[\s\x20-\x7E]*$/;
|
|
7
|
+
export const isAscii: RegExp = /^[\s\x20-\x7E]*$/;
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Encode all ascii values in a map to base64
|
|
11
11
|
* @param obj The object to encode
|
|
12
12
|
* @param skip A list of keys to skip encoding
|
|
13
13
|
*/
|
|
14
|
-
export function convertToBase64Map(obj: { data?: Record<string, string> }, skip: string[]) {
|
|
14
|
+
export function convertToBase64Map(obj: { data?: Record<string, string> }, skip: string[]): void {
|
|
15
15
|
obj.data = obj.data ?? {};
|
|
16
16
|
for (const key in obj.data) {
|
|
17
17
|
const value = obj.data[key];
|
|
@@ -25,7 +25,7 @@ export function convertToBase64Map(obj: { data?: Record<string, string> }, skip:
|
|
|
25
25
|
* @param obj The object to decode
|
|
26
26
|
* @returns A list of keys that were skipped
|
|
27
27
|
*/
|
|
28
|
-
export function convertFromBase64Map(obj: { data?: Record<string, string> }) {
|
|
28
|
+
export function convertFromBase64Map(obj: { data?: Record<string, string> }): string[] {
|
|
29
29
|
const skip: string[] = [];
|
|
30
30
|
|
|
31
31
|
obj.data = obj.data ?? {};
|
|
@@ -47,11 +47,11 @@ export function convertFromBase64Map(obj: { data?: Record<string, string> }) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Decode a base64 string */
|
|
50
|
-
export function base64Decode(data: string) {
|
|
50
|
+
export function base64Decode(data: string): string {
|
|
51
51
|
return Buffer.from(data, "base64").toString("utf-8");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/** Encode a string to base64 */
|
|
55
|
-
export function base64Encode(data: string) {
|
|
55
|
+
export function base64Encode(data: string): string {
|
|
56
56
|
return Buffer.from(data).toString("base64");
|
|
57
57
|
}
|
|
@@ -1,17 +1,56 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
import { kind } from "kubernetes-fluent-client";
|
|
5
|
-
|
|
4
|
+
import { kind, KubernetesObject } from "kubernetes-fluent-client";
|
|
6
5
|
import { Capability } from "./capability";
|
|
7
6
|
import { shouldSkipRequest } from "./filter/filter";
|
|
8
7
|
import { ValidateResponse } from "./k8s";
|
|
9
|
-
import { AdmissionRequest } from "./types";
|
|
8
|
+
import { AdmissionRequest, Binding } from "./types";
|
|
10
9
|
import Log from "./telemetry/logger";
|
|
11
10
|
import { convertFromBase64Map } from "./utils";
|
|
12
11
|
import { PeprValidateRequest } from "./validate-request";
|
|
13
12
|
import { ModuleConfig } from "./module";
|
|
14
13
|
|
|
14
|
+
export async function processRequest(
|
|
15
|
+
binding: Binding,
|
|
16
|
+
actionMetadata: Record<string, string>,
|
|
17
|
+
peprValidateRequest: PeprValidateRequest<KubernetesObject>,
|
|
18
|
+
): Promise<ValidateResponse> {
|
|
19
|
+
const label = binding.validateCallback!.name;
|
|
20
|
+
Log.info(actionMetadata, `Processing validation action (${label})`);
|
|
21
|
+
|
|
22
|
+
const valResp: ValidateResponse = {
|
|
23
|
+
uid: peprValidateRequest.Request.uid,
|
|
24
|
+
allowed: true, // Assume it's allowed until a validation check fails
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Run the validation callback, if it fails set allowed to false
|
|
29
|
+
const callbackResp = await binding.validateCallback!(peprValidateRequest);
|
|
30
|
+
valResp.allowed = callbackResp.allowed;
|
|
31
|
+
|
|
32
|
+
// If the validation callback returned a status code or message, set it in the Response
|
|
33
|
+
if (callbackResp.statusCode || callbackResp.statusMessage) {
|
|
34
|
+
valResp.status = {
|
|
35
|
+
code: callbackResp.statusCode || 400,
|
|
36
|
+
message: callbackResp.statusMessage || `Validation failed for ${name}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Log.info(actionMetadata, `Validation action complete (${label}): ${callbackResp.allowed ? "allowed" : "denied"}`);
|
|
41
|
+
return valResp;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
// If any validation throws an error, note the failure in the Response
|
|
44
|
+
Log.error(actionMetadata, `Action failed: ${JSON.stringify(e)}`);
|
|
45
|
+
valResp.allowed = false;
|
|
46
|
+
valResp.status = {
|
|
47
|
+
code: 500,
|
|
48
|
+
message: `Action failed with error: ${JSON.stringify(e)}`,
|
|
49
|
+
};
|
|
50
|
+
return valResp;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
15
54
|
export async function validateProcessor(
|
|
16
55
|
config: ModuleConfig,
|
|
17
56
|
capabilities: Capability[],
|
|
@@ -32,52 +71,21 @@ export async function validateProcessor(
|
|
|
32
71
|
for (const { name, bindings, namespaces } of capabilities) {
|
|
33
72
|
const actionMetadata = { ...reqMetadata, name };
|
|
34
73
|
|
|
35
|
-
for (const
|
|
74
|
+
for (const binding of bindings) {
|
|
36
75
|
// Skip this action if it's not a validation action
|
|
37
|
-
if (!
|
|
76
|
+
if (!binding.validateCallback) {
|
|
38
77
|
continue;
|
|
39
78
|
}
|
|
40
79
|
|
|
41
|
-
const localResponse: ValidateResponse = {
|
|
42
|
-
uid: req.uid,
|
|
43
|
-
allowed: true, // Assume it's allowed until a validation check fails
|
|
44
|
-
};
|
|
45
|
-
|
|
46
80
|
// Continue to the next action without doing anything if this one should be skipped
|
|
47
|
-
const shouldSkip = shouldSkipRequest(
|
|
81
|
+
const shouldSkip = shouldSkipRequest(binding, req, namespaces, config?.alwaysIgnore?.namespaces);
|
|
48
82
|
if (shouldSkip !== "") {
|
|
49
83
|
Log.debug(shouldSkip);
|
|
50
84
|
continue;
|
|
51
85
|
}
|
|
52
86
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
// Run the validation callback, if it fails set allowed to false
|
|
58
|
-
const resp = await action.validateCallback(wrapped);
|
|
59
|
-
localResponse.allowed = resp.allowed;
|
|
60
|
-
|
|
61
|
-
// If the validation callback returned a status code or message, set it in the Response
|
|
62
|
-
if (resp.statusCode || resp.statusMessage) {
|
|
63
|
-
localResponse.status = {
|
|
64
|
-
code: resp.statusCode || 400,
|
|
65
|
-
message: resp.statusMessage || `Validation failed for ${name}`,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
Log.info(actionMetadata, `Validation action complete (${label}): ${resp.allowed ? "allowed" : "denied"}`);
|
|
70
|
-
} catch (e) {
|
|
71
|
-
// If any validation throws an error, note the failure in the Response
|
|
72
|
-
Log.error(actionMetadata, `Action failed: ${JSON.stringify(e)}`);
|
|
73
|
-
localResponse.allowed = false;
|
|
74
|
-
localResponse.status = {
|
|
75
|
-
code: 500,
|
|
76
|
-
message: `Action failed with error: ${JSON.stringify(e)}`,
|
|
77
|
-
};
|
|
78
|
-
return [localResponse];
|
|
79
|
-
}
|
|
80
|
-
response.push(localResponse);
|
|
87
|
+
const resp = await processRequest(binding, actionMetadata, wrapped);
|
|
88
|
+
response.push(resp);
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -20,7 +20,7 @@ const queues: Record<string, Queue<KubernetesObject>> = {};
|
|
|
20
20
|
* @param obj The object to derive a key from
|
|
21
21
|
* @returns The key to a Queue in the list of queues
|
|
22
22
|
*/
|
|
23
|
-
export function queueKey(obj: KubernetesObject) {
|
|
23
|
+
export function queueKey(obj: KubernetesObject): string {
|
|
24
24
|
const options = ["kind", "kindNs", "kindNsName", "global"];
|
|
25
25
|
const d3fault = "kind";
|
|
26
26
|
|
|
@@ -40,7 +40,7 @@ export function queueKey(obj: KubernetesObject) {
|
|
|
40
40
|
return lookup[strat];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function getOrCreateQueue(obj: KubernetesObject) {
|
|
43
|
+
export function getOrCreateQueue(obj: KubernetesObject): Queue<KubernetesObject> {
|
|
44
44
|
const key = queueKey(obj);
|
|
45
45
|
if (!queues[key]) {
|
|
46
46
|
queues[key] = new Queue<KubernetesObject>(key);
|
|
@@ -74,7 +74,7 @@ const eventToPhaseMap = {
|
|
|
74
74
|
*
|
|
75
75
|
* @param capabilities The capabilities to load watches for
|
|
76
76
|
*/
|
|
77
|
-
export function setupWatch(capabilities: Capability[], ignoredNamespaces?: string[]) {
|
|
77
|
+
export function setupWatch(capabilities: Capability[], ignoredNamespaces?: string[]): void {
|
|
78
78
|
capabilities.map(capability =>
|
|
79
79
|
capability.bindings
|
|
80
80
|
.filter(binding => binding.isWatch)
|
|
@@ -88,14 +88,18 @@ export function setupWatch(capabilities: Capability[], ignoredNamespaces?: strin
|
|
|
88
88
|
* @param binding the binding to watch
|
|
89
89
|
* @param capabilityNamespaces list of namespaces to filter on
|
|
90
90
|
*/
|
|
91
|
-
async function runBinding(
|
|
91
|
+
async function runBinding(
|
|
92
|
+
binding: Binding,
|
|
93
|
+
capabilityNamespaces: string[],
|
|
94
|
+
ignoredNamespaces?: string[],
|
|
95
|
+
): Promise<void> {
|
|
92
96
|
// Get the phases to match, fallback to any
|
|
93
97
|
const phaseMatch: WatchPhase[] = eventToPhaseMap[binding.event] || eventToPhaseMap[Event.ANY];
|
|
94
98
|
|
|
95
99
|
// The watch callback is run when an object is received or dequeued
|
|
96
100
|
Log.debug({ watchCfg }, "Effective WatchConfig");
|
|
97
101
|
|
|
98
|
-
const watchCallback = async (kubernetesObject: KubernetesObject, phase: WatchPhase) => {
|
|
102
|
+
const watchCallback = async (kubernetesObject: KubernetesObject, phase: WatchPhase): Promise<void> => {
|
|
99
103
|
// First, filter the object based on the phase
|
|
100
104
|
if (phaseMatch.includes(phase)) {
|
|
101
105
|
try {
|
|
@@ -117,7 +121,7 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[], igno
|
|
|
117
121
|
}
|
|
118
122
|
};
|
|
119
123
|
|
|
120
|
-
const handleFinalizerRemoval = async (kubernetesObject: KubernetesObject) => {
|
|
124
|
+
const handleFinalizerRemoval = async (kubernetesObject: KubernetesObject): Promise<void> => {
|
|
121
125
|
if (!kubernetesObject.metadata?.deletionTimestamp) {
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
@@ -191,7 +195,7 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[], igno
|
|
|
191
195
|
}
|
|
192
196
|
}
|
|
193
197
|
|
|
194
|
-
export function logEvent(event: WatchEvent, message: string = "", obj?: KubernetesObject) {
|
|
198
|
+
export function logEvent(event: WatchEvent, message: string = "", obj?: KubernetesObject): void {
|
|
195
199
|
const logMessage = `Watch event ${event} received${message ? `. ${message}.` : "."}`;
|
|
196
200
|
if (obj) {
|
|
197
201
|
Log.debug(obj, logMessage);
|
package/src/sdk/cosign.ts
CHANGED
|
@@ -212,15 +212,15 @@ export async function verifyImage(
|
|
|
212
212
|
url: `https://${X.iref.host}/v2/${X.iref.name}/manifests/${X.iref.tag}`,
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
-
const supportsMediaType = async (url: string, mediaType: string) => {
|
|
215
|
+
const supportsMediaType = async (url: string, mediaType: string): Promise<boolean> => {
|
|
216
216
|
return (await head(url, mediaType, { ca: tlsCrts }))["content-type"] === mediaType;
|
|
217
217
|
};
|
|
218
218
|
|
|
219
|
-
const canOciV1Manifest = async (manifestUrl: string) => {
|
|
219
|
+
const canOciV1Manifest = async (manifestUrl: string): Promise<boolean> => {
|
|
220
220
|
return supportsMediaType(manifestUrl, MediaTypeOciV1.Manifest);
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
-
const canDockerV2Manifest = async (manifestUrl: string) => {
|
|
223
|
+
const canDockerV2Manifest = async (manifestUrl: string): Promise<boolean> => {
|
|
224
224
|
return supportsMediaType(manifestUrl, MediaTypeDockerV2.Manifest);
|
|
225
225
|
};
|
|
226
226
|
|
|
@@ -228,7 +228,7 @@ export async function verifyImage(
|
|
|
228
228
|
const manifestResp =
|
|
229
229
|
await canOciV1Manifest(X.manifest.url) ? await get(X.manifest.url, MediaTypeOciV1.Manifest, {ca: tlsCrts}) :
|
|
230
230
|
await canDockerV2Manifest(X.manifest.url) ? await get(X.manifest.url, MediaTypeDockerV2.Manifest, {ca: tlsCrts}) :
|
|
231
|
-
(() => { throw "Can't pull image manifest with supported MediaType." })();
|
|
231
|
+
(():never => { throw "Can't pull image manifest with supported MediaType." })();
|
|
232
232
|
X.manifest.content = manifestResp.body;
|
|
233
233
|
|
|
234
234
|
X.manifest.digest = `sha256:${crypto
|