kubernetes-fluent-client 0.0.0-development → 1.0.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.
Files changed (73) hide show
  1. package/README.md +15 -0
  2. package/__mocks__/@kubernetes/client-node.ts +22 -0
  3. package/dist/fetch.d.ts +23 -0
  4. package/dist/fetch.d.ts.map +1 -0
  5. package/dist/fetch.js +85 -0
  6. package/dist/fetch.test.d.ts +2 -0
  7. package/dist/fetch.test.d.ts.map +1 -0
  8. package/dist/fetch.test.js +97 -0
  9. package/dist/fluent/kube.d.ts +11 -0
  10. package/dist/fluent/kube.d.ts.map +1 -0
  11. package/dist/fluent/kube.js +100 -0
  12. package/dist/fluent/kube.test.d.ts +2 -0
  13. package/dist/fluent/kube.test.d.ts.map +1 -0
  14. package/dist/fluent/kube.test.js +92 -0
  15. package/dist/fluent/types.d.ts +121 -0
  16. package/dist/fluent/types.d.ts.map +1 -0
  17. package/dist/fluent/types.js +14 -0
  18. package/dist/fluent/utils.d.ts +31 -0
  19. package/dist/fluent/utils.d.ts.map +1 -0
  20. package/dist/fluent/utils.js +116 -0
  21. package/dist/fluent/utils.test.d.ts +2 -0
  22. package/dist/fluent/utils.test.d.ts.map +1 -0
  23. package/dist/fluent/utils.test.js +88 -0
  24. package/dist/fluent/watch.d.ts +8 -0
  25. package/dist/fluent/watch.d.ts.map +1 -0
  26. package/dist/fluent/watch.js +78 -0
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +44 -1
  30. package/dist/kinds.d.ts +10 -0
  31. package/dist/kinds.d.ts.map +1 -0
  32. package/dist/kinds.js +489 -0
  33. package/dist/kinds.test.d.ts +2 -0
  34. package/dist/kinds.test.d.ts.map +1 -0
  35. package/dist/kinds.test.js +142 -0
  36. package/dist/types.d.ts +26 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +16 -0
  39. package/dist/upstream.d.ts +4 -0
  40. package/dist/upstream.d.ts.map +1 -0
  41. package/dist/upstream.js +54 -0
  42. package/package.json +21 -8
  43. package/src/fetch.test.ts +115 -0
  44. package/src/fetch.ts +72 -0
  45. package/src/fluent/kube.test.ts +136 -0
  46. package/src/fluent/kube.ts +126 -0
  47. package/src/fluent/types.ts +151 -0
  48. package/src/fluent/utils.test.ts +110 -0
  49. package/src/fluent/utils.ts +152 -0
  50. package/src/fluent/watch.ts +94 -0
  51. package/src/index.ts +8 -3
  52. package/src/kinds.test.ts +1 -1
  53. package/src/kinds.ts +1 -1
  54. package/src/types.ts +1 -154
  55. package/.eslintrc.json +0 -26
  56. package/.prettierrc +0 -13
  57. package/coverage/clover.xml +0 -83
  58. package/coverage/coverage-final.json +0 -5
  59. package/coverage/lcov-report/base.css +0 -224
  60. package/coverage/lcov-report/block-navigation.js +0 -87
  61. package/coverage/lcov-report/favicon.png +0 -0
  62. package/coverage/lcov-report/index.html +0 -161
  63. package/coverage/lcov-report/index.ts.html +0 -127
  64. package/coverage/lcov-report/kinds.ts.html +0 -1675
  65. package/coverage/lcov-report/prettify.css +0 -1
  66. package/coverage/lcov-report/prettify.js +0 -2
  67. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  68. package/coverage/lcov-report/sorter.js +0 -196
  69. package/coverage/lcov-report/types.ts.html +0 -643
  70. package/coverage/lcov-report/upstream.ts.html +0 -244
  71. package/coverage/lcov.info +0 -208
  72. package/jest.config.json +0 -4
  73. package/tsconfig.json +0 -18
@@ -0,0 +1,4 @@
1
+ /** a is a collection of K8s types to be used within an action: `When(a.Configmap)` */
2
+ export { CoreV1Event as Event, V1APIService as APIService, V1CertificateSigningRequest as CertificateSigningRequest, V1ClusterRole as ClusterRole, V1ClusterRoleBinding as ClusterRoleBinding, V1ConfigMap as ConfigMap, V1ControllerRevision as ControllerRevision, V1CronJob as CronJob, V1CSIDriver as CSIDriver, V1CustomResourceDefinition as CustomResourceDefinition, V1DaemonSet as DaemonSet, V1Deployment as Deployment, V1EndpointSlice as EndpointSlice, V1HorizontalPodAutoscaler as HorizontalPodAutoscaler, V1Ingress as Ingress, V1IngressClass as IngressClass, V1Job as Job, V1LimitRange as LimitRange, V1LocalSubjectAccessReview as LocalSubjectAccessReview, V1MutatingWebhookConfiguration as MutatingWebhookConfiguration, V1Namespace as Namespace, V1NetworkPolicy as NetworkPolicy, V1Node as Node, V1PersistentVolume as PersistentVolume, V1PersistentVolumeClaim as PersistentVolumeClaim, V1Pod as Pod, V1PodDisruptionBudget as PodDisruptionBudget, V1PodTemplate as PodTemplate, V1ReplicaSet as ReplicaSet, V1ReplicationController as ReplicationController, V1ResourceQuota as ResourceQuota, V1Role as Role, V1RoleBinding as RoleBinding, V1RuntimeClass as RuntimeClass, V1Secret as Secret, V1SelfSubjectAccessReview as SelfSubjectAccessReview, V1SelfSubjectRulesReview as SelfSubjectRulesReview, V1Service as Service, V1ServiceAccount as ServiceAccount, V1StatefulSet as StatefulSet, V1StorageClass as StorageClass, V1SubjectAccessReview as SubjectAccessReview, V1TokenReview as TokenReview, V1ValidatingWebhookConfiguration as ValidatingWebhookConfiguration, V1VolumeAttachment as VolumeAttachment, } from "@kubernetes/client-node";
3
+ export { GenericKind } from "./types";
4
+ //# sourceMappingURL=upstream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream.d.ts","sourceRoot":"","sources":["../src/upstream.ts"],"names":[],"mappings":"AAGA,sFAAsF;AACtF,OAAO,EACL,WAAW,IAAI,KAAK,EACpB,YAAY,IAAI,UAAU,EAC1B,2BAA2B,IAAI,yBAAyB,EACxD,aAAa,IAAI,WAAW,EAC5B,oBAAoB,IAAI,kBAAkB,EAC1C,WAAW,IAAI,SAAS,EACxB,oBAAoB,IAAI,kBAAkB,EAC1C,SAAS,IAAI,OAAO,EACpB,WAAW,IAAI,SAAS,EACxB,0BAA0B,IAAI,wBAAwB,EACtD,WAAW,IAAI,SAAS,EACxB,YAAY,IAAI,UAAU,EAC1B,eAAe,IAAI,aAAa,EAChC,yBAAyB,IAAI,uBAAuB,EACpD,SAAS,IAAI,OAAO,EACpB,cAAc,IAAI,YAAY,EAC9B,KAAK,IAAI,GAAG,EACZ,YAAY,IAAI,UAAU,EAC1B,0BAA0B,IAAI,wBAAwB,EACtD,8BAA8B,IAAI,4BAA4B,EAC9D,WAAW,IAAI,SAAS,EACxB,eAAe,IAAI,aAAa,EAChC,MAAM,IAAI,IAAI,EACd,kBAAkB,IAAI,gBAAgB,EACtC,uBAAuB,IAAI,qBAAqB,EAChD,KAAK,IAAI,GAAG,EACZ,qBAAqB,IAAI,mBAAmB,EAC5C,aAAa,IAAI,WAAW,EAC5B,YAAY,IAAI,UAAU,EAC1B,uBAAuB,IAAI,qBAAqB,EAChD,eAAe,IAAI,aAAa,EAChC,MAAM,IAAI,IAAI,EACd,aAAa,IAAI,WAAW,EAC5B,cAAc,IAAI,YAAY,EAC9B,QAAQ,IAAI,MAAM,EAClB,yBAAyB,IAAI,uBAAuB,EACpD,wBAAwB,IAAI,sBAAsB,EAClD,SAAS,IAAI,OAAO,EACpB,gBAAgB,IAAI,cAAc,EAClC,aAAa,IAAI,WAAW,EAC5B,cAAc,IAAI,YAAY,EAC9B,qBAAqB,IAAI,mBAAmB,EAC5C,aAAa,IAAI,WAAW,EAC5B,gCAAgC,IAAI,8BAA8B,EAClE,kBAAkB,IAAI,gBAAgB,GACvC,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.GenericKind = exports.VolumeAttachment = exports.ValidatingWebhookConfiguration = exports.TokenReview = exports.SubjectAccessReview = exports.StorageClass = exports.StatefulSet = exports.ServiceAccount = exports.Service = exports.SelfSubjectRulesReview = exports.SelfSubjectAccessReview = exports.Secret = exports.RuntimeClass = exports.RoleBinding = exports.Role = exports.ResourceQuota = exports.ReplicationController = exports.ReplicaSet = exports.PodTemplate = exports.PodDisruptionBudget = exports.Pod = exports.PersistentVolumeClaim = exports.PersistentVolume = exports.Node = exports.NetworkPolicy = exports.Namespace = exports.MutatingWebhookConfiguration = exports.LocalSubjectAccessReview = exports.LimitRange = exports.Job = exports.IngressClass = exports.Ingress = exports.HorizontalPodAutoscaler = exports.EndpointSlice = exports.Deployment = exports.DaemonSet = exports.CustomResourceDefinition = exports.CSIDriver = exports.CronJob = exports.ControllerRevision = exports.ConfigMap = exports.ClusterRoleBinding = exports.ClusterRole = exports.CertificateSigningRequest = exports.APIService = exports.Event = void 0;
6
+ /** a is a collection of K8s types to be used within an action: `When(a.Configmap)` */
7
+ var client_node_1 = require("@kubernetes/client-node");
8
+ Object.defineProperty(exports, "Event", { enumerable: true, get: function () { return client_node_1.CoreV1Event; } });
9
+ Object.defineProperty(exports, "APIService", { enumerable: true, get: function () { return client_node_1.V1APIService; } });
10
+ Object.defineProperty(exports, "CertificateSigningRequest", { enumerable: true, get: function () { return client_node_1.V1CertificateSigningRequest; } });
11
+ Object.defineProperty(exports, "ClusterRole", { enumerable: true, get: function () { return client_node_1.V1ClusterRole; } });
12
+ Object.defineProperty(exports, "ClusterRoleBinding", { enumerable: true, get: function () { return client_node_1.V1ClusterRoleBinding; } });
13
+ Object.defineProperty(exports, "ConfigMap", { enumerable: true, get: function () { return client_node_1.V1ConfigMap; } });
14
+ Object.defineProperty(exports, "ControllerRevision", { enumerable: true, get: function () { return client_node_1.V1ControllerRevision; } });
15
+ Object.defineProperty(exports, "CronJob", { enumerable: true, get: function () { return client_node_1.V1CronJob; } });
16
+ Object.defineProperty(exports, "CSIDriver", { enumerable: true, get: function () { return client_node_1.V1CSIDriver; } });
17
+ Object.defineProperty(exports, "CustomResourceDefinition", { enumerable: true, get: function () { return client_node_1.V1CustomResourceDefinition; } });
18
+ Object.defineProperty(exports, "DaemonSet", { enumerable: true, get: function () { return client_node_1.V1DaemonSet; } });
19
+ Object.defineProperty(exports, "Deployment", { enumerable: true, get: function () { return client_node_1.V1Deployment; } });
20
+ Object.defineProperty(exports, "EndpointSlice", { enumerable: true, get: function () { return client_node_1.V1EndpointSlice; } });
21
+ Object.defineProperty(exports, "HorizontalPodAutoscaler", { enumerable: true, get: function () { return client_node_1.V1HorizontalPodAutoscaler; } });
22
+ Object.defineProperty(exports, "Ingress", { enumerable: true, get: function () { return client_node_1.V1Ingress; } });
23
+ Object.defineProperty(exports, "IngressClass", { enumerable: true, get: function () { return client_node_1.V1IngressClass; } });
24
+ Object.defineProperty(exports, "Job", { enumerable: true, get: function () { return client_node_1.V1Job; } });
25
+ Object.defineProperty(exports, "LimitRange", { enumerable: true, get: function () { return client_node_1.V1LimitRange; } });
26
+ Object.defineProperty(exports, "LocalSubjectAccessReview", { enumerable: true, get: function () { return client_node_1.V1LocalSubjectAccessReview; } });
27
+ Object.defineProperty(exports, "MutatingWebhookConfiguration", { enumerable: true, get: function () { return client_node_1.V1MutatingWebhookConfiguration; } });
28
+ Object.defineProperty(exports, "Namespace", { enumerable: true, get: function () { return client_node_1.V1Namespace; } });
29
+ Object.defineProperty(exports, "NetworkPolicy", { enumerable: true, get: function () { return client_node_1.V1NetworkPolicy; } });
30
+ Object.defineProperty(exports, "Node", { enumerable: true, get: function () { return client_node_1.V1Node; } });
31
+ Object.defineProperty(exports, "PersistentVolume", { enumerable: true, get: function () { return client_node_1.V1PersistentVolume; } });
32
+ Object.defineProperty(exports, "PersistentVolumeClaim", { enumerable: true, get: function () { return client_node_1.V1PersistentVolumeClaim; } });
33
+ Object.defineProperty(exports, "Pod", { enumerable: true, get: function () { return client_node_1.V1Pod; } });
34
+ Object.defineProperty(exports, "PodDisruptionBudget", { enumerable: true, get: function () { return client_node_1.V1PodDisruptionBudget; } });
35
+ Object.defineProperty(exports, "PodTemplate", { enumerable: true, get: function () { return client_node_1.V1PodTemplate; } });
36
+ Object.defineProperty(exports, "ReplicaSet", { enumerable: true, get: function () { return client_node_1.V1ReplicaSet; } });
37
+ Object.defineProperty(exports, "ReplicationController", { enumerable: true, get: function () { return client_node_1.V1ReplicationController; } });
38
+ Object.defineProperty(exports, "ResourceQuota", { enumerable: true, get: function () { return client_node_1.V1ResourceQuota; } });
39
+ Object.defineProperty(exports, "Role", { enumerable: true, get: function () { return client_node_1.V1Role; } });
40
+ Object.defineProperty(exports, "RoleBinding", { enumerable: true, get: function () { return client_node_1.V1RoleBinding; } });
41
+ Object.defineProperty(exports, "RuntimeClass", { enumerable: true, get: function () { return client_node_1.V1RuntimeClass; } });
42
+ Object.defineProperty(exports, "Secret", { enumerable: true, get: function () { return client_node_1.V1Secret; } });
43
+ Object.defineProperty(exports, "SelfSubjectAccessReview", { enumerable: true, get: function () { return client_node_1.V1SelfSubjectAccessReview; } });
44
+ Object.defineProperty(exports, "SelfSubjectRulesReview", { enumerable: true, get: function () { return client_node_1.V1SelfSubjectRulesReview; } });
45
+ Object.defineProperty(exports, "Service", { enumerable: true, get: function () { return client_node_1.V1Service; } });
46
+ Object.defineProperty(exports, "ServiceAccount", { enumerable: true, get: function () { return client_node_1.V1ServiceAccount; } });
47
+ Object.defineProperty(exports, "StatefulSet", { enumerable: true, get: function () { return client_node_1.V1StatefulSet; } });
48
+ Object.defineProperty(exports, "StorageClass", { enumerable: true, get: function () { return client_node_1.V1StorageClass; } });
49
+ Object.defineProperty(exports, "SubjectAccessReview", { enumerable: true, get: function () { return client_node_1.V1SubjectAccessReview; } });
50
+ Object.defineProperty(exports, "TokenReview", { enumerable: true, get: function () { return client_node_1.V1TokenReview; } });
51
+ Object.defineProperty(exports, "ValidatingWebhookConfiguration", { enumerable: true, get: function () { return client_node_1.V1ValidatingWebhookConfiguration; } });
52
+ Object.defineProperty(exports, "VolumeAttachment", { enumerable: true, get: function () { return client_node_1.V1VolumeAttachment; } });
53
+ var types_1 = require("./types");
54
+ Object.defineProperty(exports, "GenericKind", { enumerable: true, get: function () { return types_1.GenericKind; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubernetes-fluent-client",
3
- "version": "0.0.0-development",
3
+ "version": "1.0.0",
4
4
  "description": "A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,9 +10,12 @@
10
10
  "semantic-release": "semantic-release",
11
11
  "test": "jest src --coverage"
12
12
  },
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ },
13
16
  "repository": {
14
17
  "type": "git",
15
- "url": "https://github.com/defenseunicorns/kubernetes-fluent-client.git"
18
+ "url": "git+https://github.com/defenseunicorns/kubernetes-fluent-client.git"
16
19
  },
17
20
  "keywords": [
18
21
  "kubernetes",
@@ -29,16 +32,26 @@
29
32
  },
30
33
  "homepage": "https://github.com/defenseunicorns/kubernetes-fluent-client#readme",
31
34
  "dependencies": {
32
- "@kubernetes/client-node": "1.0.0-rc3"
35
+ "@kubernetes/client-node": "1.0.0-rc3",
36
+ "fast-json-patch": "3.1.1",
37
+ "http-status-codes": "2.3.0",
38
+ "node-fetch": "2.7.0"
33
39
  },
34
40
  "devDependencies": {
35
41
  "@jest/globals": "29.7.0",
36
- "@typescript-eslint/eslint-plugin": "6.5.0",
37
- "@typescript-eslint/parser": "6.5.0",
42
+ "@typescript-eslint/eslint-plugin": "6.7.2",
43
+ "@typescript-eslint/parser": "6.7.2",
38
44
  "jest": "29.7.0",
45
+ "nock": "13.3.3",
39
46
  "prettier": "3.0.3",
40
- "semantic-release": "22.0.1",
41
- "typescript": "5.2.2",
42
- "ts-jest": "29.1.1"
47
+ "semantic-release": "22.0.4",
48
+ "ts-jest": "29.1.1",
49
+ "typescript": "5.2.2"
50
+ },
51
+ "release": {
52
+ "branches": [
53
+ "main",
54
+ "next"
55
+ ]
43
56
  }
44
57
  }
@@ -0,0 +1,115 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { expect, test, beforeEach } from "@jest/globals";
5
+
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
+ 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 () => {
37
+ const url = "https://jsonplaceholder.typicode.com/todos/1";
38
+ const { data, ok } = await fetch<{ title: string }>(url);
39
+ expect(ok).toBe(true);
40
+ expect(data["title"]).toBe("Example title");
41
+ });
42
+
43
+ test("fetch: should return parsed JSON response as a specific type", async () => {
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
+ expect(ok).toBe(true);
54
+ expect(data.id).toBe(1);
55
+ expect(typeof data.title).toBe("string");
56
+ expect(typeof data.completed).toBe("boolean");
57
+ });
58
+
59
+ test("fetch: should handle additional request options", async () => {
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
+ expect(ok).toBe(true);
76
+ expect(data["title"]).toBe("test todo");
77
+ expect(data["userId"]).toBe(1);
78
+ expect(data["completed"]).toBe(false);
79
+ });
80
+
81
+ test("fetch: should handle empty (null) responses", async () => {
82
+ const url = "https://jsonplaceholder.typicode.com/todos/empty-null";
83
+ const resp = await fetch(url);
84
+
85
+ expect(resp.data).toBe("");
86
+ expect(resp.ok).toBe(true);
87
+ expect(resp.status).toBe(StatusCodes.OK);
88
+ });
89
+
90
+ test("fetch: should handle empty (string) responses", async () => {
91
+ const url = "https://jsonplaceholder.typicode.com/todos/empty-string";
92
+ const resp = await fetch(url);
93
+
94
+ expect(resp.data).toBe("");
95
+ expect(resp.ok).toBe(true);
96
+ expect(resp.status).toBe(StatusCodes.OK);
97
+ });
98
+
99
+ test("fetch: should handle empty (object) responses", async () => {
100
+ const url = "https://jsonplaceholder.typicode.com/todos/empty-object";
101
+ const resp = await fetch(url);
102
+
103
+ expect(resp.data).toEqual({});
104
+ expect(resp.ok).toBe(true);
105
+ expect(resp.status).toBe(StatusCodes.OK);
106
+ });
107
+
108
+ test("fetch: should handle failed requests without throwing an error", async () => {
109
+ const url = "https://jsonplaceholder.typicode.com/todos/invalid";
110
+ const resp = await fetch(url);
111
+
112
+ expect(resp.data).toBe(undefined);
113
+ expect(resp.ok).toBe(false);
114
+ expect(resp.status).toBe(StatusCodes.BAD_REQUEST);
115
+ });
package/src/fetch.ts ADDED
@@ -0,0 +1,72 @@
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 fetchRaw, { FetchError, RequestInfo, RequestInit } from "node-fetch";
6
+
7
+ export type FetchResponse<T> = {
8
+ data: T;
9
+ ok: boolean;
10
+ status: number;
11
+ statusText: string;
12
+ };
13
+
14
+ /**
15
+ * Perform an async HTTP call and return the parsed JSON response, optionally
16
+ * as a specific type.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * fetch<string[]>("https://example.com/api/foo");
21
+ * ```
22
+ *
23
+ * @param url The URL or Request object to fetch
24
+ * @param init Additional options for the request
25
+ * @returns
26
+ */
27
+ export async function fetch<T>(
28
+ url: URL | RequestInfo,
29
+ init?: RequestInit,
30
+ ): Promise<FetchResponse<T>> {
31
+ let data = undefined as unknown as T;
32
+ try {
33
+ const resp = await fetchRaw(url, init);
34
+ const contentType = resp.headers.get("content-type") || "";
35
+
36
+ if (resp.ok) {
37
+ // Parse the response as JSON if the content type is JSON
38
+ if (contentType.includes("application/json")) {
39
+ data = await resp.json();
40
+ } else {
41
+ // Otherwise, return however the response was read
42
+ data = (await resp.text()) as unknown as T;
43
+ }
44
+ }
45
+
46
+ return {
47
+ data,
48
+ ok: resp.ok,
49
+ status: resp.status,
50
+ statusText: resp.statusText,
51
+ };
52
+ } catch (e) {
53
+ if (e instanceof FetchError) {
54
+ // Parse the error code from the FetchError or default to 400 (Bad Request)
55
+ const status = parseInt(e.code || "400");
56
+
57
+ return {
58
+ data,
59
+ ok: false,
60
+ status,
61
+ statusText: e.message,
62
+ };
63
+ }
64
+
65
+ return {
66
+ data,
67
+ ok: false,
68
+ status: StatusCodes.BAD_REQUEST,
69
+ statusText: "Unknown error",
70
+ };
71
+ }
72
+ }
@@ -0,0 +1,136 @@
1
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
2
+ import { Operation } from "fast-json-patch";
3
+
4
+ import { Pod } from "../upstream";
5
+ import { Kube } from "./kube";
6
+ import { kubeExec } from "./utils";
7
+
8
+ // Setup mocks
9
+ jest.mock("./utils");
10
+
11
+ describe("Kube", () => {
12
+ const fakeResource = { metadata: { name: "fake", namespace: "default" } };
13
+ const mockedKubeExec = jest.mocked(kubeExec).mockResolvedValue(fakeResource);
14
+
15
+ beforeEach(() => {
16
+ // Clear all instances and calls to constructor and all methods:
17
+ mockedKubeExec.mockClear();
18
+ });
19
+
20
+ it("should create a resource", async () => {
21
+ const kube = Kube(Pod);
22
+ const result = await kube.Create(fakeResource);
23
+
24
+ expect(result).toEqual(fakeResource);
25
+ expect(mockedKubeExec).toHaveBeenCalledWith(
26
+ Pod,
27
+ expect.objectContaining({
28
+ name: "fake",
29
+ namespace: "default",
30
+ }),
31
+ "POST",
32
+ fakeResource,
33
+ );
34
+ });
35
+
36
+ it("should delete a resource", async () => {
37
+ const kube = Kube(Pod);
38
+ await kube.Delete(fakeResource);
39
+
40
+ expect(mockedKubeExec).toHaveBeenCalledWith(
41
+ Pod,
42
+ expect.objectContaining({
43
+ name: "fake",
44
+ namespace: "default",
45
+ }),
46
+ "DELETE",
47
+ );
48
+ });
49
+
50
+ it("should patch a resource", async () => {
51
+ const patchOperations: Operation[] = [
52
+ { op: "replace", path: "/metadata/name", value: "new-fake" },
53
+ ];
54
+
55
+ const kube = Kube(Pod);
56
+ const result = await kube.Patch(patchOperations);
57
+
58
+ expect(result).toEqual(fakeResource);
59
+ expect(mockedKubeExec).toHaveBeenCalledWith(Pod, {}, "PATCH", patchOperations);
60
+ });
61
+
62
+ it("should filter with WithField", async () => {
63
+ const kube = Kube(Pod).WithField("metadata.name", "fake");
64
+ await kube.Get();
65
+ expect(mockedKubeExec).toHaveBeenCalledWith(
66
+ Pod,
67
+ expect.objectContaining({
68
+ fields: {
69
+ "metadata.name": "fake",
70
+ },
71
+ }),
72
+ "GET",
73
+ );
74
+ });
75
+
76
+ it("should filter with WithLabel", async () => {
77
+ const kube = Kube(Pod).WithLabel("app", "fakeApp");
78
+ await kube.Get();
79
+ expect(mockedKubeExec).toHaveBeenCalledWith(
80
+ Pod,
81
+ expect.objectContaining({
82
+ labels: {
83
+ app: "fakeApp",
84
+ },
85
+ }),
86
+ "GET",
87
+ );
88
+ });
89
+
90
+ it("should use InNamespace", async () => {
91
+ const kube = Kube(Pod).InNamespace("fakeNamespace");
92
+ await kube.Get();
93
+ expect(mockedKubeExec).toHaveBeenCalledWith(
94
+ Pod,
95
+ expect.objectContaining({
96
+ namespace: "fakeNamespace",
97
+ }),
98
+ "GET",
99
+ );
100
+ });
101
+
102
+ it("should throw an error if namespace is already specified", async () => {
103
+ const kube = Kube(Pod, { namespace: "default" });
104
+ expect(() => kube.InNamespace("fakeNamespace")).toThrow("Namespace already specified: default");
105
+ });
106
+
107
+ it("should handle Delete when the resource doesn't exist", async () => {
108
+ mockedKubeExec.mockRejectedValueOnce({ status: 404 }); // Not Found on first call
109
+ const kube = Kube(Pod);
110
+ await expect(kube.Delete("fakeResource")).resolves.toBeUndefined();
111
+ });
112
+
113
+ it("should handle Get", async () => {
114
+ const kube = Kube(Pod);
115
+ const result = await kube.Get("fakeResource");
116
+
117
+ expect(result).toEqual(fakeResource);
118
+ expect(mockedKubeExec).toHaveBeenCalledWith(
119
+ Pod,
120
+ expect.objectContaining({
121
+ name: "fakeResource",
122
+ }),
123
+ "GET",
124
+ );
125
+ });
126
+
127
+ it("should thrown an error if Get is called with a name and filters are already specified a name", async () => {
128
+ const kube = Kube(Pod, { name: "fake" });
129
+ await expect(kube.Get("fakeResource")).rejects.toThrow("Name already specified: fake");
130
+ });
131
+
132
+ it("should throw an error if no patch operations provided", async () => {
133
+ const kube = Kube(Pod);
134
+ await expect(kube.Patch([])).rejects.toThrow("No operations specified");
135
+ });
136
+ });
@@ -0,0 +1,126 @@
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 { StatusCodes } from "http-status-codes";
7
+
8
+ import { modelToGroupVersionKind } from "../kinds";
9
+ import { GenericClass } from "../types";
10
+ import { Filters, KubeInit, Paths, WatchAction } from "./types";
11
+ import { kubeExec } from "./utils";
12
+ import { ExecWatch } from "./watch";
13
+
14
+ /**
15
+ * Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
16
+ *
17
+ * @param model - the model to use for the API
18
+ * @param filters - (optional) filter overrides, can also be chained
19
+ */
20
+ export function Kube<T extends GenericClass, K extends KubernetesObject = InstanceType<T>>(
21
+ model: T,
22
+ filters: Filters = {},
23
+ ): KubeInit<K> {
24
+ const withFilters = { WithField, WithLabel, Get, Delete, Watch };
25
+ const matchedKind = filters.kindOverride || modelToGroupVersionKind(model.name);
26
+
27
+ function InNamespace(namespaces: string) {
28
+ if (filters.namespace) {
29
+ throw new Error(`Namespace already specified: ${filters.namespace}`);
30
+ }
31
+
32
+ filters.namespace = namespaces;
33
+ return withFilters;
34
+ }
35
+
36
+ function WithField<P extends Paths<K>>(key: P, value = "") {
37
+ filters.fields = filters.fields || {};
38
+ filters.fields[key] = value;
39
+ return withFilters;
40
+ }
41
+
42
+ function WithLabel(key: string, value = "") {
43
+ filters.labels = filters.labels || {};
44
+ filters.labels[key] = value;
45
+ return withFilters;
46
+ }
47
+
48
+ function syncFilters(payload: K) {
49
+ // Ensure the payload has metadata
50
+ payload.metadata = payload.metadata || {};
51
+
52
+ if (!filters.namespace) {
53
+ filters.namespace = payload.metadata.namespace;
54
+ }
55
+
56
+ if (!filters.name) {
57
+ filters.name = payload.metadata.name;
58
+ }
59
+
60
+ if (!payload.apiVersion) {
61
+ payload.apiVersion = [matchedKind.group, matchedKind.version].filter(Boolean).join("/");
62
+ }
63
+
64
+ if (!payload.kind) {
65
+ payload.kind = matchedKind.kind;
66
+ }
67
+ }
68
+
69
+ async function Get(): Promise<KubernetesListObject<K>>;
70
+ async function Get(name: string): Promise<K>;
71
+ async function Get(name?: string) {
72
+ if (name) {
73
+ if (filters.name) {
74
+ throw new Error(`Name already specified: ${filters.name}`);
75
+ }
76
+ filters.name = name;
77
+ }
78
+
79
+ return kubeExec<T, K | KubernetesListObject<K>>(model, filters, "GET");
80
+ }
81
+
82
+ async function Delete(filter?: K | string): Promise<void> {
83
+ if (typeof filter === "string") {
84
+ filters.name = filter;
85
+ } else if (filter) {
86
+ syncFilters(filter);
87
+ }
88
+
89
+ try {
90
+ // Try to delete the resource
91
+ await kubeExec<T, void>(model, filters, "DELETE");
92
+ } catch (e) {
93
+ // If the resource doesn't exist, ignore the error
94
+ if (e.status === StatusCodes.NOT_FOUND) {
95
+ return;
96
+ }
97
+
98
+ throw e;
99
+ }
100
+ }
101
+
102
+ async function Apply(resource: K): Promise<K> {
103
+ syncFilters(resource);
104
+ return kubeExec(model, filters, "APPLY", resource);
105
+ }
106
+
107
+ async function Create(resource: K): Promise<K> {
108
+ syncFilters(resource);
109
+ return kubeExec(model, filters, "POST", resource);
110
+ }
111
+
112
+ async function Patch(payload: Operation[]): Promise<K> {
113
+ // If there are no operations, throw an error
114
+ if (payload.length < 1) {
115
+ throw new Error("No operations specified");
116
+ }
117
+
118
+ return kubeExec<T, K>(model, filters, "PATCH", payload);
119
+ }
120
+
121
+ async function Watch(callback: WatchAction<T>): Promise<void> {
122
+ await ExecWatch(model, filters, callback);
123
+ }
124
+
125
+ return { InNamespace, Apply, Create, Patch, ...withFilters };
126
+ }