pepr 0.13.0 → 0.13.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.js +37 -28
- package/dist/controller.js +1 -1
- package/dist/lib/assets/index.d.ts +5 -6
- package/dist/lib/assets/index.d.ts.map +1 -1
- package/dist/lib/assets/loader.d.ts +2 -8
- package/dist/lib/assets/loader.d.ts.map +1 -1
- package/dist/lib/capability.d.ts +4 -7
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/controller.d.ts +3 -49
- package/dist/lib/controller.d.ts.map +1 -1
- package/dist/lib/errors.d.ts +12 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/k8s/types.d.ts +5 -1
- package/dist/lib/k8s/types.d.ts.map +1 -1
- package/dist/lib/metrics.d.ts +6 -12
- 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.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/types.d.ts +3 -9
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/validate-processor.d.ts.map +1 -1
- package/dist/lib/validate-request.d.ts +2 -2
- package/dist/lib/validate-request.d.ts.map +1 -1
- package/dist/lib.js +247 -191
- package/dist/lib.js.map +3 -3
- package/jest.config.json +4 -0
- package/journey/before.ts +21 -0
- package/journey/k8s.ts +81 -0
- package/journey/pepr-build.ts +69 -0
- package/journey/pepr-deploy.ts +133 -0
- package/journey/pepr-dev.ts +155 -0
- package/journey/pepr-format.ts +13 -0
- package/journey/pepr-init.ts +12 -0
- package/package.json +20 -21
- package/src/lib/assets/index.ts +15 -8
- package/src/lib/assets/loader.ts +4 -12
- package/src/lib/assets/webhooks.ts +2 -2
- package/src/lib/capability.ts +34 -32
- package/src/lib/controller.ts +111 -93
- package/src/lib/errors.ts +20 -0
- package/src/lib/k8s/types.ts +5 -1
- package/src/lib/metrics.ts +45 -32
- package/src/lib/module.ts +24 -10
- package/src/lib/mutate-processor.ts +5 -3
- package/src/lib/mutate-request.ts +22 -9
- package/src/lib/types.ts +4 -10
- package/src/lib/validate-processor.ts +8 -0
- package/src/lib/validate-request.ts +19 -7
package/src/lib/capability.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
BindingFilter,
|
|
12
12
|
BindingWithName,
|
|
13
13
|
CapabilityCfg,
|
|
14
|
+
CapabilityExport,
|
|
14
15
|
Event,
|
|
15
16
|
GenericClass,
|
|
16
17
|
MutateAction,
|
|
@@ -22,35 +23,37 @@ import {
|
|
|
22
23
|
/**
|
|
23
24
|
* A capability is a unit of functionality that can be registered with the Pepr runtime.
|
|
24
25
|
*/
|
|
25
|
-
export class Capability implements
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return this._bindings;
|
|
26
|
+
export class Capability implements CapabilityExport {
|
|
27
|
+
#name: string;
|
|
28
|
+
#description: string;
|
|
29
|
+
#namespaces?: string[] | undefined;
|
|
30
|
+
#bindings: Binding[] = [];
|
|
31
|
+
|
|
32
|
+
get bindings() {
|
|
33
|
+
return this.#bindings;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
get name() {
|
|
37
|
-
return this
|
|
37
|
+
return this.#name;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
get description() {
|
|
41
|
-
return this
|
|
41
|
+
return this.#description;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
get namespaces() {
|
|
45
|
-
return this
|
|
45
|
+
return this.#namespaces || [];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
constructor(cfg: CapabilityCfg) {
|
|
49
|
-
this
|
|
50
|
-
this
|
|
51
|
-
this
|
|
49
|
+
this.#name = cfg.name;
|
|
50
|
+
this.#description = cfg.description;
|
|
51
|
+
this.#namespaces = cfg.namespaces;
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// Bind When() to this instance
|
|
54
|
+
this.When = this.When.bind(this);
|
|
55
|
+
|
|
56
|
+
Log.info(`Capability ${this.#name} registered`);
|
|
54
57
|
Log.debug(cfg);
|
|
55
58
|
}
|
|
56
59
|
|
|
@@ -63,7 +66,7 @@ export class Capability implements CapabilityCfg {
|
|
|
63
66
|
* @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
|
|
64
67
|
* @returns
|
|
65
68
|
*/
|
|
66
|
-
When
|
|
69
|
+
When<T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> {
|
|
67
70
|
const matchedKind = modelToGroupVersionKind(model.name);
|
|
68
71
|
|
|
69
72
|
// If the kind is not specified and the model is not a KubernetesObject, throw an error
|
|
@@ -83,8 +86,9 @@ export class Capability implements CapabilityCfg {
|
|
|
83
86
|
},
|
|
84
87
|
};
|
|
85
88
|
|
|
86
|
-
const
|
|
87
|
-
|
|
89
|
+
const bindings = this.#bindings;
|
|
90
|
+
const prefix = `${this.#name}: ${model.name}`;
|
|
91
|
+
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate };
|
|
88
92
|
const isNotEmpty = (value: object) => Object.keys(value).length > 0;
|
|
89
93
|
const log = (message: string, cbString: string) => {
|
|
90
94
|
const filteredObj = pickBy(isNotEmpty, binding.filters);
|
|
@@ -94,27 +98,27 @@ export class Capability implements CapabilityCfg {
|
|
|
94
98
|
Log.debug(cbString, prefix);
|
|
95
99
|
};
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
function Validate(validateCallback: ValidateAction<T>): void {
|
|
98
102
|
if (!isWatchMode) {
|
|
99
103
|
log("Validate Action", validateCallback.toString());
|
|
100
104
|
|
|
101
105
|
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
102
106
|
// with the callback function to preserve
|
|
103
|
-
|
|
107
|
+
bindings.push({
|
|
104
108
|
...binding,
|
|
105
109
|
isValidate: true,
|
|
106
110
|
validateCallback,
|
|
107
111
|
});
|
|
108
112
|
}
|
|
109
|
-
}
|
|
113
|
+
}
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
function Mutate(mutateCallback: MutateAction<T>): MutateActionChain<T> {
|
|
112
116
|
if (!isWatchMode) {
|
|
113
117
|
log("Mutate Action", mutateCallback.toString());
|
|
114
118
|
|
|
115
119
|
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
116
120
|
// with the callback function to preserve
|
|
117
|
-
|
|
121
|
+
bindings.push({
|
|
118
122
|
...binding,
|
|
119
123
|
isMutate: true,
|
|
120
124
|
mutateCallback,
|
|
@@ -123,7 +127,7 @@ export class Capability implements CapabilityCfg {
|
|
|
123
127
|
|
|
124
128
|
// Now only allow adding actions to the same binding
|
|
125
129
|
return { Validate };
|
|
126
|
-
}
|
|
130
|
+
}
|
|
127
131
|
|
|
128
132
|
function InNamespace(...namespaces: string[]): BindingWithName<T> {
|
|
129
133
|
Log.debug(`Add namespaces filter ${namespaces}`, prefix);
|
|
@@ -143,22 +147,20 @@ export class Capability implements CapabilityCfg {
|
|
|
143
147
|
return commonChain;
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
|
|
150
|
+
function WithAnnotation(key: string, value = ""): BindingFilter<T> {
|
|
147
151
|
Log.debug(`Add annotation filter ${key}=${value}`, prefix);
|
|
148
152
|
binding.filters.annotations[key] = value;
|
|
149
153
|
return commonChain;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate };
|
|
154
|
+
}
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
function bindEvent(event: Event) {
|
|
155
157
|
binding.event = event;
|
|
156
158
|
return {
|
|
157
159
|
...commonChain,
|
|
158
160
|
InNamespace,
|
|
159
161
|
WithName,
|
|
160
162
|
};
|
|
161
|
-
}
|
|
163
|
+
}
|
|
162
164
|
|
|
163
165
|
return {
|
|
164
166
|
IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate),
|
|
@@ -166,5 +168,5 @@ export class Capability implements CapabilityCfg {
|
|
|
166
168
|
IsUpdated: () => bindEvent(Event.Update),
|
|
167
169
|
IsDeleted: () => bindEvent(Event.Delete),
|
|
168
170
|
};
|
|
169
|
-
}
|
|
171
|
+
}
|
|
170
172
|
}
|
package/src/lib/controller.ts
CHANGED
|
@@ -15,61 +15,59 @@ import { ModuleConfig } from "./types";
|
|
|
15
15
|
import { validateProcessor } from "./validate-processor";
|
|
16
16
|
|
|
17
17
|
export class Controller {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// Track whether the server is running
|
|
19
|
+
#running = false;
|
|
20
|
+
|
|
21
|
+
// Metrics collector
|
|
22
|
+
#metricsCollector = new MetricsCollector("pepr");
|
|
21
23
|
|
|
22
24
|
// The token used to authenticate requests
|
|
23
|
-
|
|
25
|
+
#token = "";
|
|
26
|
+
|
|
27
|
+
// The express app instance
|
|
28
|
+
readonly #app = express();
|
|
29
|
+
|
|
30
|
+
// Initialized with the constructor
|
|
31
|
+
readonly #config: ModuleConfig;
|
|
32
|
+
readonly #capabilities: Capability[];
|
|
33
|
+
readonly #beforeHook?: (req: Request) => void;
|
|
34
|
+
readonly #afterHook?: (res: MutateResponse) => void;
|
|
24
35
|
|
|
25
36
|
constructor(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
config: ModuleConfig,
|
|
38
|
+
capabilities: Capability[],
|
|
39
|
+
beforeHook?: (req: Request) => void,
|
|
40
|
+
afterHook?: (res: MutateResponse) => void,
|
|
30
41
|
) {
|
|
42
|
+
this.#config = config;
|
|
43
|
+
this.#capabilities = capabilities;
|
|
44
|
+
this.#beforeHook = beforeHook;
|
|
45
|
+
this.#afterHook = afterHook;
|
|
46
|
+
|
|
47
|
+
// Bind public methods
|
|
48
|
+
this.startServer = this.startServer.bind(this);
|
|
49
|
+
|
|
31
50
|
// Middleware for logging requests
|
|
32
|
-
this.
|
|
51
|
+
this.#app.use(Controller.#logger);
|
|
33
52
|
|
|
34
53
|
// Middleware for parsing JSON, limit to 2mb vs 100K for K8s compatibility
|
|
35
|
-
this.
|
|
54
|
+
this.#app.use(express.json({ limit: "2mb" }));
|
|
36
55
|
|
|
37
|
-
if (
|
|
38
|
-
Log.info(`Using beforeHook: ${
|
|
56
|
+
if (beforeHook) {
|
|
57
|
+
Log.info(`Using beforeHook: ${beforeHook}`);
|
|
39
58
|
}
|
|
40
59
|
|
|
41
|
-
if (
|
|
42
|
-
Log.info(`Using afterHook: ${
|
|
60
|
+
if (afterHook) {
|
|
61
|
+
Log.info(`Using afterHook: ${afterHook}`);
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
// Bind endpoints
|
|
46
|
-
this
|
|
65
|
+
this.#bindEndpoints();
|
|
47
66
|
}
|
|
48
67
|
|
|
49
|
-
private bindEndpoints = () => {
|
|
50
|
-
// Health check endpoint
|
|
51
|
-
this._app.get("/healthz", this.healthz);
|
|
52
|
-
|
|
53
|
-
// Metrics endpoint
|
|
54
|
-
this._app.get("/metrics", this.metrics);
|
|
55
|
-
|
|
56
|
-
if (isWatchMode) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
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
|
-
};
|
|
69
|
-
|
|
70
68
|
/** Start the webhook server */
|
|
71
|
-
|
|
72
|
-
if (this
|
|
69
|
+
startServer(port: number) {
|
|
70
|
+
if (this.#running) {
|
|
73
71
|
throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
|
|
74
72
|
}
|
|
75
73
|
|
|
@@ -82,22 +80,22 @@ export class Controller {
|
|
|
82
80
|
// Get the API token if not in watch mode
|
|
83
81
|
if (!isWatchMode) {
|
|
84
82
|
// Get the API token from the environment variable or the mounted secret
|
|
85
|
-
this
|
|
86
|
-
Log.info(`Using API token: ${this
|
|
83
|
+
this.#token = process.env.PEPR_API_TOKEN || fs.readFileSync("/app/api-token/value").toString().trim();
|
|
84
|
+
Log.info(`Using API token: ${this.#token}`);
|
|
87
85
|
|
|
88
|
-
if (!this
|
|
86
|
+
if (!this.#token) {
|
|
89
87
|
throw new Error("API token not found");
|
|
90
88
|
}
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
// Create HTTPS server
|
|
94
|
-
const server = https.createServer(options, this
|
|
92
|
+
const server = https.createServer(options, this.#app).listen(port);
|
|
95
93
|
|
|
96
94
|
// Handle server listening event
|
|
97
95
|
server.on("listening", () => {
|
|
98
96
|
Log.info(`Server listening on port ${port}`);
|
|
99
97
|
// Track that the server is running
|
|
100
|
-
this
|
|
98
|
+
this.#running = true;
|
|
101
99
|
});
|
|
102
100
|
|
|
103
101
|
// Handle EADDRINUSE errors
|
|
@@ -121,32 +119,27 @@ export class Controller {
|
|
|
121
119
|
process.exit(0);
|
|
122
120
|
});
|
|
123
121
|
});
|
|
124
|
-
}
|
|
122
|
+
}
|
|
125
123
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
* @param req the incoming request
|
|
130
|
-
* @param res the outgoing response
|
|
131
|
-
* @param next the next middleware function
|
|
132
|
-
*/
|
|
133
|
-
private logger = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
134
|
-
const startTime = Date.now();
|
|
124
|
+
#bindEndpoints = () => {
|
|
125
|
+
// Health check endpoint
|
|
126
|
+
this.#app.get("/healthz", Controller.#healthz);
|
|
135
127
|
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
};
|
|
128
|
+
// Metrics endpoint
|
|
129
|
+
this.#app.get("/metrics", this.#metrics);
|
|
145
130
|
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
if (isWatchMode) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
148
134
|
|
|
149
|
-
|
|
135
|
+
// Require auth for webhook endpoints
|
|
136
|
+
this.#app.use(["/mutate/:token", "/validate/:token"], this.#validateToken);
|
|
137
|
+
|
|
138
|
+
// Mutate endpoint
|
|
139
|
+
this.#app.post("/mutate/:token", this.#admissionReq("Mutate"));
|
|
140
|
+
|
|
141
|
+
// Validate endpoint
|
|
142
|
+
this.#app.post("/validate/:token", this.#admissionReq("Validate"));
|
|
150
143
|
};
|
|
151
144
|
|
|
152
145
|
/**
|
|
@@ -157,14 +150,14 @@ export class Controller {
|
|
|
157
150
|
* @param next The next middleware function
|
|
158
151
|
* @returns
|
|
159
152
|
*/
|
|
160
|
-
|
|
153
|
+
#validateToken = (req: express.Request, res: express.Response, next: NextFunction) => {
|
|
161
154
|
// Validate the token
|
|
162
155
|
const { token } = req.params;
|
|
163
|
-
if (token !== this
|
|
156
|
+
if (token !== this.#token) {
|
|
164
157
|
const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
|
|
165
158
|
Log.warn(err);
|
|
166
159
|
res.status(401).send(err);
|
|
167
|
-
this
|
|
160
|
+
this.#metricsCollector.alert();
|
|
168
161
|
return;
|
|
169
162
|
}
|
|
170
163
|
|
|
@@ -172,30 +165,15 @@ export class Controller {
|
|
|
172
165
|
next();
|
|
173
166
|
};
|
|
174
167
|
|
|
175
|
-
/**
|
|
176
|
-
* Health check endpoint handler
|
|
177
|
-
*
|
|
178
|
-
* @param req the incoming request
|
|
179
|
-
* @param res the outgoing response
|
|
180
|
-
*/
|
|
181
|
-
private healthz = (req: express.Request, res: express.Response) => {
|
|
182
|
-
try {
|
|
183
|
-
res.send("OK");
|
|
184
|
-
} catch (err) {
|
|
185
|
-
Log.error(err);
|
|
186
|
-
res.status(500).send("Internal Server Error");
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
|
|
190
168
|
/**
|
|
191
169
|
* Metrics endpoint handler
|
|
192
170
|
*
|
|
193
171
|
* @param req the incoming request
|
|
194
172
|
* @param res the outgoing response
|
|
195
173
|
*/
|
|
196
|
-
|
|
174
|
+
#metrics = async (req: express.Request, res: express.Response) => {
|
|
197
175
|
try {
|
|
198
|
-
res.send(await this
|
|
176
|
+
res.send(await this.#metricsCollector.getMetrics());
|
|
199
177
|
} catch (err) {
|
|
200
178
|
Log.error(err);
|
|
201
179
|
res.status(500).send("Internal Server Error");
|
|
@@ -208,18 +186,18 @@ export class Controller {
|
|
|
208
186
|
* @param admissionKind the type of admission request
|
|
209
187
|
* @returns the request handler
|
|
210
188
|
*/
|
|
211
|
-
|
|
189
|
+
#admissionReq = (admissionKind: "Mutate" | "Validate") => {
|
|
212
190
|
// Create the admission request handler
|
|
213
191
|
return async (req: express.Request, res: express.Response) => {
|
|
214
192
|
// Start the metrics timer
|
|
215
|
-
const startTime = this
|
|
193
|
+
const startTime = this.#metricsCollector.observeStart();
|
|
216
194
|
|
|
217
195
|
try {
|
|
218
196
|
// Get the request from the body or create an empty request
|
|
219
197
|
const request: Request = req.body?.request || ({} as Request);
|
|
220
198
|
|
|
221
199
|
// Run the before hook if it exists
|
|
222
|
-
this
|
|
200
|
+
this.#beforeHook && this.#beforeHook(request || {});
|
|
223
201
|
|
|
224
202
|
// Setup identifiers for logging
|
|
225
203
|
const name = request?.name ? `/${request.name}` : "";
|
|
@@ -240,13 +218,13 @@ export class Controller {
|
|
|
240
218
|
|
|
241
219
|
// Call mutate or validate based on the admission kind
|
|
242
220
|
if (admissionKind === "Mutate") {
|
|
243
|
-
response = await mutateProcessor(this
|
|
221
|
+
response = await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata);
|
|
244
222
|
} else {
|
|
245
|
-
response = await validateProcessor(this
|
|
223
|
+
response = await validateProcessor(this.#capabilities, request, reqMetadata);
|
|
246
224
|
}
|
|
247
225
|
|
|
248
226
|
// Run the after hook if it exists
|
|
249
|
-
this
|
|
227
|
+
this.#afterHook && this.#afterHook(response);
|
|
250
228
|
|
|
251
229
|
// Log the response
|
|
252
230
|
Log.debug({ ...reqMetadata, response }, "Outgoing response");
|
|
@@ -257,12 +235,52 @@ export class Controller {
|
|
|
257
235
|
kind: "AdmissionReview",
|
|
258
236
|
response,
|
|
259
237
|
});
|
|
260
|
-
this
|
|
238
|
+
this.#metricsCollector.observeEnd(startTime, admissionKind);
|
|
261
239
|
} catch (err) {
|
|
262
240
|
Log.error(err);
|
|
263
241
|
res.status(500).send("Internal Server Error");
|
|
264
|
-
this
|
|
242
|
+
this.#metricsCollector.error();
|
|
265
243
|
}
|
|
266
244
|
};
|
|
267
245
|
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Middleware for logging requests
|
|
249
|
+
*
|
|
250
|
+
* @param req the incoming request
|
|
251
|
+
* @param res the outgoing response
|
|
252
|
+
* @param next the next middleware function
|
|
253
|
+
*/
|
|
254
|
+
static #logger(req: express.Request, res: express.Response, next: express.NextFunction) {
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
|
|
257
|
+
res.on("finish", () => {
|
|
258
|
+
const elapsedTime = Date.now() - startTime;
|
|
259
|
+
const message = {
|
|
260
|
+
uid: req.body?.request?.uid,
|
|
261
|
+
method: req.method,
|
|
262
|
+
url: req.originalUrl,
|
|
263
|
+
status: res.statusCode,
|
|
264
|
+
duration: `${elapsedTime} ms`,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
res.statusCode >= 300 ? Log.warn(message) : Log.info(message);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
next();
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Health check endpoint handler
|
|
274
|
+
*
|
|
275
|
+
* @param req the incoming request
|
|
276
|
+
* @param res the outgoing response
|
|
277
|
+
*/
|
|
278
|
+
static #healthz(req: express.Request, res: express.Response) {
|
|
279
|
+
try {
|
|
280
|
+
res.send("OK");
|
|
281
|
+
} catch (err) {
|
|
282
|
+
Log.error(err);
|
|
283
|
+
res.status(500).send("Internal Server Error");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
268
286
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
export const Errors = {
|
|
5
|
+
audit: "audit",
|
|
6
|
+
ignore: "ignore",
|
|
7
|
+
reject: "reject",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ErrorList = Object.values(Errors);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate the error or throw an error
|
|
14
|
+
* @param error
|
|
15
|
+
*/
|
|
16
|
+
export function ValidateError(error = "") {
|
|
17
|
+
if (!ErrorList.includes(error)) {
|
|
18
|
+
throw new Error(`Invalid error: ${error}. Must be one of: ${ErrorList.join(", ")}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/lib/k8s/types.ts
CHANGED
|
@@ -144,7 +144,11 @@ export interface MutateResponse {
|
|
|
144
144
|
/** The type of Patch. Currently we only allow "JSONPatch". */
|
|
145
145
|
patchType?: "JSONPatch";
|
|
146
146
|
|
|
147
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted).
|
|
149
|
+
*
|
|
150
|
+
* See https://kubernetes.io/docs/reference/labels-annotations-taints/audit-annotations/ for more information
|
|
151
|
+
*/
|
|
148
152
|
auditAnnotations?: {
|
|
149
153
|
[key: string]: string;
|
|
150
154
|
};
|
package/src/lib/metrics.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/* eslint-disable class-methods-use-this */
|
|
5
|
+
|
|
5
6
|
import { performance } from "perf_hooks";
|
|
7
|
+
import promClient, { Counter, Registry, Summary } from "prom-client";
|
|
6
8
|
import Log from "./logger";
|
|
7
9
|
|
|
8
10
|
const loggingPrefix = "MetricsCollector";
|
|
@@ -24,12 +26,12 @@ interface MetricArgs {
|
|
|
24
26
|
* MetricsCollector class handles metrics collection using prom-client and performance hooks.
|
|
25
27
|
*/
|
|
26
28
|
export class MetricsCollector {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
#registry: Registry;
|
|
30
|
+
#counters: Map<string, Counter<string>> = new Map();
|
|
31
|
+
#summaries: Map<string, Summary<string>> = new Map();
|
|
32
|
+
#prefix: string;
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
#metricNames: MetricNames = {
|
|
33
35
|
errors: "errors",
|
|
34
36
|
alerts: "alerts",
|
|
35
37
|
mutate: "Mutate",
|
|
@@ -38,67 +40,78 @@ export class MetricsCollector {
|
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Creates a MetricsCollector instance with prefixed metrics.
|
|
41
|
-
* @param
|
|
43
|
+
* @param [prefix='pepr'] - The prefix for the metric names.
|
|
42
44
|
*/
|
|
43
45
|
constructor(prefix = "pepr") {
|
|
44
|
-
this
|
|
45
|
-
this
|
|
46
|
-
this.addCounter(this.
|
|
47
|
-
this.addCounter(this.
|
|
48
|
-
this.addSummary(this.
|
|
49
|
-
this.addSummary(this.
|
|
46
|
+
this.#registry = new Registry();
|
|
47
|
+
this.#prefix = prefix;
|
|
48
|
+
this.addCounter(this.#metricNames.errors, "Mutation/Validate errors encountered");
|
|
49
|
+
this.addCounter(this.#metricNames.alerts, "Mutation/Validate bad api token received");
|
|
50
|
+
this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
|
|
51
|
+
this.addSummary(this.#metricNames.validate, "Validation operation summary");
|
|
50
52
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
|
|
54
|
+
#getMetricName(name: string) {
|
|
55
|
+
return `${this.#prefix}_${name}`;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
#addMetric<T extends Counter<string> | Summary<string>>(
|
|
56
59
|
collection: Map<string, T>,
|
|
57
60
|
MetricType: new (args: MetricArgs) => T,
|
|
58
61
|
name: string,
|
|
59
62
|
help: string,
|
|
60
63
|
) {
|
|
61
|
-
if (collection.has(this
|
|
64
|
+
if (collection.has(this.#getMetricName(name))) {
|
|
62
65
|
Log.debug(`Metric for ${name} already exists`, loggingPrefix);
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
68
|
+
|
|
69
|
+
// Bind public methods
|
|
70
|
+
this.incCounter = this.incCounter.bind(this);
|
|
71
|
+
this.error = this.error.bind(this);
|
|
72
|
+
this.alert = this.alert.bind(this);
|
|
73
|
+
this.observeStart = this.observeStart.bind(this);
|
|
74
|
+
this.observeEnd = this.observeEnd.bind(this);
|
|
75
|
+
this.getMetrics = this.getMetrics.bind(this);
|
|
76
|
+
|
|
65
77
|
const metric = new MetricType({
|
|
66
|
-
name: this
|
|
78
|
+
name: this.#getMetricName(name),
|
|
67
79
|
help,
|
|
68
|
-
registers: [this
|
|
80
|
+
registers: [this.#registry],
|
|
69
81
|
});
|
|
70
|
-
|
|
82
|
+
|
|
83
|
+
collection.set(this.#getMetricName(name), metric);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
addCounter(name: string, help: string) {
|
|
74
|
-
this
|
|
87
|
+
this.#addMetric(this.#counters, promClient.Counter, name, help);
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
addSummary(name: string, help: string) {
|
|
78
|
-
this
|
|
91
|
+
this.#addMetric(this.#summaries, promClient.Summary, name, help);
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
incCounter(name: string) {
|
|
82
|
-
this.
|
|
95
|
+
this.#counters.get(this.#getMetricName(name))?.inc();
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
/**
|
|
86
99
|
* Increments the error counter.
|
|
87
100
|
*/
|
|
88
101
|
error() {
|
|
89
|
-
this.incCounter(this.
|
|
102
|
+
this.incCounter(this.#metricNames.errors);
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
/**
|
|
93
106
|
* Increments the alerts counter.
|
|
94
107
|
*/
|
|
95
108
|
alert() {
|
|
96
|
-
this.incCounter(this.
|
|
109
|
+
this.incCounter(this.#metricNames.alerts);
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
/**
|
|
100
113
|
* Returns the current timestamp from performance.now() method. Useful for start timing an operation.
|
|
101
|
-
* @returns
|
|
114
|
+
* @returns The timestamp.
|
|
102
115
|
*/
|
|
103
116
|
observeStart() {
|
|
104
117
|
return performance.now();
|
|
@@ -106,18 +119,18 @@ export class MetricsCollector {
|
|
|
106
119
|
|
|
107
120
|
/**
|
|
108
121
|
* Observes the duration since the provided start time and updates the summary.
|
|
109
|
-
* @param
|
|
110
|
-
* @param
|
|
122
|
+
* @param startTime - The start time.
|
|
123
|
+
* @param name - The metrics summary to increment.
|
|
111
124
|
*/
|
|
112
|
-
observeEnd(startTime: number, name: string = this.
|
|
113
|
-
this.
|
|
125
|
+
observeEnd(startTime: number, name: string = this.#metricNames.mutate) {
|
|
126
|
+
this.#summaries.get(this.#getMetricName(name))?.observe(performance.now() - startTime);
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
/**
|
|
117
130
|
* Fetches the current metrics from the registry.
|
|
118
|
-
* @returns
|
|
131
|
+
* @returns The metrics.
|
|
119
132
|
*/
|
|
120
133
|
async getMetrics() {
|
|
121
|
-
return this.
|
|
134
|
+
return this.#registry.metrics();
|
|
122
135
|
}
|
|
123
136
|
}
|