pepr 0.6.1 → 0.7.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/dist/cli.js +132 -75
- package/dist/controller.js +1 -1
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/controller.d.ts.map +1 -1
- package/dist/lib/filter.d.ts.map +1 -1
- package/dist/lib/k8s/kinds.d.ts.map +1 -1
- package/dist/lib/k8s/types.d.ts +2 -0
- package/dist/lib/k8s/types.d.ts.map +1 -1
- package/dist/lib/k8s/webhook.d.ts +5 -4
- package/dist/lib/k8s/webhook.d.ts.map +1 -1
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/types.d.ts +3 -2
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib.js +17 -8
- package/dist/lib.js.map +3 -3
- package/package.json +1 -1
- package/src/lib/capability.ts +1 -0
- package/src/lib/controller.ts +9 -7
- package/src/lib/filter.ts +2 -2
- package/src/lib/k8s/kinds.ts +2 -0
- package/src/lib/k8s/types.ts +2 -0
- package/src/lib/k8s/webhook.ts +120 -26
- package/src/lib/module.ts +7 -1
- package/src/lib/types.ts +2 -1
package/src/lib/k8s/webhook.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
AdmissionregistrationV1Api,
|
|
6
|
-
AdmissionregistrationV1WebhookClientConfig,
|
|
5
|
+
AdmissionregistrationV1Api as AdmissionRegV1API,
|
|
6
|
+
AdmissionregistrationV1WebhookClientConfig as AdmissionRegnV1WebhookClientCfg,
|
|
7
7
|
AppsV1Api,
|
|
8
8
|
CoreV1Api,
|
|
9
9
|
HttpError,
|
|
@@ -17,15 +17,20 @@ import {
|
|
|
17
17
|
V1MutatingWebhookConfiguration,
|
|
18
18
|
V1Namespace,
|
|
19
19
|
V1NetworkPolicy,
|
|
20
|
+
V1RuleWithOperations,
|
|
20
21
|
V1Secret,
|
|
21
22
|
V1Service,
|
|
22
23
|
V1ServiceAccount,
|
|
23
24
|
dumpYaml,
|
|
24
25
|
} from "@kubernetes/client-node";
|
|
26
|
+
import { fork } from "child_process";
|
|
25
27
|
import crypto from "crypto";
|
|
28
|
+
import { promises as fs } from "fs";
|
|
29
|
+
import { equals, uniqWith } from "ramda";
|
|
26
30
|
import { gzipSync } from "zlib";
|
|
31
|
+
|
|
27
32
|
import Log from "../logger";
|
|
28
|
-
import { ModuleConfig } from "../types";
|
|
33
|
+
import { Binding, Event, HookPhase, ModuleConfig } from "../types";
|
|
29
34
|
import { TLSOut, genTLS } from "./tls";
|
|
30
35
|
|
|
31
36
|
const peprIgnore: V1LabelSelectorRequirement = {
|
|
@@ -132,7 +137,95 @@ export class Webhook {
|
|
|
132
137
|
};
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
|
|
140
|
+
generateWebhookRules(path: string): Promise<V1RuleWithOperations[]> {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const rules: V1RuleWithOperations[] = [];
|
|
143
|
+
|
|
144
|
+
// Add a default rule that allows all resources as a fallback
|
|
145
|
+
const defaultRule = {
|
|
146
|
+
apiGroups: ["*"],
|
|
147
|
+
apiVersions: ["*"],
|
|
148
|
+
operations: ["CREATE", "UPDATE", "DELETE"],
|
|
149
|
+
resources: ["*/*"],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Fork is needed with the PEPR_MODE env var to ensure the module is loaded in build mode and will send back the capabilities
|
|
153
|
+
const program = fork(path, {
|
|
154
|
+
env: {
|
|
155
|
+
...process.env,
|
|
156
|
+
LOG_LEVEL: "warn",
|
|
157
|
+
PEPR_MODE: "build",
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// We are receiving javscript so the private fields are now public
|
|
162
|
+
interface ModuleCapabilities {
|
|
163
|
+
capabilities: {
|
|
164
|
+
_name: string;
|
|
165
|
+
_description: string;
|
|
166
|
+
_namespaces: string[];
|
|
167
|
+
_mutateOrValidate: HookPhase;
|
|
168
|
+
_bindings: Binding[];
|
|
169
|
+
}[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Wait for the module to send back the capabilities
|
|
173
|
+
program.on("message", message => {
|
|
174
|
+
// Cast the message to the ModuleCapabilities type
|
|
175
|
+
const { capabilities } = message.valueOf() as ModuleCapabilities;
|
|
176
|
+
|
|
177
|
+
for (const capability of capabilities) {
|
|
178
|
+
Log.info(`Module ${this.config.uuid} has capability: ${capability._name}`);
|
|
179
|
+
|
|
180
|
+
const { _bindings } = capability;
|
|
181
|
+
|
|
182
|
+
// Read the bindings and generate the rules
|
|
183
|
+
for (const binding of _bindings) {
|
|
184
|
+
const { event, kind } = binding;
|
|
185
|
+
|
|
186
|
+
const operations: string[] = [];
|
|
187
|
+
|
|
188
|
+
// CreateOrUpdate is a Pepr-specific event that is translated to Create and Update
|
|
189
|
+
if (event === Event.CreateOrUpdate) {
|
|
190
|
+
operations.push(Event.Create, Event.Update);
|
|
191
|
+
} else {
|
|
192
|
+
operations.push(event);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Use the plural property if it exists, otherwise use lowercase kind + s
|
|
196
|
+
const resource = kind.plural || `${kind.kind.toLowerCase()}s`;
|
|
197
|
+
|
|
198
|
+
rules.push({
|
|
199
|
+
apiGroups: [kind.group],
|
|
200
|
+
apiVersions: [kind.version || "*"],
|
|
201
|
+
operations,
|
|
202
|
+
resources: [resource],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
program.on("exit", code => {
|
|
209
|
+
if (code !== 0) {
|
|
210
|
+
reject(new Error(`Child process exited with code ${code}`));
|
|
211
|
+
} else {
|
|
212
|
+
// If there are no rules, add a catch-all
|
|
213
|
+
if (rules.length < 1) {
|
|
214
|
+
resolve([defaultRule]);
|
|
215
|
+
} else {
|
|
216
|
+
const reducedRules = uniqWith(equals, rules);
|
|
217
|
+
resolve(reducedRules);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
program.on("error", error => {
|
|
223
|
+
reject(error);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async mutatingWebhook(path: string, timeoutSeconds = 10): Promise<V1MutatingWebhookConfiguration> {
|
|
136
229
|
const { name } = this;
|
|
137
230
|
const ignore = [peprIgnore];
|
|
138
231
|
|
|
@@ -145,7 +238,7 @@ export class Webhook {
|
|
|
145
238
|
});
|
|
146
239
|
}
|
|
147
240
|
|
|
148
|
-
const clientConfig:
|
|
241
|
+
const clientConfig: AdmissionRegnV1WebhookClientCfg = {
|
|
149
242
|
caBundle: this._tls.ca,
|
|
150
243
|
};
|
|
151
244
|
|
|
@@ -161,6 +254,8 @@ export class Webhook {
|
|
|
161
254
|
};
|
|
162
255
|
}
|
|
163
256
|
|
|
257
|
+
const rules = await this.generateWebhookRules(path);
|
|
258
|
+
|
|
164
259
|
return {
|
|
165
260
|
apiVersion: "admissionregistration.k8s.io/v1",
|
|
166
261
|
kind: "MutatingWebhookConfiguration",
|
|
@@ -179,15 +274,7 @@ export class Webhook {
|
|
|
179
274
|
objectSelector: {
|
|
180
275
|
matchExpressions: ignore,
|
|
181
276
|
},
|
|
182
|
-
|
|
183
|
-
rules: [
|
|
184
|
-
{
|
|
185
|
-
apiGroups: ["*"],
|
|
186
|
-
apiVersions: ["*"],
|
|
187
|
-
operations: ["CREATE", "UPDATE", "DELETE"],
|
|
188
|
-
resources: ["*/*"],
|
|
189
|
-
},
|
|
190
|
-
],
|
|
277
|
+
rules,
|
|
191
278
|
// @todo: track side effects state
|
|
192
279
|
sideEffects: "None",
|
|
193
280
|
},
|
|
@@ -390,10 +477,14 @@ export class Webhook {
|
|
|
390
477
|
return dumpYaml(zarfCfg, { noRefs: true });
|
|
391
478
|
}
|
|
392
479
|
|
|
393
|
-
allYaml(
|
|
480
|
+
async allYaml(path: string) {
|
|
481
|
+
const code = await fs.readFile(path);
|
|
482
|
+
|
|
394
483
|
// Generate a hash of the code
|
|
395
484
|
const hash = crypto.createHash("sha256").update(code).digest("hex");
|
|
396
485
|
|
|
486
|
+
const webhook = await this.mutatingWebhook(path);
|
|
487
|
+
|
|
397
488
|
const resources = [
|
|
398
489
|
this.namespace(),
|
|
399
490
|
this.networkPolicy(),
|
|
@@ -401,7 +492,7 @@ export class Webhook {
|
|
|
401
492
|
this.clusterRoleBinding(),
|
|
402
493
|
this.serviceAccount(),
|
|
403
494
|
this.tlsSecret(),
|
|
404
|
-
|
|
495
|
+
webhook,
|
|
405
496
|
this.deployment(hash),
|
|
406
497
|
this.service(),
|
|
407
498
|
this.moduleSecret(code, hash),
|
|
@@ -411,7 +502,7 @@ export class Webhook {
|
|
|
411
502
|
return resources.map(r => dumpYaml(r, { noRefs: true })).join("---\n");
|
|
412
503
|
}
|
|
413
504
|
|
|
414
|
-
async deploy(
|
|
505
|
+
async deploy(path: string, webhookTimeout?: number) {
|
|
415
506
|
Log.info("Establishing connection to Kubernetes");
|
|
416
507
|
|
|
417
508
|
const namespace = "pepr-system";
|
|
@@ -421,10 +512,7 @@ export class Webhook {
|
|
|
421
512
|
kubeConfig.loadFromDefault();
|
|
422
513
|
|
|
423
514
|
const coreV1Api = kubeConfig.makeApiClient(CoreV1Api);
|
|
424
|
-
const
|
|
425
|
-
const appsApi = kubeConfig.makeApiClient(AppsV1Api);
|
|
426
|
-
const admissionApi = kubeConfig.makeApiClient(AdmissionregistrationV1Api);
|
|
427
|
-
const networkApi = kubeConfig.makeApiClient(NetworkingV1Api);
|
|
515
|
+
const admissionApi = kubeConfig.makeApiClient(AdmissionRegV1API);
|
|
428
516
|
|
|
429
517
|
const ns = this.namespace();
|
|
430
518
|
try {
|
|
@@ -436,7 +524,7 @@ export class Webhook {
|
|
|
436
524
|
await coreV1Api.createNamespace(ns);
|
|
437
525
|
}
|
|
438
526
|
|
|
439
|
-
const wh = this.mutatingWebhook(webhookTimeout);
|
|
527
|
+
const wh = await this.mutatingWebhook(path, webhookTimeout);
|
|
440
528
|
try {
|
|
441
529
|
Log.info("Creating mutating webhook");
|
|
442
530
|
await admissionApi.createMutatingWebhookConfiguration(wh);
|
|
@@ -452,20 +540,26 @@ export class Webhook {
|
|
|
452
540
|
return;
|
|
453
541
|
}
|
|
454
542
|
|
|
455
|
-
if (!
|
|
543
|
+
if (!path) {
|
|
456
544
|
throw new Error("No code provided");
|
|
457
545
|
}
|
|
458
546
|
|
|
547
|
+
const code = await fs.readFile(path);
|
|
548
|
+
|
|
459
549
|
const hash = crypto.createHash("sha256").update(code).digest("hex");
|
|
460
550
|
|
|
461
|
-
const
|
|
551
|
+
const appsApi = kubeConfig.makeApiClient(AppsV1Api);
|
|
552
|
+
const rbacApi = kubeConfig.makeApiClient(RbacAuthorizationV1Api);
|
|
553
|
+
const networkApi = kubeConfig.makeApiClient(NetworkingV1Api);
|
|
554
|
+
|
|
555
|
+
const networkPolicy = this.networkPolicy();
|
|
462
556
|
try {
|
|
463
557
|
Log.info("Checking for network policy");
|
|
464
|
-
await networkApi.readNamespacedNetworkPolicy(
|
|
558
|
+
await networkApi.readNamespacedNetworkPolicy(networkPolicy.metadata?.name ?? "", namespace);
|
|
465
559
|
} catch (e) {
|
|
466
560
|
Log.debug(e instanceof HttpError ? e.body : e);
|
|
467
561
|
Log.info("Creating network policy");
|
|
468
|
-
await networkApi.createNamespacedNetworkPolicy(namespace,
|
|
562
|
+
await networkApi.createNamespacedNetworkPolicy(namespace, networkPolicy);
|
|
469
563
|
}
|
|
470
564
|
|
|
471
565
|
const crb = this.clusterRoleBinding();
|
package/src/lib/module.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type PeprModuleOptions = {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
export class PeprModule {
|
|
32
|
-
private _controller
|
|
32
|
+
private _controller!: Controller;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Create a new Pepr runtime
|
|
@@ -42,6 +42,12 @@ export class PeprModule {
|
|
|
42
42
|
const config: ModuleConfig = mergeDeepWith(concat, pepr, alwaysIgnore);
|
|
43
43
|
config.description = description;
|
|
44
44
|
|
|
45
|
+
// Handle build mode
|
|
46
|
+
if (process.env.PEPR_MODE === "build") {
|
|
47
|
+
process.send?.({ capabilities });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
this._controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook);
|
|
46
52
|
|
|
47
53
|
// Stop processing if deferStart is set to true
|
package/src/lib/types.ts
CHANGED
|
@@ -44,6 +44,7 @@ export enum Event {
|
|
|
44
44
|
Update = "UPDATE",
|
|
45
45
|
Delete = "DELETE",
|
|
46
46
|
CreateOrUpdate = "CREATEORUPDATE",
|
|
47
|
+
Any = "*",
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
export interface CapabilityCfg {
|
|
@@ -123,7 +124,7 @@ export type WhenSelector<T extends GenericClass> = {
|
|
|
123
124
|
};
|
|
124
125
|
|
|
125
126
|
export type Binding = {
|
|
126
|
-
event
|
|
127
|
+
event: Event;
|
|
127
128
|
readonly kind: GroupVersionKind;
|
|
128
129
|
readonly filters: {
|
|
129
130
|
name: string;
|