pepr 0.53.1 → 0.54.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/dist/cli/build/build.helpers.d.ts +7 -0
- package/dist/cli/build/build.helpers.d.ts.map +1 -1
- package/dist/cli/build/index.d.ts.map +1 -1
- package/dist/cli.js +116 -10
- package/dist/controller.js +1 -1
- package/dist/lib/assets/webhooks.d.ts +1 -0
- package/dist/lib/assets/webhooks.d.ts.map +1 -1
- package/dist/lib/controller/index.d.ts.map +1 -1
- package/dist/lib/features/FeatureFlags.d.ts +12 -0
- package/dist/lib/features/FeatureFlags.d.ts.map +1 -0
- package/dist/lib/features/store.d.ts +13 -0
- package/dist/lib/features/store.d.ts.map +1 -0
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib.js +81 -0
- package/dist/lib.js.map +3 -3
- package/package.json +5 -5
- package/src/cli/build/build.helpers.ts +23 -1
- package/src/cli/build/index.ts +6 -1
- package/src/cli.ts +12 -0
- package/src/lib/assets/webhooks.ts +7 -0
- package/src/lib/controller/index.ts +8 -0
- package/src/lib/features/FeatureFlags.ts +23 -0
- package/src/lib/features/store.ts +92 -0
- package/src/lib/helpers.ts +8 -7
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"!src/fixtures/**",
|
|
17
17
|
"!dist/**/*.test.d.ts*"
|
|
18
18
|
],
|
|
19
|
-
"version": "0.
|
|
19
|
+
"version": "0.54.0",
|
|
20
20
|
"main": "dist/lib.js",
|
|
21
21
|
"types": "dist/lib.d.ts",
|
|
22
22
|
"scripts": {
|
|
@@ -95,10 +95,10 @@
|
|
|
95
95
|
},
|
|
96
96
|
"peerDependencies": {
|
|
97
97
|
"@types/prompts": "2.4.9",
|
|
98
|
-
"@typescript-eslint/eslint-plugin": "8.
|
|
99
|
-
"@typescript-eslint/parser": "8.
|
|
100
|
-
"esbuild": "0.25.
|
|
101
|
-
"eslint": "9.
|
|
98
|
+
"@typescript-eslint/eslint-plugin": "8.42.0",
|
|
99
|
+
"@typescript-eslint/parser": "8.42.0",
|
|
100
|
+
"esbuild": "0.25.9",
|
|
101
|
+
"eslint": "9.34.0",
|
|
102
102
|
"node-forge": "1.3.1",
|
|
103
103
|
"prettier": "3.6.2",
|
|
104
104
|
"prompts": "2.4.2",
|
|
@@ -7,7 +7,7 @@ import { validateCapabilityNames } from "../../lib/helpers";
|
|
|
7
7
|
import { BuildOptions, context, BuildContext } from "esbuild";
|
|
8
8
|
import { Assets } from "../../lib/assets/assets";
|
|
9
9
|
import { resolve } from "path";
|
|
10
|
-
import { promises as fs } from "fs";
|
|
10
|
+
import { promises as fs, accessSync, constants, statSync } from "fs";
|
|
11
11
|
import { generateAllYaml } from "../../lib/assets/yaml/generateAllYaml";
|
|
12
12
|
import { webhookConfigGenerator } from "../../lib/assets/webhooks";
|
|
13
13
|
import { generateZarfYamlGeneric } from "../../lib/assets/yaml/generateZarfYaml";
|
|
@@ -19,6 +19,28 @@ import {
|
|
|
19
19
|
watcherService,
|
|
20
20
|
} from "../../lib/assets/k8sObjects";
|
|
21
21
|
import { Reloader } from "../types";
|
|
22
|
+
import Log from "../../lib/telemetry/logger";
|
|
23
|
+
/**
|
|
24
|
+
* Check if a file exists at a given path.
|
|
25
|
+
*
|
|
26
|
+
* @param filePath - Path to the file (relative or absolute).
|
|
27
|
+
* @returns true if the file exists, false otherwise.
|
|
28
|
+
*/
|
|
29
|
+
export function fileExists(entryPoint: string = "pepr.ts"): string {
|
|
30
|
+
const fullPath = resolve(entryPoint);
|
|
31
|
+
try {
|
|
32
|
+
accessSync(resolve(entryPoint), constants.F_OK);
|
|
33
|
+
if (!statSync(fullPath).isFile()) {
|
|
34
|
+
throw new Error("Not a file");
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
Log.error(
|
|
38
|
+
`The entry-point option requires a file (e.g., pepr.ts), ${entryPoint} is not a file`,
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
return entryPoint;
|
|
43
|
+
}
|
|
22
44
|
|
|
23
45
|
interface ImageOptions {
|
|
24
46
|
customImage?: string;
|
package/src/cli/build/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
handleCustomImageBuild,
|
|
14
14
|
validImagePullSecret,
|
|
15
15
|
generateYamlAndWriteToDisk,
|
|
16
|
+
fileExists,
|
|
16
17
|
} from "./build.helpers";
|
|
17
18
|
import { buildModule } from "./buildModule";
|
|
18
19
|
import Log from "../../lib/telemetry/logger";
|
|
@@ -40,7 +41,11 @@ export default function (program: Command): void {
|
|
|
40
41
|
"Set name for zarf component and service monitors in helm charts.",
|
|
41
42
|
),
|
|
42
43
|
)
|
|
43
|
-
.option(
|
|
44
|
+
.option(
|
|
45
|
+
"-e, --entry-point <file>",
|
|
46
|
+
"Specify the entry point file to build with. (default: pepr.ts)",
|
|
47
|
+
fileExists,
|
|
48
|
+
)
|
|
44
49
|
.addOption(
|
|
45
50
|
new Option(
|
|
46
51
|
"-i, --custom-image <image>",
|
package/src/cli.ts
CHANGED
|
@@ -17,6 +17,8 @@ import update from "./cli/update";
|
|
|
17
17
|
import kfc from "./cli/kfc";
|
|
18
18
|
import crd from "./cli/crd";
|
|
19
19
|
import Log from "./lib/telemetry/logger";
|
|
20
|
+
import { featureFlagStore } from "./lib/features/store";
|
|
21
|
+
|
|
20
22
|
if (process.env.npm_lifecycle_event !== "npx") {
|
|
21
23
|
Log.info("Pepr should be run via `npx pepr <command>` instead of `pepr <command>`.");
|
|
22
24
|
}
|
|
@@ -29,6 +31,16 @@ program
|
|
|
29
31
|
.enablePositionalOptions()
|
|
30
32
|
.version(version)
|
|
31
33
|
.description(`Pepr (v${version}) - Type safe K8s middleware for humans`)
|
|
34
|
+
.option("--features <features>", "Comma-separated feature flags (feature=value,feature2=value2)")
|
|
35
|
+
.hook("preAction", thisCommand => {
|
|
36
|
+
try {
|
|
37
|
+
featureFlagStore.initialize(thisCommand.opts().features);
|
|
38
|
+
Log.debug(`Feature flag store initialized: ${JSON.stringify(featureFlagStore.getAll())}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
Log.error(error, "Failed to initialize feature store:");
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
})
|
|
32
44
|
.addCommand(crd())
|
|
33
45
|
.addCommand(init())
|
|
34
46
|
.action(() => {
|
|
@@ -142,6 +142,12 @@ export async function webhookConfigGenerator(
|
|
|
142
142
|
return webhookConfig;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
export function checkFailurePolicy(failurePolicy: string): void {
|
|
146
|
+
if (failurePolicy !== "Fail" && failurePolicy !== "Ignore") {
|
|
147
|
+
throw new Error(`Invalid failure policy: ${failurePolicy}. Must be either 'Fail' or 'Ignore'.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
145
151
|
export function configureAdditionalWebhooks(
|
|
146
152
|
webhookConfig: V1MutatingWebhookConfiguration | V1ValidatingWebhookConfiguration,
|
|
147
153
|
additionalWebhooks: AdditionalWebhook[],
|
|
@@ -158,6 +164,7 @@ export function configureAdditionalWebhooks(
|
|
|
158
164
|
expr.values!.push(...additionalWebhooks.map(w => w.namespace));
|
|
159
165
|
|
|
160
166
|
additionalWebhooks.forEach(additionalWebhook => {
|
|
167
|
+
checkFailurePolicy(additionalWebhook.failurePolicy);
|
|
161
168
|
webhooks.push({
|
|
162
169
|
name: `${webhookConfig.metadata!.name}-${additionalWebhook.namespace}.pepr.dev`,
|
|
163
170
|
admissionReviewVersions: ["v1", "v1beta1"],
|
|
@@ -16,6 +16,7 @@ import { validateProcessor } from "../processors/validate-processor";
|
|
|
16
16
|
import { StoreController } from "./store";
|
|
17
17
|
import { karForMutate, karForValidate, KubeAdmissionReview } from "./index.util";
|
|
18
18
|
import { AdmissionRequest } from "../common-types";
|
|
19
|
+
import { featureFlagStore } from "../features/store";
|
|
19
20
|
|
|
20
21
|
export interface ControllerHooks {
|
|
21
22
|
beforeHook?: (req: AdmissionRequest) => void;
|
|
@@ -90,6 +91,13 @@ export class Controller {
|
|
|
90
91
|
);
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
// Initialize feature store
|
|
95
|
+
try {
|
|
96
|
+
featureFlagStore.initialize();
|
|
97
|
+
Log.info(`Feature flag store initialized: ${JSON.stringify(featureFlagStore.getAll())}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
Log.warn(error, "Could not initialize feature flags");
|
|
100
|
+
}
|
|
93
101
|
// Load SSL certificate and key
|
|
94
102
|
const options = {
|
|
95
103
|
key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface FeatureMetadata {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
defaultValue: FeatureValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface FeatureInfo {
|
|
8
|
+
key: string;
|
|
9
|
+
metadata: FeatureMetadata;
|
|
10
|
+
}
|
|
11
|
+
// All known feature flags with their metadata
|
|
12
|
+
export const FeatureFlags: Record<string, FeatureInfo> = {
|
|
13
|
+
REFERENCE_FLAG: {
|
|
14
|
+
key: "reference_flag",
|
|
15
|
+
metadata: {
|
|
16
|
+
name: "Reference Flag",
|
|
17
|
+
description: "A feature flag to show intended usage.",
|
|
18
|
+
defaultValue: false,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type FeatureValue = string | boolean | number;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { FeatureFlags, FeatureValue } from "./FeatureFlags";
|
|
5
|
+
|
|
6
|
+
export class FeatureStore {
|
|
7
|
+
private featureFlagLimit: number = 4;
|
|
8
|
+
private features: Record<string, FeatureValue> = {};
|
|
9
|
+
|
|
10
|
+
private addFeature(key: string, value: string): void {
|
|
11
|
+
if (!key || value === undefined || value === "") return;
|
|
12
|
+
|
|
13
|
+
const validKeys = Object.values(FeatureFlags)
|
|
14
|
+
.filter(f => f?.key)
|
|
15
|
+
.map(f => f.key);
|
|
16
|
+
if (!validKeys.includes(key)) {
|
|
17
|
+
throw new Error(`Unknown feature flag: ${key}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lowerValue = value.toLowerCase();
|
|
21
|
+
if (lowerValue === "true") {
|
|
22
|
+
this.features[key] = true;
|
|
23
|
+
} else if (lowerValue === "false") {
|
|
24
|
+
this.features[key] = false;
|
|
25
|
+
} else if (!isNaN(Number(value))) {
|
|
26
|
+
this.features[key] = Number(value);
|
|
27
|
+
} else {
|
|
28
|
+
this.features[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get<T extends FeatureValue>(key: string): T {
|
|
33
|
+
if (!Object.values(FeatureFlags).some(f => f?.key === key)) {
|
|
34
|
+
throw new Error(`Unknown feature flag: ${key}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!(key in this.features)) {
|
|
38
|
+
throw new Error(`Feature flag '${key}' exists but has not been set`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.features[key] as T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getAll(): Record<string, FeatureValue> {
|
|
45
|
+
return { ...this.features };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
initialize(featuresStr?: string, env: Record<string, string | undefined> = process.env): void {
|
|
49
|
+
Object.keys(env)
|
|
50
|
+
.filter(key => key.startsWith("PEPR_FEATURE_"))
|
|
51
|
+
.forEach(key => {
|
|
52
|
+
this.addFeature(key.replace("PEPR_FEATURE_", "").toLowerCase(), env[key] || "");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (featuresStr) {
|
|
56
|
+
featuresStr
|
|
57
|
+
.split(",")
|
|
58
|
+
.map(feature => feature.split("="))
|
|
59
|
+
.filter(parts => parts.length === 2)
|
|
60
|
+
.forEach(([key, value]) => {
|
|
61
|
+
this.addFeature(key.trim(), value.trim());
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.applyDefaultValues();
|
|
66
|
+
this.validateFeatureCount();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private applyDefaultValues(): void {
|
|
70
|
+
Object.values(FeatureFlags)
|
|
71
|
+
.filter(
|
|
72
|
+
feature =>
|
|
73
|
+
feature?.key &&
|
|
74
|
+
feature?.metadata?.defaultValue !== undefined &&
|
|
75
|
+
!(feature.key in this.features),
|
|
76
|
+
)
|
|
77
|
+
.forEach(feature => {
|
|
78
|
+
this.features[feature.key] = feature.metadata.defaultValue;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
validateFeatureCount(): void {
|
|
83
|
+
const featureCount = Object.keys(this.features).length;
|
|
84
|
+
if (featureCount > this.featureFlagLimit) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Too many feature flags active: ${featureCount} (maximum: ${this.featureFlagLimit}). Use of more than ${this.featureFlagLimit} feature flags is not supported.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const featureFlagStore = new FeatureStore();
|
package/src/lib/helpers.ts
CHANGED
|
@@ -227,16 +227,17 @@ export function secretOverLimit(str: string): boolean {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
export const parseTimeout = (value: string): number => {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
throw new Error("Not a number.");
|
|
234
|
-
} else if (parsedValue !== floatValue) {
|
|
230
|
+
const num = Number(value);
|
|
231
|
+
|
|
232
|
+
if (!Number.isInteger(num) || value.includes(".")) {
|
|
235
233
|
throw new Error("Value must be an integer.");
|
|
236
|
-
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (num < 1 || num > 30) {
|
|
237
237
|
throw new Error("Number must be between 1 and 30.");
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
|
|
240
|
+
return num;
|
|
240
241
|
};
|
|
241
242
|
|
|
242
243
|
// Remove leading whitespace while keeping format of file
|