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.
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +70 -0
- package/README.md +28 -30
- package/dist/cli.js +666 -692
- package/dist/controller.js +13 -81
- package/dist/lib/assets/deploy.d.ts +3 -0
- package/dist/lib/assets/deploy.d.ts.map +1 -0
- package/dist/lib/assets/index.d.ts +17 -0
- package/dist/lib/assets/index.d.ts.map +1 -0
- package/dist/lib/assets/loader.d.ts +8 -0
- package/dist/lib/assets/loader.d.ts.map +1 -0
- package/dist/lib/assets/networking.d.ts +6 -0
- package/dist/lib/assets/networking.d.ts.map +1 -0
- package/dist/lib/assets/pods.d.ts +8 -0
- package/dist/lib/assets/pods.d.ts.map +1 -0
- package/dist/lib/assets/rbac.d.ts +11 -0
- package/dist/lib/assets/rbac.d.ts.map +1 -0
- package/dist/lib/assets/webhooks.d.ts +6 -0
- package/dist/lib/assets/webhooks.d.ts.map +1 -0
- package/dist/lib/assets/yaml.d.ts +4 -0
- package/dist/lib/assets/yaml.d.ts.map +1 -0
- package/dist/lib/capability.d.ts +4 -9
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/controller.d.ts +4 -15
- package/dist/lib/controller.d.ts.map +1 -1
- package/dist/lib/errors.d.ts +12 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/filter.d.ts +1 -1
- package/dist/lib/filter.d.ts.map +1 -1
- package/dist/lib/k8s/index.d.ts +2 -1
- package/dist/lib/k8s/index.d.ts.map +1 -1
- package/dist/lib/k8s/kinds.d.ts.map +1 -1
- package/dist/lib/k8s/types.d.ts +18 -14
- package/dist/lib/k8s/types.d.ts.map +1 -1
- package/dist/lib/k8s/upstream.d.ts +2 -2
- package/dist/lib/k8s/upstream.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +8 -54
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/metrics.d.ts +10 -9
- package/dist/lib/metrics.d.ts.map +1 -1
- package/dist/lib/module.d.ts +4 -4
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/mutate-processor.d.ts +5 -0
- package/dist/lib/mutate-processor.d.ts.map +1 -0
- package/dist/lib/{request.d.ts → mutate-request.d.ts} +7 -7
- package/dist/lib/mutate-request.d.ts.map +1 -0
- package/dist/lib/types.d.ts +48 -55
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/validate-processor.d.ts +4 -0
- package/dist/lib/validate-processor.d.ts.map +1 -0
- package/dist/lib/validate-request.d.ts +54 -0
- package/dist/lib/validate-request.d.ts.map +1 -0
- package/dist/lib.d.ts +3 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +610 -354
- package/dist/lib.js.map +4 -4
- package/jest.config.json +4 -0
- package/journey/before.ts +21 -0
- package/journey/k8s.ts +81 -0
- package/journey/pepr-build.ts +69 -0
- package/journey/pepr-deploy.ts +133 -0
- package/journey/pepr-dev.ts +155 -0
- package/journey/pepr-format.ts +13 -0
- package/journey/pepr-init.ts +12 -0
- package/package.json +29 -27
- package/src/cli.ts +2 -11
- package/src/lib/assets/deploy.ts +179 -0
- package/src/lib/assets/index.ts +53 -0
- package/src/lib/assets/loader.ts +41 -0
- package/src/lib/assets/networking.ts +58 -0
- package/src/lib/assets/pods.ts +148 -0
- package/src/lib/assets/rbac.ts +57 -0
- package/src/lib/assets/webhooks.ts +139 -0
- package/src/lib/assets/yaml.ts +75 -0
- package/src/lib/capability.ts +80 -68
- package/src/lib/controller.ts +199 -99
- package/src/lib/errors.ts +20 -0
- package/src/lib/fetch.ts +1 -1
- package/src/lib/filter.ts +1 -3
- package/src/lib/k8s/index.ts +4 -1
- package/src/lib/k8s/kinds.ts +40 -0
- package/src/lib/k8s/types.ts +21 -15
- package/src/lib/k8s/upstream.ts +5 -1
- package/src/lib/logger.ts +14 -125
- package/src/lib/metrics.ts +86 -29
- package/src/lib/module.ts +32 -16
- package/src/lib/{processor.ts → mutate-processor.ts} +39 -28
- package/src/lib/{request.ts → mutate-request.ts} +26 -13
- package/src/lib/types.ts +54 -60
- package/src/lib/validate-processor.ts +76 -0
- package/src/lib/validate-request.ts +106 -0
- package/src/lib.ts +4 -2
- package/src/runtime/controller.ts +1 -1
- package/dist/lib/k8s/webhook.d.ts +0 -37
- package/dist/lib/k8s/webhook.d.ts.map +0 -1
- package/dist/lib/processor.d.ts +0 -5
- package/dist/lib/processor.d.ts.map +0 -1
- package/dist/lib/request.d.ts.map +0 -1
- 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
|
-
|
|
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: () =>
|
|
55
|
-
CSIDriver: () =>
|
|
56
|
-
CSIStorageCapacity: () =>
|
|
57
|
-
CertificateSigningRequest: () =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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: () =>
|
|
67
|
-
Ingress: () =>
|
|
68
|
-
IngressClass: () =>
|
|
69
|
-
Job: () =>
|
|
70
|
-
LimitRange: () =>
|
|
71
|
-
LocalSubjectAccessReview: () =>
|
|
72
|
-
MutatingWebhookConfiguration: () =>
|
|
73
|
-
Namespace: () =>
|
|
74
|
-
NetworkPolicy: () =>
|
|
75
|
-
Node: () =>
|
|
76
|
-
PersistentVolume: () =>
|
|
77
|
-
PersistentVolumeClaim: () =>
|
|
78
|
-
Pod: () =>
|
|
79
|
-
PodDisruptionBudget: () =>
|
|
80
|
-
PodTemplate: () =>
|
|
81
|
-
ReplicaSet: () =>
|
|
82
|
-
ReplicationController: () =>
|
|
83
|
-
ResourceQuota: () =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
_mutateOrValidate = "mutate" /* mutate */;
|
|
635
|
-
_bindings = [];
|
|
614
|
+
#name;
|
|
615
|
+
#description;
|
|
616
|
+
#namespaces;
|
|
617
|
+
#bindings = [];
|
|
636
618
|
get bindings() {
|
|
637
|
-
return this
|
|
619
|
+
return this.#bindings;
|
|
638
620
|
}
|
|
639
621
|
get name() {
|
|
640
|
-
return this
|
|
622
|
+
return this.#name;
|
|
641
623
|
}
|
|
642
624
|
get description() {
|
|
643
|
-
return this
|
|
625
|
+
return this.#description;
|
|
644
626
|
}
|
|
645
627
|
get namespaces() {
|
|
646
|
-
return this
|
|
647
|
-
}
|
|
648
|
-
get mutateOrValidate() {
|
|
649
|
-
return this._mutateOrValidate;
|
|
628
|
+
return this.#namespaces || [];
|
|
650
629
|
}
|
|
651
630
|
constructor(cfg) {
|
|
652
|
-
this
|
|
653
|
-
this
|
|
654
|
-
this
|
|
655
|
-
|
|
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
|
|
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
|
|
696
|
-
|
|
697
|
-
|
|
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 {
|
|
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
|
|
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
|
|
707
|
+
return commonChain;
|
|
713
708
|
}
|
|
714
|
-
|
|
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
|
|
718
|
-
}
|
|
719
|
-
|
|
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
|
|
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/
|
|
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
|
|
847
|
-
var
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
1008
|
-
const wrapped = new
|
|
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
|
|
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
|
|
1125
|
+
logger_default.info(reqMetadata, `Processing request`);
|
|
1021
1126
|
for (const { name, bindings } of capabilities) {
|
|
1022
|
-
const
|
|
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.
|
|
1028
|
-
logger_default.info(`Processing matched action ${label}
|
|
1029
|
-
|
|
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.
|
|
1042
|
-
logger_default.info(`Action succeeded
|
|
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
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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 (!
|
|
1060
|
-
logger_default.info(`No matching
|
|
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 =
|
|
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,
|
|
1190
|
+
logger_default.debug({ ...reqMetadata, patches }, `Patches generated`);
|
|
1079
1191
|
return response;
|
|
1080
1192
|
}
|
|
1081
1193
|
|
|
1082
|
-
// src/lib/
|
|
1083
|
-
var
|
|
1084
|
-
var
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
*
|
|
1092
|
-
* @
|
|
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
|
-
|
|
1095
|
-
this.
|
|
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
|
-
*
|
|
1207
|
+
* Provides access to the request object.
|
|
1208
|
+
* @returns The request object containing the Kubernetes resource.
|
|
1114
1209
|
*/
|
|
1115
|
-
|
|
1116
|
-
this
|
|
1210
|
+
get Request() {
|
|
1211
|
+
return this.#input;
|
|
1117
1212
|
}
|
|
1118
1213
|
/**
|
|
1119
|
-
*
|
|
1214
|
+
* Creates a new instance of the Action class.
|
|
1215
|
+
* @param input - The request object containing the Kubernetes resource to modify.
|
|
1120
1216
|
*/
|
|
1121
|
-
|
|
1122
|
-
this
|
|
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
|
-
*
|
|
1126
|
-
*
|
|
1233
|
+
* Check if a label exists on the Kubernetes resource.
|
|
1234
|
+
*
|
|
1235
|
+
* @param key the label key to check
|
|
1236
|
+
* @returns
|
|
1127
1237
|
*/
|
|
1128
|
-
|
|
1129
|
-
return
|
|
1238
|
+
HasLabel(key) {
|
|
1239
|
+
return this.Raw.metadata?.labels?.[key] !== void 0;
|
|
1130
1240
|
}
|
|
1131
1241
|
/**
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1242
|
+
* Check if an annotation exists on the Kubernetes resource.
|
|
1243
|
+
*
|
|
1244
|
+
* @param key the annotation key to check
|
|
1245
|
+
* @returns
|
|
1134
1246
|
*/
|
|
1135
|
-
|
|
1136
|
-
this.
|
|
1247
|
+
HasAnnotation(key) {
|
|
1248
|
+
return this.Raw.metadata?.annotations?.[key] !== void 0;
|
|
1137
1249
|
}
|
|
1138
1250
|
/**
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1251
|
+
* Create a validation response that allows the request.
|
|
1252
|
+
*
|
|
1253
|
+
* @returns The validation response.
|
|
1141
1254
|
*/
|
|
1142
|
-
|
|
1143
|
-
return
|
|
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
|
|
1151
|
-
this
|
|
1152
|
-
this
|
|
1153
|
-
this
|
|
1154
|
-
this.
|
|
1155
|
-
this
|
|
1156
|
-
this
|
|
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
|
-
|
|
1348
|
+
logger_default.info(`Using beforeHook: ${beforeHook}`);
|
|
1161
1349
|
}
|
|
1162
1350
|
if (afterHook) {
|
|
1163
|
-
|
|
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
|
|
1173
|
-
if (this
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
|
1371
|
+
const server = import_https.default.createServer(options, this.#app).listen(port);
|
|
1186
1372
|
server.on("listening", () => {
|
|
1187
|
-
|
|
1188
|
-
this
|
|
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
|
-
|
|
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
|
-
|
|
1388
|
+
logger_default.info("Received SIGTERM, closing server");
|
|
1203
1389
|
server.close(() => {
|
|
1204
|
-
|
|
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
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
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(
|
|
1432
|
+
res.send(await this.#metricsCollector.getMetrics());
|
|
1223
1433
|
} catch (err) {
|
|
1224
|
-
|
|
1434
|
+
logger_default.error(err);
|
|
1225
1435
|
res.status(500).send("Internal Server Error");
|
|
1226
1436
|
}
|
|
1227
1437
|
};
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1527
|
+
* @param opts Options for the Pepr runtime
|
|
1284
1528
|
*/
|
|
1285
1529
|
constructor({ description, pepr }, capabilities = [], opts = {}) {
|
|
1286
|
-
const config = (0,
|
|
1530
|
+
const config = (0, import_ramda4.clone)(pepr);
|
|
1287
1531
|
config.description = description;
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
1568
|
+
PeprMutateRequest,
|
|
1314
1569
|
PeprUtils,
|
|
1570
|
+
PeprValidateRequest,
|
|
1315
1571
|
R,
|
|
1316
1572
|
RegisterKind,
|
|
1317
1573
|
a,
|