pepr 0.53.1 → 0.54.0-nightly.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.53.1",
19
+ "version": "0.54.0-nightly.1",
20
20
  "main": "dist/lib.js",
21
21
  "types": "dist/lib.d.ts",
22
22
  "scripts": {
@@ -58,7 +58,7 @@
58
58
  "http-status-codes": "^2.3.0",
59
59
  "json-pointer": "^0.6.2",
60
60
  "kubernetes-fluent-client": "3.10.1",
61
- "pino": "9.7.0",
61
+ "pino": "9.9.1",
62
62
  "pino-pretty": "13.1.1",
63
63
  "prom-client": "15.1.3",
64
64
  "quicktype-core": "^23.2.6",
@@ -95,14 +95,14 @@
95
95
  },
96
96
  "peerDependencies": {
97
97
  "@types/prompts": "2.4.9",
98
- "@typescript-eslint/eslint-plugin": "8.38.0",
99
- "@typescript-eslint/parser": "8.38.0",
100
- "esbuild": "0.25.8",
101
- "eslint": "9.32.0",
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",
105
105
  "typescript": "5.8.3",
106
106
  "uuid": "11.1.0"
107
107
  }
108
- }
108
+ }
@@ -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;
@@ -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("-e, --entry-point <file>", "Specify the entry point file to build with.", "pepr.ts")
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>",
@@ -23,8 +23,9 @@ export default function (program: Command): void {
23
23
  .command("update")
24
24
  .description("Update this Pepr module. Not recommended for prod as it may change files.")
25
25
  .option("-s, --skip-template-update", "Do not update template files")
26
+ .option("-y, --yes", "Skip confirmation prompt")
26
27
  .action(async opts => {
27
- if (!opts.skipTemplateUpdate) {
28
+ if (!opts.skipTemplateUpdate && !opts.yes) {
28
29
  const { confirm } = await prompt({
29
30
  type: "confirm",
30
31
  name: "confirm",
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"),
@@ -231,9 +231,9 @@ export class Capability implements CapabilityExport {
231
231
  const log = (message: string, cbString: string): void => {
232
232
  const filteredObj = pickBy(isNotEmpty, binding.filters);
233
233
 
234
- Log.info(`${message} configured for ${binding.event}`, prefix);
235
- Log.info(filteredObj, prefix);
236
- Log.debug(cbString, prefix);
234
+ Log.info({ prefix }, `${message} configured for ${binding.event}`);
235
+ Log.info({ prefix }, JSON.stringify(filteredObj));
236
+ Log.debug({ prefix }, cbString);
237
237
  };
238
238
 
239
239
  function Validate(validateCallback: ValidateAction<T>): ValidateActionChain<T> {
@@ -376,49 +376,49 @@ export class Capability implements CapabilityExport {
376
376
  }
377
377
 
378
378
  function InNamespace(...namespaces: string[]): BindingWithName<T> {
379
- Log.debug(`Add namespaces filter ${namespaces}`, prefix);
379
+ Log.debug({ prefix }, `Add namespaces filter ${namespaces}`);
380
380
  binding.filters.namespaces.push(...namespaces);
381
381
  return { ...commonChain, WithName, WithNameRegex };
382
382
  }
383
383
 
384
384
  function InNamespaceRegex(...namespaces: RegExp[]): BindingWithName<T> {
385
- Log.debug(`Add regex namespaces filter ${namespaces}`, prefix);
385
+ Log.debug({ prefix }, `Add regex namespaces filter ${namespaces}`);
386
386
  binding.filters.regexNamespaces.push(...namespaces.map(regex => regex.source));
387
387
  return { ...commonChain, WithName, WithNameRegex };
388
388
  }
389
389
 
390
390
  function WithDeletionTimestamp(): BindingFilter<T> {
391
- Log.debug("Add deletionTimestamp filter");
391
+ Log.debug({ prefix }, "Add deletionTimestamp filter");
392
392
  binding.filters.deletionTimestamp = true;
393
393
  return commonChain;
394
394
  }
395
395
 
396
396
  function WithNameRegex(regexName: RegExp): BindingFilter<T> {
397
- Log.debug(`Add regex name filter ${regexName}`, prefix);
397
+ Log.debug({ prefix }, `Add regex name filter ${regexName}`);
398
398
  binding.filters.regexName = regexName.source;
399
399
  return commonChain;
400
400
  }
401
401
 
402
402
  function WithName(name: string): BindingFilter<T> {
403
- Log.debug(`Add name filter ${name}`, prefix);
403
+ Log.debug({ prefix }, `Add name filter ${name}`);
404
404
  binding.filters.name = name;
405
405
  return commonChain;
406
406
  }
407
407
 
408
408
  function WithLabel(key: string, value = ""): BindingFilter<T> {
409
- Log.debug(`Add label filter ${key}=${value}`, prefix);
409
+ Log.debug({ prefix }, `Add label filter ${key}=${value}`);
410
410
  binding.filters.labels[key] = value;
411
411
  return commonChain;
412
412
  }
413
413
 
414
414
  function WithAnnotation(key: string, value = ""): BindingFilter<T> {
415
- Log.debug(`Add annotation filter ${key}=${value}`, prefix);
415
+ Log.debug({ prefix }, `Add annotation filter ${key}=${value}`);
416
416
  binding.filters.annotations[key] = value;
417
417
  return commonChain;
418
418
  }
419
419
 
420
420
  function Alias(alias: string): CommonChainType {
421
- Log.debug(`Adding prefix alias ${alias}`, prefix);
421
+ Log.debug({ prefix }, `Adding prefix alias ${alias}`);
422
422
  binding.alias = alias;
423
423
  return commonChain;
424
424
  }
@@ -116,7 +116,7 @@ export class Queue<K extends KubernetesObject> {
116
116
 
117
117
  element.resolve();
118
118
  } catch (e) {
119
- Log.debug(`Error reconciling ${element.item.metadata!.name}`, { error: e });
119
+ Log.debug({ error: e }, `Error reconciling ${element.item.metadata!.name}`);
120
120
  element.reject(e);
121
121
  } finally {
122
122
  Log.debug(this.stats(), "Queue stats - shift");
@@ -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();
@@ -227,16 +227,17 @@ export function secretOverLimit(str: string): boolean {
227
227
  }
228
228
 
229
229
  export const parseTimeout = (value: string): number => {
230
- const parsedValue = parseInt(value, 10);
231
- const floatValue = parseFloat(value);
232
- if (isNaN(parsedValue)) {
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
- } else if (parsedValue < 1 || parsedValue > 30) {
234
+ }
235
+
236
+ if (num < 1 || num > 30) {
237
237
  throw new Error("Number must be between 1 and 30.");
238
238
  }
239
- return parsedValue;
239
+
240
+ return num;
240
241
  };
241
242
 
242
243
  // Remove leading whitespace while keeping format of file
@@ -69,7 +69,7 @@ export class MetricsCollector {
69
69
  { name, help, labelNames }: Omit<MetricArgs, "registers">,
70
70
  ): void => {
71
71
  if (collection.has(this.#getMetricName(name))) {
72
- Log.debug(`Metric for ${name} already exists`, loggingPrefix);
72
+ Log.debug({ loggingPrefix }, `Metric for ${name} already exists`);
73
73
  return;
74
74
  }
75
75
 
@@ -66,7 +66,7 @@ When(a.Namespace)
66
66
  });
67
67
  } catch (error) {
68
68
  // You can use the Log object to log messages to the Pepr controller pod
69
- Log.error(error, "Failed to apply ConfigMap using server-side apply.");
69
+ Log.error({ error }, "Failed to apply ConfigMap using server-side apply.");
70
70
  }
71
71
 
72
72
  // You can share data between actions using the Store, including between different types of actions
@@ -139,7 +139,7 @@ When(a.ConfigMap)
139
139
  })
140
140
  .Watch((cm, phase) => {
141
141
  // This Watch Action will watch the ConfigMap after it has been persisted to the cluster
142
- Log.info(cm, `ConfigMap was ${phase} with the name example-2`);
142
+ Log.info({ cm }, `ConfigMap was ${phase} with the name example-2`);
143
143
  });
144
144
 
145
145
  /**
@@ -310,9 +310,7 @@ When(a.ConfigMap)
310
310
  },
311
311
  });
312
312
  } catch (error) {
313
- Log.error(error, "Failed to apply ConfigMap using server-side apply.", {
314
- cm,
315
- });
313
+ Log.error({ error, cm }, "Failed to apply ConfigMap using server-side apply.");
316
314
  }
317
315
  }
318
316
 
@@ -454,5 +452,5 @@ When(UnicornKind)
454
452
  * A callback function that is called once the Pepr Store is fully loaded.
455
453
  */
456
454
  Store.onReady(data => {
457
- Log.info(data, "Pepr Store Ready");
455
+ Log.info({ data }, "Pepr Store Ready");
458
456
  });