pepr 0.12.2 → 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/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +70 -0
- package/README.md +28 -30
- package/dist/cli.js +666 -692
- 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 +17 -0
- package/dist/lib/assets/index.d.ts.map +1 -0
- package/dist/lib/assets/loader.d.ts +8 -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 +4 -9
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/controller.d.ts +4 -15
- 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/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 +18 -14
- 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 +10 -9
- package/dist/lib/metrics.d.ts.map +1 -1
- package/dist/lib/module.d.ts +4 -4
- 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} +7 -7
- package/dist/lib/mutate-request.d.ts.map +1 -0
- package/dist/lib/types.d.ts +48 -55
- package/dist/lib/types.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 +610 -354
- package/dist/lib.js.map +4 -4
- 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 +29 -27
- package/src/cli.ts +2 -11
- package/src/lib/assets/deploy.ts +179 -0
- package/src/lib/assets/index.ts +53 -0
- package/src/lib/assets/loader.ts +41 -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 +80 -68
- package/src/lib/controller.ts +199 -99
- package/src/lib/errors.ts +20 -0
- 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 +21 -15
- package/src/lib/k8s/upstream.ts +5 -1
- package/src/lib/logger.ts +14 -125
- package/src/lib/metrics.ts +86 -29
- package/src/lib/module.ts +32 -16
- package/src/lib/{processor.ts → mutate-processor.ts} +39 -28
- package/src/lib/{request.ts → mutate-request.ts} +26 -13
- package/src/lib/types.ts +54 -60
- package/src/lib/validate-processor.ts +76 -0
- package/src/lib/validate-request.ts +106 -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,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { expect, it } from "@jest/globals";
|
|
5
|
+
import { loadYaml } from "@kubernetes/client-node";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
|
|
10
|
+
import { cwd } from "./entrypoint.test";
|
|
11
|
+
|
|
12
|
+
export function peprBuild() {
|
|
13
|
+
it("should successfully build the Pepr project", async () => {
|
|
14
|
+
execSync("npx pepr build", { cwd: cwd, stdio: "inherit" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should generate produce the K8s yaml file", async () => {
|
|
18
|
+
await fs.access(resolve(cwd, "dist", "pepr-module-static-test.yaml"));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should generate the zarf.yaml f", async () => {
|
|
22
|
+
await fs.access(resolve(cwd, "dist", "zarf.yaml"));
|
|
23
|
+
await validateZarfYaml();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function validateZarfYaml() {
|
|
28
|
+
// Get the version of the pepr binary
|
|
29
|
+
const peprVer = execSync("npx pepr --version", { cwd }).toString().trim();
|
|
30
|
+
|
|
31
|
+
// Read the generated yaml files
|
|
32
|
+
const k8sYaml = await fs.readFile(resolve(cwd, "dist", "pepr-module-static-test.yaml"), "utf8");
|
|
33
|
+
const zarfYAML = await fs.readFile(resolve(cwd, "dist", "zarf.yaml"), "utf8");
|
|
34
|
+
|
|
35
|
+
// The expected image name
|
|
36
|
+
const expectedImage = `ghcr.io/defenseunicorns/pepr/controller:v${peprVer}`;
|
|
37
|
+
|
|
38
|
+
// The expected zarf yaml contents
|
|
39
|
+
const expectedZarfYaml = {
|
|
40
|
+
kind: "ZarfPackageConfig",
|
|
41
|
+
metadata: {
|
|
42
|
+
name: "pepr-static-test",
|
|
43
|
+
description: "Pepr Module: A test module for Pepr",
|
|
44
|
+
url: "https://github.com/defenseunicorns/pepr",
|
|
45
|
+
version: "0.0.1",
|
|
46
|
+
},
|
|
47
|
+
components: [
|
|
48
|
+
{
|
|
49
|
+
name: "module",
|
|
50
|
+
required: true,
|
|
51
|
+
manifests: [
|
|
52
|
+
{
|
|
53
|
+
name: "module",
|
|
54
|
+
namespace: "pepr-system",
|
|
55
|
+
files: ["pepr-module-static-test.yaml"],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
images: [expectedImage],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Check the generated zarf yaml
|
|
64
|
+
const actualZarfYaml = loadYaml(zarfYAML);
|
|
65
|
+
expect(actualZarfYaml).toEqual(expectedZarfYaml);
|
|
66
|
+
|
|
67
|
+
// Check the generated k8s yaml
|
|
68
|
+
expect(k8sYaml).toMatch(`image: ${expectedImage}`);
|
|
69
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from "@jest/globals";
|
|
5
|
+
import { execSync, spawnSync } from "child_process";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
|
|
8
|
+
import { cwd } from "./entrypoint.test";
|
|
9
|
+
import { waitForConfigMap, waitForDeploymentReady, waitForNamespace, waitForSecret } from "./k8s";
|
|
10
|
+
|
|
11
|
+
export function peprDeploy() {
|
|
12
|
+
it("should deploy the Pepr controller into the test cluster", async () => {
|
|
13
|
+
execSync("npx pepr deploy -i pepr:dev --confirm", { cwd, stdio: "inherit" });
|
|
14
|
+
|
|
15
|
+
// Wait for the deployment to be ready
|
|
16
|
+
await waitForDeploymentReady("pepr-system", "pepr-static-test");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
cleanupSamples();
|
|
20
|
+
|
|
21
|
+
it("should perform validation of resources applied to the test cluster", testValidate);
|
|
22
|
+
|
|
23
|
+
describe("should perform mutation of resources applied to the test cluster", testMutate);
|
|
24
|
+
|
|
25
|
+
cleanupSamples();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cleanupSamples() {
|
|
29
|
+
try {
|
|
30
|
+
// Remove the sample yaml for the HelloPepr capability
|
|
31
|
+
execSync("kubectl delete -f hello-pepr.samples.yaml --ignore-not-found", {
|
|
32
|
+
cwd: resolve(cwd, "capabilities"),
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
});
|
|
35
|
+
} catch (e) {
|
|
36
|
+
// Ignore errors
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function testValidate() {
|
|
41
|
+
// Apply the sample yaml for the HelloPepr capability
|
|
42
|
+
const applyOut = spawnSync("kubectl apply -f hello-pepr.samples.yaml", {
|
|
43
|
+
shell: true, // Run command in a shell
|
|
44
|
+
encoding: "utf-8", // Encode result as string
|
|
45
|
+
cwd: resolve(cwd, "capabilities"),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { stderr, status } = applyOut;
|
|
49
|
+
|
|
50
|
+
// Validation should return an error
|
|
51
|
+
expect(status).toBe(1);
|
|
52
|
+
|
|
53
|
+
// Check if the expected lines are in the output
|
|
54
|
+
const expected = [
|
|
55
|
+
`Error from server: error when creating "hello-pepr.samples.yaml": `,
|
|
56
|
+
`admission webhook "pepr-static-test.pepr.dev" denied the request: `,
|
|
57
|
+
`No evil CM annotations allowed.\n`,
|
|
58
|
+
].join("");
|
|
59
|
+
expect(stderr).toBe(expected);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function testMutate() {
|
|
63
|
+
it("should mutate the namespace", async () => {
|
|
64
|
+
// Wait for the namespace to be created
|
|
65
|
+
const ns = await waitForNamespace("pepr-demo");
|
|
66
|
+
|
|
67
|
+
// Check if the namespace has the correct labels and annotations
|
|
68
|
+
expect(ns.metadata?.labels).toEqual({
|
|
69
|
+
"keep-me": "please",
|
|
70
|
+
"kubernetes.io/metadata.name": "pepr-demo",
|
|
71
|
+
});
|
|
72
|
+
expect(ns.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should mutate example-1", async () => {
|
|
76
|
+
const cm1 = await waitForConfigMap("pepr-demo", "example-1");
|
|
77
|
+
expect(cm1.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
78
|
+
expect(cm1.metadata?.annotations?.["pepr.dev"]).toBe("annotations-work-too");
|
|
79
|
+
expect(cm1.metadata?.labels?.["pepr"]).toBe("was-here");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should mutate example-2", async () => {
|
|
83
|
+
const cm2 = await waitForConfigMap("pepr-demo", "example-2");
|
|
84
|
+
expect(cm2.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
85
|
+
expect(cm2.metadata?.annotations?.["pepr.dev"]).toBe("annotations-work-too");
|
|
86
|
+
expect(cm2.metadata?.labels?.["pepr"]).toBe("was-here");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should mutate example-3", async () => {
|
|
90
|
+
const cm3 = await waitForConfigMap("pepr-demo", "example-3");
|
|
91
|
+
|
|
92
|
+
expect(cm3.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
93
|
+
expect(cm3.metadata?.annotations?.["pepr.dev"]).toBe("making-waves");
|
|
94
|
+
expect(cm3.data).toEqual({ key: "ex-3-val", username: "system:admin" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should mutate example-4", async () => {
|
|
98
|
+
const cm4 = await waitForConfigMap("pepr-demo", "example-4");
|
|
99
|
+
expect(cm4.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
100
|
+
expect(cm4.metadata?.labels?.["pepr.dev/first"]).toBe("true");
|
|
101
|
+
expect(cm4.metadata?.labels?.["pepr.dev/second"]).toBe("true");
|
|
102
|
+
expect(cm4.metadata?.labels?.["pepr.dev/third"]).toBe("true");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should mutate example-4a", async () => {
|
|
106
|
+
const cm4a = await waitForConfigMap("pepr-demo-2", "example-4a");
|
|
107
|
+
expect(cm4a.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
108
|
+
expect(cm4a.metadata?.labels?.["pepr.dev/first"]).toBe("true");
|
|
109
|
+
expect(cm4a.metadata?.labels?.["pepr.dev/second"]).toBe("true");
|
|
110
|
+
expect(cm4a.metadata?.labels?.["pepr.dev/third"]).toBe("true");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should mutate example-5", async () => {
|
|
114
|
+
const cm5 = await waitForConfigMap("pepr-demo", "example-5");
|
|
115
|
+
|
|
116
|
+
expect(cm5.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
117
|
+
expect(cm5.data?.["chuck-says"]).toBeTruthy();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should mutate secret-1", async () => {
|
|
121
|
+
const s1 = await waitForSecret("pepr-demo", "secret-1");
|
|
122
|
+
|
|
123
|
+
expect(s1.metadata?.annotations?.["static-test.pepr.dev/hello-pepr"]).toBe("succeeded");
|
|
124
|
+
expect(s1.data?.["example"]).toBe("dW5pY29ybiBtYWdpYyAtIG1vZGlmaWVkIGJ5IFBlcHI=");
|
|
125
|
+
expect(s1.data?.["magic"]).toBe("Y2hhbmdlLXdpdGhvdXQtZW5jb2Rpbmc=");
|
|
126
|
+
expect(s1.data?.["binary-data"]).toBe(
|
|
127
|
+
"iCZQUg8xYucNUqD+8lyl2YcKjYYygvTtiDSEV9b9WKUkxSSLFJTgIWMJ9GcFFYs4T9JCdda51u74jfq8yHzRuEASl60EdTS/NfWgIIFTGqcNRfqMw+vgpyTMmCyJVaJEDFq6AA==",
|
|
128
|
+
);
|
|
129
|
+
expect(s1.data?.["ascii-with-white-space"]).toBe(
|
|
130
|
+
"VGhpcyBpcyBzb21lIHJhbmRvbSB0ZXh0OgoKICAgIC0gd2l0aCBsaW5lIGJyZWFrcwogICAgLSBhbmQgdGFicw==",
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { afterAll, expect, it } from "@jest/globals";
|
|
5
|
+
import { KubeConfig } from "@kubernetes/client-node";
|
|
6
|
+
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
|
7
|
+
import { Agent } from "https";
|
|
8
|
+
import { RequestInit } from "node-fetch";
|
|
9
|
+
|
|
10
|
+
import { fetch } from "../src/lib/fetch";
|
|
11
|
+
import { cwd } from "./entrypoint.test";
|
|
12
|
+
|
|
13
|
+
const kc = new KubeConfig();
|
|
14
|
+
kc.loadFromDefault();
|
|
15
|
+
|
|
16
|
+
let expectedLines = [
|
|
17
|
+
"Establishing connection to Kubernetes",
|
|
18
|
+
"Capability hello-pepr registered",
|
|
19
|
+
"Mutate Action configured for CREATE",
|
|
20
|
+
"Validate Action configured for CREATE",
|
|
21
|
+
"Server listening on port 3000",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function peprDev() {
|
|
25
|
+
let cmd: ChildProcessWithoutNullStreams;
|
|
26
|
+
let success = false;
|
|
27
|
+
|
|
28
|
+
it("should start the Pepr dev server", () => {
|
|
29
|
+
cmd = spawn("npx", ["pepr", "dev", "--confirm"], { cwd });
|
|
30
|
+
|
|
31
|
+
// This command should not exit on its own
|
|
32
|
+
cmd.on("close", code => {
|
|
33
|
+
if (!success) {
|
|
34
|
+
throw new Error(`Command exited with code ${code}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Log stderr
|
|
39
|
+
cmd.stderr.on("data", data => {
|
|
40
|
+
if (!success) {
|
|
41
|
+
console.error(`stderr: ${data}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Reject on error
|
|
46
|
+
cmd.on("error", error => {
|
|
47
|
+
if (!success) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should be properly configured by the test module", done => {
|
|
54
|
+
cmd.stdout.on("data", (data: Buffer) => {
|
|
55
|
+
if (success) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convert buffer to string
|
|
60
|
+
const strData = data.toString();
|
|
61
|
+
console.log(strData);
|
|
62
|
+
|
|
63
|
+
// Check if any expected lines are found
|
|
64
|
+
expectedLines = expectedLines.filter(expectedLine => {
|
|
65
|
+
// Check if the expected line is found in the output, ignoring whitespace
|
|
66
|
+
return !strData.replace(/\s+/g, " ").includes(expectedLine);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// If all expected lines are found, resolve the promise
|
|
70
|
+
if (expectedLines.length < 1) {
|
|
71
|
+
// Abort all further processing
|
|
72
|
+
success = true;
|
|
73
|
+
|
|
74
|
+
// Finish the test
|
|
75
|
+
done();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should protect the controller endpoint with an API token", async () => {
|
|
81
|
+
await validateAPIKey();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should expose Prometheus metrics", async () => {
|
|
85
|
+
const metrics = await validateMetrics();
|
|
86
|
+
expect(metrics).toMatch("pepr_Validate");
|
|
87
|
+
expect(metrics).toMatch("pepr_Mutate");
|
|
88
|
+
expect(metrics).toMatch("pepr_errors");
|
|
89
|
+
expect(metrics).toMatch("pepr_alerts");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterAll(() => {
|
|
93
|
+
// Close or destroy the streams
|
|
94
|
+
if (cmd.stdin) {
|
|
95
|
+
cmd.stdin.end();
|
|
96
|
+
}
|
|
97
|
+
if (cmd.stdout) {
|
|
98
|
+
cmd.stdout.destroy();
|
|
99
|
+
}
|
|
100
|
+
if (cmd.stderr) {
|
|
101
|
+
cmd.stderr.destroy();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Remove the event listeners
|
|
105
|
+
cmd.removeAllListeners("close");
|
|
106
|
+
cmd.removeAllListeners("error");
|
|
107
|
+
|
|
108
|
+
// Kill the process
|
|
109
|
+
cmd.kill(9);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function validateAPIKey() {
|
|
114
|
+
const base = "https://localhost:3000/mutate/";
|
|
115
|
+
|
|
116
|
+
const fetchOpts: RequestInit = {
|
|
117
|
+
agent: new Agent({
|
|
118
|
+
// Avoid tls issues for self-signed certs
|
|
119
|
+
rejectUnauthorized: false,
|
|
120
|
+
}),
|
|
121
|
+
method: "POST",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Test api token validation
|
|
125
|
+
const evilToken = await fetch(`${base}evil-token`, fetchOpts);
|
|
126
|
+
|
|
127
|
+
// Test for empty api token
|
|
128
|
+
const emptyToken = await fetch(base, fetchOpts);
|
|
129
|
+
|
|
130
|
+
if (evilToken.status !== 401) {
|
|
131
|
+
throw new Error("Expected evil token to return 401");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (emptyToken.status !== 404) {
|
|
135
|
+
throw new Error("Expected empty token to return 404");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function validateMetrics() {
|
|
140
|
+
const metricsEndpoint = "https://localhost:3000/metrics";
|
|
141
|
+
|
|
142
|
+
const fetchOpts: RequestInit = {
|
|
143
|
+
agent: new Agent({
|
|
144
|
+
// Avoid tls issues for self-signed certs
|
|
145
|
+
rejectUnauthorized: false,
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
const metricsOk = await fetch<string>(metricsEndpoint, fetchOpts);
|
|
149
|
+
|
|
150
|
+
if (metricsOk.status !== 200) {
|
|
151
|
+
throw new Error("Expected metrics ok to return a 200");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return metricsOk.data;
|
|
155
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { it } from "@jest/globals";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
|
|
7
|
+
import { cwd } from "./entrypoint.test";
|
|
8
|
+
|
|
9
|
+
export function peprFormat() {
|
|
10
|
+
it("should format the Pepr project", () => {
|
|
11
|
+
execSync("npx pepr format", { cwd, stdio: "inherit" });
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { it } from "@jest/globals";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
|
|
7
|
+
export function peprInit() {
|
|
8
|
+
it("should create a new Pepr project", () => {
|
|
9
|
+
const peprAlias = "file:pepr-0.0.0-development.tgz";
|
|
10
|
+
execSync(`TEST_MODE=true npx --yes ${peprAlias} init`, { stdio: "inherit" });
|
|
11
|
+
});
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -9,19 +9,20 @@
|
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=18.0.0"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.13.1",
|
|
13
13
|
"main": "dist/lib.js",
|
|
14
14
|
"types": "dist/lib.d.ts",
|
|
15
15
|
"scripts": {
|
|
16
|
-
"
|
|
16
|
+
"gen-data-json": "node hack/build-template-data.js",
|
|
17
|
+
"prebuild": "rm -fr dist/* && npm run gen-data-json",
|
|
17
18
|
"build": "tsc && node build.mjs",
|
|
18
|
-
"test": "npm run test:unit && npm run test:
|
|
19
|
-
"test:unit": "npm run
|
|
20
|
-
"test:
|
|
21
|
-
"test:
|
|
22
|
-
"test:
|
|
23
|
-
"test:
|
|
24
|
-
"test:
|
|
19
|
+
"test": "npm run test:unit && npm run test:journey",
|
|
20
|
+
"test:unit": "npm run gen-data-json && jest src --coverage",
|
|
21
|
+
"test:journey": "npm run test:journey:k3d && npm run test:journey:build && npm run test:journey:image && npm run test:journey:run",
|
|
22
|
+
"test:journey:k3d": "k3d cluster delete pepr-dev && k3d cluster create pepr-dev --k3s-arg '--debug@server:0'",
|
|
23
|
+
"test:journey:build": "npm run build && npm pack",
|
|
24
|
+
"test:journey:image": "docker buildx build --tag pepr:dev . && k3d image import pepr:dev -c pepr-dev",
|
|
25
|
+
"test:journey:run": "jest journey/entrypoint.test.ts",
|
|
25
26
|
"format:check": "eslint src && prettier src --check",
|
|
26
27
|
"format:fix": "eslint src --fix && prettier src --write"
|
|
27
28
|
},
|
|
@@ -30,36 +31,37 @@
|
|
|
30
31
|
"express": "4.18.2",
|
|
31
32
|
"fast-json-patch": "3.1.1",
|
|
32
33
|
"http-status-codes": "2.2.0",
|
|
33
|
-
"node-fetch": "2.
|
|
34
|
-
"
|
|
34
|
+
"node-fetch": "2.7.0",
|
|
35
|
+
"pino": "8.15.0",
|
|
36
|
+
"pino-pretty": "10.2.0",
|
|
37
|
+
"prom-client": "14.2.0",
|
|
35
38
|
"ramda": "0.29.0"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@
|
|
41
|
+
"@jest/globals": "29.6.4",
|
|
42
|
+
"@types/eslint": "8.44.2",
|
|
39
43
|
"@types/express": "4.17.17",
|
|
44
|
+
"@types/node": "18.x.x",
|
|
40
45
|
"@types/node-fetch": "2.6.4",
|
|
41
46
|
"@types/node-forge": "1.3.4",
|
|
42
|
-
"@types/prettier": "
|
|
47
|
+
"@types/prettier": "3.0.0",
|
|
43
48
|
"@types/prompts": "2.4.4",
|
|
44
49
|
"@types/ramda": "0.29.3",
|
|
45
|
-
"@types/uuid": "9.0.
|
|
46
|
-
"
|
|
47
|
-
"nock": "13.3.
|
|
50
|
+
"@types/uuid": "9.0.3",
|
|
51
|
+
"jest": "29.6.4",
|
|
52
|
+
"nock": "13.3.3",
|
|
53
|
+
"ts-jest": "29.1.1"
|
|
48
54
|
},
|
|
49
55
|
"peerDependencies": {
|
|
50
|
-
"@typescript-eslint/eslint-plugin": "5.
|
|
51
|
-
"@typescript-eslint/parser": "5.
|
|
52
|
-
"commander": "
|
|
53
|
-
"esbuild": "0.
|
|
54
|
-
"eslint": "8.
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "6.5.0",
|
|
57
|
+
"@typescript-eslint/parser": "6.5.0",
|
|
58
|
+
"commander": "11.0.0",
|
|
59
|
+
"esbuild": "0.19.2",
|
|
60
|
+
"eslint": "8.48.0",
|
|
55
61
|
"node-forge": "1.3.1",
|
|
56
|
-
"prettier": "
|
|
62
|
+
"prettier": "3.0.3",
|
|
57
63
|
"prompts": "2.4.2",
|
|
58
|
-
"typescript": "5.
|
|
64
|
+
"typescript": "5.2.2",
|
|
59
65
|
"uuid": "9.0.0"
|
|
60
|
-
},
|
|
61
|
-
"ava": {
|
|
62
|
-
"failFast": true,
|
|
63
|
-
"verbose": true
|
|
64
66
|
}
|
|
65
67
|
}
|
package/src/cli.ts
CHANGED
|
@@ -12,14 +12,9 @@ import init from "./cli/init/index";
|
|
|
12
12
|
import { version } from "./cli/init/templates";
|
|
13
13
|
import { RootCmd } from "./cli/root";
|
|
14
14
|
import update from "./cli/update";
|
|
15
|
-
import { Log } from "./lib";
|
|
16
15
|
|
|
17
16
|
if (process.env.npm_lifecycle_event !== "npx") {
|
|
18
|
-
|
|
19
|
-
"Pepr should be run via `npx pepr <command>` instead of `pepr <command>`.",
|
|
20
|
-
"npx required"
|
|
21
|
-
);
|
|
22
|
-
process.exit(1);
|
|
17
|
+
console.warn("Pepr should be run via `npx pepr <command>` instead of `pepr <command>`.");
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
const program = new RootCmd();
|
|
@@ -32,7 +27,7 @@ program
|
|
|
32
27
|
console.log(banner);
|
|
33
28
|
program.help();
|
|
34
29
|
} else {
|
|
35
|
-
|
|
30
|
+
console.error(`Invalid command '${program.args.join(" ")}'\n`);
|
|
36
31
|
program.outputHelp();
|
|
37
32
|
process.exitCode = 1;
|
|
38
33
|
}
|
|
@@ -45,8 +40,4 @@ dev(program);
|
|
|
45
40
|
update(program);
|
|
46
41
|
format(program);
|
|
47
42
|
|
|
48
|
-
// @todo: finish/re-evaluate these commands
|
|
49
|
-
// test(program);
|
|
50
|
-
// capability(program);
|
|
51
|
-
|
|
52
43
|
program.parse();
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AdmissionregistrationV1Api as AdmissionRegV1API,
|
|
6
|
+
AppsV1Api,
|
|
7
|
+
CoreV1Api,
|
|
8
|
+
HttpError,
|
|
9
|
+
KubeConfig,
|
|
10
|
+
RbacAuthorizationV1Api,
|
|
11
|
+
} from "@kubernetes/client-node";
|
|
12
|
+
import crypto from "crypto";
|
|
13
|
+
import { promises as fs } from "fs";
|
|
14
|
+
|
|
15
|
+
import { Assets } from ".";
|
|
16
|
+
import Log from "../logger";
|
|
17
|
+
import { apiTokenSecret, service, tlsSecret } from "./networking";
|
|
18
|
+
import { deployment, moduleSecret, namespace } from "./pods";
|
|
19
|
+
import { clusterRole, clusterRoleBinding, serviceAccount } from "./rbac";
|
|
20
|
+
import { webhookConfig } from "./webhooks";
|
|
21
|
+
|
|
22
|
+
export async function deploy(assets: Assets, webhookTimeout?: number) {
|
|
23
|
+
Log.info("Establishing connection to Kubernetes");
|
|
24
|
+
|
|
25
|
+
const peprNS = "pepr-system";
|
|
26
|
+
const { name, host, path } = assets;
|
|
27
|
+
|
|
28
|
+
// Deploy the resources using the k8s API
|
|
29
|
+
const kubeConfig = new KubeConfig();
|
|
30
|
+
kubeConfig.loadFromDefault();
|
|
31
|
+
|
|
32
|
+
const coreV1Api = kubeConfig.makeApiClient(CoreV1Api);
|
|
33
|
+
const admissionApi = kubeConfig.makeApiClient(AdmissionRegV1API);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
Log.info("Checking for namespace");
|
|
37
|
+
await coreV1Api.readNamespace(peprNS);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
40
|
+
Log.info("Creating namespace");
|
|
41
|
+
await coreV1Api.createNamespace(namespace);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Create the mutating webhook configuration if it is needed
|
|
45
|
+
const mutateWebhook = await webhookConfig(assets, "mutate", webhookTimeout);
|
|
46
|
+
if (mutateWebhook) {
|
|
47
|
+
try {
|
|
48
|
+
Log.info("Creating mutating webhook");
|
|
49
|
+
await admissionApi.createMutatingWebhookConfiguration(mutateWebhook);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
52
|
+
Log.info("Removing and re-creating mutating webhook");
|
|
53
|
+
await admissionApi.deleteMutatingWebhookConfiguration(mutateWebhook.metadata?.name ?? "");
|
|
54
|
+
await admissionApi.createMutatingWebhookConfiguration(mutateWebhook);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create the validating webhook configuration if it is needed
|
|
59
|
+
const validateWebhook = await webhookConfig(assets, "validate", webhookTimeout);
|
|
60
|
+
if (validateWebhook) {
|
|
61
|
+
try {
|
|
62
|
+
Log.info("Creating validating webhook");
|
|
63
|
+
await admissionApi.createValidatingWebhookConfiguration(validateWebhook);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
66
|
+
Log.info("Removing and re-creating validating webhook");
|
|
67
|
+
await admissionApi.deleteValidatingWebhookConfiguration(validateWebhook.metadata?.name ?? "");
|
|
68
|
+
await admissionApi.createValidatingWebhookConfiguration(validateWebhook);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If a host is specified, we don't need to deploy the rest of the resources
|
|
73
|
+
if (host) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!path) {
|
|
78
|
+
throw new Error("No code provided");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const code = await fs.readFile(path);
|
|
82
|
+
|
|
83
|
+
const hash = crypto.createHash("sha256").update(code).digest("hex");
|
|
84
|
+
|
|
85
|
+
const appsApi = kubeConfig.makeApiClient(AppsV1Api);
|
|
86
|
+
const rbacApi = kubeConfig.makeApiClient(RbacAuthorizationV1Api);
|
|
87
|
+
|
|
88
|
+
const crb = clusterRoleBinding(name);
|
|
89
|
+
try {
|
|
90
|
+
Log.info("Creating cluster role binding");
|
|
91
|
+
await rbacApi.createClusterRoleBinding(crb);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
94
|
+
Log.info("Removing and re-creating cluster role binding");
|
|
95
|
+
await rbacApi.deleteClusterRoleBinding(crb.metadata?.name ?? "");
|
|
96
|
+
await rbacApi.createClusterRoleBinding(crb);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const cr = clusterRole(name);
|
|
100
|
+
try {
|
|
101
|
+
Log.info("Creating cluster role");
|
|
102
|
+
await rbacApi.createClusterRole(cr);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
105
|
+
Log.info("Removing and re-creating the cluster role");
|
|
106
|
+
try {
|
|
107
|
+
await rbacApi.deleteClusterRole(cr.metadata?.name ?? "");
|
|
108
|
+
await rbacApi.createClusterRole(cr);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sa = serviceAccount(name);
|
|
115
|
+
try {
|
|
116
|
+
Log.info("Creating service account");
|
|
117
|
+
await coreV1Api.createNamespacedServiceAccount(peprNS, sa);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
120
|
+
Log.info("Removing and re-creating service account");
|
|
121
|
+
await coreV1Api.deleteNamespacedServiceAccount(sa.metadata?.name ?? "", peprNS);
|
|
122
|
+
await coreV1Api.createNamespacedServiceAccount(peprNS, sa);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const mod = moduleSecret(name, code, hash);
|
|
126
|
+
try {
|
|
127
|
+
Log.info("Creating module secret");
|
|
128
|
+
await coreV1Api.createNamespacedSecret(peprNS, mod);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
131
|
+
Log.info("Removing and re-creating module secret");
|
|
132
|
+
await coreV1Api.deleteNamespacedSecret(mod.metadata?.name ?? "", peprNS);
|
|
133
|
+
await coreV1Api.createNamespacedSecret(peprNS, mod);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const svc = service(name);
|
|
137
|
+
try {
|
|
138
|
+
Log.info("Creating service");
|
|
139
|
+
await coreV1Api.createNamespacedService(peprNS, svc);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
142
|
+
Log.info("Removing and re-creating service");
|
|
143
|
+
await coreV1Api.deleteNamespacedService(svc.metadata?.name ?? "", peprNS);
|
|
144
|
+
await coreV1Api.createNamespacedService(peprNS, svc);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tls = tlsSecret(name, assets.tls);
|
|
148
|
+
try {
|
|
149
|
+
Log.info("Creating TLS secret");
|
|
150
|
+
await coreV1Api.createNamespacedSecret(peprNS, tls);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
153
|
+
Log.info("Removing and re-creating TLS secret");
|
|
154
|
+
await coreV1Api.deleteNamespacedSecret(tls.metadata?.name ?? "", peprNS);
|
|
155
|
+
await coreV1Api.createNamespacedSecret(peprNS, tls);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const apiToken = apiTokenSecret(name, assets.apiToken);
|
|
159
|
+
try {
|
|
160
|
+
Log.info("Creating API token secret");
|
|
161
|
+
await coreV1Api.createNamespacedSecret(peprNS, apiToken);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
164
|
+
Log.info("Removing and re-creating API token secret");
|
|
165
|
+
await coreV1Api.deleteNamespacedSecret(apiToken.metadata?.name ?? "", peprNS);
|
|
166
|
+
await coreV1Api.createNamespacedSecret(peprNS, apiToken);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const dep = deployment(assets, hash);
|
|
170
|
+
try {
|
|
171
|
+
Log.info("Creating deployment");
|
|
172
|
+
await appsApi.createNamespacedDeployment(peprNS, dep);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
Log.debug(e instanceof HttpError ? e.body : e);
|
|
175
|
+
Log.info("Removing and re-creating deployment");
|
|
176
|
+
await appsApi.deleteNamespacedDeployment(dep.metadata?.name ?? "", peprNS);
|
|
177
|
+
await appsApi.createNamespacedDeployment(peprNS, dep);
|
|
178
|
+
}
|
|
179
|
+
}
|