pepr 0.2.10 → 0.3.0-rc0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fixtures/data/cm1.json +75 -0
- package/dist/fixtures/data/deployment1.json +170 -0
- package/dist/fixtures/data/ns1.json +72 -0
- package/dist/fixtures/data/pod1.json +271 -0
- package/dist/fixtures/data/pod2.json +257 -0
- package/dist/fixtures/data/svc1.json +100 -0
- package/dist/fixtures/loader.js +60 -0
- package/dist/package.json +21 -39
- package/dist/src/cli/build.js +3 -1
- package/dist/src/cli/dev.js +31 -19
- package/dist/src/cli/index.js +1 -0
- package/dist/src/cli/init/index.js +3 -1
- package/dist/src/cli/init/templates.js +3 -2
- package/dist/src/cli/init/utils.js +1 -1
- package/dist/src/cli/init/utils.test.js +29 -0
- package/dist/src/cli/init/walkthrough.js +1 -1
- package/dist/src/cli/init/walkthrough.test.js +21 -0
- package/dist/src/cli/run.js +17 -17
- package/dist/src/cli/update.js +3 -1
- package/dist/src/lib/capability.js +1 -1
- package/dist/src/lib/controller.js +9 -1
- package/dist/src/lib/fetch.js +39 -6
- package/dist/src/lib/fetch.test.js +98 -0
- package/dist/src/lib/filter.test.js +208 -0
- package/dist/src/lib/k8s/kinds.test.js +296 -0
- package/dist/src/lib/k8s/webhook.js +22 -22
- package/dist/src/lib/logger.test.js +64 -0
- package/dist/src/lib/processor.js +4 -1
- package/{dist/index.d.ts → index.ts} +21 -3
- package/package.json +21 -39
- package/src/lib/capability.ts +158 -0
- package/src/lib/controller.ts +127 -0
- package/src/lib/fetch.test.ts +115 -0
- package/src/lib/fetch.ts +75 -0
- package/src/lib/filter.test.ts +231 -0
- package/src/lib/filter.ts +87 -0
- package/{dist/src/lib/k8s/index.d.ts → src/lib/k8s/index.ts} +6 -0
- package/src/lib/k8s/kinds.test.ts +333 -0
- package/src/lib/k8s/kinds.ts +489 -0
- package/src/lib/k8s/tls.ts +90 -0
- package/src/lib/k8s/types.ts +183 -0
- package/src/lib/k8s/upstream.ts +49 -0
- package/src/lib/k8s/webhook.ts +547 -0
- package/src/lib/logger.test.ts +80 -0
- package/src/lib/logger.ts +136 -0
- package/src/lib/module.ts +63 -0
- package/src/lib/processor.ts +98 -0
- package/src/lib/request.ts +140 -0
- package/src/lib/types.ts +211 -0
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -4
- package/dist/run.d.ts +0 -2
- package/dist/run.js +0 -4
- package/dist/src/cli/banner.d.ts +0 -1
- package/dist/src/cli/build.d.ts +0 -7
- package/dist/src/cli/capability.d.ts +0 -2
- package/dist/src/cli/deploy.d.ts +0 -2
- package/dist/src/cli/dev.d.ts +0 -2
- package/dist/src/cli/index.d.ts +0 -1
- package/dist/src/cli/init/index.d.ts +0 -2
- package/dist/src/cli/init/templates.d.ts +0 -94
- package/dist/src/cli/init/utils.d.ts +0 -20
- package/dist/src/cli/init/walkthrough.d.ts +0 -7
- package/dist/src/cli/root.d.ts +0 -4
- package/dist/src/cli/run.d.ts +0 -1
- package/dist/src/cli/test.d.ts +0 -2
- package/dist/src/cli/update.d.ts +0 -2
- package/dist/src/lib/capability.d.ts +0 -28
- package/dist/src/lib/controller.d.ts +0 -17
- package/dist/src/lib/fetch.d.ts +0 -23
- package/dist/src/lib/filter.d.ts +0 -10
- package/dist/src/lib/k8s/kinds.d.ts +0 -11
- package/dist/src/lib/k8s/tls.d.ts +0 -17
- package/dist/src/lib/k8s/types.d.ts +0 -147
- package/dist/src/lib/k8s/upstream.d.ts +0 -3
- package/dist/src/lib/k8s/webhook.d.ts +0 -34
- package/dist/src/lib/logger.d.ts +0 -55
- package/dist/src/lib/module.d.ts +0 -32
- package/dist/src/lib/processor.d.ts +0 -4
- package/dist/src/lib/request.d.ts +0 -77
- package/dist/src/lib/types.d.ts +0 -187
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { GroupVersionKind, modelToGroupVersionKind } from "./k8s";
|
|
5
|
+
import logger from "./logger";
|
|
6
|
+
import {
|
|
7
|
+
BindToAction,
|
|
8
|
+
Binding,
|
|
9
|
+
BindingFilter,
|
|
10
|
+
BindingWithName,
|
|
11
|
+
CapabilityAction,
|
|
12
|
+
CapabilityCfg,
|
|
13
|
+
DeepPartial,
|
|
14
|
+
Event,
|
|
15
|
+
GenericClass,
|
|
16
|
+
HookPhase,
|
|
17
|
+
WhenSelector,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A capability is a unit of functionality that can be registered with the Pepr runtime.
|
|
22
|
+
*/
|
|
23
|
+
export class Capability implements CapabilityCfg {
|
|
24
|
+
private _name: string;
|
|
25
|
+
private _description: string;
|
|
26
|
+
private _namespaces?: string[] | undefined;
|
|
27
|
+
|
|
28
|
+
// Currently everything is considered a mutation
|
|
29
|
+
private _mutateOrValidate = HookPhase.mutate;
|
|
30
|
+
|
|
31
|
+
private _bindings: Binding[] = [];
|
|
32
|
+
|
|
33
|
+
get bindings(): Binding[] {
|
|
34
|
+
return this._bindings;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get name() {
|
|
38
|
+
return this._name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get description() {
|
|
42
|
+
return this._description;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get namespaces() {
|
|
46
|
+
return this._namespaces || [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get mutateOrValidate() {
|
|
50
|
+
return this._mutateOrValidate;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
constructor(cfg: CapabilityCfg) {
|
|
54
|
+
this._name = cfg.name;
|
|
55
|
+
this._description = cfg.description;
|
|
56
|
+
this._namespaces = cfg.namespaces;
|
|
57
|
+
logger.info(`Capability ${this._name} registered`);
|
|
58
|
+
logger.debug(cfg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The When method is used to register a capability action to be executed when a Kubernetes resource is
|
|
63
|
+
* processed by Pepr. The action will be executed if the resource matches the specified kind and any
|
|
64
|
+
* filters that are applied.
|
|
65
|
+
*
|
|
66
|
+
* @param model the KubernetesObject model to match
|
|
67
|
+
* @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
|
|
68
|
+
* @returns
|
|
69
|
+
*/
|
|
70
|
+
When = <T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> => {
|
|
71
|
+
const matchedKind = modelToGroupVersionKind(model.name);
|
|
72
|
+
|
|
73
|
+
// If the kind is not specified and the model is not a KubernetesObject, throw an error
|
|
74
|
+
if (!matchedKind && !kind) {
|
|
75
|
+
throw new Error(`Kind not specified for ${model.name}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const binding: Binding = {
|
|
79
|
+
// If the kind is not specified, use the matched kind from the model
|
|
80
|
+
kind: kind || matchedKind,
|
|
81
|
+
filters: {
|
|
82
|
+
name: "",
|
|
83
|
+
namespaces: [],
|
|
84
|
+
labels: {},
|
|
85
|
+
annotations: {},
|
|
86
|
+
},
|
|
87
|
+
callback: () => undefined,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const prefix = `${this._name}: ${model.name}`;
|
|
91
|
+
|
|
92
|
+
logger.info(`Binding created`, prefix);
|
|
93
|
+
|
|
94
|
+
const Then = (cb: CapabilityAction<T>): BindToAction<T> => {
|
|
95
|
+
logger.info(`Binding action created`, prefix);
|
|
96
|
+
logger.debug(cb.toString(), prefix);
|
|
97
|
+
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
98
|
+
// with the callback function to preserve
|
|
99
|
+
this._bindings.push({
|
|
100
|
+
...binding,
|
|
101
|
+
callback: cb,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Now only allow adding actions to the same binding
|
|
105
|
+
return { Then };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const ThenSet = (merge: DeepPartial<InstanceType<T>>): BindToAction<T> => {
|
|
109
|
+
// Add the new action to the binding
|
|
110
|
+
Then(req => req.Merge(merge));
|
|
111
|
+
|
|
112
|
+
return { Then };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function InNamespace(...namespaces: string[]): BindingWithName<T> {
|
|
116
|
+
logger.debug(`Add namespaces filter ${namespaces}`, prefix);
|
|
117
|
+
binding.filters.namespaces.push(...namespaces);
|
|
118
|
+
return { WithLabel, WithAnnotation, WithName, Then, ThenSet };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function WithName(name: string): BindingFilter<T> {
|
|
122
|
+
logger.debug(`Add name filter ${name}`, prefix);
|
|
123
|
+
binding.filters.name = name;
|
|
124
|
+
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function WithLabel(key: string, value = ""): BindingFilter<T> {
|
|
128
|
+
logger.debug(`Add label filter ${key}=${value}`, prefix);
|
|
129
|
+
binding.filters.labels[key] = value;
|
|
130
|
+
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const WithAnnotation = (key: string, value = ""): BindingFilter<T> => {
|
|
134
|
+
logger.debug(`Add annotation filter ${key}=${value}`, prefix);
|
|
135
|
+
binding.filters.annotations[key] = value;
|
|
136
|
+
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const bindEvent = (event: Event) => {
|
|
140
|
+
binding.event = event;
|
|
141
|
+
return {
|
|
142
|
+
InNamespace,
|
|
143
|
+
Then,
|
|
144
|
+
ThenSet,
|
|
145
|
+
WithAnnotation,
|
|
146
|
+
WithLabel,
|
|
147
|
+
WithName,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate),
|
|
153
|
+
IsCreated: () => bindEvent(Event.Create),
|
|
154
|
+
IsUpdated: () => bindEvent(Event.Update),
|
|
155
|
+
IsDeleted: () => bindEvent(Event.Delete),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import express from "express";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import https from "https";
|
|
7
|
+
import { Capability } from "./capability";
|
|
8
|
+
import { Request, Response } from "./k8s/types";
|
|
9
|
+
import { processor } from "./processor";
|
|
10
|
+
import { ModuleConfig } from "./types";
|
|
11
|
+
|
|
12
|
+
// Load SSL certificate and key
|
|
13
|
+
const options = {
|
|
14
|
+
key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
|
|
15
|
+
cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class Controller {
|
|
19
|
+
private readonly app = express();
|
|
20
|
+
private running = false;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly config: ModuleConfig,
|
|
24
|
+
private readonly capabilities: Capability[],
|
|
25
|
+
private readonly beforeHook?: (req: Request) => void,
|
|
26
|
+
private readonly afterHook?: (res: Response) => void
|
|
27
|
+
) {
|
|
28
|
+
// Middleware for logging requests
|
|
29
|
+
this.app.use(this.logger);
|
|
30
|
+
|
|
31
|
+
// Middleware for parsing JSON
|
|
32
|
+
this.app.use(express.json());
|
|
33
|
+
|
|
34
|
+
// Health check endpoint
|
|
35
|
+
this.app.get("/healthz", this.healthz);
|
|
36
|
+
|
|
37
|
+
// Mutate endpoint
|
|
38
|
+
this.app.post("/mutate", this.mutate);
|
|
39
|
+
|
|
40
|
+
if (beforeHook) {
|
|
41
|
+
console.info(`Using beforeHook: ${beforeHook}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (afterHook) {
|
|
45
|
+
console.info(`Using afterHook: ${afterHook}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Start the webhook server */
|
|
50
|
+
public startServer = (port: number) => {
|
|
51
|
+
if (this.running) {
|
|
52
|
+
throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create HTTPS server
|
|
56
|
+
const server = https.createServer(options, this.app).listen(port, () => {
|
|
57
|
+
console.log(`Server listening on port ${port}`);
|
|
58
|
+
// Track that the server is running
|
|
59
|
+
this.running = true;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Listen for the SIGTERM signal and gracefully close the server
|
|
63
|
+
process.on("SIGTERM", () => {
|
|
64
|
+
console.log("Received SIGTERM, closing server");
|
|
65
|
+
server.close(() => {
|
|
66
|
+
console.log("Server closed");
|
|
67
|
+
process.exit(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
private logger = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
|
|
75
|
+
res.on("finish", () => {
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
const elapsedTime = Date.now() - startTime;
|
|
78
|
+
const message = `[${now}] ${req.method} ${req.originalUrl} - ${res.statusCode} - ${elapsedTime} ms\n`;
|
|
79
|
+
|
|
80
|
+
res.statusCode >= 400 ? console.error(message) : console.info(message);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
next();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
private healthz = (req: express.Request, res: express.Response) => {
|
|
87
|
+
try {
|
|
88
|
+
res.send("OK");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error(err);
|
|
91
|
+
res.status(500).send("Internal Server Error");
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
private mutate = async (req: express.Request, res: express.Response) => {
|
|
96
|
+
try {
|
|
97
|
+
// Run the before hook if it exists
|
|
98
|
+
this.beforeHook && this.beforeHook(req.body?.request || {});
|
|
99
|
+
|
|
100
|
+
const name = req.body?.request?.name || "";
|
|
101
|
+
const namespace = req.body?.request?.namespace || "";
|
|
102
|
+
const gvk = req.body?.request?.kind || { group: "", version: "", kind: "" };
|
|
103
|
+
|
|
104
|
+
console.log(`Mutate request: ${gvk.group}/${gvk.version}/${gvk.kind}`);
|
|
105
|
+
name && console.log(` ${namespace}/${name}\n`);
|
|
106
|
+
|
|
107
|
+
// Process the request
|
|
108
|
+
const response = await processor(this.config, this.capabilities, req.body.request);
|
|
109
|
+
|
|
110
|
+
// Run the after hook if it exists
|
|
111
|
+
this.afterHook && this.afterHook(response);
|
|
112
|
+
|
|
113
|
+
// Log the response
|
|
114
|
+
console.debug(response);
|
|
115
|
+
|
|
116
|
+
// Send a no prob bob response
|
|
117
|
+
res.send({
|
|
118
|
+
apiVersion: "admission.k8s.io/v1",
|
|
119
|
+
kind: "AdmissionReview",
|
|
120
|
+
response,
|
|
121
|
+
});
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(err);
|
|
124
|
+
res.status(500).send("Internal Server Error");
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
// fetch.test.ts
|
|
4
|
+
|
|
5
|
+
import test from "ava";
|
|
6
|
+
import { StatusCodes } from "http-status-codes";
|
|
7
|
+
import nock from "nock";
|
|
8
|
+
import { RequestInit } from "node-fetch";
|
|
9
|
+
import { fetch } from "./fetch";
|
|
10
|
+
|
|
11
|
+
test.beforeEach(() => {
|
|
12
|
+
nock("https://jsonplaceholder.typicode.com")
|
|
13
|
+
.get("/todos/1")
|
|
14
|
+
.reply(200, {
|
|
15
|
+
userId: 1,
|
|
16
|
+
id: 1,
|
|
17
|
+
title: "Example title",
|
|
18
|
+
completed: false,
|
|
19
|
+
})
|
|
20
|
+
.post("/todos", {
|
|
21
|
+
title: "test todo",
|
|
22
|
+
userId: 1,
|
|
23
|
+
completed: false,
|
|
24
|
+
})
|
|
25
|
+
.reply(200, (uri, requestBody) => requestBody)
|
|
26
|
+
.get("/todos/empty-null")
|
|
27
|
+
.reply(200, undefined)
|
|
28
|
+
.get("/todos/empty-string")
|
|
29
|
+
.reply(200, "")
|
|
30
|
+
.get("/todos/empty-object")
|
|
31
|
+
.reply(200, {})
|
|
32
|
+
.get("/todos/invalid")
|
|
33
|
+
.replyWithError("Something bad happened");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("fetch: should return without type data", async t => {
|
|
37
|
+
const url = "https://jsonplaceholder.typicode.com/todos/1";
|
|
38
|
+
const { data, ok } = await fetch<{ title: string }>(url);
|
|
39
|
+
t.is(ok, true);
|
|
40
|
+
t.is(data["title"], "Example title");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("fetch: should return parsed JSON response as a specific type", async t => {
|
|
44
|
+
interface Todo {
|
|
45
|
+
userId: number;
|
|
46
|
+
id: number;
|
|
47
|
+
title: string;
|
|
48
|
+
completed: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const url = "https://jsonplaceholder.typicode.com/todos/1";
|
|
52
|
+
const { data, ok } = await fetch<Todo>(url);
|
|
53
|
+
t.is(ok, true);
|
|
54
|
+
t.is(data.id, 1);
|
|
55
|
+
t.is(typeof data.title, "string");
|
|
56
|
+
t.is(typeof data.completed, "boolean");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("fetch: should handle additional request options", async t => {
|
|
60
|
+
const url = "https://jsonplaceholder.typicode.com/todos";
|
|
61
|
+
const requestOptions: RequestInit = {
|
|
62
|
+
method: "POST",
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
title: "test todo",
|
|
65
|
+
userId: 1,
|
|
66
|
+
completed: false,
|
|
67
|
+
}),
|
|
68
|
+
headers: {
|
|
69
|
+
"Content-type": "application/json; charset=UTF-8",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
const { data, ok } = await fetch<any>(url, requestOptions);
|
|
75
|
+
t.is(ok, true);
|
|
76
|
+
t.is(data["title"], "test todo");
|
|
77
|
+
t.is(data["userId"], 1);
|
|
78
|
+
t.is(data["completed"], false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("fetch: should handle empty (null) responses", async t => {
|
|
82
|
+
const url = "https://jsonplaceholder.typicode.com/todos/empty-null";
|
|
83
|
+
const resp = await fetch(url);
|
|
84
|
+
|
|
85
|
+
t.is(resp.data, "");
|
|
86
|
+
t.is(resp.ok, true);
|
|
87
|
+
t.is(resp.status, StatusCodes.OK);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("fetch: should handle empty (string) responses", async t => {
|
|
91
|
+
const url = "https://jsonplaceholder.typicode.com/todos/empty-string";
|
|
92
|
+
const resp = await fetch(url);
|
|
93
|
+
|
|
94
|
+
t.is(resp.data, "");
|
|
95
|
+
t.is(resp.ok, true);
|
|
96
|
+
t.is(resp.status, StatusCodes.OK);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("fetch: should handle empty (object) responses", async t => {
|
|
100
|
+
const url = "https://jsonplaceholder.typicode.com/todos/empty-object";
|
|
101
|
+
const resp = await fetch(url);
|
|
102
|
+
|
|
103
|
+
t.deepEqual(resp.data, {});
|
|
104
|
+
t.is(resp.ok, true);
|
|
105
|
+
t.is(resp.status, StatusCodes.OK);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("fetch: should handle failed requests without throwing an error", async t => {
|
|
109
|
+
const url = "https://jsonplaceholder.typicode.com/todos/invalid";
|
|
110
|
+
const resp = await fetch(url);
|
|
111
|
+
|
|
112
|
+
t.is(resp.data, undefined);
|
|
113
|
+
t.is(resp.ok, false);
|
|
114
|
+
t.is(resp.status, StatusCodes.BAD_REQUEST);
|
|
115
|
+
});
|
package/src/lib/fetch.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { StatusCodes } from "http-status-codes";
|
|
5
|
+
import f, { FetchError, RequestInfo, RequestInit } from "node-fetch";
|
|
6
|
+
import logger from "./logger";
|
|
7
|
+
export { f as fetchRaw };
|
|
8
|
+
|
|
9
|
+
export type FetchResponse<T> = {
|
|
10
|
+
data: T;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
statusText: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Perform an async HTTP call and return the parsed JSON response, optionally
|
|
18
|
+
* as a specific type.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* fetch<string[]>("https://example.com/api/foo");
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param url The URL or Request object to fetch
|
|
26
|
+
* @param init Additional options for the request
|
|
27
|
+
* @returns
|
|
28
|
+
*/
|
|
29
|
+
export async function fetch<T>(url: URL | RequestInfo, init?: RequestInit): Promise<FetchResponse<T>> {
|
|
30
|
+
let data = undefined as unknown as T;
|
|
31
|
+
try {
|
|
32
|
+
logger.debug(`Fetching ${url}`);
|
|
33
|
+
|
|
34
|
+
const resp = await f(url, init);
|
|
35
|
+
const contentType = resp.headers.get("content-type") || "";
|
|
36
|
+
|
|
37
|
+
if (resp.ok) {
|
|
38
|
+
// Parse the response as JSON if the content type is JSON
|
|
39
|
+
if (contentType.includes("application/json")) {
|
|
40
|
+
data = await resp.json();
|
|
41
|
+
} else {
|
|
42
|
+
// Otherwise, return however the response was read
|
|
43
|
+
data = (await resp.text()) as unknown as T;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
data,
|
|
49
|
+
ok: resp.ok,
|
|
50
|
+
status: resp.status,
|
|
51
|
+
statusText: resp.statusText,
|
|
52
|
+
};
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e instanceof FetchError) {
|
|
55
|
+
logger.debug(`Fetch failed: ${e instanceof Error ? e.message : e}`);
|
|
56
|
+
|
|
57
|
+
// Parse the error code from the FetchError or default to 400 (Bad Request)
|
|
58
|
+
const status = parseInt(e.code || "400");
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
data,
|
|
62
|
+
ok: false,
|
|
63
|
+
status,
|
|
64
|
+
statusText: e.message,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
data,
|
|
70
|
+
ok: false,
|
|
71
|
+
status: StatusCodes.BAD_REQUEST,
|
|
72
|
+
statusText: "Unknown error",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import test from "ava";
|
|
2
|
+
import { POD1 } from "../../fixtures/loader";
|
|
3
|
+
import { shouldSkipRequest } from "./filter";
|
|
4
|
+
import { gvkMap } from "./k8s";
|
|
5
|
+
|
|
6
|
+
const callback = () => undefined;
|
|
7
|
+
|
|
8
|
+
test("should reject when name does not match", t => {
|
|
9
|
+
const binding = {
|
|
10
|
+
kind: gvkMap.V1Pod,
|
|
11
|
+
filters: {
|
|
12
|
+
name: "bleh",
|
|
13
|
+
namespaces: [],
|
|
14
|
+
labels: {},
|
|
15
|
+
annotations: {},
|
|
16
|
+
},
|
|
17
|
+
callback,
|
|
18
|
+
};
|
|
19
|
+
const pod = POD1();
|
|
20
|
+
|
|
21
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("should reject when kind does not match", t => {
|
|
25
|
+
const binding = {
|
|
26
|
+
kind: gvkMap.V1ConfigMap,
|
|
27
|
+
filters: {
|
|
28
|
+
name: "",
|
|
29
|
+
namespaces: [],
|
|
30
|
+
labels: {},
|
|
31
|
+
annotations: {},
|
|
32
|
+
},
|
|
33
|
+
callback,
|
|
34
|
+
};
|
|
35
|
+
const pod = POD1();
|
|
36
|
+
|
|
37
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("should reject when group does not match", t => {
|
|
41
|
+
const binding = {
|
|
42
|
+
kind: gvkMap.V1CronJob,
|
|
43
|
+
filters: {
|
|
44
|
+
name: "",
|
|
45
|
+
namespaces: [],
|
|
46
|
+
labels: {},
|
|
47
|
+
annotations: {},
|
|
48
|
+
},
|
|
49
|
+
callback,
|
|
50
|
+
};
|
|
51
|
+
const pod = POD1();
|
|
52
|
+
|
|
53
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("should reject when version does not match", t => {
|
|
57
|
+
const binding = {
|
|
58
|
+
kind: {
|
|
59
|
+
group: "",
|
|
60
|
+
version: "v2",
|
|
61
|
+
kind: "Pod",
|
|
62
|
+
},
|
|
63
|
+
filters: {
|
|
64
|
+
name: "",
|
|
65
|
+
namespaces: [],
|
|
66
|
+
labels: {},
|
|
67
|
+
annotations: {},
|
|
68
|
+
},
|
|
69
|
+
callback,
|
|
70
|
+
};
|
|
71
|
+
const pod = POD1();
|
|
72
|
+
|
|
73
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("should allow when group, version, and kind match", t => {
|
|
77
|
+
const binding = {
|
|
78
|
+
kind: gvkMap.V1Pod,
|
|
79
|
+
filters: {
|
|
80
|
+
name: "",
|
|
81
|
+
namespaces: [],
|
|
82
|
+
labels: {},
|
|
83
|
+
annotations: {},
|
|
84
|
+
},
|
|
85
|
+
callback,
|
|
86
|
+
};
|
|
87
|
+
const pod = POD1();
|
|
88
|
+
|
|
89
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("should allow when kind match and others are empty", t => {
|
|
93
|
+
const binding = {
|
|
94
|
+
kind: {
|
|
95
|
+
group: "",
|
|
96
|
+
version: "",
|
|
97
|
+
kind: "Pod",
|
|
98
|
+
},
|
|
99
|
+
filters: {
|
|
100
|
+
name: "",
|
|
101
|
+
namespaces: [],
|
|
102
|
+
labels: {},
|
|
103
|
+
annotations: {},
|
|
104
|
+
},
|
|
105
|
+
callback,
|
|
106
|
+
};
|
|
107
|
+
const pod = POD1();
|
|
108
|
+
|
|
109
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("should reject when namespace does not match", t => {
|
|
113
|
+
const binding = {
|
|
114
|
+
kind: gvkMap.V1Pod,
|
|
115
|
+
filters: {
|
|
116
|
+
name: "",
|
|
117
|
+
namespaces: ["bleh"],
|
|
118
|
+
labels: {},
|
|
119
|
+
annotations: {},
|
|
120
|
+
},
|
|
121
|
+
callback,
|
|
122
|
+
};
|
|
123
|
+
const pod = POD1();
|
|
124
|
+
|
|
125
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("should allow when namespace is match", t => {
|
|
129
|
+
const binding = {
|
|
130
|
+
kind: gvkMap.V1Pod,
|
|
131
|
+
filters: {
|
|
132
|
+
name: "",
|
|
133
|
+
namespaces: ["default", "unicorn", "things"],
|
|
134
|
+
labels: {},
|
|
135
|
+
annotations: {},
|
|
136
|
+
},
|
|
137
|
+
callback,
|
|
138
|
+
};
|
|
139
|
+
const pod = POD1();
|
|
140
|
+
|
|
141
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("should reject when label does not match", t => {
|
|
145
|
+
const binding = {
|
|
146
|
+
kind: gvkMap.V1Pod,
|
|
147
|
+
filters: {
|
|
148
|
+
name: "",
|
|
149
|
+
namespaces: [],
|
|
150
|
+
labels: {
|
|
151
|
+
foo: "bar",
|
|
152
|
+
},
|
|
153
|
+
annotations: {},
|
|
154
|
+
},
|
|
155
|
+
callback,
|
|
156
|
+
};
|
|
157
|
+
const pod = POD1();
|
|
158
|
+
|
|
159
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("should allow when label is match", t => {
|
|
163
|
+
const binding = {
|
|
164
|
+
kind: gvkMap.V1Pod,
|
|
165
|
+
filters: {
|
|
166
|
+
name: "",
|
|
167
|
+
|
|
168
|
+
namespaces: [],
|
|
169
|
+
labels: {
|
|
170
|
+
foo: "bar",
|
|
171
|
+
test: "test1",
|
|
172
|
+
},
|
|
173
|
+
annotations: {},
|
|
174
|
+
},
|
|
175
|
+
callback,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const pod = POD1();
|
|
179
|
+
pod.object.metadata = pod.object.metadata || {};
|
|
180
|
+
pod.object.metadata.labels = {
|
|
181
|
+
foo: "bar",
|
|
182
|
+
test: "test1",
|
|
183
|
+
test2: "test2",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("should reject when annotation does not match", t => {
|
|
190
|
+
const binding = {
|
|
191
|
+
kind: gvkMap.V1Pod,
|
|
192
|
+
filters: {
|
|
193
|
+
name: "",
|
|
194
|
+
namespaces: [],
|
|
195
|
+
labels: {},
|
|
196
|
+
annotations: {
|
|
197
|
+
foo: "bar",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
callback,
|
|
201
|
+
};
|
|
202
|
+
const pod = POD1();
|
|
203
|
+
|
|
204
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("should allow when annotation is match", t => {
|
|
208
|
+
const binding = {
|
|
209
|
+
kind: gvkMap.V1Pod,
|
|
210
|
+
filters: {
|
|
211
|
+
name: "",
|
|
212
|
+
namespaces: [],
|
|
213
|
+
labels: {},
|
|
214
|
+
annotations: {
|
|
215
|
+
foo: "bar",
|
|
216
|
+
test: "test1",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
callback,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const pod = POD1();
|
|
223
|
+
pod.object.metadata = pod.object.metadata || {};
|
|
224
|
+
pod.object.metadata.annotations = {
|
|
225
|
+
foo: "bar",
|
|
226
|
+
test: "test1",
|
|
227
|
+
test2: "test2",
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
231
|
+
});
|