kubernetes-fluent-client 1.4.1 → 1.5.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/dist/fluent/apply.d.ts +10 -0
- package/dist/fluent/apply.d.ts.map +1 -0
- package/dist/fluent/apply.js +4 -0
- package/dist/fluent/index.d.ts.map +1 -1
- package/dist/fluent/index.js +4 -4
- package/dist/fluent/index.test.js +50 -1
- package/dist/fluent/types.d.ts +6 -3
- package/dist/fluent/types.d.ts.map +1 -1
- package/dist/fluent/utils.d.ts +2 -1
- package/dist/fluent/utils.d.ts.map +1 -1
- package/dist/fluent/utils.js +2 -2
- package/dist/fluent/utils.test.js +35 -0
- package/package.json +4 -4
- package/src/fluent/apply.ts +12 -0
- package/src/fluent/index.test.ts +55 -1
- package/src/fluent/index.ts +8 -4
- package/src/fluent/types.ts +6 -3
- package/src/fluent/utils.test.ts +40 -0
- package/src/fluent/utils.ts +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/fluent/apply.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fluent/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAMjF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAsB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fluent/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAMjF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAsB,MAAM,SAAS,CAAC;AAK/D;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,EACtF,KAAK,EAAE,CAAC,EACR,OAAO,GAAE,OAAY,GACpB,OAAO,CAAC,CAAC,CAAC,CA0GZ"}
|
package/dist/fluent/index.js
CHANGED
|
@@ -23,12 +23,12 @@ function K8s(model, filters = {}) {
|
|
|
23
23
|
filters.namespace = namespaces;
|
|
24
24
|
return withFilters;
|
|
25
25
|
}
|
|
26
|
-
function WithField(key, value
|
|
26
|
+
function WithField(key, value) {
|
|
27
27
|
filters.fields = filters.fields || {};
|
|
28
28
|
filters.fields[key] = value;
|
|
29
29
|
return withFilters;
|
|
30
30
|
}
|
|
31
|
-
function WithLabel(key, value
|
|
31
|
+
function WithLabel(key, value) {
|
|
32
32
|
filters.labels = filters.labels || {};
|
|
33
33
|
filters.labels[key] = value;
|
|
34
34
|
return withFilters;
|
|
@@ -77,9 +77,9 @@ function K8s(model, filters = {}) {
|
|
|
77
77
|
throw e;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
async function Apply(resource) {
|
|
80
|
+
async function Apply(resource, applyCfg = { force: false }) {
|
|
81
81
|
syncFilters(resource);
|
|
82
|
-
return (0, utils_1.k8sExec)(model, filters, "APPLY", resource);
|
|
82
|
+
return (0, utils_1.k8sExec)(model, filters, "APPLY", resource, applyCfg);
|
|
83
83
|
}
|
|
84
84
|
async function Create(resource) {
|
|
85
85
|
syncFilters(resource);
|
|
@@ -6,8 +6,49 @@ const _1 = require(".");
|
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
7
|
// Setup mocks
|
|
8
8
|
globals_1.jest.mock("./utils");
|
|
9
|
+
const generateFakePodManagedFields = (manager) => {
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
apiVersion: "v1",
|
|
13
|
+
fieldsType: "FieldsV1",
|
|
14
|
+
fieldsV1: {
|
|
15
|
+
"f:metadata": {
|
|
16
|
+
"f:labels": {
|
|
17
|
+
"f:fake": {},
|
|
18
|
+
},
|
|
19
|
+
"f:spec": {
|
|
20
|
+
"f:containers": {
|
|
21
|
+
'k:{"name":"fake"}': {
|
|
22
|
+
"f:image": {},
|
|
23
|
+
"f:name": {},
|
|
24
|
+
"f:resources": {
|
|
25
|
+
"f:limits": {
|
|
26
|
+
"f:cpu": {},
|
|
27
|
+
"f:memory": {},
|
|
28
|
+
},
|
|
29
|
+
"f:requests": {
|
|
30
|
+
"f:cpu": {},
|
|
31
|
+
"f:memory": {},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
manager: manager,
|
|
40
|
+
operation: "Apply",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
};
|
|
9
44
|
(0, globals_1.describe)("Kube", () => {
|
|
10
|
-
const fakeResource = {
|
|
45
|
+
const fakeResource = {
|
|
46
|
+
metadata: {
|
|
47
|
+
name: "fake",
|
|
48
|
+
namespace: "default",
|
|
49
|
+
managedFields: generateFakePodManagedFields("pepr"),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
11
52
|
const mockedKubeExec = globals_1.jest.mocked(utils_1.k8sExec).mockResolvedValue(fakeResource);
|
|
12
53
|
(0, globals_1.beforeEach)(() => {
|
|
13
54
|
// Clear all instances and calls to constructor and all methods:
|
|
@@ -94,6 +135,14 @@ globals_1.jest.mock("./utils");
|
|
|
94
135
|
const result = await kube.Apply({ metadata: { name: "fake" }, spec: { priority: 3 } });
|
|
95
136
|
(0, globals_1.expect)(result).toEqual(fakeResource);
|
|
96
137
|
});
|
|
138
|
+
(0, globals_1.it)("should allow force apply to resolve FieldManagerConflict", async () => {
|
|
139
|
+
const kube = (0, _1.K8s)(upstream_1.Pod);
|
|
140
|
+
const result = await kube.Apply({
|
|
141
|
+
metadata: { name: "fake", managedFields: generateFakePodManagedFields("kubectl") },
|
|
142
|
+
spec: { priority: 3 },
|
|
143
|
+
}, { force: true });
|
|
144
|
+
(0, globals_1.expect)(result).toEqual(fakeResource);
|
|
145
|
+
});
|
|
97
146
|
(0, globals_1.it)("should throw an error if a Delete failed for a reason other than Not Found", async () => {
|
|
98
147
|
mockedKubeExec.mockRejectedValueOnce({ status: 500 }); // Internal Server Error on first call
|
|
99
148
|
const kube = (0, _1.K8s)(upstream_1.Pod);
|
package/dist/fluent/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Operation } from "fast-json-patch";
|
|
|
3
3
|
import type { PartialDeep } from "type-fest";
|
|
4
4
|
import { GenericClass, GroupVersionKind } from "../types";
|
|
5
5
|
import { WatchCfg, WatchController } from "./watch";
|
|
6
|
+
import { ApplyCfg } from "./apply";
|
|
6
7
|
/**
|
|
7
8
|
* The Phase matched when using the K8s Watch API.
|
|
8
9
|
*/
|
|
@@ -50,7 +51,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
|
50
51
|
* @param resource
|
|
51
52
|
* @returns
|
|
52
53
|
*/
|
|
53
|
-
Apply: (resource: PartialDeep<K
|
|
54
|
+
Apply: (resource: PartialDeep<K>, applyCfg?: ApplyCfg) => Promise<K>;
|
|
54
55
|
/**
|
|
55
56
|
* Create the provided K8s resource or throw an error if it already exists.
|
|
56
57
|
*
|
|
@@ -81,12 +82,14 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
81
82
|
* ```
|
|
82
83
|
*
|
|
83
84
|
* Will only delete the Deployment if it has the `metadata.name=bar` and `metadata.namespace=qux` fields.
|
|
85
|
+
* Not all fields are supported, see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/#supported-fields,
|
|
86
|
+
* but Typescript will limit to only fields that exist on the resource.
|
|
84
87
|
*
|
|
85
88
|
* @param key The field key
|
|
86
89
|
* @param value The field value
|
|
87
90
|
* @returns
|
|
88
91
|
*/
|
|
89
|
-
WithField: <P extends Paths<K>>(key: P, value
|
|
92
|
+
WithField: <P extends Paths<K>>(key: P, value: string) => K8sWithFilters<K>;
|
|
90
93
|
/**
|
|
91
94
|
* Filter the query by the given label. If no value is specified, the label simply must exist.
|
|
92
95
|
* Note multiple calls to this method will result in an AND condition. e.g.
|
|
@@ -103,7 +106,7 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
103
106
|
* @param key The label key
|
|
104
107
|
* @param value (optional) The label value
|
|
105
108
|
*/
|
|
106
|
-
WithLabel: (key: string, value
|
|
109
|
+
WithLabel: (key: string, value: string) => K8sWithFilters<K>;
|
|
107
110
|
};
|
|
108
111
|
export type K8sInit<K extends KubernetesObject> = K8sWithFilters<K> & K8sUnfilteredActions<K> & {
|
|
109
112
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fluent/types.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fluent/types.ts"],"names":[],"mappings":"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"}
|
package/dist/fluent/utils.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { URL } from "url";
|
|
3
3
|
import { GenericClass } from "../types";
|
|
4
4
|
import { FetchMethods, Filters } from "./types";
|
|
5
|
+
import { ApplyCfg } from "./apply";
|
|
5
6
|
/**
|
|
6
7
|
* Generate a path to a Kubernetes resource
|
|
7
8
|
*
|
|
@@ -27,5 +28,5 @@ export declare function k8sCfg(method: FetchMethods): Promise<{
|
|
|
27
28
|
opts: import("node-fetch").RequestInit;
|
|
28
29
|
serverUrl: string;
|
|
29
30
|
}>;
|
|
30
|
-
export declare function k8sExec<T extends GenericClass, K>(model: T, filters: Filters, method: FetchMethods, payload?: K | unknown): Promise<K>;
|
|
31
|
+
export declare function k8sExec<T extends GenericClass, K>(model: T, filters: Filters, method: FetchMethods, payload?: K | unknown, applyCfg?: ApplyCfg): Promise<K>;
|
|
31
32
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/fluent/utils.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/fluent/utils.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAInC;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,YAAY,EAChD,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,WAAW,UAAQ,OAsDpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY;;;GAwBhD;AAED,wBAAsB,OAAO,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,EACrD,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,EACrB,QAAQ,GAAE,QAA2B,cA8BtC"}
|
package/dist/fluent/utils.js
CHANGED
|
@@ -91,7 +91,7 @@ async function k8sCfg(method) {
|
|
|
91
91
|
return { opts, serverUrl: cluster.server };
|
|
92
92
|
}
|
|
93
93
|
exports.k8sCfg = k8sCfg;
|
|
94
|
-
async function k8sExec(model, filters, method, payload) {
|
|
94
|
+
async function k8sExec(model, filters, method, payload, applyCfg = { force: false }) {
|
|
95
95
|
const { opts, serverUrl } = await k8sCfg(method);
|
|
96
96
|
const url = pathBuilder(serverUrl, model, filters, method === "POST");
|
|
97
97
|
switch (opts.method) {
|
|
@@ -103,7 +103,7 @@ async function k8sExec(model, filters, method, payload) {
|
|
|
103
103
|
opts.method = "PATCH";
|
|
104
104
|
url.searchParams.set("fieldManager", "pepr");
|
|
105
105
|
url.searchParams.set("fieldValidation", "Strict");
|
|
106
|
-
url.searchParams.set("force", "false");
|
|
106
|
+
url.searchParams.set("force", applyCfg.force ? "true" : "false");
|
|
107
107
|
break;
|
|
108
108
|
}
|
|
109
109
|
if (payload) {
|
|
@@ -6,6 +6,7 @@ const globals_1 = require("@jest/globals");
|
|
|
6
6
|
const fetch_1 = require("../fetch");
|
|
7
7
|
const upstream_1 = require("../upstream");
|
|
8
8
|
const utils_1 = require("./utils");
|
|
9
|
+
const kinds_1 = require("../kinds");
|
|
9
10
|
globals_1.jest.mock("https");
|
|
10
11
|
globals_1.jest.mock("../fetch");
|
|
11
12
|
(0, globals_1.describe)("pathBuilder Function", () => {
|
|
@@ -15,6 +16,40 @@ globals_1.jest.mock("../fetch");
|
|
|
15
16
|
const filters = {};
|
|
16
17
|
(0, globals_1.expect)(() => (0, utils_1.pathBuilder)("", model, filters)).toThrow("Kind not specified for Unknown");
|
|
17
18
|
});
|
|
19
|
+
(0, globals_1.it)("should generate a path for core group kinds (with custom filters)", () => {
|
|
20
|
+
const filters = {
|
|
21
|
+
namespace: "default",
|
|
22
|
+
name: "mypod",
|
|
23
|
+
fields: { iamafield: "iamavalue" },
|
|
24
|
+
labels: { iamalabel: "iamalabelvalue" },
|
|
25
|
+
};
|
|
26
|
+
const result = (0, utils_1.pathBuilder)(serverUrl, upstream_1.Pod, filters);
|
|
27
|
+
const expected = new URL("/api/v1/namespaces/default/pods/mypod?fieldSelector=iamafield%3Diamavalue&labelSelector=iamalabel%3Diamalabelvalue", serverUrl);
|
|
28
|
+
(0, globals_1.expect)(result.toString()).toEqual(expected.toString());
|
|
29
|
+
});
|
|
30
|
+
(0, globals_1.it)("Version not specified in a Kind", () => {
|
|
31
|
+
const filters = {
|
|
32
|
+
namespace: "default",
|
|
33
|
+
name: "mypod",
|
|
34
|
+
};
|
|
35
|
+
class Fake {
|
|
36
|
+
name;
|
|
37
|
+
constructor() {
|
|
38
|
+
this.name = "Fake";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
(0, kinds_1.RegisterKind)(Fake, {
|
|
42
|
+
kind: "Fake",
|
|
43
|
+
version: "",
|
|
44
|
+
group: "fake",
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
(0, utils_1.pathBuilder)(serverUrl, Fake, filters);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
(0, globals_1.expect)(e.message).toEqual(`Version not specified for Fake`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
18
53
|
(0, globals_1.it)("should generate a path for core group kinds", () => {
|
|
19
54
|
const filters = { namespace: "default", name: "mypod" };
|
|
20
55
|
const result = (0, utils_1.pathBuilder)(serverUrl, upstream_1.Pod, filters);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kubernetes-fluent-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -39,15 +39,15 @@
|
|
|
39
39
|
"fast-json-patch": "3.1.1",
|
|
40
40
|
"http-status-codes": "2.3.0",
|
|
41
41
|
"node-fetch": "2.7.0",
|
|
42
|
-
"type-fest": "4.3.
|
|
42
|
+
"type-fest": "4.3.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@commitlint/cli": "17.7.2",
|
|
46
46
|
"@commitlint/config-conventional": "17.7.0",
|
|
47
47
|
"@jest/globals": "29.7.0",
|
|
48
48
|
"@types/byline": "4.2.34",
|
|
49
|
-
"@typescript-eslint/eslint-plugin": "6.7.
|
|
50
|
-
"@typescript-eslint/parser": "6.7.
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "6.7.4",
|
|
50
|
+
"@typescript-eslint/parser": "6.7.4",
|
|
51
51
|
"jest": "29.7.0",
|
|
52
52
|
"nock": "13.3.3",
|
|
53
53
|
"prettier": "3.0.3",
|
package/src/fluent/index.test.ts
CHANGED
|
@@ -8,8 +8,50 @@ import { k8sExec } from "./utils";
|
|
|
8
8
|
// Setup mocks
|
|
9
9
|
jest.mock("./utils");
|
|
10
10
|
|
|
11
|
+
const generateFakePodManagedFields = (manager: string) => {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
apiVersion: "v1",
|
|
15
|
+
fieldsType: "FieldsV1",
|
|
16
|
+
fieldsV1: {
|
|
17
|
+
"f:metadata": {
|
|
18
|
+
"f:labels": {
|
|
19
|
+
"f:fake": {},
|
|
20
|
+
},
|
|
21
|
+
"f:spec": {
|
|
22
|
+
"f:containers": {
|
|
23
|
+
'k:{"name":"fake"}': {
|
|
24
|
+
"f:image": {},
|
|
25
|
+
"f:name": {},
|
|
26
|
+
"f:resources": {
|
|
27
|
+
"f:limits": {
|
|
28
|
+
"f:cpu": {},
|
|
29
|
+
"f:memory": {},
|
|
30
|
+
},
|
|
31
|
+
"f:requests": {
|
|
32
|
+
"f:cpu": {},
|
|
33
|
+
"f:memory": {},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
manager: manager,
|
|
42
|
+
operation: "Apply",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
};
|
|
11
46
|
describe("Kube", () => {
|
|
12
|
-
const fakeResource = {
|
|
47
|
+
const fakeResource = {
|
|
48
|
+
metadata: {
|
|
49
|
+
name: "fake",
|
|
50
|
+
namespace: "default",
|
|
51
|
+
managedFields: generateFakePodManagedFields("pepr"),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
13
55
|
const mockedKubeExec = jest.mocked(k8sExec).mockResolvedValue(fakeResource);
|
|
14
56
|
|
|
15
57
|
beforeEach(() => {
|
|
@@ -140,6 +182,18 @@ describe("Kube", () => {
|
|
|
140
182
|
expect(result).toEqual(fakeResource);
|
|
141
183
|
});
|
|
142
184
|
|
|
185
|
+
it("should allow force apply to resolve FieldManagerConflict", async () => {
|
|
186
|
+
const kube = K8s(Pod);
|
|
187
|
+
const result = await kube.Apply(
|
|
188
|
+
{
|
|
189
|
+
metadata: { name: "fake", managedFields: generateFakePodManagedFields("kubectl") },
|
|
190
|
+
spec: { priority: 3 },
|
|
191
|
+
},
|
|
192
|
+
{ force: true },
|
|
193
|
+
);
|
|
194
|
+
expect(result).toEqual(fakeResource);
|
|
195
|
+
});
|
|
196
|
+
|
|
143
197
|
it("should throw an error if a Delete failed for a reason other than Not Found", async () => {
|
|
144
198
|
mockedKubeExec.mockRejectedValueOnce({ status: 500 }); // Internal Server Error on first call
|
|
145
199
|
const kube = K8s(Pod);
|
package/src/fluent/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { GenericClass } from "../types";
|
|
|
11
11
|
import { Filters, K8sInit, Paths, WatchAction } from "./types";
|
|
12
12
|
import { k8sExec } from "./utils";
|
|
13
13
|
import { ExecWatch, WatchCfg } from "./watch";
|
|
14
|
+
import { ApplyCfg } from "./apply";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
|
|
@@ -34,13 +35,13 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
|
|
|
34
35
|
return withFilters;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function WithField<P extends Paths<K>>(key: P, value
|
|
38
|
+
function WithField<P extends Paths<K>>(key: P, value: string) {
|
|
38
39
|
filters.fields = filters.fields || {};
|
|
39
40
|
filters.fields[key] = value;
|
|
40
41
|
return withFilters;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function WithLabel(key: string, value
|
|
44
|
+
function WithLabel(key: string, value: string) {
|
|
44
45
|
filters.labels = filters.labels || {};
|
|
45
46
|
filters.labels[key] = value;
|
|
46
47
|
return withFilters;
|
|
@@ -100,9 +101,12 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
async function Apply(
|
|
104
|
+
async function Apply(
|
|
105
|
+
resource: PartialDeep<K>,
|
|
106
|
+
applyCfg: ApplyCfg = { force: false },
|
|
107
|
+
): Promise<K> {
|
|
104
108
|
syncFilters(resource as K);
|
|
105
|
-
return k8sExec(model, filters, "APPLY", resource);
|
|
109
|
+
return k8sExec(model, filters, "APPLY", resource, applyCfg);
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
async function Create(resource: K): Promise<K> {
|
package/src/fluent/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { PartialDeep } from "type-fest";
|
|
|
7
7
|
|
|
8
8
|
import { GenericClass, GroupVersionKind } from "../types";
|
|
9
9
|
import { WatchCfg, WatchController } from "./watch";
|
|
10
|
+
import { ApplyCfg } from "./apply";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The Phase matched when using the K8s Watch API.
|
|
@@ -65,7 +66,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
|
65
66
|
* @param resource
|
|
66
67
|
* @returns
|
|
67
68
|
*/
|
|
68
|
-
Apply: (resource: PartialDeep<K
|
|
69
|
+
Apply: (resource: PartialDeep<K>, applyCfg?: ApplyCfg) => Promise<K>;
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Create the provided K8s resource or throw an error if it already exists.
|
|
@@ -99,12 +100,14 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
99
100
|
* ```
|
|
100
101
|
*
|
|
101
102
|
* Will only delete the Deployment if it has the `metadata.name=bar` and `metadata.namespace=qux` fields.
|
|
103
|
+
* Not all fields are supported, see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/#supported-fields,
|
|
104
|
+
* but Typescript will limit to only fields that exist on the resource.
|
|
102
105
|
*
|
|
103
106
|
* @param key The field key
|
|
104
107
|
* @param value The field value
|
|
105
108
|
* @returns
|
|
106
109
|
*/
|
|
107
|
-
WithField: <P extends Paths<K>>(key: P, value
|
|
110
|
+
WithField: <P extends Paths<K>>(key: P, value: string) => K8sWithFilters<K>;
|
|
108
111
|
|
|
109
112
|
/**
|
|
110
113
|
* Filter the query by the given label. If no value is specified, the label simply must exist.
|
|
@@ -122,7 +125,7 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
122
125
|
* @param key The label key
|
|
123
126
|
* @param value (optional) The label value
|
|
124
127
|
*/
|
|
125
|
-
WithLabel: (key: string, value
|
|
128
|
+
WithLabel: (key: string, value: string) => K8sWithFilters<K>;
|
|
126
129
|
};
|
|
127
130
|
|
|
128
131
|
export type K8sInit<K extends KubernetesObject> = K8sWithFilters<K> &
|
package/src/fluent/utils.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { GenericClass } from "../types";
|
|
|
8
8
|
import { ClusterRole, Ingress, Pod } from "../upstream";
|
|
9
9
|
import { Filters } from "./types";
|
|
10
10
|
import { k8sExec, pathBuilder } from "./utils";
|
|
11
|
+
import { RegisterKind } from "../kinds";
|
|
11
12
|
|
|
12
13
|
jest.mock("https");
|
|
13
14
|
jest.mock("../fetch");
|
|
@@ -20,6 +21,45 @@ describe("pathBuilder Function", () => {
|
|
|
20
21
|
expect(() => pathBuilder("", model, filters)).toThrow("Kind not specified for Unknown");
|
|
21
22
|
});
|
|
22
23
|
|
|
24
|
+
it("should generate a path for core group kinds (with custom filters)", () => {
|
|
25
|
+
const filters: Filters = {
|
|
26
|
+
namespace: "default",
|
|
27
|
+
name: "mypod",
|
|
28
|
+
fields: { iamafield: "iamavalue" },
|
|
29
|
+
labels: { iamalabel: "iamalabelvalue" },
|
|
30
|
+
};
|
|
31
|
+
const result = pathBuilder(serverUrl, Pod, filters);
|
|
32
|
+
const expected = new URL(
|
|
33
|
+
"/api/v1/namespaces/default/pods/mypod?fieldSelector=iamafield%3Diamavalue&labelSelector=iamalabel%3Diamalabelvalue",
|
|
34
|
+
serverUrl,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(result.toString()).toEqual(expected.toString());
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("Version not specified in a Kind", () => {
|
|
41
|
+
const filters: Filters = {
|
|
42
|
+
namespace: "default",
|
|
43
|
+
name: "mypod",
|
|
44
|
+
};
|
|
45
|
+
class Fake {
|
|
46
|
+
name: string;
|
|
47
|
+
constructor() {
|
|
48
|
+
this.name = "Fake";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
RegisterKind(Fake, {
|
|
52
|
+
kind: "Fake",
|
|
53
|
+
version: "",
|
|
54
|
+
group: "fake",
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
pathBuilder(serverUrl, Fake, filters);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
expect(e.message).toEqual(`Version not specified for Fake`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
23
63
|
it("should generate a path for core group kinds", () => {
|
|
24
64
|
const filters: Filters = { namespace: "default", name: "mypod" };
|
|
25
65
|
const result = pathBuilder(serverUrl, Pod, filters);
|
package/src/fluent/utils.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { fetch } from "../fetch";
|
|
|
9
9
|
import { modelToGroupVersionKind } from "../kinds";
|
|
10
10
|
import { GenericClass } from "../types";
|
|
11
11
|
import { FetchMethods, Filters } from "./types";
|
|
12
|
+
import { ApplyCfg } from "./apply";
|
|
12
13
|
|
|
13
14
|
const SSA_CONTENT_TYPE = "application/apply-patch+yaml";
|
|
14
15
|
|
|
@@ -123,6 +124,7 @@ export async function k8sExec<T extends GenericClass, K>(
|
|
|
123
124
|
filters: Filters,
|
|
124
125
|
method: FetchMethods,
|
|
125
126
|
payload?: K | unknown,
|
|
127
|
+
applyCfg: ApplyCfg = { force: false },
|
|
126
128
|
) {
|
|
127
129
|
const { opts, serverUrl } = await k8sCfg(method);
|
|
128
130
|
const url = pathBuilder(serverUrl, model, filters, method === "POST");
|
|
@@ -137,7 +139,7 @@ export async function k8sExec<T extends GenericClass, K>(
|
|
|
137
139
|
opts.method = "PATCH";
|
|
138
140
|
url.searchParams.set("fieldManager", "pepr");
|
|
139
141
|
url.searchParams.set("fieldValidation", "Strict");
|
|
140
|
-
url.searchParams.set("force", "false");
|
|
142
|
+
url.searchParams.set("force", applyCfg.force ? "true" : "false");
|
|
141
143
|
break;
|
|
142
144
|
}
|
|
143
145
|
|