pepr 0.32.7 → 0.33.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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "engines": {
10
10
  "node": ">=18.0.0"
11
11
  },
12
- "version": "0.32.7",
12
+ "version": "0.33.0",
13
13
  "main": "dist/lib.js",
14
14
  "types": "dist/lib.d.ts",
15
15
  "scripts": {
@@ -35,8 +35,9 @@
35
35
  "@types/ramda": "0.30.1",
36
36
  "express": "4.19.2",
37
37
  "fast-json-patch": "3.1.1",
38
- "kubernetes-fluent-client": "2.6.4",
39
- "pino": "9.2.0",
38
+ "json-pointer": "^0.6.2",
39
+ "kubernetes-fluent-client": "2.6.5",
40
+ "pino": "9.3.1",
40
41
  "pino-pretty": "11.2.1",
41
42
  "prom-client": "15.1.3",
42
43
  "ramda": "0.30.1"
@@ -46,16 +47,18 @@
46
47
  "@commitlint/config-conventional": "19.2.2",
47
48
  "@fast-check/jest": "^1.8.2",
48
49
  "@jest/globals": "29.7.0",
49
- "@types/eslint": "8.56.10",
50
+ "@types/eslint": "9.6.0",
50
51
  "@types/express": "4.17.21",
52
+ "@types/json-pointer": "^1.0.34",
51
53
  "@types/node": "18.x.x",
52
54
  "@types/node-forge": "1.3.11",
53
55
  "@types/prompts": "2.4.9",
54
- "fast-check": "^3.19.0",
55
56
  "@types/uuid": "10.0.0",
57
+ "fast-check": "^3.19.0",
56
58
  "jest": "29.7.0",
59
+ "js-yaml": "^4.1.0",
57
60
  "nock": "13.5.4",
58
- "ts-jest": "29.2.2"
61
+ "ts-jest": "29.2.3"
59
62
  },
60
63
  "peerDependencies": {
61
64
  "@typescript-eslint/eslint-plugin": "6.15.0",
@@ -4,15 +4,26 @@
4
4
  import { dumpYaml } from "@kubernetes/client-node";
5
5
  import crypto from "crypto";
6
6
  import { promises as fs } from "fs";
7
-
8
7
  import { Assets } from ".";
9
8
  import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking";
10
9
  import { deployment, moduleSecret, namespace, watcher } from "./pods";
11
10
  import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
12
11
  import { webhookConfig } from "./webhooks";
12
+ import { mergePkgJSONEnv, envMapToArray } from "../helpers";
13
13
 
14
14
  // Helm Chart overrides file (values.yaml) generated from assets
15
15
  export async function overridesFile({ hash, name, image, config, apiToken }: Assets, path: string) {
16
+ const pkgJSONAdmissionEnv = {
17
+ PEPR_WATCH_MODE: "false",
18
+ PEPR_PRETTY_LOG: "false",
19
+ LOG_LEVEL: "info",
20
+ };
21
+ const pkgJSONWatchEnv = {
22
+ PEPR_WATCH_MODE: "true",
23
+ PEPR_PRETTY_LOG: "false",
24
+ LOG_LEVEL: "info",
25
+ };
26
+
16
27
  const overrides = {
17
28
  secrets: {
18
29
  apiToken: Buffer.from(apiToken).toString("base64"),
@@ -29,11 +40,7 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
29
40
  terminationGracePeriodSeconds: 5,
30
41
  failurePolicy: config.onError === "reject" ? "Fail" : "Ignore",
31
42
  webhookTimeout: config.webhookTimeout,
32
- env: [
33
- { name: "PEPR_WATCH_MODE", value: "false" },
34
- { name: "PEPR_PRETTY_LOG", value: "false" },
35
- { name: "LOG_LEVEL", value: "info" },
36
- ],
43
+ env: envMapToArray(mergePkgJSONEnv(pkgJSONAdmissionEnv, config.env)),
37
44
  image,
38
45
  annotations: {
39
46
  "pepr.dev/description": `${config.description}` || "",
@@ -77,11 +84,7 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
77
84
  },
78
85
  watcher: {
79
86
  terminationGracePeriodSeconds: 5,
80
- env: [
81
- { name: "PEPR_WATCH_MODE", value: "true" },
82
- { name: "PEPR_PRETTY_LOG", value: "false" },
83
- { name: "LOG_LEVEL", value: "info" },
84
- ],
87
+ env: envMapToArray(mergePkgJSONEnv(pkgJSONWatchEnv, config.env)),
85
88
  image,
86
89
  annotations: {
87
90
  "pepr.dev/description": `${config.description}` || "",
@@ -124,12 +127,6 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
124
127
  podAnnotations: {},
125
128
  },
126
129
  };
127
- if (process.env.PEPR_MODE === "dev") {
128
- overrides.admission.env.push({ name: "ZARF_VAR", value: "###ZARF_VAR_THING###" });
129
- overrides.watcher.env.push({ name: "ZARF_VAR", value: "###ZARF_VAR_THING###" });
130
- overrides.admission.env.push({ name: "MY_CUSTOM_VAR", value: "example-value" });
131
- overrides.watcher.env.push({ name: "MY_CUSTOM_VAR", value: "example-value" });
132
- }
133
130
 
134
131
  await fs.writeFile(path, dumpYaml(overrides, { noRefs: true, forceQuotes: true }));
135
132
  }
@@ -61,8 +61,8 @@ export class PeprControllerStore {
61
61
  K8s(PeprStore)
62
62
  .InNamespace(namespace)
63
63
  .Get(this.#name)
64
- // If the get succeeds, setup the watch
65
- .then(this.#setupWatch)
64
+ // If the get succeeds, migrate and setup the watch
65
+ .then(async (store: PeprStore) => await this.#migrateAndSetupWatch(store))
66
66
  // Otherwise, create the resource
67
67
  .catch(this.#createStoreResource),
68
68
  Math.random() * 3000,
@@ -74,6 +74,91 @@ export class PeprControllerStore {
74
74
  watcher.start().catch(e => Log.error(e, "Error starting Pepr store watch"));
75
75
  };
76
76
 
77
+ #migrateAndSetupWatch = async (store: PeprStore) => {
78
+ Log.debug(store, "Pepr Store migration");
79
+ const data: DataStore = store.data || {};
80
+ const migrateCache: Record<string, Operation> = {};
81
+
82
+ // Send the cached updates to the cluster
83
+ const flushCache = async () => {
84
+ const indexes = Object.keys(migrateCache);
85
+ const payload = Object.values(migrateCache);
86
+
87
+ // Loop over each key in the cache and delete it to avoid collisions with other sender calls
88
+ for (const idx of indexes) {
89
+ delete migrateCache[idx];
90
+ }
91
+
92
+ try {
93
+ // Send the patch to the cluster
94
+ await K8s(PeprStore, { namespace, name: this.#name }).Patch(payload);
95
+ } catch (err) {
96
+ Log.error(err, "Pepr store update failure");
97
+
98
+ if (err.status === 422) {
99
+ Object.keys(migrateCache).forEach(key => delete migrateCache[key]);
100
+ } else {
101
+ // On failure to update, re-add the operations to the cache to be retried
102
+ for (const idx of indexes) {
103
+ migrateCache[idx] = payload[Number(idx)];
104
+ }
105
+ }
106
+ }
107
+ };
108
+
109
+ const fillCache = (name: string, op: DataOp, key: string[], val?: string) => {
110
+ if (op === "add") {
111
+ // adjust the path for the capability
112
+ const path = `/data/${name}-v2-${key}`;
113
+ const value = val || "";
114
+ const cacheIdx = [op, path, value].join(":");
115
+
116
+ // Add the operation to the cache
117
+ migrateCache[cacheIdx] = { op, path, value };
118
+
119
+ return;
120
+ }
121
+
122
+ if (op === "remove") {
123
+ if (key.length < 1) {
124
+ throw new Error(`Key is required for REMOVE operation`);
125
+ }
126
+
127
+ for (const k of key) {
128
+ const path = `/data/${name}-${k}`;
129
+ const cacheIdx = [op, path].join(":");
130
+
131
+ // Add the operation to the cache
132
+ migrateCache[cacheIdx] = { op, path };
133
+ }
134
+
135
+ return;
136
+ }
137
+
138
+ // If we get here, the operation is not supported
139
+ throw new Error(`Unsupported operation: ${op}`);
140
+ };
141
+
142
+ for (const name of Object.keys(this.#stores)) {
143
+ // Get the prefix offset for the keys
144
+ const offset = `${name}-`.length;
145
+
146
+ // Loop over each key in the store
147
+ for (const key of Object.keys(data)) {
148
+ // Match on the capability name as a prefix for non v2 keys
149
+ if (startsWith(name, key) && !startsWith(`${name}-v2`, key)) {
150
+ // populate migrate cache
151
+ fillCache(name, "remove", [key.slice(offset)], data[key]);
152
+ fillCache(name, "add", [key.slice(offset)], data[key]);
153
+ }
154
+ }
155
+
156
+ // await K8s(PeprStore, { namespace, name: this.#name }).Patch(payload);
157
+ }
158
+ await flushCache();
159
+ this.#setupWatch();
160
+ };
161
+
77
162
  #receive = (store: PeprStore) => {
78
163
  Log.debug(store, "Pepr Store update");
79
164
 
@@ -121,6 +206,7 @@ export class PeprControllerStore {
121
206
  // Load the sendCache with patch operations
122
207
  const fillCache = (op: DataOp, key: string[], val?: string) => {
123
208
  if (op === "add") {
209
+ // adjust the path for the capability
124
210
  const path = `/data/${capabilityName}-${key}`;
125
211
  const value = val || "";
126
212
  const cacheIdx = [op, path, value].join(":");
@@ -6,7 +6,24 @@ 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";
9
10
 
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
+ }
10
27
  export class ValidationError extends Error {}
11
28
 
12
29
  export function validateCapabilityNames(capabilities: CapabilityExport[] | undefined): void {
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { clone } from "ramda";
5
5
  import Log from "./logger";
6
-
6
+ import pointer from "json-pointer";
7
7
  export type DataOp = "add" | "remove";
8
8
  export type DataStore = Record<string, string>;
9
9
  export type DataSender = (op: DataOp, keys: string[], value?: string) => void;
@@ -11,6 +11,15 @@ export type DataReceiver = (data: DataStore) => void;
11
11
  export type Unsubscribe = () => void;
12
12
 
13
13
  const MAX_WAIT_TIME = 15000;
14
+ const STORE_VERSION_PREFIX = "v2";
15
+
16
+ export function v2StoreKey(key: string) {
17
+ return `${STORE_VERSION_PREFIX}-${pointer.escape(key)}`;
18
+ }
19
+
20
+ export function stripV2Prefix(key: string) {
21
+ return key.replace(/^v2-/, "");
22
+ }
14
23
  export interface PeprStore {
15
24
  /**
16
25
  * Returns the current value associated with the given key, or null if the given key does not exist.
@@ -60,6 +69,7 @@ export interface PeprStore {
60
69
  *
61
70
  * The API is similar to the [Storage API](https://developer.mozilla.org/docs/Web/API/Storage)
62
71
  */
72
+
63
73
  export class Storage implements PeprStore {
64
74
  #store: DataStore = {};
65
75
  #send!: DataSender;
@@ -85,8 +95,11 @@ export class Storage implements PeprStore {
85
95
  };
86
96
 
87
97
  getItem = (key: string) => {
88
- // Return null if the value is the empty string
89
- return this.#store[key] || null;
98
+ const result = this.#store[v2StoreKey(key)] || null;
99
+ if (result !== null && typeof result !== "function" && typeof result !== "object") {
100
+ return result;
101
+ }
102
+ return null;
90
103
  };
91
104
 
92
105
  clear = () => {
@@ -94,11 +107,11 @@ export class Storage implements PeprStore {
94
107
  };
95
108
 
96
109
  removeItem = (key: string) => {
97
- this.#dispatchUpdate("remove", [key]);
110
+ this.#dispatchUpdate("remove", [v2StoreKey(key)]);
98
111
  };
99
112
 
100
113
  setItem = (key: string, value: string) => {
101
- this.#dispatchUpdate("add", [key], value);
114
+ this.#dispatchUpdate("add", [v2StoreKey(key)], value);
102
115
  };
103
116
 
104
117
  /**
@@ -110,10 +123,10 @@ export class Storage implements PeprStore {
110
123
  * @returns
111
124
  */
112
125
  setItemAndWait = (key: string, value: string) => {
113
- this.#dispatchUpdate("add", [key], value);
126
+ this.#dispatchUpdate("add", [v2StoreKey(key)], value);
114
127
  return new Promise<void>((resolve, reject) => {
115
128
  const unsubscribe = this.subscribe(data => {
116
- if (data[key] === value) {
129
+ if (data[`${v2StoreKey(key)}`] === value) {
117
130
  unsubscribe();
118
131
  resolve();
119
132
  }
@@ -135,10 +148,10 @@ export class Storage implements PeprStore {
135
148
  * @returns
136
149
  */
137
150
  removeItemAndWait = (key: string) => {
138
- this.#dispatchUpdate("remove", [key]);
151
+ this.#dispatchUpdate("remove", [v2StoreKey(key)]);
139
152
  return new Promise<void>((resolve, reject) => {
140
153
  const unsubscribe = this.subscribe(data => {
141
- if (!Object.hasOwn(data, key)) {
154
+ if (!Object.hasOwn(data, `${v2StoreKey(key)}`)) {
142
155
  unsubscribe();
143
156
  resolve();
144
157
  }