pepr 0.53.1-nightly.5 → 0.53.1-nightly.7
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 +105 -3
- package/dist/controller.js +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.js +81 -0
- package/dist/lib.js.map +3 -3
- package/package.json +1 -1
- 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/controller/index.ts +8 -0
- package/src/lib/features/FeatureFlags.ts +23 -0
- package/src/lib/features/store.ts +92 -0
package/package.json
CHANGED
|
@@ -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(() => {
|
|
@@ -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(featureFlagStore.getAll(), "Feature flags store initialized");
|
|
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();
|