pepr 0.40.1 → 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/README.md +11 -5
- 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/deploy.d.ts.map +1 -1
- package/dist/cli/init/templates.d.ts +2 -2
- package/dist/cli/monitor.d.ts +23 -0
- package/dist/cli/monitor.d.ts.map +1 -1
- package/dist/cli.js +536 -429
- package/dist/controller.js +52 -27
- 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/index.util.d.ts +10 -0
- package/dist/lib/controller/index.util.d.ts.map +1 -0
- 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/deploymentChecks.d.ts +3 -0
- package/dist/lib/deploymentChecks.d.ts.map +1 -0
- package/dist/lib/enums.d.ts +5 -5
- package/dist/lib/enums.d.ts.map +1 -1
- package/dist/lib/filesystemService.d.ts +2 -0
- package/dist/lib/filesystemService.d.ts.map +1 -0
- package/dist/lib/filter/adjudicators/adjudicators.d.ts +73 -0
- package/dist/lib/filter/adjudicators/adjudicators.d.ts.map +1 -0
- package/dist/lib/filter/adjudicators/defaultTestObjects.d.ts +7 -0
- package/dist/lib/filter/adjudicators/defaultTestObjects.d.ts.map +1 -0
- package/dist/lib/helpers.d.ts +1 -4
- package/dist/lib/helpers.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/schedule.d.ts.map +1 -1
- package/dist/lib/storage.d.ts +5 -5
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/{logger.d.ts → telemetry/logger.d.ts} +1 -1
- package/dist/lib/telemetry/logger.d.ts.map +1 -0
- package/dist/lib/{metrics.d.ts → telemetry/metrics.d.ts} +3 -1
- package/dist/lib/telemetry/metrics.d.ts.map +1 -0
- package/dist/lib/types.d.ts +10 -9
- package/dist/lib/types.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.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +283 -231
- package/dist/lib.js.map +4 -4
- package/dist/sdk/sdk.d.ts +3 -4
- package/dist/sdk/sdk.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/cli/build.helpers.ts +180 -0
- package/src/cli/build.ts +85 -132
- package/src/cli/deploy.ts +2 -1
- package/src/cli/init/templates.ts +1 -1
- package/src/cli/monitor.ts +108 -65
- package/src/lib/assets/deploy.ts +7 -7
- package/src/lib/assets/destroy.ts +2 -2
- package/src/lib/assets/helm.ts +6 -6
- package/src/lib/assets/index.ts +110 -89
- package/src/lib/assets/pods.ts +10 -5
- package/src/lib/assets/webhooks.ts +3 -3
- package/src/lib/assets/yaml.ts +12 -9
- package/src/lib/capability.ts +29 -19
- package/src/lib/controller/index.ts +41 -69
- package/src/lib/controller/index.util.ts +47 -0
- package/src/lib/controller/store.ts +24 -11
- package/src/lib/controller/storeCache.ts +11 -2
- package/src/lib/deploymentChecks.ts +43 -0
- package/src/lib/enums.ts +5 -5
- package/src/lib/filesystemService.ts +16 -0
- package/src/lib/filter/{adjudicators.ts → adjudicators/adjudicators.ts} +67 -35
- package/src/lib/filter/adjudicators/defaultTestObjects.ts +46 -0
- package/src/lib/filter/filter.ts +1 -1
- package/src/lib/finalizer.ts +1 -1
- package/src/lib/helpers.ts +31 -88
- package/src/lib/mutate-processor.ts +1 -1
- package/src/lib/mutate-request.ts +11 -11
- package/src/lib/queue.ts +13 -5
- package/src/lib/schedule.ts +8 -8
- package/src/lib/storage.ts +48 -39
- package/src/lib/{logger.ts → telemetry/logger.ts} +1 -1
- package/src/lib/{metrics.ts → telemetry/metrics.ts} +18 -17
- package/src/lib/types.ts +12 -9
- package/src/lib/utils.ts +6 -6
- package/src/lib/validate-processor.ts +48 -40
- package/src/lib/watch-processor.ts +19 -15
- package/src/lib.ts +1 -1
- package/src/runtime/controller.ts +1 -1
- package/src/sdk/cosign.ts +4 -4
- package/src/sdk/sdk.ts +6 -9
- package/src/templates/capabilities/hello-pepr.ts +19 -9
- package/dist/lib/filter/adjudicators.d.ts +0 -69
- package/dist/lib/filter/adjudicators.d.ts.map +0 -1
- package/dist/lib/logger.d.ts.map +0 -1
- package/dist/lib/metrics.d.ts.map +0 -1
package/src/lib/assets/yaml.ts
CHANGED
|
@@ -6,13 +6,16 @@ import crypto from "crypto";
|
|
|
6
6
|
import { promises as fs } from "fs";
|
|
7
7
|
import { Assets } from ".";
|
|
8
8
|
import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking";
|
|
9
|
-
import {
|
|
9
|
+
import { getDeployment, getModuleSecret, getNamespace, getWatcher } from "./pods";
|
|
10
10
|
import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
|
|
11
11
|
import { webhookConfig } from "./webhooks";
|
|
12
12
|
import { genEnv } from "./pods";
|
|
13
13
|
|
|
14
14
|
// Helm Chart overrides file (values.yaml) generated from assets
|
|
15
|
-
export async function overridesFile(
|
|
15
|
+
export async function overridesFile(
|
|
16
|
+
{ hash, name, image, config, apiToken, capabilities }: Assets,
|
|
17
|
+
path: string,
|
|
18
|
+
): Promise<void> {
|
|
16
19
|
const rbacOverrides = clusterRole(name, capabilities, config.rbacMode, config.rbac).rules;
|
|
17
20
|
|
|
18
21
|
const overrides = {
|
|
@@ -166,7 +169,7 @@ export async function overridesFile({ hash, name, image, config, apiToken, capab
|
|
|
166
169
|
|
|
167
170
|
await fs.writeFile(path, dumpYaml(overrides, { noRefs: true, forceQuotes: true }));
|
|
168
171
|
}
|
|
169
|
-
export function zarfYaml({ name, image, config }: Assets, path: string) {
|
|
172
|
+
export function zarfYaml({ name, image, config }: Assets, path: string): string {
|
|
170
173
|
const zarfCfg = {
|
|
171
174
|
kind: "ZarfPackageConfig",
|
|
172
175
|
metadata: {
|
|
@@ -194,7 +197,7 @@ export function zarfYaml({ name, image, config }: Assets, path: string) {
|
|
|
194
197
|
return dumpYaml(zarfCfg, { noRefs: true });
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
export function zarfYamlChart({ name, image, config }: Assets, path: string) {
|
|
200
|
+
export function zarfYamlChart({ name, image, config }: Assets, path: string): string {
|
|
198
201
|
const zarfCfg = {
|
|
199
202
|
kind: "ZarfPackageConfig",
|
|
200
203
|
metadata: {
|
|
@@ -223,7 +226,7 @@ export function zarfYamlChart({ name, image, config }: Assets, path: string) {
|
|
|
223
226
|
return dumpYaml(zarfCfg, { noRefs: true });
|
|
224
227
|
}
|
|
225
228
|
|
|
226
|
-
export async function allYaml(assets: Assets, imagePullSecret?: string) {
|
|
229
|
+
export async function allYaml(assets: Assets, imagePullSecret?: string): Promise<string> {
|
|
227
230
|
const { name, tls, apiToken, path, config } = assets;
|
|
228
231
|
const code = await fs.readFile(path);
|
|
229
232
|
|
|
@@ -232,19 +235,19 @@ export async function allYaml(assets: Assets, imagePullSecret?: string) {
|
|
|
232
235
|
|
|
233
236
|
const mutateWebhook = await webhookConfig(assets, "mutate", assets.config.webhookTimeout);
|
|
234
237
|
const validateWebhook = await webhookConfig(assets, "validate", assets.config.webhookTimeout);
|
|
235
|
-
const watchDeployment =
|
|
238
|
+
const watchDeployment = getWatcher(assets, assets.hash, assets.buildTimestamp, imagePullSecret);
|
|
236
239
|
|
|
237
240
|
const resources = [
|
|
238
|
-
|
|
241
|
+
getNamespace(assets.config.customLabels?.namespace),
|
|
239
242
|
clusterRole(name, assets.capabilities, config.rbacMode, config.rbac),
|
|
240
243
|
clusterRoleBinding(name),
|
|
241
244
|
serviceAccount(name),
|
|
242
245
|
apiTokenSecret(name, apiToken),
|
|
243
246
|
tlsSecret(name, tls),
|
|
244
|
-
|
|
247
|
+
getDeployment(assets, assets.hash, assets.buildTimestamp, imagePullSecret),
|
|
245
248
|
service(name),
|
|
246
249
|
watcherService(name),
|
|
247
|
-
|
|
250
|
+
getModuleSecret(name, code, assets.hash),
|
|
248
251
|
storeRole(name),
|
|
249
252
|
storeRoleBinding(name),
|
|
250
253
|
];
|
package/src/lib/capability.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
/* eslint-disable max-statements */
|
|
2
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
4
3
|
|
|
5
4
|
import { GenericClass, GroupVersionKind, modelToGroupVersionKind } from "kubernetes-fluent-client";
|
|
6
5
|
import { pickBy } from "ramda";
|
|
7
|
-
import Log from "./logger";
|
|
6
|
+
import Log from "./telemetry/logger";
|
|
8
7
|
import { isBuildMode, isDevMode, isWatchMode } from "./module";
|
|
9
8
|
import { PeprStore, Storage } from "./storage";
|
|
10
9
|
import { OnSchedule, Schedule } from "./schedule";
|
|
@@ -72,7 +71,7 @@ export class Capability implements CapabilityExport {
|
|
|
72
71
|
}
|
|
73
72
|
};
|
|
74
73
|
|
|
75
|
-
public getScheduleStore() {
|
|
74
|
+
public getScheduleStore(): Storage {
|
|
76
75
|
return this.#scheduleStore;
|
|
77
76
|
}
|
|
78
77
|
|
|
@@ -112,19 +111,19 @@ export class Capability implements CapabilityExport {
|
|
|
112
111
|
onReady: this.#scheduleStore.onReady,
|
|
113
112
|
};
|
|
114
113
|
|
|
115
|
-
get bindings() {
|
|
114
|
+
get bindings(): Binding[] {
|
|
116
115
|
return this.#bindings;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
get name() {
|
|
118
|
+
get name(): string {
|
|
120
119
|
return this.#name;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
|
-
get description() {
|
|
122
|
+
get description(): string {
|
|
124
123
|
return this.#description;
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
get namespaces() {
|
|
126
|
+
get namespaces(): string[] {
|
|
128
127
|
return this.#namespaces || [];
|
|
129
128
|
}
|
|
130
129
|
|
|
@@ -193,7 +192,7 @@ export class Capability implements CapabilityExport {
|
|
|
193
192
|
model,
|
|
194
193
|
// If the kind is not specified, use the matched kind from the model
|
|
195
194
|
kind: kind || matchedKind,
|
|
196
|
-
event: Event.
|
|
195
|
+
event: Event.ANY,
|
|
197
196
|
filters: {
|
|
198
197
|
name: "",
|
|
199
198
|
namespaces: [],
|
|
@@ -208,8 +207,19 @@ export class Capability implements CapabilityExport {
|
|
|
208
207
|
const bindings = this.#bindings;
|
|
209
208
|
const prefix = `${this.#name}: ${model.name}`;
|
|
210
209
|
const commonChain = { WithLabel, WithAnnotation, WithDeletionTimestamp, Mutate, Validate, Watch, Reconcile, Alias };
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
|
|
211
|
+
type CommonChainType = typeof commonChain;
|
|
212
|
+
type ExtendedCommonChainType = CommonChainType & {
|
|
213
|
+
Alias: (alias: string) => CommonChainType;
|
|
214
|
+
InNamespace: (...namespaces: string[]) => BindingWithName<T>;
|
|
215
|
+
InNamespaceRegex: (...namespaces: RegExp[]) => BindingWithName<T>;
|
|
216
|
+
WithName: (name: string) => BindingFilter<T>;
|
|
217
|
+
WithNameRegex: (regexName: RegExp) => BindingFilter<T>;
|
|
218
|
+
WithDeletionTimestamp: () => BindingFilter<T>;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const isNotEmpty = (value: object): boolean => Object.keys(value).length > 0;
|
|
222
|
+
const log = (message: string, cbString: string): void => {
|
|
213
223
|
const filteredObj = pickBy(isNotEmpty, binding.filters);
|
|
214
224
|
|
|
215
225
|
Log.info(`${message} configured for ${binding.event}`, prefix);
|
|
@@ -317,7 +327,7 @@ export class Capability implements CapabilityExport {
|
|
|
317
327
|
...binding,
|
|
318
328
|
isMutate: true,
|
|
319
329
|
isFinalize: true,
|
|
320
|
-
event: Event.
|
|
330
|
+
event: Event.ANY,
|
|
321
331
|
mutateCallback: addFinalizer,
|
|
322
332
|
};
|
|
323
333
|
bindings.push(mutateBinding);
|
|
@@ -329,8 +339,8 @@ export class Capability implements CapabilityExport {
|
|
|
329
339
|
...binding,
|
|
330
340
|
isWatch: true,
|
|
331
341
|
isFinalize: true,
|
|
332
|
-
event: Event.
|
|
333
|
-
finalizeCallback: async (update: InstanceType<T>, logger = aliasLogger) => {
|
|
342
|
+
event: Event.UPDATE,
|
|
343
|
+
finalizeCallback: async (update: InstanceType<T>, logger = aliasLogger): Promise<boolean | void> => {
|
|
334
344
|
Log.info(`Executing finalize action with alias: ${binding.alias || "no alias provided"}`);
|
|
335
345
|
return await finalizeCallback(update, logger);
|
|
336
346
|
},
|
|
@@ -381,13 +391,13 @@ export class Capability implements CapabilityExport {
|
|
|
381
391
|
return commonChain;
|
|
382
392
|
}
|
|
383
393
|
|
|
384
|
-
function Alias(alias: string) {
|
|
394
|
+
function Alias(alias: string): CommonChainType {
|
|
385
395
|
Log.debug(`Adding prefix alias ${alias}`, prefix);
|
|
386
396
|
binding.alias = alias;
|
|
387
397
|
return commonChain;
|
|
388
398
|
}
|
|
389
399
|
|
|
390
|
-
function bindEvent(event: Event) {
|
|
400
|
+
function bindEvent(event: Event): ExtendedCommonChainType {
|
|
391
401
|
binding.event = event;
|
|
392
402
|
return {
|
|
393
403
|
...commonChain,
|
|
@@ -401,10 +411,10 @@ export class Capability implements CapabilityExport {
|
|
|
401
411
|
}
|
|
402
412
|
|
|
403
413
|
return {
|
|
404
|
-
IsCreatedOrUpdated: () => bindEvent(Event.
|
|
405
|
-
IsCreated: () => bindEvent(Event.
|
|
406
|
-
IsUpdated: () => bindEvent(Event.
|
|
407
|
-
IsDeleted: () => bindEvent(Event.
|
|
414
|
+
IsCreatedOrUpdated: () => bindEvent(Event.CREATE_OR_UPDATE),
|
|
415
|
+
IsCreated: () => bindEvent(Event.CREATE),
|
|
416
|
+
IsUpdated: () => bindEvent(Event.UPDATE),
|
|
417
|
+
IsDeleted: () => bindEvent(Event.DELETE),
|
|
408
418
|
};
|
|
409
419
|
};
|
|
410
420
|
}
|
|
@@ -7,17 +7,19 @@ import https from "https";
|
|
|
7
7
|
|
|
8
8
|
import { Capability } from "../capability";
|
|
9
9
|
import { MutateResponse, ValidateResponse } from "../k8s";
|
|
10
|
-
import Log from "../logger";
|
|
11
|
-
import { metricsCollector, MetricsCollector } from "../metrics";
|
|
10
|
+
import Log from "../telemetry/logger";
|
|
11
|
+
import { metricsCollector, MetricsCollector } from "../telemetry/metrics";
|
|
12
12
|
import { ModuleConfig, isWatchMode } from "../module";
|
|
13
13
|
import { mutateProcessor } from "../mutate-processor";
|
|
14
14
|
import { validateProcessor } from "../validate-processor";
|
|
15
15
|
import { StoreController } from "./store";
|
|
16
|
-
import {
|
|
16
|
+
import { AdmissionRequest } from "../types";
|
|
17
|
+
import { karForMutate, karForValidate, KubeAdmissionReview } from "./index.util";
|
|
17
18
|
|
|
18
19
|
if (!process.env.PEPR_NODE_WARNINGS) {
|
|
19
20
|
process.removeAllListeners("warning");
|
|
20
21
|
}
|
|
22
|
+
|
|
21
23
|
export class Controller {
|
|
22
24
|
// Track whether the server is running
|
|
23
25
|
#running = false;
|
|
@@ -76,7 +78,7 @@ export class Controller {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
/** Start the webhook server */
|
|
79
|
-
startServer = (port: number) => {
|
|
81
|
+
startServer = (port: number): void => {
|
|
80
82
|
if (this.#running) {
|
|
81
83
|
throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
|
|
82
84
|
}
|
|
@@ -131,7 +133,7 @@ export class Controller {
|
|
|
131
133
|
});
|
|
132
134
|
};
|
|
133
135
|
|
|
134
|
-
#bindEndpoints = () => {
|
|
136
|
+
#bindEndpoints = (): void => {
|
|
135
137
|
// Health check endpoint
|
|
136
138
|
this.#app.get("/healthz", Controller.#healthz);
|
|
137
139
|
|
|
@@ -160,7 +162,7 @@ export class Controller {
|
|
|
160
162
|
* @param next The next middleware function
|
|
161
163
|
* @returns
|
|
162
164
|
*/
|
|
163
|
-
#validateToken = (req: express.Request, res: express.Response, next: NextFunction) => {
|
|
165
|
+
#validateToken = (req: express.Request, res: express.Response, next: NextFunction): void => {
|
|
164
166
|
// Validate the token
|
|
165
167
|
const { token } = req.params;
|
|
166
168
|
if (token !== this.#token) {
|
|
@@ -181,8 +183,10 @@ export class Controller {
|
|
|
181
183
|
* @param req the incoming request
|
|
182
184
|
* @param res the outgoing response
|
|
183
185
|
*/
|
|
184
|
-
#metrics = async (req: express.Request, res: express.Response) => {
|
|
186
|
+
#metrics = async (req: express.Request, res: express.Response): Promise<void> => {
|
|
185
187
|
try {
|
|
188
|
+
// https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#basic-info
|
|
189
|
+
res.set("Content-Type", "text/plain; version=0.0.4");
|
|
186
190
|
res.send(await this.#metricsCollector.getMetrics());
|
|
187
191
|
} catch (err) {
|
|
188
192
|
Log.error(err, `Error getting metrics`);
|
|
@@ -196,7 +200,9 @@ export class Controller {
|
|
|
196
200
|
* @param admissionKind the type of admission request
|
|
197
201
|
* @returns the request handler
|
|
198
202
|
*/
|
|
199
|
-
#admissionReq = (
|
|
203
|
+
#admissionReq = (
|
|
204
|
+
admissionKind: "Mutate" | "Validate",
|
|
205
|
+
): ((req: express.Request, res: express.Response) => Promise<void>) => {
|
|
200
206
|
// Create the admission request handler
|
|
201
207
|
return async (req: express.Request, res: express.Response) => {
|
|
202
208
|
// Start the metrics timer
|
|
@@ -206,77 +212,38 @@ export class Controller {
|
|
|
206
212
|
// Get the request from the body or create an empty request
|
|
207
213
|
const request: AdmissionRequest = req.body?.request || ({} as AdmissionRequest);
|
|
208
214
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const name = request?.name ? `/${request.name}` : "";
|
|
214
|
-
const namespace = request?.namespace || "";
|
|
215
|
-
const gvk = request?.kind || { group: "", version: "", kind: "" };
|
|
216
|
-
|
|
217
|
-
const reqMetadata = {
|
|
218
|
-
uid: request.uid,
|
|
219
|
-
namespace,
|
|
220
|
-
name,
|
|
215
|
+
const { name, namespace, gvk } = {
|
|
216
|
+
name: request?.name ? `/${request.name}` : "",
|
|
217
|
+
namespace: request?.namespace || "",
|
|
218
|
+
gvk: request?.kind || { group: "", version: "", kind: "" },
|
|
221
219
|
};
|
|
222
220
|
|
|
221
|
+
const reqMetadata = { uid: request.uid, namespace, name };
|
|
223
222
|
Log.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
|
|
224
223
|
Log.debug({ ...reqMetadata, request }, "Incoming request body");
|
|
225
224
|
|
|
226
|
-
//
|
|
227
|
-
|
|
225
|
+
// Run the before hook if it exists
|
|
226
|
+
this.#beforeHook && this.#beforeHook(request || {});
|
|
228
227
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
228
|
+
// Process the request
|
|
229
|
+
const response: MutateResponse | ValidateResponse[] =
|
|
230
|
+
admissionKind === "Mutate"
|
|
231
|
+
? await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata)
|
|
232
|
+
: await validateProcessor(this.#config, this.#capabilities, request, reqMetadata);
|
|
235
233
|
|
|
236
234
|
// Run the after hook if it exists
|
|
237
|
-
|
|
238
|
-
responseList.map(res => {
|
|
235
|
+
[response].flat().map(res => {
|
|
239
236
|
this.#afterHook && this.#afterHook(res);
|
|
240
|
-
// Log the response
|
|
241
237
|
Log.info({ ...reqMetadata, res }, "Check response");
|
|
242
238
|
});
|
|
243
239
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
kind: "AdmissionReview",
|
|
252
|
-
response: kubeAdmissionResponse,
|
|
253
|
-
});
|
|
254
|
-
} else {
|
|
255
|
-
kubeAdmissionResponse =
|
|
256
|
-
responseList.length === 0
|
|
257
|
-
? {
|
|
258
|
-
uid: request.uid,
|
|
259
|
-
allowed: true,
|
|
260
|
-
status: { message: "no in-scope validations -- allowed!" },
|
|
261
|
-
}
|
|
262
|
-
: {
|
|
263
|
-
uid: responseList[0].uid,
|
|
264
|
-
allowed: responseList.filter(r => !r.allowed).length === 0,
|
|
265
|
-
status: {
|
|
266
|
-
message: (responseList as ValidateResponse[])
|
|
267
|
-
.filter(rl => !rl.allowed)
|
|
268
|
-
.map(curr => curr.status?.message)
|
|
269
|
-
.join("; "),
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
res.send({
|
|
273
|
-
apiVersion: "admission.k8s.io/v1",
|
|
274
|
-
kind: "AdmissionReview",
|
|
275
|
-
response: kubeAdmissionResponse,
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
Log.debug({ ...reqMetadata, kubeAdmissionResponse }, "Outgoing response");
|
|
240
|
+
const kar: KubeAdmissionReview =
|
|
241
|
+
admissionKind === "Mutate"
|
|
242
|
+
? karForMutate(response as MutateResponse)
|
|
243
|
+
: karForValidate(request, response as ValidateResponse[]);
|
|
244
|
+
|
|
245
|
+
Log.debug({ ...reqMetadata, kubeAdmissionResponse: kar.response }, "Outgoing response");
|
|
246
|
+
res.send(kar);
|
|
280
247
|
|
|
281
248
|
this.#metricsCollector.observeEnd(startTime, admissionKind);
|
|
282
249
|
} catch (err) {
|
|
@@ -294,10 +261,15 @@ export class Controller {
|
|
|
294
261
|
* @param res the outgoing response
|
|
295
262
|
* @param next the next middleware function
|
|
296
263
|
*/
|
|
297
|
-
static #logger(req: express.Request, res: express.Response, next: express.NextFunction) {
|
|
264
|
+
static #logger(req: express.Request, res: express.Response, next: express.NextFunction): void {
|
|
298
265
|
const startTime = Date.now();
|
|
299
266
|
|
|
300
267
|
res.on("finish", () => {
|
|
268
|
+
const excludedRoutes = ["/healthz", "/metrics"];
|
|
269
|
+
if (excludedRoutes.includes(req.originalUrl)) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
301
273
|
const elapsedTime = Date.now() - startTime;
|
|
302
274
|
const message = {
|
|
303
275
|
uid: req.body?.request?.uid,
|
|
@@ -318,7 +290,7 @@ export class Controller {
|
|
|
318
290
|
* @param req the incoming request
|
|
319
291
|
* @param res the outgoing response
|
|
320
292
|
*/
|
|
321
|
-
static #healthz(req: express.Request, res: express.Response) {
|
|
293
|
+
static #healthz(req: express.Request, res: express.Response): void {
|
|
322
294
|
try {
|
|
323
295
|
res.send("OK");
|
|
324
296
|
} catch (err) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { MutateResponse, ValidateResponse } from "../k8s";
|
|
5
|
+
import { ResponseItem, AdmissionRequest } from "../types";
|
|
6
|
+
|
|
7
|
+
export interface KubeAdmissionReview {
|
|
8
|
+
apiVersion: string;
|
|
9
|
+
kind: string;
|
|
10
|
+
response: ValidateResponse[] | MutateResponse | ResponseItem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function karForMutate(mr: MutateResponse): KubeAdmissionReview {
|
|
14
|
+
return {
|
|
15
|
+
apiVersion: "admission.k8s.io/v1",
|
|
16
|
+
kind: "AdmissionReview",
|
|
17
|
+
response: mr,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function karForValidate(ar: AdmissionRequest, vr: ValidateResponse[]): KubeAdmissionReview {
|
|
22
|
+
const isAllowed = vr.filter(r => !r.allowed).length === 0;
|
|
23
|
+
|
|
24
|
+
const resp: ValidateResponse =
|
|
25
|
+
vr.length === 0
|
|
26
|
+
? {
|
|
27
|
+
uid: ar.uid,
|
|
28
|
+
allowed: true,
|
|
29
|
+
status: { code: 200, message: "no in-scope validations -- allowed!" },
|
|
30
|
+
}
|
|
31
|
+
: {
|
|
32
|
+
uid: vr[0].uid,
|
|
33
|
+
allowed: isAllowed,
|
|
34
|
+
status: {
|
|
35
|
+
code: isAllowed ? 200 : 422,
|
|
36
|
+
message: vr
|
|
37
|
+
.filter(rl => !rl.allowed)
|
|
38
|
+
.map(curr => curr.status?.message)
|
|
39
|
+
.join("; "),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
apiVersion: "admission.k8s.io/v1",
|
|
44
|
+
kind: "AdmissionReview",
|
|
45
|
+
response: resp,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -7,12 +7,13 @@ import { startsWith } from "ramda";
|
|
|
7
7
|
|
|
8
8
|
import { Capability } from "../capability";
|
|
9
9
|
import { Store } from "../k8s";
|
|
10
|
-
import Log, { redactedPatch, redactedStore } from "../logger";
|
|
10
|
+
import Log, { redactedPatch, redactedStore } from "../telemetry/logger";
|
|
11
11
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DataOp } from "../storage";
|
|
2
|
-
import Log from "../logger";
|
|
2
|
+
import Log from "../telemetry/logger";
|
|
3
3
|
import { K8s } from "kubernetes-fluent-client";
|
|
4
4
|
import { Store } from "../k8s";
|
|
5
5
|
import { StatusCodes } from "http-status-codes";
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// check to see if all replicas are ready for all deployments in the pepr-system namespace
|
|
2
|
+
|
|
3
|
+
import { K8s, kind } from "kubernetes-fluent-client";
|
|
4
|
+
import Log from "./telemetry/logger";
|
|
5
|
+
|
|
6
|
+
// returns true if all deployments are ready, false otherwise
|
|
7
|
+
export async function checkDeploymentStatus(namespace: string) {
|
|
8
|
+
const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get();
|
|
9
|
+
let status = false;
|
|
10
|
+
let readyCount = 0;
|
|
11
|
+
|
|
12
|
+
for (const deployment of deployments.items) {
|
|
13
|
+
const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0;
|
|
14
|
+
if (deployment.status?.readyReplicas !== deployment.spec?.replicas) {
|
|
15
|
+
Log.info(
|
|
16
|
+
`Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`,
|
|
17
|
+
);
|
|
18
|
+
} else {
|
|
19
|
+
Log.info(
|
|
20
|
+
`Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`,
|
|
21
|
+
);
|
|
22
|
+
readyCount++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (readyCount === deployments.items.length) {
|
|
26
|
+
status = true;
|
|
27
|
+
}
|
|
28
|
+
return status;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// wait for all deployments in the pepr-system namespace to be ready
|
|
32
|
+
export async function namespaceDeploymentsReady(namespace: string = "pepr-system") {
|
|
33
|
+
Log.info(`Checking ${namespace} deployments status...`);
|
|
34
|
+
let ready = false;
|
|
35
|
+
while (!ready) {
|
|
36
|
+
ready = await checkDeploymentStatus(namespace);
|
|
37
|
+
if (ready) {
|
|
38
|
+
return ready;
|
|
39
|
+
}
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
41
|
+
}
|
|
42
|
+
Log.info(`All ${namespace} deployments are ready`);
|
|
43
|
+
}
|
package/src/lib/enums.ts
CHANGED
|
@@ -13,9 +13,9 @@ export enum Operation {
|
|
|
13
13
|
* The type of Kubernetes mutating webhook event that the action is registered for.
|
|
14
14
|
*/
|
|
15
15
|
export enum Event {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
CREATE = "CREATE",
|
|
17
|
+
UPDATE = "UPDATE",
|
|
18
|
+
DELETE = "DELETE",
|
|
19
|
+
CREATE_OR_UPDATE = "CREATEORUPDATE",
|
|
20
|
+
ANY = "*",
|
|
21
21
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { promises } from "fs";
|
|
5
|
+
|
|
6
|
+
export async function createDirectoryIfNotExists(path: string) {
|
|
7
|
+
try {
|
|
8
|
+
await promises.access(path);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error.code === "ENOENT") {
|
|
11
|
+
await promises.mkdir(path, { recursive: true });
|
|
12
|
+
} else {
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|