pepr 0.32.6 → 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/LICENSE +1 -1
- package/dist/cli.js +32 -21
- package/dist/controller.js +2 -1
- package/dist/lib/assets/yaml.d.ts.map +1 -1
- package/dist/lib/controller/store.d.ts.map +1 -1
- package/dist/lib/helpers.d.ts +5 -0
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/storage.d.ts +2 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib.js +81 -15
- package/dist/lib.js.map +3 -3
- package/package.json +10 -7
- package/src/lib/assets/yaml.ts +14 -17
- package/src/lib/controller/store.ts +88 -2
- package/src/lib/helpers.ts +17 -0
- package/src/lib/storage.ts +22 -9
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=18.0.0"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.33.0",
|
|
13
13
|
"main": "dist/lib.js",
|
|
14
14
|
"types": "dist/lib.d.ts",
|
|
15
15
|
"scripts": {
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
"format:fix": "eslint src --fix && prettier src --write"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@types/ramda": "0.30.
|
|
35
|
+
"@types/ramda": "0.30.1",
|
|
36
36
|
"express": "4.19.2",
|
|
37
37
|
"fast-json-patch": "3.1.1",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
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": "
|
|
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.
|
|
61
|
+
"ts-jest": "29.2.3"
|
|
59
62
|
},
|
|
60
63
|
"peerDependencies": {
|
|
61
64
|
"@typescript-eslint/eslint-plugin": "6.15.0",
|
package/src/lib/assets/yaml.ts
CHANGED
|
@@ -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.#
|
|
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(":");
|
package/src/lib/helpers.ts
CHANGED
|
@@ -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 {
|
package/src/lib/storage.ts
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
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
|
}
|