pepr 0.47.0 → 0.48.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/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "!src/fixtures/**",
17
17
  "!dist/**/*.test.d.ts*"
18
18
  ],
19
- "version": "0.47.0",
19
+ "version": "0.48.1",
20
20
  "main": "dist/lib.js",
21
21
  "types": "dist/lib.d.ts",
22
22
  "scripts": {
@@ -36,11 +36,11 @@
36
36
  "test:journey-wasm": "npm run test:journey:k3d && npm run build && npm run test:journey:image && npm run test:journey:run-wasm",
37
37
  "test:journey-wasm:unicorn": "npm run test:journey:k3d && npm run build && npm run test:journey:image:unicorn && npm run test:journey:run-wasm",
38
38
  "test:journey:image": "docker buildx build --output type=docker --tag pepr:dev . && k3d image import pepr:dev -c pepr-dev",
39
- "test:journey:image:unicorn": "docker buildx build --output type=docker --tag pepr:dev $(node scripts/read-unicorn-build-args.mjs) . && k3d image import pepr:dev -c pepr-dev",
39
+ "test:journey:image:unicorn": "npm run build && docker buildx build --output type=docker --tag pepr:dev $(node scripts/read-unicorn-build-args.mjs) . && k3d image import pepr:dev -c pepr-dev",
40
40
  "test:journey:k3d": "k3d cluster delete pepr-dev && k3d cluster create pepr-dev --k3s-arg '--debug@server:0' --wait && kubectl rollout status deployment -n kube-system",
41
41
  "test:journey:run": "jest --detectOpenHandles journey/entrypoint.test.ts && npm run test:journey:upgrade",
42
42
  "test:journey:run-wasm": "jest --detectOpenHandles journey/entrypoint-wasm.test.ts",
43
- "test:journey:unicorn": "npm run test:journey:k3d && npm run build && npm run test:journey:image:unicorn && npm run test:journey:run",
43
+ "test:journey:unicorn": "npm run test:journey:k3d && npm run test:journey:image:unicorn && npm run test:journey:run",
44
44
  "test:journey:upgrade": "npm run test:journey:k3d && npm run test:journey:image && jest --detectOpenHandles journey/pepr-upgrade.test.ts",
45
45
  "test:unit": "npm run gen-data-json && jest src --coverage --detectOpenHandles --coverageDirectory=./coverage --testPathIgnorePatterns='build-artifact.test.ts'",
46
46
  "format:check": "eslint src && prettier --config .prettierrc src --check",
@@ -54,7 +54,7 @@
54
54
  "heredoc": "^1.3.1",
55
55
  "http-status-codes": "^2.3.0",
56
56
  "json-pointer": "^0.6.2",
57
- "kubernetes-fluent-client": "3.4.6",
57
+ "kubernetes-fluent-client": "3.4.10",
58
58
  "pino": "9.6.0",
59
59
  "pino-pretty": "13.0.0",
60
60
  "prom-client": "15.1.3",
@@ -7,3 +7,5 @@ export enum OnError {
7
7
  IGNORE = "ignore",
8
8
  REJECT = "reject",
9
9
  }
10
+
11
+ export const UUID_LENGTH_LIMIT = 36;
@@ -23,6 +23,7 @@ import {
23
23
  import { createDir, sanitizeName, write } from "./utils";
24
24
  import { confirm, PromptOptions, walkthrough } from "./walkthrough";
25
25
  import { ErrorList } from "../../lib/errors";
26
+ import { UUID_LENGTH_LIMIT } from "./enums";
26
27
 
27
28
  export default function (program: RootCmd): void {
28
29
  let response = {} as PromptOptions;
@@ -37,11 +38,9 @@ export default function (program: RootCmd): void {
37
38
  .option(`--errorBehavior <${ErrorList.join("|")}>`, "Set an errorBehavior.")
38
39
  .option(
39
40
  "--uuid [string]",
40
- "Unique identifier for your module with a max length of 32 characters.",
41
+ "Unique identifier for your module with a max length of 36 characters.",
41
42
  (uuid: string): string => {
42
- const uuidLengthLimit = 36;
43
- // length of generated uuid
44
- if (uuid.length > uuidLengthLimit) {
43
+ if (uuid.length > UUID_LENGTH_LIMIT) {
45
44
  throw new Error("The UUID must be 36 characters or fewer.");
46
45
  }
47
46
  return uuid.toLocaleLowerCase();
@@ -6,7 +6,7 @@ import prompt, { Answers, PromptObject } from "prompts";
6
6
 
7
7
  import { eslint, gitignore, prettier, readme, tsConfig } from "./templates";
8
8
  import { sanitizeName } from "./utils";
9
- import { OnError } from "./enums";
9
+ import { OnError, UUID_LENGTH_LIMIT } from "./enums";
10
10
  import { ErrorList } from "../../lib/errors";
11
11
 
12
12
  export type PromptOptions = {
@@ -33,9 +33,9 @@ async function setUUID(uuid?: string): Promise<Answers<string>> {
33
33
  name: "uuid",
34
34
  message: "Enter a unique identifier for the new Pepr module.\n",
35
35
  validate: (val: string) => {
36
- const uuidLengthLimit = 36;
37
36
  return (
38
- val.length <= uuidLengthLimit || `The UUID must be ${uuidLengthLimit} characters or fewer.`
37
+ val.length <= UUID_LENGTH_LIMIT ||
38
+ `The UUID must be ${UUID_LENGTH_LIMIT} characters or fewer.`
39
39
  );
40
40
  },
41
41
  };
@@ -95,6 +95,7 @@ export type ValidateActionResponse = {
95
95
  allowed: boolean;
96
96
  statusCode?: number;
97
97
  statusMessage?: string;
98
+ warnings?: string[];
98
99
  };
99
100
 
100
101
  // DeepPartial utility type for deep optional properties
@@ -22,12 +22,21 @@ export function karForMutate(mr: MutateResponse): KubeAdmissionReview {
22
22
  export function karForValidate(ar: AdmissionRequest, vr: ValidateResponse[]): KubeAdmissionReview {
23
23
  const isAllowed = vr.filter(r => !r.allowed).length === 0;
24
24
 
25
+ // Collect all warnings from the ValidateResponse array
26
+ const warnings = vr.reduce<string[]>((acc, curr) => {
27
+ if (curr.warnings && curr.warnings.length > 0) {
28
+ return [...acc, ...curr.warnings];
29
+ }
30
+ return acc;
31
+ }, []);
32
+
25
33
  const resp: ValidateResponse =
26
34
  vr.length === 0
27
35
  ? {
28
36
  uid: ar.uid,
29
37
  allowed: true,
30
38
  status: { code: 200, message: "no in-scope validations -- allowed!" },
39
+ warnings: warnings.length > 0 ? warnings : undefined,
31
40
  }
32
41
  : {
33
42
  uid: vr[0].uid,
@@ -39,6 +48,7 @@ export function karForValidate(ar: AdmissionRequest, vr: ValidateResponse[]): Ku
39
48
  .map(curr => curr.status?.message)
40
49
  .join("; "),
41
50
  },
51
+ warnings: warnings.length > 0 ? warnings : undefined,
42
52
  };
43
53
  return {
44
54
  apiVersion: "admission.k8s.io/v1",
@@ -0,0 +1,56 @@
1
+ import { DataStore, Storage } from "../core/storage";
2
+ import { startsWith } from "ramda";
3
+ import Log, { redactedStore } from "../telemetry/logger";
4
+ import { K8s } from "kubernetes-fluent-client";
5
+ import { Store } from "../k8s";
6
+ import { Operation } from "fast-json-patch";
7
+ import { fillStoreCache, sendUpdatesAndFlushCache } from "./storeCache";
8
+
9
+ export interface StoreMigration {
10
+ name: string;
11
+ namespace: string;
12
+ store: Store;
13
+ stores: Record<string, Storage>;
14
+ setupWatch: () => void;
15
+ }
16
+
17
+ export async function migrateAndSetupWatch(storeData: StoreMigration): Promise<void> {
18
+ const { store, namespace, name, stores, setupWatch } = storeData;
19
+
20
+ Log.debug(redactedStore(store), "Pepr Store migration");
21
+ // Add cacheID label to store
22
+ await K8s(Store, { namespace, name }).Patch([
23
+ {
24
+ op: "add",
25
+ path: "/metadata/labels/pepr.dev-cacheID",
26
+ value: `${Date.now()}`,
27
+ },
28
+ ]);
29
+
30
+ const data: DataStore = store.data;
31
+ let storeCache: Record<string, Operation> = {};
32
+
33
+ for (const name of Object.keys(stores)) {
34
+ // Get the prefix offset for the keys
35
+ const offset = `${name}-`.length;
36
+
37
+ // Loop over each key in the store
38
+ for (const key of Object.keys(data)) {
39
+ // Match on the capability name as a prefix for non v2 keys
40
+ if (startsWith(name, key) && !startsWith(`${name}-v2`, key)) {
41
+ // populate migrate cache
42
+ storeCache = fillStoreCache(storeCache, name, "remove", {
43
+ key: [key.slice(offset)],
44
+ value: data[key],
45
+ });
46
+ storeCache = fillStoreCache(storeCache, name, "add", {
47
+ key: [key.slice(offset)],
48
+ value: data[key],
49
+ version: "v2",
50
+ });
51
+ }
52
+ }
53
+ }
54
+ storeCache = await sendUpdatesAndFlushCache(storeCache, namespace, name);
55
+ setupWatch();
56
+ }
@@ -10,6 +10,7 @@ import { Store } from "../k8s";
10
10
  import Log, { redactedPatch, redactedStore } from "../telemetry/logger";
11
11
  import { DataOp, DataSender, DataStore, Storage } from "../core/storage";
12
12
  import { fillStoreCache, sendUpdatesAndFlushCache } from "./storeCache";
13
+ import { migrateAndSetupWatch } from "./migrateStore";
13
14
 
14
15
  const namespace = "pepr-system";
15
16
  const debounceBackoffReceive = 1000;
@@ -56,7 +57,16 @@ export class StoreController {
56
57
  K8s(Store)
57
58
  .InNamespace(namespace)
58
59
  .Get(this.#name)
59
- .then(async (store: Store) => await this.#migrateAndSetupWatch(store))
60
+ .then(
61
+ async (store: Store) =>
62
+ await migrateAndSetupWatch({
63
+ name,
64
+ namespace,
65
+ store,
66
+ stores: this.#stores,
67
+ setupWatch: this.#setupWatch,
68
+ }),
69
+ )
60
70
  .catch(this.#createStoreResource),
61
71
  Math.random() * 3000, // Add a jitter to the Store creation to avoid collisions
62
72
  );
@@ -67,45 +77,6 @@ export class StoreController {
67
77
  watcher.start().catch(e => Log.error(e, "Error starting Pepr store watch"));
68
78
  };
69
79
 
70
- #migrateAndSetupWatch = async (store: Store): Promise<void> => {
71
- Log.debug(redactedStore(store), "Pepr Store migration");
72
- // Add cacheID label to store
73
- await K8s(Store, { namespace, name: this.#name }).Patch([
74
- {
75
- op: "add",
76
- path: "/metadata/labels/pepr.dev-cacheID",
77
- value: `${Date.now()}`,
78
- },
79
- ]);
80
-
81
- const data: DataStore = store.data || {};
82
- let storeCache: Record<string, Operation> = {};
83
-
84
- for (const name of Object.keys(this.#stores)) {
85
- // Get the prefix offset for the keys
86
- const offset = `${name}-`.length;
87
-
88
- // Loop over each key in the store
89
- for (const key of Object.keys(data)) {
90
- // Match on the capability name as a prefix for non v2 keys
91
- if (startsWith(name, key) && !startsWith(`${name}-v2`, key)) {
92
- // populate migrate cache
93
- storeCache = fillStoreCache(storeCache, name, "remove", {
94
- key: [key.slice(offset)],
95
- value: data[key],
96
- });
97
- storeCache = fillStoreCache(storeCache, name, "add", {
98
- key: [key.slice(offset)],
99
- value: data[key],
100
- version: "v2",
101
- });
102
- }
103
- }
104
- }
105
- storeCache = await sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
106
- this.#setupWatch();
107
- };
108
-
109
80
  #receive = (store: Store): void => {
110
81
  Log.debug(redactedStore(store), "Pepr Store update");
111
82
 
@@ -62,7 +62,7 @@ export class PeprModule {
62
62
  const controllerHooks: ControllerHooks = {
63
63
  beforeHook: opts.beforeHook,
64
64
  afterHook: opts.afterHook,
65
- onReady: (): void => {
65
+ onReady: async (): Promise<void> => {
66
66
  // Wait for the controller to be ready before setting up watches
67
67
  if (isWatchMode() || isDevMode()) {
68
68
  try {
@@ -41,6 +41,11 @@ export async function processRequest(
41
41
  };
42
42
  }
43
43
 
44
+ // Transfer any warnings from the callback response to the validation response
45
+ if (callbackResp.warnings && callbackResp.warnings.length > 0) {
46
+ valResp.warnings = callbackResp.warnings;
47
+ }
48
+
44
49
  Log.info(
45
50
  actionMetadata,
46
51
  `Validation action complete (${label}): ${callbackResp.allowed ? "allowed" : "denied"}`,
@@ -86,13 +86,11 @@ const eventToPhaseMap = {
86
86
  * @param capabilities The capabilities to load watches for
87
87
  */
88
88
  export function setupWatch(capabilities: Capability[], ignoredNamespaces?: string[]): void {
89
- capabilities.map(capability =>
90
- capability.bindings
91
- .filter(binding => binding.isWatch)
92
- .forEach(bindingElement =>
93
- runBinding(bindingElement, capability.namespaces, ignoredNamespaces),
94
- ),
95
- );
89
+ for (const capability of capabilities) {
90
+ for (const binding of capability.bindings.filter(b => b.isWatch)) {
91
+ runBinding(binding, capability.namespaces, ignoredNamespaces);
92
+ }
93
+ }
96
94
  }
97
95
 
98
96
  /**
@@ -101,7 +99,7 @@ export function setupWatch(capabilities: Capability[], ignoredNamespaces?: strin
101
99
  * @param binding the binding to watch
102
100
  * @param capabilityNamespaces list of namespaces to filter on
103
101
  */
104
- async function runBinding(
102
+ export async function runBinding(
105
103
  binding: Binding,
106
104
  capabilityNamespaces: string[],
107
105
  ignoredNamespaces?: string[],
@@ -185,14 +183,20 @@ async function runBinding(
185
183
  );
186
184
 
187
185
  // Register event handlers
188
- registerWatchEventHandlers(watcher, logEvent, metricsCollector);
186
+ try {
187
+ registerWatchEventHandlers(watcher, logEvent, metricsCollector);
188
+ } catch (err) {
189
+ throw new Error(
190
+ "WatchEventHandler Registration Error: Unable to register event watch handler.",
191
+ { cause: err },
192
+ );
193
+ }
189
194
 
190
195
  // Start the watch
191
196
  try {
192
197
  await watcher.start();
193
198
  } catch (err) {
194
- Log.error(err, "Error starting watch");
195
- process.exit(1);
199
+ throw new Error("WatchStart Error: Unable to start watch.", { cause: err });
196
200
  }
197
201
  }
198
202
 
@@ -235,7 +239,10 @@ export function registerWatchEventHandlers(
235
239
  [WatchEvent.GIVE_UP]: err => {
236
240
  // If failure continues, log and exit
237
241
  logEvent(WatchEvent.GIVE_UP, err.message);
238
- process.exit(1);
242
+ throw new Error(
243
+ "WatchEvent GiveUp Error: The watch has failed to start after several attempts.",
244
+ { cause: err },
245
+ );
239
246
  },
240
247
  [WatchEvent.CONNECT]: url => logEvent(WatchEvent.CONNECT, url),
241
248
  [WatchEvent.DATA_ERROR]: err => logEvent(WatchEvent.DATA_ERROR, err.message),
@@ -6,8 +6,8 @@
6
6
  import { KubernetesObject } from "kubernetes-fluent-client";
7
7
 
8
8
  import { clone } from "ramda";
9
- import { Operation } from "./enums";
10
9
  import { AdmissionRequest, ValidateActionResponse } from "./common-types";
10
+ import { Operation } from "./enums";
11
11
 
12
12
  /**
13
13
  * The RequestWrapper class provides methods to modify Kubernetes objects in the context
@@ -79,9 +79,10 @@ export class PeprValidateRequest<T extends KubernetesObject> {
79
79
  *
80
80
  * @returns The validation response.
81
81
  */
82
- Approve = (): ValidateActionResponse => {
82
+ Approve = (warnings?: string[]): ValidateActionResponse => {
83
83
  return {
84
84
  allowed: true,
85
+ warnings,
85
86
  };
86
87
  };
87
88
 
@@ -92,11 +93,16 @@ export class PeprValidateRequest<T extends KubernetesObject> {
92
93
  * @param statusCode Optional status code to return to the user.
93
94
  * @returns The validation response.
94
95
  */
95
- Deny = (statusMessage?: string, statusCode?: number): ValidateActionResponse => {
96
+ Deny = (
97
+ statusMessage?: string,
98
+ statusCode?: number,
99
+ warnings?: string[],
100
+ ): ValidateActionResponse => {
96
101
  return {
97
102
  allowed: false,
98
103
  statusCode,
99
104
  statusMessage,
105
+ warnings,
100
106
  };
101
107
  };
102
108
  }