pepr 0.33.0 → 0.34.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.
Files changed (140) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/banner.d.ts +2 -0
  3. package/dist/cli/banner.d.ts.map +1 -0
  4. package/dist/cli/build.d.ts +19 -0
  5. package/dist/cli/build.d.ts.map +1 -0
  6. package/dist/cli/deploy.d.ts +3 -0
  7. package/dist/cli/deploy.d.ts.map +1 -0
  8. package/dist/cli/dev.d.ts +3 -0
  9. package/dist/cli/dev.d.ts.map +1 -0
  10. package/dist/cli/format.d.ts +9 -0
  11. package/dist/cli/format.d.ts.map +1 -0
  12. package/dist/cli/init/index.d.ts +3 -0
  13. package/dist/cli/init/index.d.ts.map +1 -0
  14. package/dist/cli/init/templates.d.ts +196 -0
  15. package/dist/cli/init/templates.d.ts.map +1 -0
  16. package/dist/cli/init/utils.d.ts +21 -0
  17. package/dist/cli/init/utils.d.ts.map +1 -0
  18. package/dist/cli/init/utils.test.d.ts +2 -0
  19. package/dist/cli/init/utils.test.d.ts.map +1 -0
  20. package/dist/cli/init/walkthrough.d.ts +8 -0
  21. package/dist/cli/init/walkthrough.d.ts.map +1 -0
  22. package/dist/cli/init/walkthrough.test.d.ts +2 -0
  23. package/dist/cli/init/walkthrough.test.d.ts.map +1 -0
  24. package/dist/cli/kfc.d.ts +3 -0
  25. package/dist/cli/kfc.d.ts.map +1 -0
  26. package/dist/cli/monitor.d.ts +3 -0
  27. package/dist/cli/monitor.d.ts.map +1 -0
  28. package/dist/cli/root.d.ts +5 -0
  29. package/dist/cli/root.d.ts.map +1 -0
  30. package/dist/cli/update.d.ts +3 -0
  31. package/dist/cli/update.d.ts.map +1 -0
  32. package/dist/cli/uuid.d.ts +3 -0
  33. package/dist/cli/uuid.d.ts.map +1 -0
  34. package/dist/cli.js +68 -38
  35. package/dist/controller.js +1 -2
  36. package/dist/fixtures/loader.d.ts +5 -0
  37. package/dist/fixtures/loader.d.ts.map +1 -0
  38. package/dist/lib/assets/helm.d.ts +1 -0
  39. package/dist/lib/assets/helm.d.ts.map +1 -1
  40. package/dist/lib/assets/helm.test.d.ts +2 -0
  41. package/dist/lib/assets/helm.test.d.ts.map +1 -0
  42. package/dist/lib/assets/index.d.ts.map +1 -1
  43. package/dist/lib/assets/pods.d.ts +3 -0
  44. package/dist/lib/assets/pods.d.ts.map +1 -1
  45. package/dist/lib/assets/pods.test.d.ts +2 -0
  46. package/dist/lib/assets/pods.test.d.ts.map +1 -0
  47. package/dist/lib/assets/yaml.d.ts.map +1 -1
  48. package/dist/lib/errors.test.d.ts +2 -0
  49. package/dist/lib/errors.test.d.ts.map +1 -0
  50. package/dist/lib/filter.test.d.ts +2 -0
  51. package/dist/lib/filter.test.d.ts.map +1 -0
  52. package/dist/lib/helpers.d.ts +0 -5
  53. package/dist/lib/helpers.d.ts.map +1 -1
  54. package/dist/lib/helpers.test.d.ts +2 -0
  55. package/dist/lib/helpers.test.d.ts.map +1 -0
  56. package/dist/lib/included-files.test.d.ts +2 -0
  57. package/dist/lib/included-files.test.d.ts.map +1 -0
  58. package/dist/lib/logger.test.d.ts +2 -0
  59. package/dist/lib/logger.test.d.ts.map +1 -0
  60. package/dist/lib/metrics.d.ts +18 -0
  61. package/dist/lib/metrics.d.ts.map +1 -1
  62. package/dist/lib/metrics.test.d.ts +2 -0
  63. package/dist/lib/metrics.test.d.ts.map +1 -0
  64. package/dist/lib/module.test.d.ts +2 -0
  65. package/dist/lib/module.test.d.ts.map +1 -0
  66. package/dist/lib/mutate-request.test.d.ts +2 -0
  67. package/dist/lib/mutate-request.test.d.ts.map +1 -0
  68. package/dist/lib/queue.test.d.ts +2 -0
  69. package/dist/lib/queue.test.d.ts.map +1 -0
  70. package/dist/lib/schedule.test.d.ts +15 -0
  71. package/dist/lib/schedule.test.d.ts.map +1 -0
  72. package/dist/lib/storage.test.d.ts +2 -0
  73. package/dist/lib/storage.test.d.ts.map +1 -0
  74. package/dist/lib/tls.test.d.ts +2 -0
  75. package/dist/lib/tls.test.d.ts.map +1 -0
  76. package/dist/lib/utils.test.d.ts +2 -0
  77. package/dist/lib/utils.test.d.ts.map +1 -0
  78. package/dist/lib/validate-request.test.d.ts +2 -0
  79. package/dist/lib/validate-request.test.d.ts.map +1 -0
  80. package/dist/lib/watch-processor.d.ts.map +1 -1
  81. package/dist/lib/watch-processor.test.d.ts +2 -0
  82. package/dist/lib/watch-processor.test.d.ts.map +1 -0
  83. package/dist/lib.js +76 -20
  84. package/dist/lib.js.map +3 -3
  85. package/dist/sdk/sdk.test.d.ts +2 -0
  86. package/dist/sdk/sdk.test.d.ts.map +1 -0
  87. package/package.json +21 -15
  88. package/src/cli/banner.ts +63 -0
  89. package/src/cli/build.ts +370 -0
  90. package/src/cli/deploy.ts +105 -0
  91. package/src/cli/dev.ts +118 -0
  92. package/src/cli/format.ts +83 -0
  93. package/src/cli/init/index.ts +99 -0
  94. package/src/cli/init/templates.ts +124 -0
  95. package/src/cli/init/utils.test.ts +28 -0
  96. package/src/cli/init/utils.ts +55 -0
  97. package/src/cli/init/walkthrough.test.ts +21 -0
  98. package/src/cli/init/walkthrough.ts +96 -0
  99. package/src/cli/kfc.ts +45 -0
  100. package/src/cli/monitor.ts +101 -0
  101. package/src/cli/root.ts +12 -0
  102. package/src/cli/update.ts +95 -0
  103. package/src/cli/uuid.ts +44 -0
  104. package/src/fixtures/data/create-pod.json +271 -0
  105. package/src/fixtures/data/delete-pod.json +271 -0
  106. package/src/fixtures/loader.ts +18 -0
  107. package/src/lib/.prettierrc +14 -0
  108. package/src/lib/assets/helm.test.ts +64 -0
  109. package/src/lib/assets/helm.ts +35 -0
  110. package/src/lib/assets/index.ts +5 -1
  111. package/src/lib/assets/pods.test.ts +553 -0
  112. package/src/lib/assets/pods.ts +14 -6
  113. package/src/lib/assets/yaml.ts +15 -15
  114. package/src/lib/controller/index.ts +2 -2
  115. package/src/lib/errors.test.ts +85 -0
  116. package/src/lib/filter.test.ts +384 -0
  117. package/src/lib/helpers.test.ts +1192 -0
  118. package/src/lib/helpers.ts +0 -17
  119. package/src/lib/included-files.test.ts +22 -0
  120. package/src/lib/logger.test.ts +18 -0
  121. package/src/lib/metrics.test.ts +132 -0
  122. package/src/lib/metrics.ts +68 -6
  123. package/src/lib/module.test.ts +126 -0
  124. package/src/lib/mutate-request.test.ts +188 -0
  125. package/src/lib/queue.test.ts +58 -0
  126. package/src/lib/schedule.test.ts +217 -0
  127. package/src/lib/storage.test.ts +203 -0
  128. package/src/lib/tls.test.ts +18 -0
  129. package/src/lib/utils.test.ts +69 -0
  130. package/src/lib/validate-request.test.ts +124 -0
  131. package/src/lib/watch-processor.test.ts +322 -0
  132. package/src/lib/watch-processor.ts +20 -4
  133. package/src/sdk/sdk.test.ts +243 -0
  134. package/src/templates/.eslintrc.json +6 -0
  135. package/.prettierignore +0 -1
  136. package/CODE_OF_CONDUCT.md +0 -133
  137. package/SECURITY.md +0 -18
  138. package/SUPPORT.md +0 -16
  139. package/codecov.yaml +0 -19
  140. package/commitlint.config.js +0 -1
@@ -6,24 +6,7 @@ import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
6
6
  import Log from "./logger";
7
7
  import { Binding, CapabilityExport } from "./types";
8
8
  import { sanitizeResourceName } from "../sdk/sdk";
9
- import { mergeDeepRight } from "ramda";
10
9
 
11
- export function mergePkgJSONEnv(env: Record<string, string>, pkgJSONEnv?: Record<string, string>) {
12
- if (!pkgJSONEnv) {
13
- return env;
14
- }
15
- // Cannot override watch mode because it is critical to deployments
16
- Object.keys(pkgJSONEnv).forEach(key => {
17
- if (key === "PEPR_WATCH_MODE") {
18
- delete pkgJSONEnv[key];
19
- }
20
- });
21
- return mergeDeepRight(env, pkgJSONEnv);
22
- }
23
-
24
- export function envMapToArray(env: Record<string, string>) {
25
- return Object.entries(env).map(([key, value]) => ({ name: key, value }));
26
- }
27
10
  export class ValidationError extends Error {}
28
11
 
29
12
  export function validateCapabilityNames(capabilities: CapabilityExport[] | undefined): void {
@@ -0,0 +1,22 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { createDockerfile } from "./included-files";
5
+ import { expect, describe, test } from "@jest/globals";
6
+ import { promises as fs } from "fs";
7
+
8
+ describe("createDockerfile", () => {
9
+ const version = "0.0.1";
10
+ const description = "Pepr supports WASM modules!";
11
+ const includedFiles = ["main.wasm", "wasm_exec.js"];
12
+ test("should create a Dockerfile.controller with the correct content", async () => {
13
+ await createDockerfile(version, description, includedFiles);
14
+
15
+ const generatedContent = await fs.readFile("Dockerfile.controller", "utf-8");
16
+ expect(generatedContent).toContain(`FROM ghcr.io/defenseunicorns/pepr/controller:v${version}`);
17
+ expect(generatedContent).toContain(`LABEL description="${description}"`);
18
+ includedFiles.forEach(file => {
19
+ expect(generatedContent).toContain(`ADD ${file} ${file}`);
20
+ });
21
+ });
22
+ });
@@ -0,0 +1,18 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
5
+
6
+ describe("Logger", () => {
7
+ beforeEach(() => {
8
+ jest.resetModules(); // Clear the cache for modules
9
+ process.env = {}; // Clear environment variables
10
+ });
11
+
12
+ it("should set log level based on LOG_LEVEL environment variable", async () => {
13
+ process.env.LOG_LEVEL = "debug";
14
+ const { default: logger } = await import("./logger");
15
+
16
+ expect(logger.level).toBe("debug");
17
+ });
18
+ });
@@ -0,0 +1,132 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { expect, test } from "@jest/globals";
5
+ import { performance } from "perf_hooks";
6
+
7
+ import { MetricsCollector } from "./metrics";
8
+
9
+ test("constructor initializes counters correctly", () => {
10
+ const collector = new MetricsCollector("testPrefix");
11
+
12
+ expect(collector).toBeTruthy();
13
+ });
14
+
15
+ test("error method increments error counter", async () => {
16
+ const collector = new MetricsCollector("testPrefix");
17
+
18
+ collector.error();
19
+
20
+ const metrics = await collector.getMetrics();
21
+ expect(metrics).toMatch(/testPrefix_errors 1/);
22
+ });
23
+
24
+ test("alert method increments alerts counter", async () => {
25
+ const collector = new MetricsCollector("testPrefix");
26
+
27
+ collector.alert();
28
+
29
+ const metrics = await collector.getMetrics();
30
+ expect(metrics).toMatch(/testPrefix_alerts 1/);
31
+ });
32
+
33
+ test("observeStart returns current timestamp", () => {
34
+ const timeBefore = performance.now();
35
+ const startTime = MetricsCollector.observeStart();
36
+ const timeAfter = performance.now();
37
+
38
+ expect(timeBefore <= startTime).toBe(true);
39
+ expect(timeAfter >= startTime).toBe(true);
40
+ });
41
+
42
+ test("observeEnd updates summary", async () => {
43
+ const collector = new MetricsCollector("testPrefix");
44
+
45
+ const startTime = MetricsCollector.observeStart();
46
+ await new Promise(resolve => setTimeout(resolve, 100)); // Delay to simulate operation
47
+ collector.observeEnd(startTime);
48
+
49
+ await new Promise(resolve => setTimeout(resolve, 100)); // Delay to simulate operation
50
+ collector.observeEnd(startTime, "validate");
51
+ collector.observeEnd(startTime, "validate");
52
+
53
+ const metrics = await collector.getMetrics();
54
+ expect(metrics).toMatch(/testPrefix_mutate_count 1/);
55
+ expect(metrics).toMatch(/testPrefix_mutate_sum \d+\.\d+/);
56
+
57
+ expect(metrics).toMatch(/testPrefix_validate_count 2/);
58
+ expect(metrics).toMatch(/testPrefix_validate_sum \d+\.\d+/);
59
+ });
60
+
61
+ test("coverage tests, with duplicate counters, default prefix (pepr) and still works properly", async () => {
62
+ const collector = new MetricsCollector();
63
+ collector.addCounter("testCounter", "testHelp");
64
+ // second one should log, but still work fine TODO: validate log
65
+ collector.addCounter("testCounter", "testHelp");
66
+ let metrics = await collector.getMetrics();
67
+ expect(metrics).toMatch(/pepr_testCounter 0/);
68
+ collector.incCounter("testCounter");
69
+ metrics = await collector.getMetrics();
70
+ expect(metrics).toMatch(/pepr_testCounter 1/);
71
+ collector.addSummary("testSummary", "testHelp");
72
+ // second one should log, but still work fine TODO: validate log
73
+ collector.addSummary("testSummary", "testHelp");
74
+ const startTime = MetricsCollector.observeStart();
75
+
76
+ await new Promise(resolve => setTimeout(resolve, 100)); // Delay to simulate operation
77
+ collector.observeEnd(startTime, "testSummary");
78
+ collector.observeEnd(startTime, "testSummary");
79
+ metrics = await collector.getMetrics();
80
+ expect(metrics).toMatch(/pepr_testSummary_count 2/);
81
+ expect(metrics).toMatch(/pepr_testSummary_sum \d+\.\d+/);
82
+ });
83
+
84
+ test("incCacheMiss increments cache miss gauge", async () => {
85
+ const collector = new MetricsCollector("testPrefix");
86
+
87
+ collector.incCacheMiss("window1");
88
+
89
+ const metrics = await collector.getMetrics();
90
+ expect(metrics).toMatch(/testPrefix_cache_miss{window="window1"} 1/);
91
+ });
92
+
93
+ test("incRetryCount increments retry count gauge", async () => {
94
+ const collector = new MetricsCollector("testPrefix");
95
+
96
+ collector.incRetryCount("1");
97
+
98
+ const metrics = await collector.getMetrics();
99
+ expect(metrics).toMatch(/testPrefix_resync_failure_count{count="1"} 1/);
100
+ });
101
+
102
+ test("initCacheMissWindow initializes cache miss gauge to zero", async () => {
103
+ const collector = new MetricsCollector("testPrefix");
104
+
105
+ collector.initCacheMissWindow("window1");
106
+
107
+ const metrics = await collector.getMetrics();
108
+ expect(metrics).toMatch(/testPrefix_cache_miss{window="window1"} 0/);
109
+ });
110
+
111
+ test("should initialize cache miss window and maintain size limit", async () => {
112
+ process.env.PEPR_MAX_CACHE_MISS_WINDOWS = "3";
113
+ const collector = new MetricsCollector("pepr");
114
+ collector.initCacheMissWindow("window1");
115
+ collector.initCacheMissWindow("window2");
116
+ collector.initCacheMissWindow("window3");
117
+ collector.initCacheMissWindow("window4");
118
+
119
+ const metrics = await collector.getMetrics();
120
+ expect(metrics).not.toContain("window1");
121
+ expect(metrics).toContain("window4");
122
+
123
+ collector.initCacheMissWindow("window5");
124
+ collector.initCacheMissWindow("window6");
125
+ collector.initCacheMissWindow("window7");
126
+
127
+ const updatedMetrics = await collector.getMetrics();
128
+ expect(updatedMetrics).not.toContain("window4");
129
+ expect(updatedMetrics).toContain("window5");
130
+ expect(updatedMetrics).toContain("window6");
131
+ expect(updatedMetrics).toContain("window7");
132
+ });
@@ -4,7 +4,7 @@
4
4
  /* eslint-disable class-methods-use-this */
5
5
 
6
6
  import { performance } from "perf_hooks";
7
- import promClient, { Counter, Registry, Summary } from "prom-client";
7
+ import promClient, { Counter, Registry, Gauge, Summary } from "prom-client";
8
8
  import Log from "./logger";
9
9
 
10
10
  const loggingPrefix = "MetricsCollector";
@@ -14,12 +14,15 @@ interface MetricNames {
14
14
  alerts: string;
15
15
  mutate: string;
16
16
  validate: string;
17
+ cacheMiss: string;
18
+ resyncFailureCount: string;
17
19
  }
18
20
 
19
21
  interface MetricArgs {
20
22
  name: string;
21
23
  help: string;
22
24
  registers: Registry[];
25
+ labelNames?: string[];
23
26
  }
24
27
 
25
28
  /**
@@ -28,14 +31,18 @@ interface MetricArgs {
28
31
  export class MetricsCollector {
29
32
  #registry: Registry;
30
33
  #counters: Map<string, Counter<string>> = new Map();
34
+ #gauges: Map<string, Gauge<string>> = new Map();
31
35
  #summaries: Map<string, Summary<string>> = new Map();
32
36
  #prefix: string;
37
+ #cacheMissWindows: Map<string, number> = new Map();
33
38
 
34
39
  #metricNames: MetricNames = {
35
40
  errors: "errors",
36
41
  alerts: "alerts",
37
- mutate: "Mutate",
38
- validate: "Validate",
42
+ mutate: "mutate",
43
+ validate: "validate",
44
+ cacheMiss: "cache_miss",
45
+ resyncFailureCount: "resync_failure_count",
39
46
  };
40
47
 
41
48
  /**
@@ -49,15 +56,18 @@ export class MetricsCollector {
49
56
  this.addCounter(this.#metricNames.alerts, "Mutation/Validate bad api token received");
50
57
  this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
51
58
  this.addSummary(this.#metricNames.validate, "Validation operation summary");
59
+ this.addGauge(this.#metricNames.cacheMiss, "Number of cache misses per window", ["window"]);
60
+ this.addGauge(this.#metricNames.resyncFailureCount, "Number of failures per resync operation", ["count"]);
52
61
  }
53
62
 
54
63
  #getMetricName = (name: string) => `${this.#prefix}_${name}`;
55
64
 
56
- #addMetric = <T extends Counter<string> | Summary<string>>(
65
+ #addMetric = <T extends Counter<string> | Gauge<string> | Summary<string>>(
57
66
  collection: Map<string, T>,
58
67
  MetricType: new (args: MetricArgs) => T,
59
68
  name: string,
60
69
  help: string,
70
+ labelNames?: string[],
61
71
  ) => {
62
72
  if (collection.has(this.#getMetricName(name))) {
63
73
  Log.debug(`Metric for ${name} already exists`, loggingPrefix);
@@ -68,23 +78,32 @@ export class MetricsCollector {
68
78
  name: this.#getMetricName(name),
69
79
  help,
70
80
  registers: [this.#registry],
81
+ labelNames,
71
82
  });
72
83
 
73
84
  collection.set(this.#getMetricName(name), metric);
74
85
  };
75
86
 
76
87
  addCounter = (name: string, help: string) => {
77
- this.#addMetric(this.#counters, promClient.Counter, name, help);
88
+ this.#addMetric(this.#counters, promClient.Counter, name, help, []);
78
89
  };
79
90
 
80
91
  addSummary = (name: string, help: string) => {
81
- this.#addMetric(this.#summaries, promClient.Summary, name, help);
92
+ this.#addMetric(this.#summaries, promClient.Summary, name, help, []);
93
+ };
94
+
95
+ addGauge = (name: string, help: string, labelNames?: string[]) => {
96
+ this.#addMetric(this.#gauges, promClient.Gauge, name, help, labelNames);
82
97
  };
83
98
 
84
99
  incCounter = (name: string) => {
85
100
  this.#counters.get(this.#getMetricName(name))?.inc();
86
101
  };
87
102
 
103
+ incGauge = (name: string, labels?: Record<string, string>, value: number = 1) => {
104
+ this.#gauges.get(this.#getMetricName(name))?.inc(labels || {}, value);
105
+ };
106
+
88
107
  /**
89
108
  * Increments the error counter.
90
109
  */
@@ -117,4 +136,47 @@ export class MetricsCollector {
117
136
  static observeStart() {
118
137
  return performance.now();
119
138
  }
139
+
140
+ /**
141
+ * Increments the cache miss gauge for a given label.
142
+ * @param label - The label for the cache miss.
143
+ */
144
+ incCacheMiss = (window: string) => {
145
+ this.incGauge(this.#metricNames.cacheMiss, { window });
146
+ };
147
+
148
+ /**
149
+ * Increments the retry count gauge.
150
+ * @param count - The count to increment by.
151
+ */
152
+ incRetryCount = (count: string) => {
153
+ this.incGauge(this.#metricNames.resyncFailureCount, { count });
154
+ };
155
+
156
+ /**
157
+ * Initializes the cache miss gauge for a given label.
158
+ * @param label - The label for the cache miss.
159
+ */
160
+ initCacheMissWindow = (window: string) => {
161
+ this.#rollCacheMissWindows();
162
+ this.#gauges.get(this.#getMetricName(this.#metricNames.cacheMiss))?.set({ window }, 0);
163
+ this.#cacheMissWindows.set(window, 0);
164
+ };
165
+
166
+ /**
167
+ * Manages the size of the cache miss gauge map.
168
+ */
169
+ #rollCacheMissWindows = () => {
170
+ const maxCacheMissWindows = process.env.PEPR_MAX_CACHE_MISS_WINDOWS
171
+ ? parseInt(process.env.PEPR_MAX_CACHE_MISS_WINDOWS, 10)
172
+ : undefined;
173
+
174
+ if (maxCacheMissWindows !== undefined && this.#cacheMissWindows.size >= maxCacheMissWindows) {
175
+ const firstKey = this.#cacheMissWindows.keys().next().value;
176
+ this.#cacheMissWindows.delete(firstKey);
177
+ this.#gauges.get(this.#getMetricName(this.#metricNames.cacheMiss))?.remove({ window: firstKey });
178
+ }
179
+ };
120
180
  }
181
+
182
+ export const metricsCollector = new MetricsCollector("pepr");
@@ -0,0 +1,126 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { beforeEach, expect, jest, test, describe } from "@jest/globals";
5
+ import { clone } from "ramda";
6
+ import { Capability } from "./capability";
7
+ import { Schedule } from "./schedule";
8
+ import { Errors } from "./errors";
9
+ import { PackageJSON, PeprModule } from "./module";
10
+ import { CapabilityExport } from "./types";
11
+
12
+ // Mock Controller
13
+ const startServerMock = jest.fn();
14
+ jest.mock("./controller", () => {
15
+ return {
16
+ Controller: jest.fn().mockImplementation(() => {
17
+ return { startServer: startServerMock };
18
+ }),
19
+ };
20
+ });
21
+
22
+ // Reset the mocks before each test
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+
27
+ // Mock PackageJSON
28
+ const packageJSON: PackageJSON = {
29
+ description: "Test Description",
30
+ pepr: {
31
+ uuid: "20e17cf6-a2e4-46b2-b626-75d88d96c88b",
32
+ description: "Development module for pepr",
33
+ onError: "ignore",
34
+ alwaysIgnore: {
35
+ namespaces: [],
36
+ },
37
+ },
38
+ };
39
+
40
+ test("should instantiate Controller and start it with the default port", () => {
41
+ new PeprModule(packageJSON);
42
+ expect(startServerMock).toHaveBeenCalledWith(3000);
43
+ });
44
+
45
+ test("should instantiate Controller and start it with the specified port", () => {
46
+ const module = new PeprModule(packageJSON, [], { deferStart: true });
47
+ const port = Math.floor(Math.random() * 10000) + 1000;
48
+ module.start(port);
49
+ expect(startServerMock).toHaveBeenCalledWith(port);
50
+ });
51
+
52
+ test("should not start if deferStart is true", () => {
53
+ new PeprModule(packageJSON, [], { deferStart: true });
54
+ expect(startServerMock).not.toHaveBeenCalled();
55
+ });
56
+
57
+ test("should reject invalid pepr onError conditions", () => {
58
+ const cfg = clone(packageJSON);
59
+ cfg.pepr.onError = "invalidError";
60
+ expect(() => new PeprModule(cfg)).toThrow();
61
+ });
62
+
63
+ test("should allow valid pepr onError conditions", () => {
64
+ const cfg = clone(packageJSON);
65
+ cfg.pepr.onError = Errors.audit;
66
+ expect(() => new PeprModule(cfg)).not.toThrow();
67
+
68
+ cfg.pepr.onError = Errors.ignore;
69
+ expect(() => new PeprModule(cfg)).not.toThrow();
70
+
71
+ cfg.pepr.onError = Errors.reject;
72
+ expect(() => new PeprModule(cfg)).not.toThrow();
73
+ });
74
+
75
+ test("should not create a controller if PEPR_MODE is set to build", () => {
76
+ process.env.PEPR_MODE = "build";
77
+ new PeprModule(packageJSON);
78
+ expect(startServerMock).not.toHaveBeenCalled();
79
+ });
80
+
81
+ test("should send the capabilities to the parent process if PEPR_MODE is set to build", () => {
82
+ const sendMock = jest.spyOn(process, "send").mockImplementation(() => true);
83
+ process.env.PEPR_MODE = "build";
84
+
85
+ const capability = new Capability({
86
+ name: "test",
87
+ description: "test",
88
+ });
89
+
90
+ const expected: CapabilityExport = {
91
+ name: capability.name,
92
+ description: capability.description,
93
+ namespaces: capability.namespaces,
94
+ bindings: capability.bindings,
95
+ hasSchedule: capability.hasSchedule,
96
+ };
97
+
98
+ new PeprModule(packageJSON, [capability]);
99
+ expect(sendMock).toHaveBeenCalledWith([expected]);
100
+ });
101
+
102
+ describe("Capability", () => {
103
+ let capability: Capability;
104
+ let schedule: Schedule;
105
+
106
+ beforeEach(() => {
107
+ capability = new Capability({
108
+ name: "test",
109
+ description: "test",
110
+ });
111
+ schedule = {
112
+ name: "test-name",
113
+ every: 1,
114
+ unit: "seconds",
115
+ run: jest.fn(),
116
+ startTime: new Date(),
117
+ completions: 1,
118
+ };
119
+ });
120
+
121
+ test("should handle OnSchedule", () => {
122
+ const { OnSchedule } = capability;
123
+ OnSchedule(schedule);
124
+ expect(capability.hasSchedule).toBe(true);
125
+ });
126
+ });
@@ -0,0 +1,188 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { beforeEach, describe, expect, it } from "@jest/globals";
5
+ import { KubernetesObject } from "kubernetes-fluent-client";
6
+
7
+ import { Operation, AdmissionRequest } from "./k8s";
8
+ import { PeprMutateRequest } from "./mutate-request";
9
+
10
+ describe("PeprMutateRequest", () => {
11
+ let mockRequest: AdmissionRequest<KubernetesObject>;
12
+
13
+ beforeEach(() => {
14
+ mockRequest = {
15
+ operation: Operation.CREATE,
16
+ object: {
17
+ apiVersion: "v1",
18
+ kind: "Pod",
19
+ metadata: {
20
+ name: "test-pod",
21
+ labels: {
22
+ "existing-label": "true",
23
+ },
24
+ annotations: {
25
+ "existing-annotation": "true",
26
+ },
27
+ },
28
+ },
29
+ dryRun: false,
30
+ uid: "test-uid",
31
+ name: "test-pod",
32
+ kind: { group: "", version: "v1", kind: "Pod" },
33
+ resource: { group: "", version: "v1", resource: "pods" },
34
+ userInfo: {},
35
+ };
36
+ });
37
+
38
+ it("should initialize correctly for non-DELETE operations", () => {
39
+ const wrapper = new PeprMutateRequest(mockRequest);
40
+ expect(wrapper.Raw).toEqual(mockRequest.object);
41
+ });
42
+
43
+ it("should initialize correctly for DELETE operations", () => {
44
+ mockRequest = {
45
+ ...mockRequest,
46
+ operation: Operation.DELETE,
47
+ oldObject: {
48
+ apiVersion: "v1",
49
+ kind: "Pod",
50
+ metadata: {
51
+ name: "test-pod",
52
+ },
53
+ },
54
+ };
55
+
56
+ const wrapper = new PeprMutateRequest(mockRequest);
57
+ expect(wrapper.Raw).toEqual(mockRequest.oldObject);
58
+ });
59
+
60
+ it("should provide correct value for PermitSideEffects", () => {
61
+ const wrapper = new PeprMutateRequest(mockRequest);
62
+ expect(wrapper.PermitSideEffects).toEqual(true);
63
+ });
64
+
65
+ it("should provide correct value for IsDryRun", () => {
66
+ const wrapper = new PeprMutateRequest(mockRequest);
67
+ expect(wrapper.IsDryRun).toEqual(false);
68
+ });
69
+
70
+ it("should provide access to old resource", () => {
71
+ mockRequest = {
72
+ ...mockRequest,
73
+ oldObject: {
74
+ apiVersion: "v1",
75
+ kind: "Pod",
76
+ metadata: {
77
+ name: "old-test-pod",
78
+ },
79
+ },
80
+ };
81
+ const wrapper = new PeprMutateRequest(mockRequest);
82
+ expect(wrapper.OldResource).toEqual(mockRequest.oldObject);
83
+ });
84
+
85
+ it("should provide access to the request object", () => {
86
+ const wrapper = new PeprMutateRequest(mockRequest);
87
+ expect(wrapper.Request).toEqual(mockRequest);
88
+ });
89
+
90
+ it("should throw an error if the request object is not available", () => {
91
+ mockRequest = {
92
+ ...mockRequest,
93
+ object: undefined as unknown as KubernetesObject,
94
+ };
95
+
96
+ expect(() => new PeprMutateRequest(mockRequest)).toThrow("unable to load the request object into PeprRequest.RawP");
97
+ });
98
+
99
+ it("should merge the provided object with the current resource", () => {
100
+ const wrapper = new PeprMutateRequest(mockRequest);
101
+ wrapper.Merge({
102
+ metadata: {
103
+ labels: {
104
+ "test-label-2": "true",
105
+ },
106
+ },
107
+ });
108
+
109
+ expect(wrapper.Raw.metadata?.labels).toEqual({
110
+ "existing-label": "true",
111
+ "test-label-2": "true",
112
+ });
113
+ });
114
+
115
+ it("should set a label", () => {
116
+ const wrapper = new PeprMutateRequest(mockRequest);
117
+ wrapper.SetLabel("new-label", "newValue");
118
+ expect(wrapper.Raw.metadata?.labels?.["new-label"]).toEqual("newValue");
119
+ });
120
+
121
+ it("should set an annotation", () => {
122
+ const wrapper = new PeprMutateRequest(mockRequest);
123
+ wrapper.SetAnnotation("new-annotation", "newValue");
124
+ expect(wrapper.Raw.metadata?.annotations?.["new-annotation"]).toEqual("newValue");
125
+ });
126
+
127
+ it("should remove an existing label", () => {
128
+ const wrapper = new PeprMutateRequest(mockRequest);
129
+ wrapper.RemoveLabel("existing-label");
130
+ expect(wrapper.Raw.metadata?.labels?.["existing-label"]).toBeUndefined();
131
+ });
132
+
133
+ it("should remove an existing annotation", () => {
134
+ const wrapper = new PeprMutateRequest(mockRequest);
135
+ wrapper.RemoveAnnotation("existing-annotation");
136
+ expect(wrapper.Raw.metadata?.annotations?.["existing-annotation"]).toBeUndefined();
137
+ });
138
+
139
+ it("should check if a label exists", () => {
140
+ const wrapper = new PeprMutateRequest(mockRequest);
141
+ expect(wrapper.HasLabel("existing-label")).toBeTruthy();
142
+ expect(wrapper.HasLabel("non-existent-label")).toBeFalsy();
143
+ });
144
+
145
+ it("should check if an annotation exists", () => {
146
+ const wrapper = new PeprMutateRequest(mockRequest);
147
+ expect(wrapper.HasAnnotation("existing-annotation")).toBeTruthy();
148
+ expect(wrapper.HasAnnotation("non-existent-annotation")).toBeFalsy();
149
+ });
150
+
151
+ it("should set a label when metadata and labels do not exist", () => {
152
+ delete mockRequest.object.metadata;
153
+ const wrapper = new PeprMutateRequest(mockRequest);
154
+ wrapper.SetLabel("new-label", "newValue");
155
+ expect(wrapper.Raw.metadata?.labels?.["new-label"]).toEqual("newValue");
156
+ });
157
+
158
+ it("should set an annotation when metadata and annotations do not exist", () => {
159
+ delete mockRequest.object.metadata;
160
+ const wrapper = new PeprMutateRequest(mockRequest);
161
+ wrapper.SetAnnotation("new-annotation", "newValue");
162
+ expect(wrapper.Raw.metadata?.annotations?.["new-annotation"]).toEqual("newValue");
163
+ });
164
+
165
+ it("should not throw an error when removing a non-existent label", () => {
166
+ delete mockRequest.object.metadata;
167
+ const wrapper = new PeprMutateRequest(mockRequest);
168
+ expect(() => wrapper.RemoveLabel("non-existent-label")).not.toThrow();
169
+ });
170
+
171
+ it("should not throw an error when removing a non-existent annotation", () => {
172
+ delete mockRequest.object.metadata;
173
+ const wrapper = new PeprMutateRequest(mockRequest);
174
+ expect(() => wrapper.RemoveAnnotation("non-existent-annotation")).not.toThrow();
175
+ });
176
+
177
+ it("should return false for HasLabel when metadata or labels do not exist", () => {
178
+ delete mockRequest.object.metadata;
179
+ const wrapper = new PeprMutateRequest(mockRequest);
180
+ expect(wrapper.HasLabel("any-label")).toBeFalsy();
181
+ });
182
+
183
+ it("should return false for HasAnnotation when metadata or annotations do not exist", () => {
184
+ delete mockRequest.object.metadata;
185
+ const wrapper = new PeprMutateRequest(mockRequest);
186
+ expect(wrapper.HasAnnotation("any-annotation")).toBeFalsy();
187
+ });
188
+ });