pepr 0.12.2 → 0.13.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.
Files changed (99) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +70 -0
  3. package/README.md +28 -30
  4. package/dist/cli.js +666 -692
  5. package/dist/controller.js +13 -81
  6. package/dist/lib/assets/deploy.d.ts +3 -0
  7. package/dist/lib/assets/deploy.d.ts.map +1 -0
  8. package/dist/lib/assets/index.d.ts +17 -0
  9. package/dist/lib/assets/index.d.ts.map +1 -0
  10. package/dist/lib/assets/loader.d.ts +8 -0
  11. package/dist/lib/assets/loader.d.ts.map +1 -0
  12. package/dist/lib/assets/networking.d.ts +6 -0
  13. package/dist/lib/assets/networking.d.ts.map +1 -0
  14. package/dist/lib/assets/pods.d.ts +8 -0
  15. package/dist/lib/assets/pods.d.ts.map +1 -0
  16. package/dist/lib/assets/rbac.d.ts +11 -0
  17. package/dist/lib/assets/rbac.d.ts.map +1 -0
  18. package/dist/lib/assets/webhooks.d.ts +6 -0
  19. package/dist/lib/assets/webhooks.d.ts.map +1 -0
  20. package/dist/lib/assets/yaml.d.ts +4 -0
  21. package/dist/lib/assets/yaml.d.ts.map +1 -0
  22. package/dist/lib/capability.d.ts +4 -9
  23. package/dist/lib/capability.d.ts.map +1 -1
  24. package/dist/lib/controller.d.ts +4 -15
  25. package/dist/lib/controller.d.ts.map +1 -1
  26. package/dist/lib/errors.d.ts +12 -0
  27. package/dist/lib/errors.d.ts.map +1 -0
  28. package/dist/lib/filter.d.ts +1 -1
  29. package/dist/lib/filter.d.ts.map +1 -1
  30. package/dist/lib/k8s/index.d.ts +2 -1
  31. package/dist/lib/k8s/index.d.ts.map +1 -1
  32. package/dist/lib/k8s/kinds.d.ts.map +1 -1
  33. package/dist/lib/k8s/types.d.ts +18 -14
  34. package/dist/lib/k8s/types.d.ts.map +1 -1
  35. package/dist/lib/k8s/upstream.d.ts +2 -2
  36. package/dist/lib/k8s/upstream.d.ts.map +1 -1
  37. package/dist/lib/logger.d.ts +8 -54
  38. package/dist/lib/logger.d.ts.map +1 -1
  39. package/dist/lib/metrics.d.ts +10 -9
  40. package/dist/lib/metrics.d.ts.map +1 -1
  41. package/dist/lib/module.d.ts +4 -4
  42. package/dist/lib/module.d.ts.map +1 -1
  43. package/dist/lib/mutate-processor.d.ts +5 -0
  44. package/dist/lib/mutate-processor.d.ts.map +1 -0
  45. package/dist/lib/{request.d.ts → mutate-request.d.ts} +7 -7
  46. package/dist/lib/mutate-request.d.ts.map +1 -0
  47. package/dist/lib/types.d.ts +48 -55
  48. package/dist/lib/types.d.ts.map +1 -1
  49. package/dist/lib/validate-processor.d.ts +4 -0
  50. package/dist/lib/validate-processor.d.ts.map +1 -0
  51. package/dist/lib/validate-request.d.ts +54 -0
  52. package/dist/lib/validate-request.d.ts.map +1 -0
  53. package/dist/lib.d.ts +3 -2
  54. package/dist/lib.d.ts.map +1 -1
  55. package/dist/lib.js +610 -354
  56. package/dist/lib.js.map +4 -4
  57. package/jest.config.json +4 -0
  58. package/journey/before.ts +21 -0
  59. package/journey/k8s.ts +81 -0
  60. package/journey/pepr-build.ts +69 -0
  61. package/journey/pepr-deploy.ts +133 -0
  62. package/journey/pepr-dev.ts +155 -0
  63. package/journey/pepr-format.ts +13 -0
  64. package/journey/pepr-init.ts +12 -0
  65. package/package.json +29 -27
  66. package/src/cli.ts +2 -11
  67. package/src/lib/assets/deploy.ts +179 -0
  68. package/src/lib/assets/index.ts +53 -0
  69. package/src/lib/assets/loader.ts +41 -0
  70. package/src/lib/assets/networking.ts +58 -0
  71. package/src/lib/assets/pods.ts +148 -0
  72. package/src/lib/assets/rbac.ts +57 -0
  73. package/src/lib/assets/webhooks.ts +139 -0
  74. package/src/lib/assets/yaml.ts +75 -0
  75. package/src/lib/capability.ts +80 -68
  76. package/src/lib/controller.ts +199 -99
  77. package/src/lib/errors.ts +20 -0
  78. package/src/lib/fetch.ts +1 -1
  79. package/src/lib/filter.ts +1 -3
  80. package/src/lib/k8s/index.ts +4 -1
  81. package/src/lib/k8s/kinds.ts +40 -0
  82. package/src/lib/k8s/types.ts +21 -15
  83. package/src/lib/k8s/upstream.ts +5 -1
  84. package/src/lib/logger.ts +14 -125
  85. package/src/lib/metrics.ts +86 -29
  86. package/src/lib/module.ts +32 -16
  87. package/src/lib/{processor.ts → mutate-processor.ts} +39 -28
  88. package/src/lib/{request.ts → mutate-request.ts} +26 -13
  89. package/src/lib/types.ts +54 -60
  90. package/src/lib/validate-processor.ts +76 -0
  91. package/src/lib/validate-request.ts +106 -0
  92. package/src/lib.ts +4 -2
  93. package/src/runtime/controller.ts +1 -1
  94. package/dist/lib/k8s/webhook.d.ts +0 -37
  95. package/dist/lib/k8s/webhook.d.ts.map +0 -1
  96. package/dist/lib/processor.d.ts +0 -5
  97. package/dist/lib/processor.d.ts.map +0 -1
  98. package/dist/lib/request.d.ts.map +0 -1
  99. package/src/lib/k8s/webhook.ts +0 -643
package/dist/lib.js CHANGED
@@ -33,8 +33,9 @@ __export(lib_exports, {
33
33
  Capability: () => Capability,
34
34
  Log: () => logger_default,
35
35
  PeprModule: () => PeprModule,
36
- PeprRequest: () => PeprRequest,
36
+ PeprMutateRequest: () => PeprMutateRequest,
37
37
  PeprUtils: () => utils_exports,
38
+ PeprValidateRequest: () => PeprValidateRequest,
38
39
  R: () => R,
39
40
  RegisterKind: () => RegisterKind,
40
41
  a: () => upstream_exports,
@@ -48,55 +49,63 @@ var k8s = __toESM(require("@kubernetes/client-node"));
48
49
  var import_http_status_codes2 = require("http-status-codes");
49
50
  var R = __toESM(require("ramda"));
50
51
 
52
+ // src/lib/capability.ts
53
+ var import_ramda = require("ramda");
54
+
51
55
  // src/lib/k8s/upstream.ts
52
56
  var upstream_exports = {};
53
57
  __export(upstream_exports, {
54
- APIService: () => import_client_node.V1APIService,
55
- CSIDriver: () => import_client_node.V1CSIDriver,
56
- CSIStorageCapacity: () => import_client_node.V1CSIStorageCapacity,
57
- CertificateSigningRequest: () => import_client_node.V1CertificateSigningRequest,
58
- ConfigMap: () => import_client_node.V1ConfigMap,
59
- ControllerRevision: () => import_client_node.V1ControllerRevision,
60
- CronJob: () => import_client_node.V1CronJob,
61
- CustomResourceDefinition: () => import_client_node.V1CustomResourceDefinition,
62
- DaemonSet: () => import_client_node.V1DaemonSet,
63
- Deployment: () => import_client_node.V1Deployment,
64
- EndpointSlice: () => import_client_node.V1EndpointSlice,
58
+ APIService: () => import_client_node2.V1APIService,
59
+ CSIDriver: () => import_client_node2.V1CSIDriver,
60
+ CSIStorageCapacity: () => import_client_node2.V1CSIStorageCapacity,
61
+ CertificateSigningRequest: () => import_client_node2.V1CertificateSigningRequest,
62
+ ClusterRole: () => import_client_node2.V1ClusterRole,
63
+ ClusterRoleBinding: () => import_client_node2.V1ClusterRoleBinding,
64
+ ConfigMap: () => import_client_node2.V1ConfigMap,
65
+ ControllerRevision: () => import_client_node2.V1ControllerRevision,
66
+ CronJob: () => import_client_node2.V1CronJob,
67
+ CustomResourceDefinition: () => import_client_node2.V1CustomResourceDefinition,
68
+ DaemonSet: () => import_client_node2.V1DaemonSet,
69
+ Deployment: () => import_client_node2.V1Deployment,
70
+ EndpointSlice: () => import_client_node2.V1EndpointSlice,
65
71
  GenericKind: () => GenericKind,
66
- HorizontalPodAutoscaler: () => import_client_node.V1HorizontalPodAutoscaler,
67
- Ingress: () => import_client_node.V1Ingress,
68
- IngressClass: () => import_client_node.V1IngressClass,
69
- Job: () => import_client_node.V1Job,
70
- LimitRange: () => import_client_node.V1LimitRange,
71
- LocalSubjectAccessReview: () => import_client_node.V1LocalSubjectAccessReview,
72
- MutatingWebhookConfiguration: () => import_client_node.V1MutatingWebhookConfiguration,
73
- Namespace: () => import_client_node.V1Namespace,
74
- NetworkPolicy: () => import_client_node.V1NetworkPolicy,
75
- Node: () => import_client_node.V1Node,
76
- PersistentVolume: () => import_client_node.V1PersistentVolume,
77
- PersistentVolumeClaim: () => import_client_node.V1PersistentVolumeClaim,
78
- Pod: () => import_client_node.V1Pod,
79
- PodDisruptionBudget: () => import_client_node.V1PodDisruptionBudget,
80
- PodTemplate: () => import_client_node.V1PodTemplate,
81
- ReplicaSet: () => import_client_node.V1ReplicaSet,
82
- ReplicationController: () => import_client_node.V1ReplicationController,
83
- ResourceQuota: () => import_client_node.V1ResourceQuota,
84
- RuntimeClass: () => import_client_node.V1RuntimeClass,
85
- Secret: () => import_client_node.V1Secret,
86
- SelfSubjectAccessReview: () => import_client_node.V1SelfSubjectAccessReview,
87
- SelfSubjectRulesReview: () => import_client_node.V1SelfSubjectRulesReview,
88
- Service: () => import_client_node.V1Service,
89
- ServiceAccount: () => import_client_node.V1ServiceAccount,
90
- StatefulSet: () => import_client_node.V1StatefulSet,
91
- StorageClass: () => import_client_node.V1StorageClass,
92
- SubjectAccessReview: () => import_client_node.V1SubjectAccessReview,
93
- TokenReview: () => import_client_node.V1TokenReview,
94
- ValidatingWebhookConfiguration: () => import_client_node.V1ValidatingWebhookConfiguration,
95
- VolumeAttachment: () => import_client_node.V1VolumeAttachment
72
+ HorizontalPodAutoscaler: () => import_client_node2.V1HorizontalPodAutoscaler,
73
+ Ingress: () => import_client_node2.V1Ingress,
74
+ IngressClass: () => import_client_node2.V1IngressClass,
75
+ Job: () => import_client_node2.V1Job,
76
+ LimitRange: () => import_client_node2.V1LimitRange,
77
+ LocalSubjectAccessReview: () => import_client_node2.V1LocalSubjectAccessReview,
78
+ MutatingWebhookConfiguration: () => import_client_node2.V1MutatingWebhookConfiguration,
79
+ Namespace: () => import_client_node2.V1Namespace,
80
+ NetworkPolicy: () => import_client_node2.V1NetworkPolicy,
81
+ Node: () => import_client_node2.V1Node,
82
+ PersistentVolume: () => import_client_node2.V1PersistentVolume,
83
+ PersistentVolumeClaim: () => import_client_node2.V1PersistentVolumeClaim,
84
+ Pod: () => import_client_node2.V1Pod,
85
+ PodDisruptionBudget: () => import_client_node2.V1PodDisruptionBudget,
86
+ PodTemplate: () => import_client_node2.V1PodTemplate,
87
+ ReplicaSet: () => import_client_node2.V1ReplicaSet,
88
+ ReplicationController: () => import_client_node2.V1ReplicationController,
89
+ ResourceQuota: () => import_client_node2.V1ResourceQuota,
90
+ Role: () => import_client_node2.V1Role,
91
+ RoleBinding: () => import_client_node2.V1RoleBinding,
92
+ RuntimeClass: () => import_client_node2.V1RuntimeClass,
93
+ Secret: () => import_client_node2.V1Secret,
94
+ SelfSubjectAccessReview: () => import_client_node2.V1SelfSubjectAccessReview,
95
+ SelfSubjectRulesReview: () => import_client_node2.V1SelfSubjectRulesReview,
96
+ Service: () => import_client_node2.V1Service,
97
+ ServiceAccount: () => import_client_node2.V1ServiceAccount,
98
+ StatefulSet: () => import_client_node2.V1StatefulSet,
99
+ StorageClass: () => import_client_node2.V1StorageClass,
100
+ SubjectAccessReview: () => import_client_node2.V1SubjectAccessReview,
101
+ TokenReview: () => import_client_node2.V1TokenReview,
102
+ ValidatingWebhookConfiguration: () => import_client_node2.V1ValidatingWebhookConfiguration,
103
+ VolumeAttachment: () => import_client_node2.V1VolumeAttachment
96
104
  });
97
- var import_client_node = require("@kubernetes/client-node");
105
+ var import_client_node2 = require("@kubernetes/client-node");
98
106
 
99
107
  // src/lib/k8s/types.ts
108
+ var import_client_node = require("@kubernetes/client-node");
100
109
  var GenericKind = class {
101
110
  apiVersion;
102
111
  kind;
@@ -105,6 +114,46 @@ var GenericKind = class {
105
114
 
106
115
  // src/lib/k8s/kinds.ts
107
116
  var gvkMap = {
117
+ /**
118
+ * Represents a K8s ClusterRole resource.
119
+ * ClusterRole is a set of permissions that can be bound to a user or group in a cluster-wide scope.
120
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole}
121
+ */
122
+ V1ClusterRole: {
123
+ kind: "ClusterRole",
124
+ version: "v1",
125
+ group: "rbac.authorization.k8s.io"
126
+ },
127
+ /**
128
+ * Represents a K8s ClusterRoleBinding resource.
129
+ * ClusterRoleBinding binds a ClusterRole to a user or group in a cluster-wide scope.
130
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding}
131
+ */
132
+ V1ClusterRoleBinding: {
133
+ kind: "ClusterRoleBinding",
134
+ version: "v1",
135
+ group: "rbac.authorization.k8s.io"
136
+ },
137
+ /**
138
+ * Represents a K8s Role resource.
139
+ * Role is a set of permissions that can be bound to a user or group in a namespace scope.
140
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole}
141
+ */
142
+ V1Role: {
143
+ kind: "Role",
144
+ version: "v1",
145
+ group: "rbac.authorization.k8s.io"
146
+ },
147
+ /**
148
+ * Represents a K8s RoleBinding resource.
149
+ * RoleBinding binds a Role to a user or group in a namespace scope.
150
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding}
151
+ */
152
+ V1RoleBinding: {
153
+ kind: "RoleBinding",
154
+ version: "v1",
155
+ group: "rbac.authorization.k8s.io"
156
+ },
108
157
  /**
109
158
  * Represents a K8s ConfigMap resource.
110
159
  * ConfigMap holds configuration data for pods to consume.
@@ -539,120 +588,51 @@ var RegisterKind = (model, groupVersionKind) => {
539
588
  gvkMap[name] = groupVersionKind;
540
589
  };
541
590
 
591
+ // src/lib/k8s/index.ts
592
+ var isWatchMode = process.env.PEPR_WATCH_MODE === "true";
593
+
542
594
  // src/lib/logger.ts
543
- var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
544
- LogLevel2[LogLevel2["debug"] = 0] = "debug";
545
- LogLevel2[LogLevel2["info"] = 1] = "info";
546
- LogLevel2[LogLevel2["warn"] = 2] = "warn";
547
- LogLevel2[LogLevel2["error"] = 3] = "error";
548
- return LogLevel2;
549
- })(LogLevel || {});
550
- var Logger = class {
551
- _logLevel;
552
- /**
553
- * Create a new logger instance.
554
- * @param logLevel - The minimum log level to log messages for.
555
- */
556
- constructor(logLevel) {
557
- this._logLevel = logLevel;
558
- }
559
- /**
560
- * Change the log level of the logger.
561
- * @param logLevel - The log level to log the message at.
562
- */
563
- SetLogLevel(logLevel) {
564
- this._logLevel = LogLevel[logLevel];
565
- this.debug(`Log level set to ${logLevel}`);
566
- }
567
- /**
568
- * Log a debug message.
569
- * @param message - The message to log.
570
- */
571
- debug(message, prefix) {
572
- this.log(0 /* debug */, message, prefix);
573
- }
574
- /**
575
- * Log an info message.
576
- * @param message - The message to log.
577
- */
578
- info(message, prefix) {
579
- this.log(1 /* info */, message, prefix);
580
- }
581
- /**
582
- * Log a warning message.
583
- * @param message - The message to log.
584
- */
585
- warn(message, prefix) {
586
- this.log(2 /* warn */, message, prefix);
587
- }
588
- /**
589
- * Log an error message.
590
- * @param message - The message to log.
591
- */
592
- error(message, prefix) {
593
- this.log(3 /* error */, message, prefix);
594
- }
595
- /**
596
- * Log a message at the specified log level.
597
- * @param logLevel - The log level of the message.
598
- * @param message - The message to log.
599
- */
600
- log(logLevel, message, callerPrefix = "") {
601
- const color = {
602
- [0 /* debug */]: "\x1B[30m" /* FgBlack */,
603
- [1 /* info */]: "\x1B[36m" /* FgCyan */,
604
- [2 /* warn */]: "\x1B[33m" /* FgYellow */,
605
- [3 /* error */]: "\x1B[31m" /* FgRed */
606
- };
607
- if (logLevel >= this._logLevel) {
608
- let prefix = "[" + LogLevel[logLevel] + "] " + callerPrefix;
609
- prefix = this.colorize(prefix, color[logLevel]);
610
- if (typeof message !== "string") {
611
- console.log(prefix);
612
- console.debug("%o", message);
613
- } else {
614
- console.log(prefix + " " + message);
615
- }
616
- }
617
- }
618
- colorize(text, color) {
619
- return color + text + "\x1B[0m" /* Reset */;
595
+ var import_pino = require("pino");
596
+ var isPrettyLog = process.env.PEPR_PRETTY_LOGS === "true";
597
+ var pretty = {
598
+ target: "pino-pretty",
599
+ options: {
600
+ colorize: true
620
601
  }
621
602
  };
622
- var Log = new Logger(1 /* info */);
603
+ var transport = isPrettyLog ? pretty : void 0;
604
+ var Log = (0, import_pino.pino)({
605
+ transport
606
+ });
623
607
  if (process.env.LOG_LEVEL) {
624
- Log.SetLogLevel(process.env.LOG_LEVEL);
608
+ Log.level = process.env.LOG_LEVEL;
625
609
  }
626
610
  var logger_default = Log;
627
611
 
628
612
  // src/lib/capability.ts
629
613
  var Capability = class {
630
- _name;
631
- _description;
632
- _namespaces;
633
- // Currently everything is considered a mutation
634
- _mutateOrValidate = "mutate" /* mutate */;
635
- _bindings = [];
614
+ #name;
615
+ #description;
616
+ #namespaces;
617
+ #bindings = [];
636
618
  get bindings() {
637
- return this._bindings;
619
+ return this.#bindings;
638
620
  }
639
621
  get name() {
640
- return this._name;
622
+ return this.#name;
641
623
  }
642
624
  get description() {
643
- return this._description;
625
+ return this.#description;
644
626
  }
645
627
  get namespaces() {
646
- return this._namespaces || [];
647
- }
648
- get mutateOrValidate() {
649
- return this._mutateOrValidate;
628
+ return this.#namespaces || [];
650
629
  }
651
630
  constructor(cfg) {
652
- this._name = cfg.name;
653
- this._description = cfg.description;
654
- this._namespaces = cfg.namespaces;
655
- logger_default.info(`Capability ${this._name} registered`);
631
+ this.#name = cfg.name;
632
+ this.#description = cfg.description;
633
+ this.#namespaces = cfg.namespaces;
634
+ this.When = this.When.bind(this);
635
+ logger_default.info(`Capability ${this.#name} registered`);
656
636
  logger_default.debug(cfg);
657
637
  }
658
638
  /**
@@ -664,7 +644,7 @@ var Capability = class {
664
644
  * @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
665
645
  * @returns
666
646
  */
667
- When = (model, kind) => {
647
+ When(model, kind) {
668
648
  const matchedKind = modelToGroupVersionKind(model.name);
669
649
  if (!matchedKind && !kind) {
670
650
  throw new Error(`Kind not specified for ${model.name}`);
@@ -678,62 +658,74 @@ var Capability = class {
678
658
  namespaces: [],
679
659
  labels: {},
680
660
  annotations: {}
681
- },
682
- callback: () => void 0
683
- };
684
- const prefix = `${this._name}: ${model.name}`;
685
- logger_default.info(`Binding created`, prefix);
686
- const Then = (cb) => {
687
- logger_default.info(`Binding action created`, prefix);
688
- logger_default.debug(cb.toString(), prefix);
689
- this._bindings.push({
690
- ...binding,
691
- callback: cb
692
- });
693
- return { Then };
661
+ }
694
662
  };
695
- const ThenSet = (merge) => {
696
- Then((req) => req.Merge(merge));
697
- return { Then };
663
+ const bindings = this.#bindings;
664
+ const prefix = `${this.#name}: ${model.name}`;
665
+ const commonChain = { WithLabel, WithAnnotation, Mutate, Validate };
666
+ const isNotEmpty = (value) => Object.keys(value).length > 0;
667
+ const log = (message, cbString) => {
668
+ const filteredObj = (0, import_ramda.pickBy)(isNotEmpty, binding.filters);
669
+ logger_default.info(`${message} configured for ${binding.event}`, prefix);
670
+ logger_default.info(filteredObj, prefix);
671
+ logger_default.debug(cbString, prefix);
698
672
  };
673
+ function Validate(validateCallback) {
674
+ if (!isWatchMode) {
675
+ log("Validate Action", validateCallback.toString());
676
+ bindings.push({
677
+ ...binding,
678
+ isValidate: true,
679
+ validateCallback
680
+ });
681
+ }
682
+ }
683
+ function Mutate(mutateCallback) {
684
+ if (!isWatchMode) {
685
+ log("Mutate Action", mutateCallback.toString());
686
+ bindings.push({
687
+ ...binding,
688
+ isMutate: true,
689
+ mutateCallback
690
+ });
691
+ }
692
+ return { Validate };
693
+ }
699
694
  function InNamespace(...namespaces) {
700
695
  logger_default.debug(`Add namespaces filter ${namespaces}`, prefix);
701
696
  binding.filters.namespaces.push(...namespaces);
702
- return { WithLabel, WithAnnotation, WithName, Then, ThenSet };
697
+ return { ...commonChain, WithName };
703
698
  }
704
699
  function WithName(name) {
705
700
  logger_default.debug(`Add name filter ${name}`, prefix);
706
701
  binding.filters.name = name;
707
- return { WithLabel, WithAnnotation, Then, ThenSet };
702
+ return commonChain;
708
703
  }
709
704
  function WithLabel(key, value = "") {
710
705
  logger_default.debug(`Add label filter ${key}=${value}`, prefix);
711
706
  binding.filters.labels[key] = value;
712
- return { WithLabel, WithAnnotation, Then, ThenSet };
707
+ return commonChain;
713
708
  }
714
- const WithAnnotation = (key, value = "") => {
709
+ function WithAnnotation(key, value = "") {
715
710
  logger_default.debug(`Add annotation filter ${key}=${value}`, prefix);
716
711
  binding.filters.annotations[key] = value;
717
- return { WithLabel, WithAnnotation, Then, ThenSet };
718
- };
719
- const bindEvent = (event) => {
712
+ return commonChain;
713
+ }
714
+ function bindEvent(event) {
720
715
  binding.event = event;
721
716
  return {
717
+ ...commonChain,
722
718
  InNamespace,
723
- Then,
724
- ThenSet,
725
- WithAnnotation,
726
- WithLabel,
727
719
  WithName
728
720
  };
729
- };
721
+ }
730
722
  return {
731
723
  IsCreatedOrUpdated: () => bindEvent("CREATEORUPDATE" /* CreateOrUpdate */),
732
724
  IsCreated: () => bindEvent("CREATE" /* Create */),
733
725
  IsUpdated: () => bindEvent("UPDATE" /* Update */),
734
726
  IsDeleted: () => bindEvent("DELETE" /* Delete */)
735
727
  };
736
- };
728
+ }
737
729
  };
738
730
 
739
731
  // src/lib/fetch.ts
@@ -743,7 +735,7 @@ var fetchRaw = import_node_fetch.default;
743
735
  async function fetch(url, init) {
744
736
  let data = void 0;
745
737
  try {
746
- logger_default.debug(`Fetching ${url}`);
738
+ logger_default.debug(init, `Fetching ${url}`);
747
739
  const resp = await fetchRaw(url, init);
748
740
  const contentType = resp.headers.get("content-type") || "";
749
741
  if (resp.ok) {
@@ -780,16 +772,122 @@ async function fetch(url, init) {
780
772
  }
781
773
 
782
774
  // src/lib/module.ts
783
- var import_ramda2 = require("ramda");
775
+ var import_ramda4 = require("ramda");
784
776
 
785
777
  // src/lib/controller.ts
786
778
  var import_express = __toESM(require("express"));
787
779
  var import_fs = __toESM(require("fs"));
788
780
  var import_https = __toESM(require("https"));
789
781
 
790
- // src/lib/processor.ts
782
+ // src/lib/metrics.ts
783
+ var import_perf_hooks = require("perf_hooks");
784
+ var import_prom_client = __toESM(require("prom-client"));
785
+ var loggingPrefix = "MetricsCollector";
786
+ var MetricsCollector = class {
787
+ #registry;
788
+ #counters = /* @__PURE__ */ new Map();
789
+ #summaries = /* @__PURE__ */ new Map();
790
+ #prefix;
791
+ #metricNames = {
792
+ errors: "errors",
793
+ alerts: "alerts",
794
+ mutate: "Mutate",
795
+ validate: "Validate"
796
+ };
797
+ /**
798
+ * Creates a MetricsCollector instance with prefixed metrics.
799
+ * @param [prefix='pepr'] - The prefix for the metric names.
800
+ */
801
+ constructor(prefix = "pepr") {
802
+ this.#registry = new import_prom_client.Registry();
803
+ this.#prefix = prefix;
804
+ this.addCounter(this.#metricNames.errors, "Mutation/Validate errors encountered");
805
+ this.addCounter(this.#metricNames.alerts, "Mutation/Validate bad api token received");
806
+ this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
807
+ this.addSummary(this.#metricNames.validate, "Validation operation summary");
808
+ }
809
+ #getMetricName(name) {
810
+ return `${this.#prefix}_${name}`;
811
+ }
812
+ #addMetric(collection, MetricType, name, help) {
813
+ if (collection.has(this.#getMetricName(name))) {
814
+ logger_default.debug(`Metric for ${name} already exists`, loggingPrefix);
815
+ return;
816
+ }
817
+ this.incCounter = this.incCounter.bind(this);
818
+ this.error = this.error.bind(this);
819
+ this.alert = this.alert.bind(this);
820
+ this.observeStart = this.observeStart.bind(this);
821
+ this.observeEnd = this.observeEnd.bind(this);
822
+ this.getMetrics = this.getMetrics.bind(this);
823
+ const metric = new MetricType({
824
+ name: this.#getMetricName(name),
825
+ help,
826
+ registers: [this.#registry]
827
+ });
828
+ collection.set(this.#getMetricName(name), metric);
829
+ }
830
+ addCounter(name, help) {
831
+ this.#addMetric(this.#counters, import_prom_client.default.Counter, name, help);
832
+ }
833
+ addSummary(name, help) {
834
+ this.#addMetric(this.#summaries, import_prom_client.default.Summary, name, help);
835
+ }
836
+ incCounter(name) {
837
+ this.#counters.get(this.#getMetricName(name))?.inc();
838
+ }
839
+ /**
840
+ * Increments the error counter.
841
+ */
842
+ error() {
843
+ this.incCounter(this.#metricNames.errors);
844
+ }
845
+ /**
846
+ * Increments the alerts counter.
847
+ */
848
+ alert() {
849
+ this.incCounter(this.#metricNames.alerts);
850
+ }
851
+ /**
852
+ * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
853
+ * @returns The timestamp.
854
+ */
855
+ observeStart() {
856
+ return import_perf_hooks.performance.now();
857
+ }
858
+ /**
859
+ * Observes the duration since the provided start time and updates the summary.
860
+ * @param startTime - The start time.
861
+ * @param name - The metrics summary to increment.
862
+ */
863
+ observeEnd(startTime, name = this.#metricNames.mutate) {
864
+ this.#summaries.get(this.#getMetricName(name))?.observe(import_perf_hooks.performance.now() - startTime);
865
+ }
866
+ /**
867
+ * Fetches the current metrics from the registry.
868
+ * @returns The metrics.
869
+ */
870
+ async getMetrics() {
871
+ return this.#registry.metrics();
872
+ }
873
+ };
874
+
875
+ // src/lib/mutate-processor.ts
791
876
  var import_fast_json_patch = __toESM(require("fast-json-patch"));
792
877
 
878
+ // src/lib/errors.ts
879
+ var Errors = {
880
+ audit: "audit",
881
+ ignore: "ignore",
882
+ reject: "reject"
883
+ };
884
+ var ErrorList = Object.values(Errors);
885
+ function ValidateError(error = "") {
886
+ if (!ErrorList.includes(error)) {
887
+ throw new Error(`Invalid error: ${error}. Must be one of: ${ErrorList.join(", ")}`);
888
+ }
889
+ }
890
+
793
891
  // src/lib/filter.ts
794
892
  function shouldSkipRequest(binding, req) {
795
893
  const { group, kind, version } = binding.kind || {};
@@ -797,7 +895,6 @@ function shouldSkipRequest(binding, req) {
797
895
  const operation = req.operation.toUpperCase();
798
896
  const srcObject = operation === "DELETE" /* DELETE */ ? req.oldObject : req.object;
799
897
  const { metadata } = srcObject || {};
800
- console.log(metadata);
801
898
  if (!binding.event.includes(operation) && !binding.event.includes("*" /* Any */)) {
802
899
  return true;
803
900
  }
@@ -842,48 +939,56 @@ function shouldSkipRequest(binding, req) {
842
939
  return false;
843
940
  }
844
941
 
845
- // src/lib/request.ts
846
- var import_ramda = require("ramda");
847
- var PeprRequest = class {
848
- /**
849
- * Creates a new instance of the Action class.
850
- * @param input - The request object containing the Kubernetes resource to modify.
851
- */
852
- constructor(_input) {
853
- this._input = _input;
854
- if (_input.operation.toUpperCase() === "DELETE" /* DELETE */) {
855
- this.Raw = (0, import_ramda.clone)(_input.oldObject);
856
- } else {
857
- this.Raw = (0, import_ramda.clone)(_input.object);
858
- }
859
- if (!this.Raw) {
860
- throw new Error("unable to load the request object into PeprRequest.RawP");
861
- }
862
- }
942
+ // src/lib/mutate-request.ts
943
+ var import_ramda2 = require("ramda");
944
+ var PeprMutateRequest = class {
863
945
  Raw;
946
+ #input;
864
947
  get PermitSideEffects() {
865
- return !this._input.dryRun;
948
+ return !this.#input.dryRun;
866
949
  }
867
950
  /**
868
951
  * Indicates whether the request is a dry run.
869
952
  * @returns true if the request is a dry run, false otherwise.
870
953
  */
871
954
  get IsDryRun() {
872
- return this._input.dryRun;
955
+ return this.#input.dryRun;
873
956
  }
874
957
  /**
875
958
  * Provides access to the old resource in the request if available.
876
959
  * @returns The old Kubernetes resource object or null if not available.
877
960
  */
878
961
  get OldResource() {
879
- return this._input.oldObject;
962
+ return this.#input.oldObject;
880
963
  }
881
964
  /**
882
965
  * Provides access to the request object.
883
966
  * @returns The request object containing the Kubernetes resource.
884
967
  */
885
968
  get Request() {
886
- return this._input;
969
+ return this.#input;
970
+ }
971
+ /**
972
+ * Creates a new instance of the action class.
973
+ * @param input - The request object containing the Kubernetes resource to modify.
974
+ */
975
+ constructor(input) {
976
+ this.#input = input;
977
+ this.Merge = this.Merge.bind(this);
978
+ this.SetLabel = this.SetLabel.bind(this);
979
+ this.SetAnnotation = this.SetAnnotation.bind(this);
980
+ this.RemoveLabel = this.RemoveLabel.bind(this);
981
+ this.RemoveAnnotation = this.RemoveAnnotation.bind(this);
982
+ this.HasLabel = this.HasLabel.bind(this);
983
+ this.HasAnnotation = this.HasAnnotation.bind(this);
984
+ if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
985
+ this.Raw = (0, import_ramda2.clone)(input.oldObject);
986
+ } else {
987
+ this.Raw = (0, import_ramda2.clone)(input.object);
988
+ }
989
+ if (!this.Raw) {
990
+ throw new Error("unable to load the request object into PeprRequest.RawP");
991
+ }
887
992
  }
888
993
  /**
889
994
  * Deep merges the provided object with the current resource.
@@ -891,13 +996,13 @@ var PeprRequest = class {
891
996
  * @param obj - The object to merge with the current resource.
892
997
  */
893
998
  Merge(obj) {
894
- this.Raw = (0, import_ramda.mergeDeepRight)(this.Raw, obj);
999
+ this.Raw = (0, import_ramda2.mergeDeepRight)(this.Raw, obj);
895
1000
  }
896
1001
  /**
897
1002
  * Updates a label on the Kubernetes resource.
898
1003
  * @param key - The key of the label to update.
899
1004
  * @param value - The value of the label.
900
- * @returns The current Action instance for method chaining.
1005
+ * @returns The current action instance for method chaining.
901
1006
  */
902
1007
  SetLabel(key, value) {
903
1008
  const ref = this.Raw;
@@ -910,7 +1015,7 @@ var PeprRequest = class {
910
1015
  * Updates an annotation on the Kubernetes resource.
911
1016
  * @param key - The key of the annotation to update.
912
1017
  * @param value - The value of the annotation.
913
- * @returns The current Action instance for method chaining.
1018
+ * @returns The current action instance for method chaining.
914
1019
  */
915
1020
  SetAnnotation(key, value) {
916
1021
  const ref = this.Raw;
@@ -1003,30 +1108,33 @@ function base64Encode(data) {
1003
1108
  return Buffer.from(data).toString("base64");
1004
1109
  }
1005
1110
 
1006
- // src/lib/processor.ts
1007
- async function processor(config, capabilities, req, parentPrefix) {
1008
- const wrapped = new PeprRequest(req);
1111
+ // src/lib/mutate-processor.ts
1112
+ async function mutateProcessor(config, capabilities, req, reqMetadata) {
1113
+ const wrapped = new PeprMutateRequest(req);
1009
1114
  const response = {
1010
1115
  uid: req.uid,
1011
1116
  warnings: [],
1012
1117
  allowed: false
1013
1118
  };
1014
- let matchedCapabilityAction = false;
1119
+ let matchedAction = false;
1015
1120
  let skipDecode = [];
1016
1121
  const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
1017
1122
  if (isSecret) {
1018
1123
  skipDecode = convertFromBase64Map(wrapped.Raw);
1019
1124
  }
1020
- logger_default.info(`Processing request`, parentPrefix);
1125
+ logger_default.info(reqMetadata, `Processing request`);
1021
1126
  for (const { name, bindings } of capabilities) {
1022
- const prefix = `${parentPrefix} ${name}:`;
1127
+ const actionMetadata = { ...reqMetadata, name };
1023
1128
  for (const action of bindings) {
1129
+ if (!action.mutateCallback) {
1130
+ continue;
1131
+ }
1024
1132
  if (shouldSkipRequest(action, req)) {
1025
1133
  continue;
1026
1134
  }
1027
- const label = action.callback.name;
1028
- logger_default.info(`Processing matched action ${label}`, prefix);
1029
- matchedCapabilityAction = true;
1135
+ const label = action.mutateCallback.name;
1136
+ logger_default.info(actionMetadata, `Processing matched action ${label}`);
1137
+ matchedAction = true;
1030
1138
  const updateStatus = (status) => {
1031
1139
  if (req.operation == "DELETE") {
1032
1140
  return;
@@ -1038,26 +1146,30 @@ async function processor(config, capabilities, req, parentPrefix) {
1038
1146
  };
1039
1147
  updateStatus("started");
1040
1148
  try {
1041
- await action.callback(wrapped);
1042
- logger_default.info(`Action succeeded`, prefix);
1149
+ await action.mutateCallback(wrapped);
1150
+ logger_default.info(actionMetadata, `Action succeeded`);
1043
1151
  updateStatus("succeeded");
1044
1152
  } catch (e) {
1153
+ logger_default.warn(actionMetadata, `Action failed: ${e}`);
1154
+ updateStatus("warning");
1045
1155
  response.warnings = response.warnings || [];
1046
1156
  response.warnings.push(`Action failed: ${e}`);
1047
- if (config.onError) {
1048
- logger_default.error(`Action failed: ${e}`, prefix);
1049
- response.result = "Pepr module configured to reject on error";
1050
- return response;
1051
- } else {
1052
- logger_default.warn(`Action failed: ${e}`, prefix);
1053
- updateStatus("warning");
1157
+ switch (config.onError) {
1158
+ case Errors.reject:
1159
+ logger_default.error(actionMetadata, `Action failed: ${e}`);
1160
+ response.result = "Pepr module configured to reject on error";
1161
+ return response;
1162
+ case Errors.audit:
1163
+ response.auditAnnotations = response.auditAnnotations || {};
1164
+ response.auditAnnotations[Date.now()] = e;
1165
+ break;
1054
1166
  }
1055
1167
  }
1056
1168
  }
1057
1169
  }
1058
1170
  response.allowed = true;
1059
- if (!matchedCapabilityAction) {
1060
- logger_default.info(`No matching capability action found`, parentPrefix);
1171
+ if (!matchedAction) {
1172
+ logger_default.info(reqMetadata, `No matching actions found`);
1061
1173
  return response;
1062
1174
  }
1063
1175
  if (req.operation == "DELETE") {
@@ -1070,126 +1182,200 @@ async function processor(config, capabilities, req, parentPrefix) {
1070
1182
  const patches = import_fast_json_patch.default.compare(req.object, transformed);
1071
1183
  if (patches.length > 0) {
1072
1184
  response.patchType = "JSONPatch";
1073
- response.patch = Buffer.from(JSON.stringify(patches)).toString("base64");
1185
+ response.patch = base64Encode(JSON.stringify(patches));
1074
1186
  }
1075
1187
  if (response.warnings && response.warnings.length < 1) {
1076
1188
  delete response.warnings;
1077
1189
  }
1078
- logger_default.debug(patches, parentPrefix);
1190
+ logger_default.debug({ ...reqMetadata, patches }, `Patches generated`);
1079
1191
  return response;
1080
1192
  }
1081
1193
 
1082
- // src/lib/metrics.ts
1083
- var import_prom_client = __toESM(require("prom-client"));
1084
- var import_perf_hooks = require("perf_hooks");
1085
- var MetricsCollector = class {
1086
- _registry;
1087
- _errors;
1088
- _alerts;
1089
- _summary;
1194
+ // src/lib/validate-request.ts
1195
+ var import_ramda3 = require("ramda");
1196
+ var PeprValidateRequest = class {
1197
+ Raw;
1198
+ #input;
1090
1199
  /**
1091
- * Creates a MetricsCollector instance with prefixed metrics.
1092
- * @param {string} [prefix='pepr'] - The prefix for the metric names.
1200
+ * Provides access to the old resource in the request if available.
1201
+ * @returns The old Kubernetes resource object or null if not available.
1093
1202
  */
1094
- constructor(prefix = "pepr") {
1095
- this._registry = new import_prom_client.default.Registry();
1096
- this._errors = new import_prom_client.default.Counter({
1097
- name: `${prefix}_errors`,
1098
- help: "error counter",
1099
- registers: [this._registry]
1100
- });
1101
- this._alerts = new import_prom_client.default.Counter({
1102
- name: `${prefix}_alerts`,
1103
- help: "alerts counter",
1104
- registers: [this._registry]
1105
- });
1106
- this._summary = new import_prom_client.default.Summary({
1107
- name: `${prefix}_summary`,
1108
- help: "summary",
1109
- registers: [this._registry]
1110
- });
1203
+ get OldResource() {
1204
+ return this.#input.oldObject;
1111
1205
  }
1112
1206
  /**
1113
- * Increments the error counter.
1207
+ * Provides access to the request object.
1208
+ * @returns The request object containing the Kubernetes resource.
1114
1209
  */
1115
- error() {
1116
- this._errors.inc();
1210
+ get Request() {
1211
+ return this.#input;
1117
1212
  }
1118
1213
  /**
1119
- * Increments the alerts counter.
1214
+ * Creates a new instance of the Action class.
1215
+ * @param input - The request object containing the Kubernetes resource to modify.
1120
1216
  */
1121
- alert() {
1122
- this._alerts.inc();
1217
+ constructor(input) {
1218
+ this.#input = input;
1219
+ this.HasLabel = this.HasLabel.bind(this);
1220
+ this.HasAnnotation = this.HasAnnotation.bind(this);
1221
+ this.Approve = this.Approve.bind(this);
1222
+ this.Deny = this.Deny.bind(this);
1223
+ if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
1224
+ this.Raw = (0, import_ramda3.clone)(input.oldObject);
1225
+ } else {
1226
+ this.Raw = (0, import_ramda3.clone)(input.object);
1227
+ }
1228
+ if (!this.Raw) {
1229
+ throw new Error("unable to load the request object into PeprRequest.RawP");
1230
+ }
1123
1231
  }
1124
1232
  /**
1125
- * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
1126
- * @returns {number} The timestamp.
1233
+ * Check if a label exists on the Kubernetes resource.
1234
+ *
1235
+ * @param key the label key to check
1236
+ * @returns
1127
1237
  */
1128
- observeStart() {
1129
- return import_perf_hooks.performance.now();
1238
+ HasLabel(key) {
1239
+ return this.Raw.metadata?.labels?.[key] !== void 0;
1130
1240
  }
1131
1241
  /**
1132
- * Observes the duration since the provided start time and updates the summary.
1133
- * @param {number} startTime - The start time.
1242
+ * Check if an annotation exists on the Kubernetes resource.
1243
+ *
1244
+ * @param key the annotation key to check
1245
+ * @returns
1134
1246
  */
1135
- observeEnd(startTime) {
1136
- this._summary.observe(import_perf_hooks.performance.now() - startTime);
1247
+ HasAnnotation(key) {
1248
+ return this.Raw.metadata?.annotations?.[key] !== void 0;
1137
1249
  }
1138
1250
  /**
1139
- * Fetches the current metrics from the registry.
1140
- * @returns {Promise<string>} The metrics.
1251
+ * Create a validation response that allows the request.
1252
+ *
1253
+ * @returns The validation response.
1141
1254
  */
1142
- async getMetrics() {
1143
- return this._registry.metrics();
1255
+ Approve() {
1256
+ return {
1257
+ allowed: true
1258
+ };
1259
+ }
1260
+ /**
1261
+ * Create a validation response that denies the request.
1262
+ *
1263
+ * @param statusMessage Optional status message to return to the user.
1264
+ * @param statusCode Optional status code to return to the user.
1265
+ * @returns The validation response.
1266
+ */
1267
+ Deny(statusMessage, statusCode) {
1268
+ return {
1269
+ allowed: false,
1270
+ statusCode,
1271
+ statusMessage
1272
+ };
1144
1273
  }
1145
1274
  };
1146
1275
 
1276
+ // src/lib/validate-processor.ts
1277
+ async function validateProcessor(capabilities, req, reqMetadata) {
1278
+ const wrapped = new PeprValidateRequest(req);
1279
+ const response = {
1280
+ uid: req.uid,
1281
+ allowed: true
1282
+ // Assume it's allowed until a validation check fails
1283
+ };
1284
+ const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
1285
+ if (isSecret) {
1286
+ convertFromBase64Map(wrapped.Raw);
1287
+ }
1288
+ logger_default.info(reqMetadata, `Processing validation request`);
1289
+ for (const { name, bindings } of capabilities) {
1290
+ const actionMetadata = { ...reqMetadata, name };
1291
+ for (const action of bindings) {
1292
+ if (!action.validateCallback) {
1293
+ continue;
1294
+ }
1295
+ if (shouldSkipRequest(action, req)) {
1296
+ continue;
1297
+ }
1298
+ const label = action.validateCallback.name;
1299
+ logger_default.info(actionMetadata, `Processing matched action ${label}`);
1300
+ try {
1301
+ const resp = await action.validateCallback(wrapped);
1302
+ response.allowed = resp.allowed;
1303
+ if (resp.statusCode || resp.statusMessage) {
1304
+ response.status = {
1305
+ code: resp.statusCode || 400,
1306
+ message: resp.statusMessage || `Validation failed for ${name}`
1307
+ };
1308
+ }
1309
+ logger_default.info(actionMetadata, `Validation Action completed: ${resp.allowed ? "allowed" : "denied"}`);
1310
+ } catch (e) {
1311
+ logger_default.error(actionMetadata, `Action failed: ${e}`);
1312
+ response.allowed = false;
1313
+ response.status = {
1314
+ code: 500,
1315
+ message: `Action failed with error: ${e}`
1316
+ };
1317
+ return response;
1318
+ }
1319
+ }
1320
+ }
1321
+ return response;
1322
+ }
1323
+
1147
1324
  // src/lib/controller.ts
1148
- var Controller = class {
1325
+ var Controller = class _Controller {
1326
+ // Track whether the server is running
1327
+ #running = false;
1328
+ // Metrics collector
1329
+ #metricsCollector = new MetricsCollector("pepr");
1330
+ // The token used to authenticate requests
1331
+ #token = "";
1332
+ // The express app instance
1333
+ #app = (0, import_express.default)();
1334
+ // Initialized with the constructor
1335
+ #config;
1336
+ #capabilities;
1337
+ #beforeHook;
1338
+ #afterHook;
1149
1339
  constructor(config, capabilities, beforeHook, afterHook) {
1150
- this.config = config;
1151
- this.capabilities = capabilities;
1152
- this.beforeHook = beforeHook;
1153
- this.afterHook = afterHook;
1154
- this.app.use(this.logger);
1155
- this.app.use(import_express.default.json({ limit: "2mb" }));
1156
- this.app.get("/healthz", this.healthz);
1157
- this.app.get("/metrics", this.metrics);
1158
- this.app.post("/mutate/:token", this.mutate);
1340
+ this.#config = config;
1341
+ this.#capabilities = capabilities;
1342
+ this.#beforeHook = beforeHook;
1343
+ this.#afterHook = afterHook;
1344
+ this.startServer = this.startServer.bind(this);
1345
+ this.#app.use(_Controller.#logger);
1346
+ this.#app.use(import_express.default.json({ limit: "2mb" }));
1159
1347
  if (beforeHook) {
1160
- console.info(`Using beforeHook: ${beforeHook}`);
1348
+ logger_default.info(`Using beforeHook: ${beforeHook}`);
1161
1349
  }
1162
1350
  if (afterHook) {
1163
- console.info(`Using afterHook: ${afterHook}`);
1351
+ logger_default.info(`Using afterHook: ${afterHook}`);
1164
1352
  }
1353
+ this.#bindEndpoints();
1165
1354
  }
1166
- app = (0, import_express.default)();
1167
- running = false;
1168
- metricsCollector = new MetricsCollector("pepr");
1169
- // The token used to authenticate requests
1170
- token = "";
1171
1355
  /** Start the webhook server */
1172
- startServer = (port) => {
1173
- if (this.running) {
1356
+ startServer(port) {
1357
+ if (this.#running) {
1174
1358
  throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
1175
1359
  }
1176
1360
  const options = {
1177
1361
  key: import_fs.default.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
1178
1362
  cert: import_fs.default.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt")
1179
1363
  };
1180
- this.token = process.env.PEPR_API_TOKEN || import_fs.default.readFileSync("/app/api-token/value").toString().trim();
1181
- console.info(`Using API token: ${this.token}`);
1182
- if (!this.token) {
1183
- throw new Error("API token not found");
1364
+ if (!isWatchMode) {
1365
+ this.#token = process.env.PEPR_API_TOKEN || import_fs.default.readFileSync("/app/api-token/value").toString().trim();
1366
+ logger_default.info(`Using API token: ${this.#token}`);
1367
+ if (!this.#token) {
1368
+ throw new Error("API token not found");
1369
+ }
1184
1370
  }
1185
- const server = import_https.default.createServer(options, this.app).listen(port);
1371
+ const server = import_https.default.createServer(options, this.#app).listen(port);
1186
1372
  server.on("listening", () => {
1187
- console.log(`Server listening on port ${port}`);
1188
- this.running = true;
1373
+ logger_default.info(`Server listening on port ${port}`);
1374
+ this.#running = true;
1189
1375
  });
1190
1376
  server.on("error", (e) => {
1191
1377
  if (e.code === "EADDRINUSE") {
1192
- console.log(
1378
+ logger_default.warn(
1193
1379
  `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`
1194
1380
  );
1195
1381
  setTimeout(() => {
@@ -1199,97 +1385,166 @@ var Controller = class {
1199
1385
  }
1200
1386
  });
1201
1387
  process.on("SIGTERM", () => {
1202
- console.log("Received SIGTERM, closing server");
1388
+ logger_default.info("Received SIGTERM, closing server");
1203
1389
  server.close(() => {
1204
- console.log("Server closed");
1390
+ logger_default.info("Server closed");
1205
1391
  process.exit(0);
1206
1392
  });
1207
1393
  });
1394
+ }
1395
+ #bindEndpoints = () => {
1396
+ this.#app.get("/healthz", _Controller.#healthz);
1397
+ this.#app.get("/metrics", this.#metrics);
1398
+ if (isWatchMode) {
1399
+ return;
1400
+ }
1401
+ this.#app.use(["/mutate/:token", "/validate/:token"], this.#validateToken);
1402
+ this.#app.post("/mutate/:token", this.#admissionReq("Mutate"));
1403
+ this.#app.post("/validate/:token", this.#admissionReq("Validate"));
1208
1404
  };
1209
- logger = (req, res, next) => {
1210
- const startTime = Date.now();
1211
- res.on("finish", () => {
1212
- const now = (/* @__PURE__ */ new Date()).toISOString();
1213
- const elapsedTime = Date.now() - startTime;
1214
- const message = `[${now}] ${req.method} ${req.originalUrl} [${res.statusCode}] ${elapsedTime} ms
1215
- `;
1216
- res.statusCode >= 400 ? console.error(message) : console.info(message);
1217
- });
1405
+ /**
1406
+ * Validate the token in the request path
1407
+ *
1408
+ * @param req The incoming request
1409
+ * @param res The outgoing response
1410
+ * @param next The next middleware function
1411
+ * @returns
1412
+ */
1413
+ #validateToken = (req, res, next) => {
1414
+ const { token } = req.params;
1415
+ if (token !== this.#token) {
1416
+ const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
1417
+ logger_default.warn(err);
1418
+ res.status(401).send(err);
1419
+ this.#metricsCollector.alert();
1420
+ return;
1421
+ }
1218
1422
  next();
1219
1423
  };
1220
- healthz = (req, res) => {
1424
+ /**
1425
+ * Metrics endpoint handler
1426
+ *
1427
+ * @param req the incoming request
1428
+ * @param res the outgoing response
1429
+ */
1430
+ #metrics = async (req, res) => {
1221
1431
  try {
1222
- res.send("OK");
1432
+ res.send(await this.#metricsCollector.getMetrics());
1223
1433
  } catch (err) {
1224
- console.error(err);
1434
+ logger_default.error(err);
1225
1435
  res.status(500).send("Internal Server Error");
1226
1436
  }
1227
1437
  };
1228
- metrics = async (req, res) => {
1229
- try {
1230
- res.send(await this.metricsCollector.getMetrics());
1231
- } catch (err) {
1232
- console.error(err);
1233
- res.status(500).send("Internal Server Error");
1234
- }
1438
+ /**
1439
+ * Admission request handler for both mutate and validate requests
1440
+ *
1441
+ * @param admissionKind the type of admission request
1442
+ * @returns the request handler
1443
+ */
1444
+ #admissionReq = (admissionKind) => {
1445
+ return async (req, res) => {
1446
+ const startTime = this.#metricsCollector.observeStart();
1447
+ try {
1448
+ const request = req.body?.request || {};
1449
+ this.#beforeHook && this.#beforeHook(request || {});
1450
+ const name = request?.name ? `/${request.name}` : "";
1451
+ const namespace = request?.namespace || "";
1452
+ const gvk = request?.kind || { group: "", version: "", kind: "" };
1453
+ const reqMetadata = {
1454
+ uid: request.uid,
1455
+ namespace,
1456
+ name
1457
+ };
1458
+ logger_default.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
1459
+ logger_default.debug({ ...reqMetadata, request }, "Incoming request body");
1460
+ let response;
1461
+ if (admissionKind === "Mutate") {
1462
+ response = await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata);
1463
+ } else {
1464
+ response = await validateProcessor(this.#capabilities, request, reqMetadata);
1465
+ }
1466
+ this.#afterHook && this.#afterHook(response);
1467
+ logger_default.debug({ ...reqMetadata, response }, "Outgoing response");
1468
+ res.send({
1469
+ apiVersion: "admission.k8s.io/v1",
1470
+ kind: "AdmissionReview",
1471
+ response
1472
+ });
1473
+ this.#metricsCollector.observeEnd(startTime, admissionKind);
1474
+ } catch (err) {
1475
+ logger_default.error(err);
1476
+ res.status(500).send("Internal Server Error");
1477
+ this.#metricsCollector.error();
1478
+ }
1479
+ };
1235
1480
  };
1236
- mutate = async (req, res) => {
1237
- const startTime = this.metricsCollector.observeStart();
1481
+ /**
1482
+ * Middleware for logging requests
1483
+ *
1484
+ * @param req the incoming request
1485
+ * @param res the outgoing response
1486
+ * @param next the next middleware function
1487
+ */
1488
+ static #logger(req, res, next) {
1489
+ const startTime = Date.now();
1490
+ res.on("finish", () => {
1491
+ const elapsedTime = Date.now() - startTime;
1492
+ const message = {
1493
+ uid: req.body?.request?.uid,
1494
+ method: req.method,
1495
+ url: req.originalUrl,
1496
+ status: res.statusCode,
1497
+ duration: `${elapsedTime} ms`
1498
+ };
1499
+ res.statusCode >= 300 ? logger_default.warn(message) : logger_default.info(message);
1500
+ });
1501
+ next();
1502
+ }
1503
+ /**
1504
+ * Health check endpoint handler
1505
+ *
1506
+ * @param req the incoming request
1507
+ * @param res the outgoing response
1508
+ */
1509
+ static #healthz(req, res) {
1238
1510
  try {
1239
- const { token } = req.params;
1240
- if (token !== this.token) {
1241
- const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
1242
- console.warn(err);
1243
- res.status(401).send(err);
1244
- this.metricsCollector.alert();
1245
- return;
1246
- }
1247
- const request = req.body?.request || {};
1248
- this.beforeHook && this.beforeHook(request || {});
1249
- const name = request?.name ? `/${request.name}` : "";
1250
- const namespace = request?.namespace || "";
1251
- const gvk = request?.kind || { group: "", version: "", kind: "" };
1252
- const prefix = `${request.uid} ${namespace}${name}`;
1253
- logger_default.info(`Mutate [${request.operation}] ${gvk.group}/${gvk.version}/${gvk.kind}`, prefix);
1254
- const response = await processor(this.config, this.capabilities, request, prefix);
1255
- this.afterHook && this.afterHook(response);
1256
- logger_default.debug(response, prefix);
1257
- res.send({
1258
- apiVersion: "admission.k8s.io/v1",
1259
- kind: "AdmissionReview",
1260
- response
1261
- });
1262
- this.metricsCollector.observeEnd(startTime);
1511
+ res.send("OK");
1263
1512
  } catch (err) {
1264
- console.error(err);
1513
+ logger_default.error(err);
1265
1514
  res.status(500).send("Internal Server Error");
1266
- this.metricsCollector.error();
1267
1515
  }
1268
- };
1516
+ }
1269
1517
  };
1270
1518
 
1271
1519
  // src/lib/module.ts
1272
- var alwaysIgnore = {
1273
- namespaces: ["kube-system", "pepr-system"],
1274
- labels: [{ "pepr.dev": "ignore" }]
1275
- };
1276
1520
  var PeprModule = class {
1277
- _controller;
1521
+ #controller;
1278
1522
  /**
1279
1523
  * Create a new Pepr runtime
1280
1524
  *
1281
1525
  * @param config The configuration for the Pepr runtime
1282
1526
  * @param capabilities The capabilities to be loaded into the Pepr runtime
1283
- * @param _deferStart (optional) If set to `true`, the Pepr runtime will not be started automatically. This can be used to start the Pepr runtime manually with `start()`.
1527
+ * @param opts Options for the Pepr runtime
1284
1528
  */
1285
1529
  constructor({ description, pepr }, capabilities = [], opts = {}) {
1286
- const config = (0, import_ramda2.mergeDeepWith)(import_ramda2.concat, pepr, alwaysIgnore);
1530
+ const config = (0, import_ramda4.clone)(pepr);
1287
1531
  config.description = description;
1288
- if (process.env.PEPR_MODE === "build") {
1289
- process.send?.({ capabilities });
1532
+ ValidateError(config.onError);
1533
+ this.start = this.start.bind(this);
1534
+ if (process.env.PEPR_MODE === "build" && process.send) {
1535
+ const exportedCapabilities = [];
1536
+ for (const capability of capabilities) {
1537
+ exportedCapabilities.push({
1538
+ name: capability.name,
1539
+ description: capability.description,
1540
+ namespaces: capability.namespaces,
1541
+ bindings: capability.bindings
1542
+ });
1543
+ }
1544
+ process.send(exportedCapabilities);
1290
1545
  return;
1291
1546
  }
1292
- this._controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook);
1547
+ this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook);
1293
1548
  if (opts.deferStart) {
1294
1549
  return;
1295
1550
  }
@@ -1302,7 +1557,7 @@ var PeprModule = class {
1302
1557
  * @param port
1303
1558
  */
1304
1559
  start(port = 3e3) {
1305
- this._controller.startServer(port);
1560
+ this.#controller.startServer(port);
1306
1561
  }
1307
1562
  };
1308
1563
  // Annotate the CommonJS export names for ESM import in node:
@@ -1310,8 +1565,9 @@ var PeprModule = class {
1310
1565
  Capability,
1311
1566
  Log,
1312
1567
  PeprModule,
1313
- PeprRequest,
1568
+ PeprMutateRequest,
1314
1569
  PeprUtils,
1570
+ PeprValidateRequest,
1315
1571
  R,
1316
1572
  RegisterKind,
1317
1573
  a,