pepr 0.12.1 → 0.13.0
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/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +70 -0
- package/README.md +28 -30
- package/dist/cli.js +644 -679
- package/dist/controller.js +13 -81
- package/dist/lib/assets/deploy.d.ts +3 -0
- package/dist/lib/assets/deploy.d.ts.map +1 -0
- package/dist/lib/assets/index.d.ts +18 -0
- package/dist/lib/assets/index.d.ts.map +1 -0
- package/dist/lib/assets/loader.d.ts +14 -0
- package/dist/lib/assets/loader.d.ts.map +1 -0
- package/dist/lib/assets/networking.d.ts +6 -0
- package/dist/lib/assets/networking.d.ts.map +1 -0
- package/dist/lib/assets/pods.d.ts +8 -0
- package/dist/lib/assets/pods.d.ts.map +1 -0
- package/dist/lib/assets/rbac.d.ts +11 -0
- package/dist/lib/assets/rbac.d.ts.map +1 -0
- package/dist/lib/assets/webhooks.d.ts +6 -0
- package/dist/lib/assets/webhooks.d.ts.map +1 -0
- package/dist/lib/assets/yaml.d.ts +4 -0
- package/dist/lib/assets/yaml.d.ts.map +1 -0
- package/dist/lib/capability.d.ts +1 -3
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/controller.d.ts +45 -10
- package/dist/lib/controller.d.ts.map +1 -1
- package/dist/lib/filter.d.ts +1 -1
- package/dist/lib/filter.d.ts.map +1 -1
- package/dist/lib/k8s/index.d.ts +2 -1
- package/dist/lib/k8s/index.d.ts.map +1 -1
- package/dist/lib/k8s/kinds.d.ts.map +1 -1
- package/dist/lib/k8s/types.d.ts +13 -13
- package/dist/lib/k8s/types.d.ts.map +1 -1
- package/dist/lib/k8s/upstream.d.ts +2 -2
- package/dist/lib/k8s/upstream.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +8 -54
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/metrics.d.ts +11 -4
- package/dist/lib/metrics.d.ts.map +1 -1
- package/dist/lib/module.d.ts +2 -2
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/mutate-processor.d.ts +5 -0
- package/dist/lib/mutate-processor.d.ts.map +1 -0
- package/dist/lib/{request.d.ts → mutate-request.d.ts} +5 -5
- package/dist/lib/mutate-request.d.ts.map +1 -0
- package/dist/lib/types.d.ts +45 -46
- 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 -0
- package/dist/lib/validate-processor.d.ts.map +1 -0
- package/dist/lib/validate-request.d.ts +54 -0
- package/dist/lib/validate-request.d.ts.map +1 -0
- package/dist/lib.d.ts +3 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +510 -306
- package/dist/lib.js.map +4 -4
- package/package.json +15 -12
- package/src/cli.ts +2 -11
- package/src/lib/assets/deploy.ts +179 -0
- package/src/lib/assets/index.ts +46 -0
- package/src/lib/assets/loader.ts +49 -0
- package/src/lib/assets/networking.ts +58 -0
- package/src/lib/assets/pods.ts +148 -0
- package/src/lib/assets/rbac.ts +57 -0
- package/src/lib/assets/webhooks.ts +139 -0
- package/src/lib/assets/yaml.ts +75 -0
- package/src/lib/capability.ts +54 -44
- package/src/lib/controller.ts +171 -89
- package/src/lib/fetch.ts +1 -1
- package/src/lib/filter.ts +1 -3
- package/src/lib/k8s/index.ts +4 -1
- package/src/lib/k8s/kinds.ts +40 -0
- package/src/lib/k8s/types.ts +16 -14
- package/src/lib/k8s/upstream.ts +5 -1
- package/src/lib/logger.ts +14 -125
- package/src/lib/metrics.ts +67 -23
- package/src/lib/module.ts +13 -11
- package/src/lib/{processor.ts → mutate-processor.ts} +37 -28
- package/src/lib/{request.ts → mutate-request.ts} +4 -4
- package/src/lib/types.ts +51 -51
- package/src/lib/utils.ts +9 -7
- package/src/lib/validate-processor.ts +68 -0
- package/src/lib/validate-request.ts +94 -0
- package/src/lib.ts +4 -2
- package/src/runtime/controller.ts +1 -1
- package/dist/lib/k8s/webhook.d.ts +0 -37
- package/dist/lib/k8s/webhook.d.ts.map +0 -1
- package/dist/lib/processor.d.ts +0 -5
- package/dist/lib/processor.d.ts.map +0 -1
- package/dist/lib/request.d.ts.map +0 -1
- package/src/lib/k8s/webhook.ts +0 -643
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { dumpYaml } from "@kubernetes/client-node";
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
|
|
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";
|
|
12
|
+
import { webhookConfig } from "./webhooks";
|
|
13
|
+
|
|
14
|
+
export function zarfYaml({ name, image, config }: Assets, path: string) {
|
|
15
|
+
const zarfCfg = {
|
|
16
|
+
kind: "ZarfPackageConfig",
|
|
17
|
+
metadata: {
|
|
18
|
+
name,
|
|
19
|
+
description: `Pepr Module: ${config.description}`,
|
|
20
|
+
url: "https://github.com/defenseunicorns/pepr",
|
|
21
|
+
version: `${config.appVersion || "0.0.1"}`,
|
|
22
|
+
},
|
|
23
|
+
components: [
|
|
24
|
+
{
|
|
25
|
+
name: "module",
|
|
26
|
+
required: true,
|
|
27
|
+
manifests: [
|
|
28
|
+
{
|
|
29
|
+
name: "module",
|
|
30
|
+
namespace: "pepr-system",
|
|
31
|
+
files: [path],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
images: [image],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return dumpYaml(zarfCfg, { noRefs: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function allYaml(assets: Assets) {
|
|
43
|
+
const { name, tls, apiToken, path } = assets;
|
|
44
|
+
|
|
45
|
+
const code = await fs.readFile(path);
|
|
46
|
+
|
|
47
|
+
// Generate a hash of the code
|
|
48
|
+
const hash = crypto.createHash("sha256").update(code).digest("hex");
|
|
49
|
+
|
|
50
|
+
const mutateWebhook = await webhookConfig(assets, "mutate");
|
|
51
|
+
const validateWebhook = await webhookConfig(assets, "validate");
|
|
52
|
+
|
|
53
|
+
const resources = [
|
|
54
|
+
namespace,
|
|
55
|
+
clusterRole(name),
|
|
56
|
+
clusterRoleBinding(name),
|
|
57
|
+
serviceAccount(name),
|
|
58
|
+
apiTokenSecret(name, apiToken),
|
|
59
|
+
tlsSecret(name, tls),
|
|
60
|
+
deployment(assets, hash),
|
|
61
|
+
service(name),
|
|
62
|
+
moduleSecret(name, code, hash),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (mutateWebhook) {
|
|
66
|
+
resources.push(mutateWebhook);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (validateWebhook) {
|
|
70
|
+
resources.push(validateWebhook);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Convert the resources to a single YAML string
|
|
74
|
+
return resources.map(r => dumpYaml(r, { noRefs: true })).join("---\n");
|
|
75
|
+
}
|
package/src/lib/capability.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { pickBy } from "ramda";
|
|
5
|
+
|
|
6
|
+
import { isWatchMode, modelToGroupVersionKind } from "./k8s/index";
|
|
5
7
|
import { GroupVersionKind } from "./k8s/types";
|
|
6
|
-
import
|
|
8
|
+
import Log from "./logger";
|
|
7
9
|
import {
|
|
8
|
-
BindToAction,
|
|
9
10
|
Binding,
|
|
10
11
|
BindingFilter,
|
|
11
12
|
BindingWithName,
|
|
12
|
-
CapabilityAction,
|
|
13
13
|
CapabilityCfg,
|
|
14
|
-
DeepPartial,
|
|
15
14
|
Event,
|
|
16
15
|
GenericClass,
|
|
17
|
-
|
|
16
|
+
MutateAction,
|
|
17
|
+
MutateActionChain,
|
|
18
|
+
ValidateAction,
|
|
18
19
|
WhenSelector,
|
|
19
20
|
} from "./types";
|
|
20
21
|
|
|
@@ -26,9 +27,6 @@ export class Capability implements CapabilityCfg {
|
|
|
26
27
|
private _description: string;
|
|
27
28
|
private _namespaces?: string[] | undefined;
|
|
28
29
|
|
|
29
|
-
// Currently everything is considered a mutation
|
|
30
|
-
private _mutateOrValidate = HookPhase.mutate;
|
|
31
|
-
|
|
32
30
|
private _bindings: Binding[] = [];
|
|
33
31
|
|
|
34
32
|
get bindings(): Binding[] {
|
|
@@ -47,16 +45,13 @@ export class Capability implements CapabilityCfg {
|
|
|
47
45
|
return this._namespaces || [];
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
get mutateOrValidate() {
|
|
51
|
-
return this._mutateOrValidate;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
48
|
constructor(cfg: CapabilityCfg) {
|
|
55
49
|
this._name = cfg.name;
|
|
56
50
|
this._description = cfg.description;
|
|
57
51
|
this._namespaces = cfg.namespaces;
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
|
|
53
|
+
Log.info(`Capability ${this._name} registered`);
|
|
54
|
+
Log.debug(cfg);
|
|
60
55
|
}
|
|
61
56
|
|
|
62
57
|
/**
|
|
@@ -86,66 +81,81 @@ export class Capability implements CapabilityCfg {
|
|
|
86
81
|
labels: {},
|
|
87
82
|
annotations: {},
|
|
88
83
|
},
|
|
89
|
-
callback: () => undefined,
|
|
90
84
|
};
|
|
91
85
|
|
|
92
86
|
const prefix = `${this._name}: ${model.name}`;
|
|
93
87
|
|
|
94
|
-
|
|
88
|
+
const isNotEmpty = (value: object) => Object.keys(value).length > 0;
|
|
89
|
+
const log = (message: string, cbString: string) => {
|
|
90
|
+
const filteredObj = pickBy(isNotEmpty, binding.filters);
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// with the callback function to preserve
|
|
101
|
-
this._bindings.push({
|
|
102
|
-
...binding,
|
|
103
|
-
callback: cb,
|
|
104
|
-
});
|
|
92
|
+
Log.info(`${message} configured for ${binding.event}`, prefix);
|
|
93
|
+
Log.info(filteredObj, prefix);
|
|
94
|
+
Log.debug(cbString, prefix);
|
|
95
|
+
};
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
const Validate = (validateCallback: ValidateAction<T>): void => {
|
|
98
|
+
if (!isWatchMode) {
|
|
99
|
+
log("Validate Action", validateCallback.toString());
|
|
100
|
+
|
|
101
|
+
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
102
|
+
// with the callback function to preserve
|
|
103
|
+
this._bindings.push({
|
|
104
|
+
...binding,
|
|
105
|
+
isValidate: true,
|
|
106
|
+
validateCallback,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
108
109
|
};
|
|
109
110
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
const Mutate = (mutateCallback: MutateAction<T>): MutateActionChain<T> => {
|
|
112
|
+
if (!isWatchMode) {
|
|
113
|
+
log("Mutate Action", mutateCallback.toString());
|
|
114
|
+
|
|
115
|
+
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
116
|
+
// with the callback function to preserve
|
|
117
|
+
this._bindings.push({
|
|
118
|
+
...binding,
|
|
119
|
+
isMutate: true,
|
|
120
|
+
mutateCallback,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
113
123
|
|
|
114
|
-
|
|
124
|
+
// Now only allow adding actions to the same binding
|
|
125
|
+
return { Validate };
|
|
115
126
|
};
|
|
116
127
|
|
|
117
128
|
function InNamespace(...namespaces: string[]): BindingWithName<T> {
|
|
118
|
-
|
|
129
|
+
Log.debug(`Add namespaces filter ${namespaces}`, prefix);
|
|
119
130
|
binding.filters.namespaces.push(...namespaces);
|
|
120
|
-
return {
|
|
131
|
+
return { ...commonChain, WithName };
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
function WithName(name: string): BindingFilter<T> {
|
|
124
|
-
|
|
135
|
+
Log.debug(`Add name filter ${name}`, prefix);
|
|
125
136
|
binding.filters.name = name;
|
|
126
|
-
return
|
|
137
|
+
return commonChain;
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
function WithLabel(key: string, value = ""): BindingFilter<T> {
|
|
130
|
-
|
|
141
|
+
Log.debug(`Add label filter ${key}=${value}`, prefix);
|
|
131
142
|
binding.filters.labels[key] = value;
|
|
132
|
-
return
|
|
143
|
+
return commonChain;
|
|
133
144
|
}
|
|
134
145
|
|
|
135
146
|
const WithAnnotation = (key: string, value = ""): BindingFilter<T> => {
|
|
136
|
-
|
|
147
|
+
Log.debug(`Add annotation filter ${key}=${value}`, prefix);
|
|
137
148
|
binding.filters.annotations[key] = value;
|
|
138
|
-
return
|
|
149
|
+
return commonChain;
|
|
139
150
|
};
|
|
140
151
|
|
|
152
|
+
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate };
|
|
153
|
+
|
|
141
154
|
const bindEvent = (event: Event) => {
|
|
142
155
|
binding.event = event;
|
|
143
156
|
return {
|
|
157
|
+
...commonChain,
|
|
144
158
|
InNamespace,
|
|
145
|
-
Then,
|
|
146
|
-
ThenSet,
|
|
147
|
-
WithAnnotation,
|
|
148
|
-
WithLabel,
|
|
149
159
|
WithName,
|
|
150
160
|
};
|
|
151
161
|
};
|
package/src/lib/controller.ts
CHANGED
|
@@ -1,58 +1,75 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
import express from "express";
|
|
4
|
+
import express, { NextFunction } from "express";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import https from "https";
|
|
7
7
|
|
|
8
8
|
import { Capability } from "./capability";
|
|
9
|
-
import {
|
|
9
|
+
import { isWatchMode } from "./k8s";
|
|
10
|
+
import { MutateResponse, Request, ValidateResponse } from "./k8s/types";
|
|
10
11
|
import Log from "./logger";
|
|
11
|
-
import { processor } from "./processor";
|
|
12
|
-
import { ModuleConfig } from "./types";
|
|
13
12
|
import { MetricsCollector } from "./metrics";
|
|
13
|
+
import { mutateProcessor } from "./mutate-processor";
|
|
14
|
+
import { ModuleConfig } from "./types";
|
|
15
|
+
import { validateProcessor } from "./validate-processor";
|
|
14
16
|
|
|
15
17
|
export class Controller {
|
|
16
|
-
private readonly
|
|
17
|
-
private
|
|
18
|
+
private readonly _app = express();
|
|
19
|
+
private _running = false;
|
|
18
20
|
private metricsCollector = new MetricsCollector("pepr");
|
|
19
21
|
|
|
20
22
|
// The token used to authenticate requests
|
|
21
|
-
private
|
|
23
|
+
private _token = "";
|
|
22
24
|
|
|
23
25
|
constructor(
|
|
24
|
-
private readonly
|
|
25
|
-
private readonly
|
|
26
|
-
private readonly
|
|
27
|
-
private readonly
|
|
26
|
+
private readonly _config: ModuleConfig,
|
|
27
|
+
private readonly _capabilities: Capability[],
|
|
28
|
+
private readonly _beforeHook?: (req: Request) => void,
|
|
29
|
+
private readonly _afterHook?: (res: MutateResponse) => void,
|
|
28
30
|
) {
|
|
29
31
|
// Middleware for logging requests
|
|
30
|
-
this.
|
|
32
|
+
this._app.use(this.logger);
|
|
31
33
|
|
|
32
34
|
// Middleware for parsing JSON, limit to 2mb vs 100K for K8s compatibility
|
|
33
|
-
this.
|
|
35
|
+
this._app.use(express.json({ limit: "2mb" }));
|
|
36
|
+
|
|
37
|
+
if (_beforeHook) {
|
|
38
|
+
Log.info(`Using beforeHook: ${_beforeHook}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (_afterHook) {
|
|
42
|
+
Log.info(`Using afterHook: ${_afterHook}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Bind endpoints
|
|
46
|
+
this.bindEndpoints();
|
|
47
|
+
}
|
|
34
48
|
|
|
49
|
+
private bindEndpoints = () => {
|
|
35
50
|
// Health check endpoint
|
|
36
|
-
this.
|
|
51
|
+
this._app.get("/healthz", this.healthz);
|
|
37
52
|
|
|
38
53
|
// Metrics endpoint
|
|
39
|
-
this.
|
|
40
|
-
|
|
41
|
-
// Mutate endpoint
|
|
42
|
-
this.app.post("/mutate/:token", this.mutate);
|
|
54
|
+
this._app.get("/metrics", this.metrics);
|
|
43
55
|
|
|
44
|
-
if (
|
|
45
|
-
|
|
56
|
+
if (isWatchMode) {
|
|
57
|
+
return;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
// Require auth for webhook endpoints
|
|
61
|
+
this._app.use(["/mutate/:token", "/validate/:token"], this.validateToken);
|
|
62
|
+
|
|
63
|
+
// Mutate endpoint
|
|
64
|
+
this._app.post("/mutate/:token", this.admissionReq("Mutate"));
|
|
65
|
+
|
|
66
|
+
// Validate endpoint
|
|
67
|
+
this._app.post("/validate/:token", this.admissionReq("Validate"));
|
|
68
|
+
};
|
|
52
69
|
|
|
53
70
|
/** Start the webhook server */
|
|
54
71
|
public startServer = (port: number) => {
|
|
55
|
-
if (this.
|
|
72
|
+
if (this._running) {
|
|
56
73
|
throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -62,29 +79,32 @@ export class Controller {
|
|
|
62
79
|
cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
|
|
63
80
|
};
|
|
64
81
|
|
|
65
|
-
// Get the API token
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
// Get the API token if not in watch mode
|
|
83
|
+
if (!isWatchMode) {
|
|
84
|
+
// Get the API token from the environment variable or the mounted secret
|
|
85
|
+
this._token = process.env.PEPR_API_TOKEN || fs.readFileSync("/app/api-token/value").toString().trim();
|
|
86
|
+
Log.info(`Using API token: ${this._token}`);
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
if (!this._token) {
|
|
89
|
+
throw new Error("API token not found");
|
|
90
|
+
}
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
// Create HTTPS server
|
|
74
|
-
const server = https.createServer(options, this.
|
|
94
|
+
const server = https.createServer(options, this._app).listen(port);
|
|
75
95
|
|
|
76
96
|
// Handle server listening event
|
|
77
97
|
server.on("listening", () => {
|
|
78
|
-
|
|
98
|
+
Log.info(`Server listening on port ${port}`);
|
|
79
99
|
// Track that the server is running
|
|
80
|
-
this.
|
|
100
|
+
this._running = true;
|
|
81
101
|
});
|
|
82
102
|
|
|
83
103
|
// Handle EADDRINUSE errors
|
|
84
104
|
server.on("error", (e: { code: string }) => {
|
|
85
105
|
if (e.code === "EADDRINUSE") {
|
|
86
|
-
|
|
87
|
-
`Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"
|
|
106
|
+
Log.warn(
|
|
107
|
+
`Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`,
|
|
88
108
|
);
|
|
89
109
|
setTimeout(() => {
|
|
90
110
|
server.close();
|
|
@@ -95,92 +115,154 @@ export class Controller {
|
|
|
95
115
|
|
|
96
116
|
// Listen for the SIGTERM signal and gracefully close the server
|
|
97
117
|
process.on("SIGTERM", () => {
|
|
98
|
-
|
|
118
|
+
Log.info("Received SIGTERM, closing server");
|
|
99
119
|
server.close(() => {
|
|
100
|
-
|
|
120
|
+
Log.info("Server closed");
|
|
101
121
|
process.exit(0);
|
|
102
122
|
});
|
|
103
123
|
});
|
|
104
124
|
};
|
|
105
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Middleware for logging requests
|
|
128
|
+
*
|
|
129
|
+
* @param req the incoming request
|
|
130
|
+
* @param res the outgoing response
|
|
131
|
+
* @param next the next middleware function
|
|
132
|
+
*/
|
|
106
133
|
private logger = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
107
134
|
const startTime = Date.now();
|
|
108
135
|
|
|
109
136
|
res.on("finish", () => {
|
|
110
|
-
const now = new Date().toISOString();
|
|
111
137
|
const elapsedTime = Date.now() - startTime;
|
|
112
|
-
const message =
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
const message = {
|
|
139
|
+
uid: req.body?.request?.uid,
|
|
140
|
+
method: req.method,
|
|
141
|
+
url: req.originalUrl,
|
|
142
|
+
status: res.statusCode,
|
|
143
|
+
duration: `${elapsedTime} ms`,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
res.statusCode >= 300 ? Log.warn(message) : Log.info(message);
|
|
115
147
|
});
|
|
116
148
|
|
|
117
149
|
next();
|
|
118
150
|
};
|
|
119
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Validate the token in the request path
|
|
154
|
+
*
|
|
155
|
+
* @param req The incoming request
|
|
156
|
+
* @param res The outgoing response
|
|
157
|
+
* @param next The next middleware function
|
|
158
|
+
* @returns
|
|
159
|
+
*/
|
|
160
|
+
private validateToken = (req: express.Request, res: express.Response, next: NextFunction) => {
|
|
161
|
+
// Validate the token
|
|
162
|
+
const { token } = req.params;
|
|
163
|
+
if (token !== this._token) {
|
|
164
|
+
const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
|
|
165
|
+
Log.warn(err);
|
|
166
|
+
res.status(401).send(err);
|
|
167
|
+
this.metricsCollector.alert();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Token is valid, continue
|
|
172
|
+
next();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Health check endpoint handler
|
|
177
|
+
*
|
|
178
|
+
* @param req the incoming request
|
|
179
|
+
* @param res the outgoing response
|
|
180
|
+
*/
|
|
120
181
|
private healthz = (req: express.Request, res: express.Response) => {
|
|
121
182
|
try {
|
|
122
183
|
res.send("OK");
|
|
123
184
|
} catch (err) {
|
|
124
|
-
|
|
185
|
+
Log.error(err);
|
|
125
186
|
res.status(500).send("Internal Server Error");
|
|
126
187
|
}
|
|
127
188
|
};
|
|
128
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Metrics endpoint handler
|
|
192
|
+
*
|
|
193
|
+
* @param req the incoming request
|
|
194
|
+
* @param res the outgoing response
|
|
195
|
+
*/
|
|
129
196
|
private metrics = async (req: express.Request, res: express.Response) => {
|
|
130
197
|
try {
|
|
131
198
|
res.send(await this.metricsCollector.getMetrics());
|
|
132
199
|
} catch (err) {
|
|
133
|
-
|
|
200
|
+
Log.error(err);
|
|
134
201
|
res.status(500).send("Internal Server Error");
|
|
135
202
|
}
|
|
136
203
|
};
|
|
137
204
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Admission request handler for both mutate and validate requests
|
|
207
|
+
*
|
|
208
|
+
* @param admissionKind the type of admission request
|
|
209
|
+
* @returns the request handler
|
|
210
|
+
*/
|
|
211
|
+
private admissionReq = (admissionKind: "Mutate" | "Validate") => {
|
|
212
|
+
// Create the admission request handler
|
|
213
|
+
return async (req: express.Request, res: express.Response) => {
|
|
214
|
+
// Start the metrics timer
|
|
215
|
+
const startTime = this.metricsCollector.observeStart();
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Get the request from the body or create an empty request
|
|
219
|
+
const request: Request = req.body?.request || ({} as Request);
|
|
220
|
+
|
|
221
|
+
// Run the before hook if it exists
|
|
222
|
+
this._beforeHook && this._beforeHook(request || {});
|
|
223
|
+
|
|
224
|
+
// Setup identifiers for logging
|
|
225
|
+
const name = request?.name ? `/${request.name}` : "";
|
|
226
|
+
const namespace = request?.namespace || "";
|
|
227
|
+
const gvk = request?.kind || { group: "", version: "", kind: "" };
|
|
228
|
+
|
|
229
|
+
const reqMetadata = {
|
|
230
|
+
uid: request.uid,
|
|
231
|
+
namespace,
|
|
232
|
+
name,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
Log.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
|
|
236
|
+
Log.debug({ ...reqMetadata, request }, "Incoming request body");
|
|
237
|
+
|
|
238
|
+
// Process the request
|
|
239
|
+
let response: MutateResponse | ValidateResponse;
|
|
240
|
+
|
|
241
|
+
// Call mutate or validate based on the admission kind
|
|
242
|
+
if (admissionKind === "Mutate") {
|
|
243
|
+
response = await mutateProcessor(this._config, this._capabilities, request, reqMetadata);
|
|
244
|
+
} else {
|
|
245
|
+
response = await validateProcessor(this._capabilities, request, reqMetadata);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Run the after hook if it exists
|
|
249
|
+
this._afterHook && this._afterHook(response);
|
|
250
|
+
|
|
251
|
+
// Log the response
|
|
252
|
+
Log.debug({ ...reqMetadata, response }, "Outgoing response");
|
|
253
|
+
|
|
254
|
+
// Send a no prob bob response
|
|
255
|
+
res.send({
|
|
256
|
+
apiVersion: "admission.k8s.io/v1",
|
|
257
|
+
kind: "AdmissionReview",
|
|
258
|
+
response,
|
|
259
|
+
});
|
|
260
|
+
this.metricsCollector.observeEnd(startTime, admissionKind);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
Log.error(err);
|
|
263
|
+
res.status(500).send("Internal Server Error");
|
|
264
|
+
this.metricsCollector.error();
|
|
150
265
|
}
|
|
151
|
-
|
|
152
|
-
const request: Request = req.body?.request || ({} as Request);
|
|
153
|
-
|
|
154
|
-
// Run the before hook if it exists
|
|
155
|
-
this.beforeHook && this.beforeHook(request || {});
|
|
156
|
-
|
|
157
|
-
const name = request?.name ? `/${request.name}` : "";
|
|
158
|
-
const namespace = request?.namespace || "";
|
|
159
|
-
const gvk = request?.kind || { group: "", version: "", kind: "" };
|
|
160
|
-
const prefix = `${request.uid} ${namespace}${name}`;
|
|
161
|
-
|
|
162
|
-
Log.info(`Mutate [${request.operation}] ${gvk.group}/${gvk.version}/${gvk.kind}`, prefix);
|
|
163
|
-
|
|
164
|
-
// Process the request
|
|
165
|
-
const response = await processor(this.config, this.capabilities, request, prefix);
|
|
166
|
-
|
|
167
|
-
// Run the after hook if it exists
|
|
168
|
-
this.afterHook && this.afterHook(response);
|
|
169
|
-
|
|
170
|
-
// Log the response
|
|
171
|
-
Log.debug(response, prefix);
|
|
172
|
-
|
|
173
|
-
// Send a no prob bob response
|
|
174
|
-
res.send({
|
|
175
|
-
apiVersion: "admission.k8s.io/v1",
|
|
176
|
-
kind: "AdmissionReview",
|
|
177
|
-
response,
|
|
178
|
-
});
|
|
179
|
-
this.metricsCollector.observeEnd(startTime);
|
|
180
|
-
} catch (err) {
|
|
181
|
-
console.error(err);
|
|
182
|
-
res.status(500).send("Internal Server Error");
|
|
183
|
-
this.metricsCollector.error();
|
|
184
|
-
}
|
|
266
|
+
};
|
|
185
267
|
};
|
|
186
268
|
}
|
package/src/lib/fetch.ts
CHANGED
|
@@ -30,7 +30,7 @@ export type FetchResponse<T> = {
|
|
|
30
30
|
export async function fetch<T>(url: URL | RequestInfo, init?: RequestInit): Promise<FetchResponse<T>> {
|
|
31
31
|
let data = undefined as unknown as T;
|
|
32
32
|
try {
|
|
33
|
-
logger.debug(`Fetching ${url}`);
|
|
33
|
+
logger.debug(init, `Fetching ${url}`);
|
|
34
34
|
|
|
35
35
|
const resp = await fetchRaw(url, init);
|
|
36
36
|
const contentType = resp.headers.get("content-type") || "";
|
package/src/lib/filter.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { Binding, Event } from "./types";
|
|
|
8
8
|
/**
|
|
9
9
|
* shouldSkipRequest determines if a request should be skipped based on the binding filters.
|
|
10
10
|
*
|
|
11
|
-
* @param binding the
|
|
11
|
+
* @param binding the action binding
|
|
12
12
|
* @param req the incoming request
|
|
13
13
|
* @returns
|
|
14
14
|
*/
|
|
@@ -20,8 +20,6 @@ export function shouldSkipRequest(binding: Binding, req: Request) {
|
|
|
20
20
|
const srcObject = operation === Operation.DELETE ? req.oldObject : req.object;
|
|
21
21
|
const { metadata } = srcObject || {};
|
|
22
22
|
|
|
23
|
-
console.log(metadata);
|
|
24
|
-
|
|
25
23
|
// Test for matching operation
|
|
26
24
|
if (!binding.event.includes(operation) && !binding.event.includes(Event.Any)) {
|
|
27
25
|
return true;
|
package/src/lib/k8s/index.ts
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
// Export kinds as a single object
|
|
5
5
|
import * as kind from "./upstream";
|
|
6
|
-
/** a is a collection of K8s types to be used within a
|
|
6
|
+
/** a is a collection of K8s types to be used within a action: `When(a.Configmap)` */
|
|
7
7
|
export { kind as a };
|
|
8
8
|
|
|
9
9
|
export { modelToGroupVersionKind, gvkMap, RegisterKind } from "./kinds";
|
|
10
10
|
|
|
11
|
+
// If the hostname is pepr-static-test-watcher-0, then we are running in watch mode
|
|
12
|
+
export const isWatchMode = process.env.PEPR_WATCH_MODE === "true";
|
|
13
|
+
|
|
11
14
|
export * from "./types";
|
package/src/lib/k8s/kinds.ts
CHANGED
|
@@ -5,6 +5,46 @@ import { GenericClass } from "../types";
|
|
|
5
5
|
import { GroupVersionKind } from "./types";
|
|
6
6
|
|
|
7
7
|
export const gvkMap: Record<string, GroupVersionKind> = {
|
|
8
|
+
/**
|
|
9
|
+
* Represents a K8s ClusterRole resource.
|
|
10
|
+
* ClusterRole is a set of permissions that can be bound to a user or group in a cluster-wide scope.
|
|
11
|
+
* @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole}
|
|
12
|
+
*/
|
|
13
|
+
V1ClusterRole: {
|
|
14
|
+
kind: "ClusterRole",
|
|
15
|
+
version: "v1",
|
|
16
|
+
group: "rbac.authorization.k8s.io",
|
|
17
|
+
},
|
|
18
|
+
/**
|
|
19
|
+
* Represents a K8s ClusterRoleBinding resource.
|
|
20
|
+
* ClusterRoleBinding binds a ClusterRole to a user or group in a cluster-wide scope.
|
|
21
|
+
* @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding}
|
|
22
|
+
*/
|
|
23
|
+
V1ClusterRoleBinding: {
|
|
24
|
+
kind: "ClusterRoleBinding",
|
|
25
|
+
version: "v1",
|
|
26
|
+
group: "rbac.authorization.k8s.io",
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* Represents a K8s Role resource.
|
|
30
|
+
* Role is a set of permissions that can be bound to a user or group in a namespace scope.
|
|
31
|
+
* @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole}
|
|
32
|
+
*/
|
|
33
|
+
V1Role: {
|
|
34
|
+
kind: "Role",
|
|
35
|
+
version: "v1",
|
|
36
|
+
group: "rbac.authorization.k8s.io",
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* Represents a K8s RoleBinding resource.
|
|
40
|
+
* RoleBinding binds a Role to a user or group in a namespace scope.
|
|
41
|
+
* @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding}
|
|
42
|
+
*/
|
|
43
|
+
V1RoleBinding: {
|
|
44
|
+
kind: "RoleBinding",
|
|
45
|
+
version: "v1",
|
|
46
|
+
group: "rbac.authorization.k8s.io",
|
|
47
|
+
},
|
|
8
48
|
/**
|
|
9
49
|
* Represents a K8s ConfigMap resource.
|
|
10
50
|
* ConfigMap holds configuration data for pods to consume.
|