kubernetes-fluent-client 0.0.0-development → 1.1.0
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/README.md +15 -0
- package/__mocks__/@kubernetes/client-node.ts +22 -0
- package/dist/fetch.d.ts +23 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +85 -0
- package/dist/fetch.test.d.ts +2 -0
- package/dist/fetch.test.d.ts.map +1 -0
- package/dist/fetch.test.js +97 -0
- package/dist/fluent/index.d.ts +11 -0
- package/dist/fluent/index.d.ts.map +1 -0
- package/dist/fluent/index.js +100 -0
- package/dist/fluent/index.test.d.ts +2 -0
- package/dist/fluent/index.test.d.ts.map +1 -0
- package/dist/fluent/index.test.js +92 -0
- package/dist/fluent/types.d.ts +121 -0
- package/dist/fluent/types.d.ts.map +1 -0
- package/dist/fluent/types.js +14 -0
- package/dist/fluent/utils.d.ts +31 -0
- package/dist/fluent/utils.d.ts.map +1 -0
- package/dist/fluent/utils.js +116 -0
- package/dist/fluent/utils.test.d.ts +2 -0
- package/dist/fluent/utils.test.d.ts.map +1 -0
- package/dist/fluent/utils.test.js +88 -0
- package/dist/fluent/watch.d.ts +8 -0
- package/dist/fluent/watch.d.ts.map +1 -0
- package/dist/fluent/watch.js +78 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -1
- package/dist/kinds.d.ts +10 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +489 -0
- package/dist/kinds.test.d.ts +2 -0
- package/dist/kinds.test.d.ts.map +1 -0
- package/dist/kinds.test.js +142 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/upstream.d.ts +4 -0
- package/dist/upstream.d.ts.map +1 -0
- package/dist/upstream.js +54 -0
- package/package.json +21 -8
- package/src/fetch.test.ts +115 -0
- package/src/fetch.ts +72 -0
- package/src/fluent/index.test.ts +136 -0
- package/src/fluent/index.ts +126 -0
- package/src/fluent/types.ts +151 -0
- package/src/fluent/utils.test.ts +110 -0
- package/src/fluent/utils.ts +152 -0
- package/src/fluent/watch.ts +94 -0
- package/src/index.ts +8 -3
- package/src/kinds.test.ts +1 -1
- package/src/kinds.ts +1 -1
- package/src/types.ts +1 -154
- package/.eslintrc.json +0 -26
- package/.prettierrc +0 -13
- package/coverage/clover.xml +0 -83
- package/coverage/coverage-final.json +0 -5
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +0 -161
- package/coverage/lcov-report/index.ts.html +0 -127
- package/coverage/lcov-report/kinds.ts.html +0 -1675
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -196
- package/coverage/lcov-report/types.ts.html +0 -643
- package/coverage/lcov-report/upstream.ts.html +0 -244
- package/coverage/lcov.info +0 -208
- package/jest.config.json +0 -4
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node";
|
|
5
|
+
import { Operation } from "fast-json-patch";
|
|
6
|
+
import { Agent } from "http";
|
|
7
|
+
|
|
8
|
+
import { GenericClass, GroupVersionKind } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The Phase matched when using the K8s Watch API.
|
|
12
|
+
*/
|
|
13
|
+
export enum WatchPhase {
|
|
14
|
+
Added = "ADDED",
|
|
15
|
+
Modified = "MODIFIED",
|
|
16
|
+
Deleted = "DELETED",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FetchMethods = "GET" | "APPLY" | "POST" | "PUT" | "DELETE" | "PATCH" | "WATCH";
|
|
20
|
+
|
|
21
|
+
export interface Filters {
|
|
22
|
+
kindOverride?: GroupVersionKind;
|
|
23
|
+
fields?: Record<string, string>;
|
|
24
|
+
labels?: Record<string, string>;
|
|
25
|
+
name?: string;
|
|
26
|
+
namespace?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type GetFunction<K extends KubernetesObject> = {
|
|
30
|
+
(): Promise<KubernetesListObject<K>>;
|
|
31
|
+
(name: string): Promise<K>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type K8sFilteredActions<K extends KubernetesObject> = {
|
|
35
|
+
/**
|
|
36
|
+
* Get the resource or resources matching the filters.
|
|
37
|
+
* If no filters are specified, all resources will be returned.
|
|
38
|
+
* If a name is specified, only a single resource will be returned.
|
|
39
|
+
*/
|
|
40
|
+
Get: GetFunction<K>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Delete the resource if it exists.
|
|
44
|
+
*
|
|
45
|
+
* @param filter - the resource or resource name to delete
|
|
46
|
+
*/
|
|
47
|
+
Delete: (filter?: K | string) => Promise<void>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @param callback
|
|
52
|
+
* @returns
|
|
53
|
+
*/
|
|
54
|
+
Watch: (callback: (payload: K, phase: WatchPhase) => void) => Promise<void>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
58
|
+
/**
|
|
59
|
+
* Perform a server-side apply of the provided K8s resource.
|
|
60
|
+
*
|
|
61
|
+
* @param resource
|
|
62
|
+
* @returns
|
|
63
|
+
*/
|
|
64
|
+
Apply: (resource: K) => Promise<K>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create the provided K8s resource or throw an error if it already exists.
|
|
68
|
+
*
|
|
69
|
+
* @param resource
|
|
70
|
+
* @returns
|
|
71
|
+
*/
|
|
72
|
+
Create: (resource: K) => Promise<K>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Advanced JSON Patch operations for when Server Side Apply, Kube().Apply(), is insufficient.
|
|
76
|
+
*
|
|
77
|
+
* Note: Throws an error on an empty list of patch operations.
|
|
78
|
+
*
|
|
79
|
+
* @param payload The patch operations to run
|
|
80
|
+
* @returns The patched resource
|
|
81
|
+
*/
|
|
82
|
+
Patch: (payload: Operation[]) => Promise<K>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> & {
|
|
86
|
+
/**
|
|
87
|
+
* Filter the query by the given field.
|
|
88
|
+
* Note multiple calls to this method will result in an AND condition. e.g.
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* Kube(given.Deployment)
|
|
92
|
+
* .WithField("metadata.name", "bar")
|
|
93
|
+
* .WithField("metadata.namespace", "qux")
|
|
94
|
+
* .Delete(...)
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* Will only delete the Deployment if it has the `metadata.name=bar` and `metadata.namespace=qux` fields.
|
|
98
|
+
*
|
|
99
|
+
* @param key The field key
|
|
100
|
+
* @param value The field value
|
|
101
|
+
* @returns
|
|
102
|
+
*/
|
|
103
|
+
WithField: <P extends Paths<K>>(key: P, value?: string) => K8sWithFilters<K>;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Filter the query by the given label. If no value is specified, the label simply must exist.
|
|
107
|
+
* Note multiple calls to this method will result in an AND condition. e.g.
|
|
108
|
+
*
|
|
109
|
+
* ```ts
|
|
110
|
+
* Kube(given.Deployment)
|
|
111
|
+
* .WithLabel("foo", "bar")
|
|
112
|
+
* .WithLabel("baz", "qux")
|
|
113
|
+
* .Delete(...)
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* Will only delete the Deployment if it has the`foo=bar` and `baz=qux` labels.
|
|
117
|
+
*
|
|
118
|
+
* @param key The label key
|
|
119
|
+
* @param value (optional) The label value
|
|
120
|
+
*/
|
|
121
|
+
WithLabel: (key: string, value?: string) => K8sWithFilters<K>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type K8sInit<K extends KubernetesObject> = K8sWithFilters<K> &
|
|
125
|
+
K8sUnfilteredActions<K> & {
|
|
126
|
+
/**
|
|
127
|
+
* Filter the query by the given namespace.
|
|
128
|
+
*
|
|
129
|
+
* @param namespace
|
|
130
|
+
* @returns
|
|
131
|
+
*/
|
|
132
|
+
InNamespace: (namespace: string) => K8sWithFilters<K>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type WatchAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
|
|
136
|
+
update: K,
|
|
137
|
+
phase: WatchPhase,
|
|
138
|
+
) => Promise<void> | void;
|
|
139
|
+
|
|
140
|
+
// Special types to handle the recursive keyof typescript lookup
|
|
141
|
+
type Join<K, P> = K extends string | number
|
|
142
|
+
? P extends string | number
|
|
143
|
+
? `${K}${"" extends P ? "" : "."}${P}`
|
|
144
|
+
: never
|
|
145
|
+
: never;
|
|
146
|
+
|
|
147
|
+
export type Paths<T, D extends number = 10> = [D] extends [never]
|
|
148
|
+
? never
|
|
149
|
+
: T extends object
|
|
150
|
+
? { [K in keyof T]-?: K extends string | number ? `${K}` | Join<K, Paths<T[K]>> : never }[keyof T]
|
|
151
|
+
: "";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
import { fetch } from "../fetch";
|
|
7
|
+
import { GenericClass } from "../types";
|
|
8
|
+
import { ClusterRole, Ingress, Pod } from "../upstream";
|
|
9
|
+
import { Filters } from "./types";
|
|
10
|
+
import { k8sExec, pathBuilder } from "./utils";
|
|
11
|
+
|
|
12
|
+
jest.mock("https");
|
|
13
|
+
jest.mock("../fetch");
|
|
14
|
+
|
|
15
|
+
describe("pathBuilder Function", () => {
|
|
16
|
+
const serverUrl = "https://jest-test:8080";
|
|
17
|
+
it("should throw an error if the kind is not specified and the model is not a KubernetesObject", () => {
|
|
18
|
+
const model = { name: "Unknown" } as unknown as GenericClass;
|
|
19
|
+
const filters: Filters = {};
|
|
20
|
+
expect(() => pathBuilder("", model, filters)).toThrow("Kind not specified for Unknown");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should generate a path for core group kinds", () => {
|
|
24
|
+
const filters: Filters = { namespace: "default", name: "mypod" };
|
|
25
|
+
const result = pathBuilder(serverUrl, Pod, filters);
|
|
26
|
+
const expected = new URL("/api/v1/namespaces/default/pods/mypod", serverUrl);
|
|
27
|
+
expect(result).toEqual(expected);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should generate a path for non-core group kinds", () => {
|
|
31
|
+
const filters: Filters = {
|
|
32
|
+
namespace: "default",
|
|
33
|
+
name: "myingress",
|
|
34
|
+
};
|
|
35
|
+
const result = pathBuilder(serverUrl, Ingress, filters);
|
|
36
|
+
const expected = new URL(
|
|
37
|
+
"/apis/networking.k8s.io/v1/namespaces/default/ingresses/myingress",
|
|
38
|
+
serverUrl,
|
|
39
|
+
);
|
|
40
|
+
expect(result).toEqual(expected);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should generate a path without a namespace if not provided", () => {
|
|
44
|
+
const filters: Filters = { name: "tester" };
|
|
45
|
+
const result = pathBuilder(serverUrl, ClusterRole, filters);
|
|
46
|
+
const expected = new URL("/apis/rbac.authorization.k8s.io/v1/clusterroles/tester", serverUrl);
|
|
47
|
+
expect(result).toEqual(expected);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should generate a path without a name if excludeName is true", () => {
|
|
51
|
+
const filters: Filters = { namespace: "default", name: "mypod" };
|
|
52
|
+
const result = pathBuilder(serverUrl, Pod, filters, true);
|
|
53
|
+
const expected = new URL("/api/v1/namespaces/default/pods", serverUrl);
|
|
54
|
+
expect(result).toEqual(expected);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("kubeExec Function", () => {
|
|
59
|
+
const mockedFetch = jest.mocked(fetch);
|
|
60
|
+
|
|
61
|
+
const fakeFilters: Filters = { name: "fake", namespace: "default" };
|
|
62
|
+
const fakeMethod = "GET";
|
|
63
|
+
const fakePayload = { metadata: { name: "fake", namespace: "default" } };
|
|
64
|
+
const fakeUrl = new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake");
|
|
65
|
+
const fakeOpts = {
|
|
66
|
+
body: JSON.stringify(fakePayload),
|
|
67
|
+
headers: {
|
|
68
|
+
"Content-Type": "application/json",
|
|
69
|
+
"User-Agent": `kubernetes-fluent-client`,
|
|
70
|
+
},
|
|
71
|
+
method: fakeMethod,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
mockedFetch.mockClear();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should make a successful fetch call", async () => {
|
|
79
|
+
mockedFetch.mockResolvedValueOnce({
|
|
80
|
+
ok: true,
|
|
81
|
+
data: fakePayload,
|
|
82
|
+
status: 200,
|
|
83
|
+
statusText: "OK",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = await k8sExec(Pod, fakeFilters, fakeMethod, fakePayload);
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual(fakePayload);
|
|
89
|
+
expect(mockedFetch).toHaveBeenCalledWith(fakeUrl, expect.objectContaining(fakeOpts));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should handle fetch call failure", async () => {
|
|
93
|
+
const fakeStatus = 404;
|
|
94
|
+
const fakeStatusText = "Not Found";
|
|
95
|
+
|
|
96
|
+
mockedFetch.mockResolvedValueOnce({
|
|
97
|
+
ok: false,
|
|
98
|
+
data: null,
|
|
99
|
+
status: fakeStatus,
|
|
100
|
+
statusText: fakeStatusText,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await expect(k8sExec(Pod, fakeFilters, fakeMethod, fakePayload)).rejects.toEqual(
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
status: fakeStatus,
|
|
106
|
+
statusText: fakeStatusText,
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { KubeConfig, PatchStrategy } from "@kubernetes/client-node";
|
|
5
|
+
|
|
6
|
+
import { Headers } from "node-fetch";
|
|
7
|
+
import { URL } from "url";
|
|
8
|
+
import { fetch } from "../fetch";
|
|
9
|
+
import { modelToGroupVersionKind } from "../kinds";
|
|
10
|
+
import { GenericClass } from "../types";
|
|
11
|
+
import { FetchMethods, Filters } from "./types";
|
|
12
|
+
|
|
13
|
+
const SSA_CONTENT_TYPE = "application/apply-patch+yaml";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a path to a Kubernetes resource
|
|
17
|
+
*
|
|
18
|
+
* @param serverUrl
|
|
19
|
+
* @param model
|
|
20
|
+
* @param filters
|
|
21
|
+
* @param excludeName
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
export function pathBuilder<T extends GenericClass>(
|
|
25
|
+
serverUrl: string,
|
|
26
|
+
model: T,
|
|
27
|
+
filters: Filters,
|
|
28
|
+
excludeName = false,
|
|
29
|
+
) {
|
|
30
|
+
const matchedKind = filters.kindOverride || modelToGroupVersionKind(model.name);
|
|
31
|
+
|
|
32
|
+
// If the kind is not specified and the model is not a KubernetesObject, throw an error
|
|
33
|
+
if (!matchedKind) {
|
|
34
|
+
throw new Error(`Kind not specified for ${model.name}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Use the plural property if it exists, otherwise use lowercase kind + s
|
|
38
|
+
const plural = matchedKind.plural || `${matchedKind.kind.toLowerCase()}s`;
|
|
39
|
+
|
|
40
|
+
let base = "/api/v1";
|
|
41
|
+
|
|
42
|
+
// If the kind is not in the core group, add the group and version to the path
|
|
43
|
+
if (matchedKind.group) {
|
|
44
|
+
if (!matchedKind.version) {
|
|
45
|
+
throw new Error(`Version not specified for ${model.name}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
base = `/apis/${matchedKind.group}/${matchedKind.version}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Namespaced paths require a namespace prefix
|
|
52
|
+
const namespace = filters.namespace ? `namespaces/${filters.namespace}` : "";
|
|
53
|
+
|
|
54
|
+
// Name should not be included in some paths
|
|
55
|
+
const name = excludeName ? "" : filters.name;
|
|
56
|
+
|
|
57
|
+
// Build the complete path to the resource
|
|
58
|
+
const path = [base, namespace, plural, name].filter(Boolean).join("/");
|
|
59
|
+
|
|
60
|
+
// Generate the URL object
|
|
61
|
+
const url = new URL(path, serverUrl);
|
|
62
|
+
|
|
63
|
+
// Add field selectors to the query params
|
|
64
|
+
if (filters.fields) {
|
|
65
|
+
const fieldSelector = Object.entries(filters.fields)
|
|
66
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
67
|
+
.join(",");
|
|
68
|
+
|
|
69
|
+
url.searchParams.set("fieldSelector", fieldSelector);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add label selectors to the query params
|
|
73
|
+
if (filters.labels) {
|
|
74
|
+
const labelSelector = Object.entries(filters.labels)
|
|
75
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
76
|
+
.join(",");
|
|
77
|
+
|
|
78
|
+
url.searchParams.set("labelSelector", labelSelector);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return url;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sets up the kubeconfig and https agent for a request
|
|
86
|
+
*
|
|
87
|
+
* A few notes:
|
|
88
|
+
* - The kubeconfig is loaded from the default location, and can check for in-cluster config
|
|
89
|
+
* - We have to create an agent to handle the TLS connection (for the custom CA + mTLS in some cases)
|
|
90
|
+
* - The K8s lib uses request instead of node-fetch today so the object is slightly different
|
|
91
|
+
*
|
|
92
|
+
* @param method
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
export async function k8sCfg(method: FetchMethods) {
|
|
96
|
+
const kubeConfig = new KubeConfig();
|
|
97
|
+
kubeConfig.loadFromDefault();
|
|
98
|
+
|
|
99
|
+
const cluster = kubeConfig.getCurrentCluster();
|
|
100
|
+
if (!cluster) {
|
|
101
|
+
throw new Error("No currently active cluster");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Setup the TLS options & auth headers, as needed
|
|
105
|
+
const opts = await kubeConfig.applyToFetchOptions({
|
|
106
|
+
method,
|
|
107
|
+
headers: {
|
|
108
|
+
// Set the default content type to JSON
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
// Set the user agent like kubectl does
|
|
111
|
+
"User-Agent": `kubernetes-fluent-client`,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { opts, serverUrl: cluster.server };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function k8sExec<T extends GenericClass, K>(
|
|
119
|
+
model: T,
|
|
120
|
+
filters: Filters,
|
|
121
|
+
method: FetchMethods,
|
|
122
|
+
payload?: K | unknown,
|
|
123
|
+
) {
|
|
124
|
+
const { opts, serverUrl } = await k8sCfg(method);
|
|
125
|
+
const url = pathBuilder(serverUrl, model, filters, method === "POST");
|
|
126
|
+
|
|
127
|
+
switch (opts.method) {
|
|
128
|
+
case "PATCH":
|
|
129
|
+
(opts.headers as Headers).set("Content-Type", PatchStrategy.JsonPatch);
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case "APPLY":
|
|
133
|
+
(opts.headers as Headers).set("Content-Type", SSA_CONTENT_TYPE);
|
|
134
|
+
opts.method = "PATCH";
|
|
135
|
+
url.searchParams.set("fieldManager", "pepr");
|
|
136
|
+
url.searchParams.set("fieldValidation", "Strict");
|
|
137
|
+
url.searchParams.set("force", "false");
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (payload) {
|
|
142
|
+
opts.body = JSON.stringify(payload);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const resp = await fetch<K>(url, opts);
|
|
146
|
+
|
|
147
|
+
if (resp.ok) {
|
|
148
|
+
return resp.data;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw resp;
|
|
152
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import readline from "readline";
|
|
5
|
+
|
|
6
|
+
import fetch from "node-fetch";
|
|
7
|
+
import { GenericClass } from "../types";
|
|
8
|
+
import { Filters, WatchAction, WatchPhase } from "./types";
|
|
9
|
+
import { k8sCfg, pathBuilder } from "./utils";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute a watch on the specified resource.
|
|
13
|
+
*/
|
|
14
|
+
export async function ExecWatch<T extends GenericClass>(
|
|
15
|
+
model: T,
|
|
16
|
+
filters: Filters,
|
|
17
|
+
callback: WatchAction<T>,
|
|
18
|
+
) {
|
|
19
|
+
// Build the path and query params for the resource, excluding the name
|
|
20
|
+
const { opts, serverUrl } = await k8sCfg("GET");
|
|
21
|
+
const url = pathBuilder(serverUrl, model, filters, true);
|
|
22
|
+
|
|
23
|
+
// Enable the watch query param
|
|
24
|
+
url.searchParams.set("watch", "true");
|
|
25
|
+
|
|
26
|
+
// Allow bookmarks to be used for the watch
|
|
27
|
+
url.searchParams.set("allowWatchBookmarks", "true");
|
|
28
|
+
|
|
29
|
+
// If a name is specified, add it to the query params
|
|
30
|
+
if (filters.name) {
|
|
31
|
+
url.searchParams.set("fieldSelector", `metadata.name=${filters.name}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add abort controller to the long-running request
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
opts.signal = controller.signal;
|
|
37
|
+
|
|
38
|
+
// Close the connection and make the callback function no-op
|
|
39
|
+
let close = (err?: Error) => {
|
|
40
|
+
controller.abort();
|
|
41
|
+
close = () => {};
|
|
42
|
+
if (err) {
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Make the actual request
|
|
49
|
+
const response = await fetch(url, opts);
|
|
50
|
+
|
|
51
|
+
// If the request is successful, start listening for events
|
|
52
|
+
if (response.ok) {
|
|
53
|
+
const { body } = response;
|
|
54
|
+
|
|
55
|
+
// Bind connection events to the close function
|
|
56
|
+
body.on("error", close);
|
|
57
|
+
body.on("close", close);
|
|
58
|
+
body.on("finish", close);
|
|
59
|
+
|
|
60
|
+
// Create a readline interface to parse the stream
|
|
61
|
+
const rl = readline.createInterface({
|
|
62
|
+
input: response.body!,
|
|
63
|
+
terminal: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Listen for events and call the callback function
|
|
67
|
+
rl.on("line", line => {
|
|
68
|
+
try {
|
|
69
|
+
// Parse the event payload
|
|
70
|
+
const { object: payload, type: phase } = JSON.parse(line) as {
|
|
71
|
+
type: WatchPhase;
|
|
72
|
+
object: InstanceType<T>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Call the callback function with the parsed payload
|
|
76
|
+
void callback(payload, phase as WatchPhase);
|
|
77
|
+
} catch (ignore) {
|
|
78
|
+
// ignore parse errors
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
// If the request fails, throw an error
|
|
83
|
+
const error = new Error(response.statusText) as Error & {
|
|
84
|
+
statusCode: number | undefined;
|
|
85
|
+
};
|
|
86
|
+
error.statusCode = response.status;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
close(e);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return controller;
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
// Export kinds as a single object
|
|
5
5
|
import * as kind from "./upstream";
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** kind is a collection of K8s types to be used within a K8s call: `K8s(kind.Secret).Apply({})`. */
|
|
8
8
|
export { kind };
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// Export the node-fetch wrapper
|
|
11
|
+
export { fetch } from "./fetch";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// Export the fluent API entrypoint
|
|
14
|
+
export { K8s } from "./fluent";
|
|
15
|
+
|
|
16
|
+
// Export helpers for working with K8s types
|
|
17
|
+
export { RegisterKind, modelToGroupVersionKind } from "./kinds";
|
|
13
18
|
|
|
14
19
|
export * from "./types";
|
package/src/kinds.test.ts
CHANGED
package/src/kinds.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { GenericClass, GroupVersionKind } from "./types";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const gvkMap: Record<string, GroupVersionKind> = {
|
|
7
7
|
/**
|
|
8
8
|
* Represents a K8s ClusterRole resource.
|
|
9
9
|
* ClusterRole is a set of permissions that can be bound to a user or group in a cluster-wide scope.
|