kubernetes-fluent-client 3.0.5 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +68 -0
- package/dist/fetch.d.ts +22 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +82 -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/fileSystem.d.ts +11 -0
- package/dist/fileSystem.d.ts.map +1 -0
- package/dist/fileSystem.js +42 -0
- package/dist/fileSystem.test.d.ts +2 -0
- package/dist/fileSystem.test.d.ts.map +1 -0
- package/dist/fileSystem.test.js +75 -0
- package/dist/fluent/http2-watch.spec.d.ts +2 -0
- package/dist/fluent/http2-watch.spec.d.ts.map +1 -0
- package/dist/fluent/http2-watch.spec.js +284 -0
- package/dist/fluent/index.d.ts +12 -0
- package/dist/fluent/index.d.ts.map +1 -0
- package/dist/fluent/index.js +228 -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 +193 -0
- package/dist/fluent/types.d.ts +187 -0
- package/dist/fluent/types.d.ts.map +1 -0
- package/dist/fluent/types.js +16 -0
- package/dist/fluent/utils.d.ts +41 -0
- package/dist/fluent/utils.d.ts.map +1 -0
- package/dist/fluent/utils.js +153 -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 +215 -0
- package/dist/fluent/watch.d.ts +88 -0
- package/dist/fluent/watch.d.ts.map +1 -0
- package/dist/fluent/watch.js +546 -0
- package/dist/fluent/watch.spec.d.ts +2 -0
- package/dist/fluent/watch.spec.d.ts.map +1 -0
- package/dist/fluent/watch.spec.js +261 -0
- package/dist/generate.d.ts +84 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +208 -0
- package/dist/generate.test.d.ts +2 -0
- package/dist/generate.test.d.ts.map +1 -0
- package/dist/generate.test.js +320 -0
- package/dist/helpers.d.ts +33 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +103 -0
- package/dist/helpers.test.d.ts +2 -0
- package/dist/helpers.test.d.ts.map +1 -0
- package/dist/helpers.test.js +37 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/kinds.d.ts +16 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +570 -0
- package/dist/kinds.test.d.ts +2 -0
- package/dist/kinds.test.d.ts.map +1 -0
- package/dist/kinds.test.js +155 -0
- package/dist/patch.d.ts +7 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +2 -0
- package/dist/postProcessing.d.ts +246 -0
- package/dist/postProcessing.d.ts.map +1 -0
- package/dist/postProcessing.js +497 -0
- package/dist/postProcessing.test.d.ts +2 -0
- package/dist/postProcessing.test.d.ts.map +1 -0
- package/dist/postProcessing.test.js +550 -0
- package/dist/types.d.ts +32 -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 +56 -0
- package/package.json +1 -1
- package/src/fluent/http2-watch.spec.ts +335 -0
- package/src/fluent/watch.ts +174 -35
package/dist/upstream.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
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.Endpoint = 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 = exports.CoreEvent = 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, "CoreEvent", { enumerable: true, get: function () { return client_node_1.CoreV1Event; } });
|
|
9
|
+
Object.defineProperty(exports, "Event", { enumerable: true, get: function () { return client_node_1.EventsV1Event; } });
|
|
10
|
+
Object.defineProperty(exports, "APIService", { enumerable: true, get: function () { return client_node_1.V1APIService; } });
|
|
11
|
+
Object.defineProperty(exports, "CertificateSigningRequest", { enumerable: true, get: function () { return client_node_1.V1CertificateSigningRequest; } });
|
|
12
|
+
Object.defineProperty(exports, "ClusterRole", { enumerable: true, get: function () { return client_node_1.V1ClusterRole; } });
|
|
13
|
+
Object.defineProperty(exports, "ClusterRoleBinding", { enumerable: true, get: function () { return client_node_1.V1ClusterRoleBinding; } });
|
|
14
|
+
Object.defineProperty(exports, "ConfigMap", { enumerable: true, get: function () { return client_node_1.V1ConfigMap; } });
|
|
15
|
+
Object.defineProperty(exports, "ControllerRevision", { enumerable: true, get: function () { return client_node_1.V1ControllerRevision; } });
|
|
16
|
+
Object.defineProperty(exports, "CronJob", { enumerable: true, get: function () { return client_node_1.V1CronJob; } });
|
|
17
|
+
Object.defineProperty(exports, "CSIDriver", { enumerable: true, get: function () { return client_node_1.V1CSIDriver; } });
|
|
18
|
+
Object.defineProperty(exports, "CustomResourceDefinition", { enumerable: true, get: function () { return client_node_1.V1CustomResourceDefinition; } });
|
|
19
|
+
Object.defineProperty(exports, "DaemonSet", { enumerable: true, get: function () { return client_node_1.V1DaemonSet; } });
|
|
20
|
+
Object.defineProperty(exports, "Deployment", { enumerable: true, get: function () { return client_node_1.V1Deployment; } });
|
|
21
|
+
Object.defineProperty(exports, "EndpointSlice", { enumerable: true, get: function () { return client_node_1.V1EndpointSlice; } });
|
|
22
|
+
Object.defineProperty(exports, "HorizontalPodAutoscaler", { enumerable: true, get: function () { return client_node_1.V1HorizontalPodAutoscaler; } });
|
|
23
|
+
Object.defineProperty(exports, "Ingress", { enumerable: true, get: function () { return client_node_1.V1Ingress; } });
|
|
24
|
+
Object.defineProperty(exports, "IngressClass", { enumerable: true, get: function () { return client_node_1.V1IngressClass; } });
|
|
25
|
+
Object.defineProperty(exports, "Job", { enumerable: true, get: function () { return client_node_1.V1Job; } });
|
|
26
|
+
Object.defineProperty(exports, "LimitRange", { enumerable: true, get: function () { return client_node_1.V1LimitRange; } });
|
|
27
|
+
Object.defineProperty(exports, "LocalSubjectAccessReview", { enumerable: true, get: function () { return client_node_1.V1LocalSubjectAccessReview; } });
|
|
28
|
+
Object.defineProperty(exports, "MutatingWebhookConfiguration", { enumerable: true, get: function () { return client_node_1.V1MutatingWebhookConfiguration; } });
|
|
29
|
+
Object.defineProperty(exports, "Namespace", { enumerable: true, get: function () { return client_node_1.V1Namespace; } });
|
|
30
|
+
Object.defineProperty(exports, "NetworkPolicy", { enumerable: true, get: function () { return client_node_1.V1NetworkPolicy; } });
|
|
31
|
+
Object.defineProperty(exports, "Node", { enumerable: true, get: function () { return client_node_1.V1Node; } });
|
|
32
|
+
Object.defineProperty(exports, "PersistentVolume", { enumerable: true, get: function () { return client_node_1.V1PersistentVolume; } });
|
|
33
|
+
Object.defineProperty(exports, "PersistentVolumeClaim", { enumerable: true, get: function () { return client_node_1.V1PersistentVolumeClaim; } });
|
|
34
|
+
Object.defineProperty(exports, "Pod", { enumerable: true, get: function () { return client_node_1.V1Pod; } });
|
|
35
|
+
Object.defineProperty(exports, "PodDisruptionBudget", { enumerable: true, get: function () { return client_node_1.V1PodDisruptionBudget; } });
|
|
36
|
+
Object.defineProperty(exports, "PodTemplate", { enumerable: true, get: function () { return client_node_1.V1PodTemplate; } });
|
|
37
|
+
Object.defineProperty(exports, "ReplicaSet", { enumerable: true, get: function () { return client_node_1.V1ReplicaSet; } });
|
|
38
|
+
Object.defineProperty(exports, "ReplicationController", { enumerable: true, get: function () { return client_node_1.V1ReplicationController; } });
|
|
39
|
+
Object.defineProperty(exports, "ResourceQuota", { enumerable: true, get: function () { return client_node_1.V1ResourceQuota; } });
|
|
40
|
+
Object.defineProperty(exports, "Role", { enumerable: true, get: function () { return client_node_1.V1Role; } });
|
|
41
|
+
Object.defineProperty(exports, "RoleBinding", { enumerable: true, get: function () { return client_node_1.V1RoleBinding; } });
|
|
42
|
+
Object.defineProperty(exports, "RuntimeClass", { enumerable: true, get: function () { return client_node_1.V1RuntimeClass; } });
|
|
43
|
+
Object.defineProperty(exports, "Secret", { enumerable: true, get: function () { return client_node_1.V1Secret; } });
|
|
44
|
+
Object.defineProperty(exports, "SelfSubjectAccessReview", { enumerable: true, get: function () { return client_node_1.V1SelfSubjectAccessReview; } });
|
|
45
|
+
Object.defineProperty(exports, "SelfSubjectRulesReview", { enumerable: true, get: function () { return client_node_1.V1SelfSubjectRulesReview; } });
|
|
46
|
+
Object.defineProperty(exports, "Service", { enumerable: true, get: function () { return client_node_1.V1Service; } });
|
|
47
|
+
Object.defineProperty(exports, "ServiceAccount", { enumerable: true, get: function () { return client_node_1.V1ServiceAccount; } });
|
|
48
|
+
Object.defineProperty(exports, "StatefulSet", { enumerable: true, get: function () { return client_node_1.V1StatefulSet; } });
|
|
49
|
+
Object.defineProperty(exports, "StorageClass", { enumerable: true, get: function () { return client_node_1.V1StorageClass; } });
|
|
50
|
+
Object.defineProperty(exports, "SubjectAccessReview", { enumerable: true, get: function () { return client_node_1.V1SubjectAccessReview; } });
|
|
51
|
+
Object.defineProperty(exports, "TokenReview", { enumerable: true, get: function () { return client_node_1.V1TokenReview; } });
|
|
52
|
+
Object.defineProperty(exports, "ValidatingWebhookConfiguration", { enumerable: true, get: function () { return client_node_1.V1ValidatingWebhookConfiguration; } });
|
|
53
|
+
Object.defineProperty(exports, "VolumeAttachment", { enumerable: true, get: function () { return client_node_1.V1VolumeAttachment; } });
|
|
54
|
+
Object.defineProperty(exports, "Endpoint", { enumerable: true, get: function () { return client_node_1.V1Endpoint; } });
|
|
55
|
+
var types_1 = require("./types");
|
|
56
|
+
Object.defineProperty(exports, "GenericKind", { enumerable: true, get: function () { return types_1.GenericKind; } });
|
package/package.json
CHANGED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
3
|
+
import http2 from "http2";
|
|
4
|
+
import { Watcher } from "./watch";
|
|
5
|
+
import { WatchEvent, kind } from "..";
|
|
6
|
+
import { WatchPhase } from "./types";
|
|
7
|
+
import { K8s } from ".";
|
|
8
|
+
|
|
9
|
+
jest.mock("http2");
|
|
10
|
+
|
|
11
|
+
describe("Watcher HTTP2", () => {
|
|
12
|
+
let watcher: Watcher<typeof kind.Pod>;
|
|
13
|
+
let mockClient: jest.Mocked<http2.ClientHttp2Session>;
|
|
14
|
+
let mockReq: jest.Mocked<http2.ClientHttp2Stream>;
|
|
15
|
+
const evtMock = jest.fn<(update: kind.Pod, phase: WatchPhase) => void>();
|
|
16
|
+
const errMock = jest.fn<(err: Error) => void>();
|
|
17
|
+
|
|
18
|
+
const setupAndStartWatcher = (eventType: WatchEvent, handler: (...args: any[]) => void) => {
|
|
19
|
+
watcher.events.on(eventType, handler);
|
|
20
|
+
watcher.start().catch(errMock);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
|
|
26
|
+
// http2.client
|
|
27
|
+
mockClient = {
|
|
28
|
+
request: jest.fn(),
|
|
29
|
+
close: jest.fn(),
|
|
30
|
+
on: jest.fn(),
|
|
31
|
+
destroy: jest.fn(),
|
|
32
|
+
} as unknown as jest.Mocked<http2.ClientHttp2Session>;
|
|
33
|
+
|
|
34
|
+
// http2.request stream
|
|
35
|
+
mockReq = {
|
|
36
|
+
on: jest.fn(),
|
|
37
|
+
end: jest.fn(),
|
|
38
|
+
setEncoding: jest.fn(),
|
|
39
|
+
} as unknown as jest.Mocked<http2.ClientHttp2Stream>;
|
|
40
|
+
|
|
41
|
+
// http2.connect function to return the mocked client session
|
|
42
|
+
(http2.connect as jest.Mock).mockReturnValue(mockClient);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
watcher.close();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should watch named resources", done => {
|
|
50
|
+
const pod = createMockPod("pod-1", "1");
|
|
51
|
+
const response = { type: "ADDED", object: pod };
|
|
52
|
+
|
|
53
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
54
|
+
useHTTP2: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
mockReq.on.mockImplementation((event, callback) => {
|
|
58
|
+
switch (event) {
|
|
59
|
+
case "response":
|
|
60
|
+
callback(
|
|
61
|
+
{ ":status": 200 } as http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader,
|
|
62
|
+
0,
|
|
63
|
+
);
|
|
64
|
+
break;
|
|
65
|
+
case "data":
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
(callback as (chunk: Buffer) => void)(Buffer.from(JSON.stringify(response)));
|
|
68
|
+
}, 10);
|
|
69
|
+
break;
|
|
70
|
+
case "end":
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
(callback as () => void)();
|
|
73
|
+
done();
|
|
74
|
+
}, 20);
|
|
75
|
+
break;
|
|
76
|
+
case "error":
|
|
77
|
+
errMock(new Error("HTTP2 connection error"));
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
return mockReq;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
mockClient.request.mockReturnValue(mockReq);
|
|
84
|
+
|
|
85
|
+
setupAndStartWatcher(WatchEvent.CONNECT, () => {
|
|
86
|
+
setupAndStartWatcher(WatchEvent.DATA, (receivedPod, phase) => {
|
|
87
|
+
expect(receivedPod.metadata?.name).toBe("pod-1");
|
|
88
|
+
expect(receivedPod.metadata?.resourceVersion).toBe("1");
|
|
89
|
+
expect(phase).toBe(WatchPhase.Added);
|
|
90
|
+
done();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should handle resource version is too old", done => {
|
|
96
|
+
const errorResponse = {
|
|
97
|
+
type: "ERROR",
|
|
98
|
+
object: {
|
|
99
|
+
kind: "Status",
|
|
100
|
+
apiVersion: "v1",
|
|
101
|
+
metadata: {},
|
|
102
|
+
status: "Failure",
|
|
103
|
+
message: "too old resource version: 123 (391079)",
|
|
104
|
+
reason: "Gone",
|
|
105
|
+
code: 410,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
110
|
+
useHTTP2: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
mockReq.on.mockImplementation((event, callback) => {
|
|
114
|
+
switch (event) {
|
|
115
|
+
case "response":
|
|
116
|
+
callback(
|
|
117
|
+
{ ":status": 200 } as http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader,
|
|
118
|
+
0,
|
|
119
|
+
);
|
|
120
|
+
break;
|
|
121
|
+
case "data":
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
(callback as (chunk: Buffer) => void)(Buffer.from(JSON.stringify(errorResponse)));
|
|
124
|
+
}, 10);
|
|
125
|
+
break;
|
|
126
|
+
case "end":
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
(callback as () => void)();
|
|
129
|
+
done();
|
|
130
|
+
}, 20);
|
|
131
|
+
break;
|
|
132
|
+
case "error":
|
|
133
|
+
errMock(new Error("HTTP2 connection error"));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
return mockReq;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
mockClient.request.mockReturnValue(mockReq);
|
|
140
|
+
|
|
141
|
+
setupAndStartWatcher(WatchEvent.OLD_RESOURCE_VERSION, res => {
|
|
142
|
+
expect(res).toEqual("123");
|
|
143
|
+
done();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should call the event handler for each event", done => {
|
|
148
|
+
const pod = createMockPod("pod-0", "1");
|
|
149
|
+
const response = { type: "ADDED", object: pod };
|
|
150
|
+
|
|
151
|
+
watcher = K8s(kind.Pod).Watch(
|
|
152
|
+
(evt, phase) => {
|
|
153
|
+
expect(evt.metadata?.name).toEqual("pod-0");
|
|
154
|
+
expect(phase).toEqual(WatchPhase.Added);
|
|
155
|
+
},
|
|
156
|
+
{ useHTTP2: true },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
mockReq.on.mockImplementation((event, callback) => {
|
|
160
|
+
switch (event) {
|
|
161
|
+
case "response":
|
|
162
|
+
callback(
|
|
163
|
+
{ ":status": 200 } as http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader,
|
|
164
|
+
0,
|
|
165
|
+
);
|
|
166
|
+
break;
|
|
167
|
+
case "data":
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
(callback as (chunk: Buffer) => void)(Buffer.from(JSON.stringify(response)));
|
|
170
|
+
}, 10);
|
|
171
|
+
break;
|
|
172
|
+
case "end":
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
(callback as () => void)();
|
|
175
|
+
}, 20);
|
|
176
|
+
break;
|
|
177
|
+
case "error":
|
|
178
|
+
errMock(new Error("HTTP2 connection error"));
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
return mockReq;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
mockClient.request.mockReturnValue(mockReq);
|
|
185
|
+
|
|
186
|
+
watcher.start().catch(errMock);
|
|
187
|
+
done();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should return the cache id", () => {
|
|
191
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
192
|
+
resyncDelaySec: 1,
|
|
193
|
+
useHTTP2: true,
|
|
194
|
+
});
|
|
195
|
+
expect(watcher.getCacheID()).toEqual("d69b75a611");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should handle the CONNECT event", done => {
|
|
199
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
200
|
+
resyncDelaySec: 1,
|
|
201
|
+
useHTTP2: true,
|
|
202
|
+
});
|
|
203
|
+
setupAndStartWatcher(WatchEvent.CONNECT, () => {});
|
|
204
|
+
done();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should handle the DATA event", done => {
|
|
208
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
209
|
+
resyncDelaySec: 1,
|
|
210
|
+
useHTTP2: true,
|
|
211
|
+
});
|
|
212
|
+
setupAndStartWatcher(WatchEvent.DATA, (pod, phase) => {
|
|
213
|
+
expect(pod.metadata?.name).toEqual("pod-0");
|
|
214
|
+
expect(phase).toEqual(WatchPhase.Added);
|
|
215
|
+
});
|
|
216
|
+
done();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should handle the NETWORK_ERROR event", done => {
|
|
220
|
+
const errorMessage = "Something bad happened";
|
|
221
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
222
|
+
useHTTP2: true,
|
|
223
|
+
resyncDelaySec: 1,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
mockReq.on.mockImplementation((event, callback) => {
|
|
227
|
+
if (event === "response") {
|
|
228
|
+
callback(
|
|
229
|
+
{ ":status": 200 } as http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader,
|
|
230
|
+
0,
|
|
231
|
+
);
|
|
232
|
+
} else if (event === "error") {
|
|
233
|
+
(callback as (err: Error) => void)(new Error(errorMessage));
|
|
234
|
+
}
|
|
235
|
+
return mockReq;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
mockClient.request.mockReturnValue(mockReq);
|
|
239
|
+
|
|
240
|
+
setupAndStartWatcher(WatchEvent.NETWORK_ERROR, error => {
|
|
241
|
+
expect(error.message).toEqual(errorMessage);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
watcher.start().catch(errMock);
|
|
245
|
+
done();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should handle the RECONNECT event on an error", done => {
|
|
249
|
+
const errorMessage = "Something bad happened";
|
|
250
|
+
|
|
251
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
252
|
+
useHTTP2: true,
|
|
253
|
+
resyncDelaySec: 0.01,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
let reconnectCount = 0;
|
|
257
|
+
|
|
258
|
+
mockReq.on.mockImplementation((event, callback) => {
|
|
259
|
+
if (event === "response") {
|
|
260
|
+
callback(
|
|
261
|
+
{ ":status": 200 } as http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader,
|
|
262
|
+
0,
|
|
263
|
+
);
|
|
264
|
+
} else if (event === "error") {
|
|
265
|
+
reconnectCount += 1;
|
|
266
|
+
(callback as (err: Error) => void)(new Error(errorMessage));
|
|
267
|
+
}
|
|
268
|
+
return mockReq;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
mockClient.request.mockReturnValue(mockReq);
|
|
272
|
+
|
|
273
|
+
setupAndStartWatcher(WatchEvent.RECONNECT, count => {
|
|
274
|
+
expect(count).toEqual(reconnectCount);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
watcher.start().catch(errMock);
|
|
278
|
+
done();
|
|
279
|
+
});
|
|
280
|
+
it("should perform a resync after the resync interval", done => {
|
|
281
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
282
|
+
useHTTP2: true,
|
|
283
|
+
resyncDelaySec: 1,
|
|
284
|
+
});
|
|
285
|
+
watcher.start().catch(errMock);
|
|
286
|
+
done();
|
|
287
|
+
});
|
|
288
|
+
it("should handle the GIVE_UP event", done => {
|
|
289
|
+
watcher = K8s(kind.Pod).Watch(evtMock, {
|
|
290
|
+
useHTTP2: true,
|
|
291
|
+
lastSeenLimitSeconds: 0.01,
|
|
292
|
+
resyncDelaySec: 0.01,
|
|
293
|
+
resyncFailureMax: 1,
|
|
294
|
+
});
|
|
295
|
+
setupAndStartWatcher(WatchEvent.GIVE_UP, () => {
|
|
296
|
+
expect(errMock).toBeCalled();
|
|
297
|
+
});
|
|
298
|
+
watcher.start().catch(errMock);
|
|
299
|
+
done();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Creates a mock pod object
|
|
305
|
+
*
|
|
306
|
+
* @param name The name of the pod
|
|
307
|
+
* @param resourceVersion The resource version of the pod
|
|
308
|
+
* @returns A mock pod object
|
|
309
|
+
*/
|
|
310
|
+
function createMockPod(name: string, resourceVersion: string): kind.Pod {
|
|
311
|
+
return {
|
|
312
|
+
kind: "Pod",
|
|
313
|
+
apiVersion: "v1",
|
|
314
|
+
metadata: {
|
|
315
|
+
name,
|
|
316
|
+
resourceVersion,
|
|
317
|
+
uid: "abc-123-xyz",
|
|
318
|
+
},
|
|
319
|
+
spec: {
|
|
320
|
+
containers: [
|
|
321
|
+
{
|
|
322
|
+
name: "nginx",
|
|
323
|
+
image: "nginx:1.14.2",
|
|
324
|
+
ports: [
|
|
325
|
+
{
|
|
326
|
+
containerPort: 80,
|
|
327
|
+
protocol: "TCP",
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
status: {},
|
|
334
|
+
};
|
|
335
|
+
}
|
package/src/fluent/watch.ts
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
import byline from "byline";
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
|
+
import https from "https";
|
|
8
|
+
import http2 from "http2";
|
|
7
9
|
import fetch from "node-fetch";
|
|
8
|
-
|
|
9
10
|
import { fetch as wrappedFetch } from "../fetch";
|
|
10
11
|
import { GenericClass, KubernetesListObject } from "../types";
|
|
11
12
|
import { Filters, WatchAction, WatchPhase } from "./types";
|
|
12
13
|
import { k8sCfg, pathBuilder } from "./utils";
|
|
14
|
+
import fs from "fs";
|
|
13
15
|
|
|
14
16
|
export enum WatchEvent {
|
|
15
17
|
/** Watch is connected successfully */
|
|
@@ -52,6 +54,8 @@ export type WatchCfg = {
|
|
|
52
54
|
relistIntervalSec?: number;
|
|
53
55
|
/** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
|
|
54
56
|
lastSeenLimitSeconds?: number;
|
|
57
|
+
/** Use http2 for the Watch */
|
|
58
|
+
useHTTP2?: boolean;
|
|
55
59
|
};
|
|
56
60
|
|
|
57
61
|
const NONE = 50;
|
|
@@ -65,6 +69,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
65
69
|
#callback: WatchAction<T>;
|
|
66
70
|
#watchCfg: WatchCfg;
|
|
67
71
|
#latestRelistWindow: string = "";
|
|
72
|
+
#useHTTP2: boolean = false;
|
|
68
73
|
|
|
69
74
|
// Track the last time data was received
|
|
70
75
|
#lastSeenTime = NONE;
|
|
@@ -97,6 +102,8 @@ export class Watcher<T extends GenericClass> {
|
|
|
97
102
|
// Track the list of items in the cache
|
|
98
103
|
#cache = new Map<string, InstanceType<T>>();
|
|
99
104
|
|
|
105
|
+
// Token Path
|
|
106
|
+
#TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
|
100
107
|
/**
|
|
101
108
|
* Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
|
|
102
109
|
* The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
|
|
@@ -125,6 +132,9 @@ export class Watcher<T extends GenericClass> {
|
|
|
125
132
|
// Set the latest relist interval to now
|
|
126
133
|
this.#latestRelistWindow = new Date().toISOString();
|
|
127
134
|
|
|
135
|
+
// Set the latest relist interval to now
|
|
136
|
+
this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
|
|
137
|
+
|
|
128
138
|
// Add random jitter to the relist/resync intervals (up to 1 second)
|
|
129
139
|
const jitter = Math.floor(Math.random() * 1000);
|
|
130
140
|
|
|
@@ -158,7 +168,11 @@ export class Watcher<T extends GenericClass> {
|
|
|
158
168
|
*/
|
|
159
169
|
public async start(): Promise<AbortController> {
|
|
160
170
|
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
161
|
-
|
|
171
|
+
if (this.#useHTTP2) {
|
|
172
|
+
await this.#http2Watch();
|
|
173
|
+
} else {
|
|
174
|
+
await this.#watch();
|
|
175
|
+
}
|
|
162
176
|
return this.#abortController;
|
|
163
177
|
}
|
|
164
178
|
|
|
@@ -198,6 +212,19 @@ export class Watcher<T extends GenericClass> {
|
|
|
198
212
|
return this.#events;
|
|
199
213
|
}
|
|
200
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Read the serviceAccount Token
|
|
217
|
+
*
|
|
218
|
+
* @returns token or null
|
|
219
|
+
*/
|
|
220
|
+
async #getToken() {
|
|
221
|
+
try {
|
|
222
|
+
return (await fs.promises.readFile(this.#TOKEN_PATH, "utf8")).trim();
|
|
223
|
+
} catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
201
228
|
/**
|
|
202
229
|
* Build the URL and request options for the watch.
|
|
203
230
|
*
|
|
@@ -351,6 +378,42 @@ export class Watcher<T extends GenericClass> {
|
|
|
351
378
|
}
|
|
352
379
|
};
|
|
353
380
|
|
|
381
|
+
// process a line from the chunk
|
|
382
|
+
#processLine = async (
|
|
383
|
+
line: string,
|
|
384
|
+
process: (payload: InstanceType<T>, phase: WatchPhase) => Promise<void>,
|
|
385
|
+
) => {
|
|
386
|
+
try {
|
|
387
|
+
// Parse the event payload
|
|
388
|
+
const { object: payload, type: phase } = JSON.parse(line) as {
|
|
389
|
+
type: WatchPhase;
|
|
390
|
+
object: InstanceType<T>;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Update the last seen time
|
|
394
|
+
this.#lastSeenTime = Date.now();
|
|
395
|
+
|
|
396
|
+
// If the watch is too old, remove the resourceVersion and reload the watch
|
|
397
|
+
if (phase === WatchPhase.Error && payload.code === 410) {
|
|
398
|
+
throw {
|
|
399
|
+
name: "TooOld",
|
|
400
|
+
message: this.#resourceVersion!,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Process the event payload, do not update the resource version as that is handled by the list operation
|
|
405
|
+
await process(payload, phase);
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err.name === "TooOld") {
|
|
408
|
+
// Reload the watch
|
|
409
|
+
void this.#errHandler(err);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
354
417
|
/**
|
|
355
418
|
* Watch for changes to the resource.
|
|
356
419
|
*/
|
|
@@ -388,38 +451,7 @@ export class Watcher<T extends GenericClass> {
|
|
|
388
451
|
|
|
389
452
|
// Listen for events and call the callback function
|
|
390
453
|
this.#stream.on("data", async line => {
|
|
391
|
-
|
|
392
|
-
// Parse the event payload
|
|
393
|
-
const { object: payload, type: phase } = JSON.parse(line) as {
|
|
394
|
-
type: WatchPhase;
|
|
395
|
-
object: InstanceType<T>;
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
// Update the last seen time
|
|
399
|
-
this.#lastSeenTime = Date.now();
|
|
400
|
-
|
|
401
|
-
// If the watch is too old, remove the resourceVersion and reload the watch
|
|
402
|
-
if (phase === WatchPhase.Error && payload.code === 410) {
|
|
403
|
-
throw {
|
|
404
|
-
name: "TooOld",
|
|
405
|
-
message: this.#resourceVersion!,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Process the event payload, do not update the resource version as that is handled by the list operation
|
|
410
|
-
await this.#process(payload, phase);
|
|
411
|
-
} catch (err) {
|
|
412
|
-
if (err.name === "TooOld") {
|
|
413
|
-
// Prevent any body events from firing
|
|
414
|
-
body.removeAllListeners();
|
|
415
|
-
|
|
416
|
-
// Reload the watch
|
|
417
|
-
void this.#errHandler(err);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
422
|
-
}
|
|
454
|
+
await this.#processLine(line, this.#process);
|
|
423
455
|
});
|
|
424
456
|
|
|
425
457
|
// Bind the body events
|
|
@@ -437,6 +469,108 @@ export class Watcher<T extends GenericClass> {
|
|
|
437
469
|
}
|
|
438
470
|
};
|
|
439
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Watch for changes to the resource.
|
|
474
|
+
*/
|
|
475
|
+
#http2Watch = async () => {
|
|
476
|
+
try {
|
|
477
|
+
// Start with a list operation
|
|
478
|
+
await this.#list();
|
|
479
|
+
|
|
480
|
+
// Build the URL and request options
|
|
481
|
+
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
482
|
+
let agentOptions;
|
|
483
|
+
|
|
484
|
+
if (opts.agent && opts.agent instanceof https.Agent) {
|
|
485
|
+
agentOptions = {
|
|
486
|
+
key: opts.agent.options.key,
|
|
487
|
+
cert: opts.agent.options.cert,
|
|
488
|
+
ca: opts.agent.options.ca,
|
|
489
|
+
rejectUnauthorized: false,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// HTTP/2 client connection setup
|
|
494
|
+
const client = http2.connect(url.origin, {
|
|
495
|
+
ca: agentOptions?.ca,
|
|
496
|
+
cert: agentOptions?.cert,
|
|
497
|
+
key: agentOptions?.key,
|
|
498
|
+
rejectUnauthorized: agentOptions?.rejectUnauthorized,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Set up headers for the HTTP/2 request
|
|
502
|
+
const token = await this.#getToken();
|
|
503
|
+
const headers: Record<string, string> = {
|
|
504
|
+
":method": "GET",
|
|
505
|
+
":path": url.pathname + url.search,
|
|
506
|
+
"content-type": "application/json",
|
|
507
|
+
"user-agent": "kubernetes-fluent-client",
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
if (token) {
|
|
511
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Make the HTTP/2 request
|
|
515
|
+
const req = client.request(headers);
|
|
516
|
+
|
|
517
|
+
req.setEncoding("utf8");
|
|
518
|
+
|
|
519
|
+
let buffer = "";
|
|
520
|
+
|
|
521
|
+
// Handle response data
|
|
522
|
+
req.on("response", headers => {
|
|
523
|
+
const statusCode = headers[":status"];
|
|
524
|
+
|
|
525
|
+
if (statusCode && statusCode >= 200 && statusCode < 300) {
|
|
526
|
+
this.#pendingReconnect = false;
|
|
527
|
+
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
528
|
+
|
|
529
|
+
// Reset the retry count
|
|
530
|
+
this.#resyncFailureCount = 0;
|
|
531
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
532
|
+
|
|
533
|
+
req.on("data", async chunk => {
|
|
534
|
+
try {
|
|
535
|
+
buffer += chunk;
|
|
536
|
+
const lines = buffer.split("\n");
|
|
537
|
+
// Avoid Watch event data_error received. Unexpected end of JSON input.
|
|
538
|
+
buffer = lines.pop()!;
|
|
539
|
+
|
|
540
|
+
for (const line of lines) {
|
|
541
|
+
await this.#processLine(line, this.#process);
|
|
542
|
+
}
|
|
543
|
+
} catch (err) {
|
|
544
|
+
void this.#errHandler(err);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
req.on("end", () => {
|
|
549
|
+
client.close();
|
|
550
|
+
this.#streamCleanup();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
req.on("close", () => {
|
|
554
|
+
client.close();
|
|
555
|
+
this.#streamCleanup();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
req.on("error", err => {
|
|
559
|
+
void this.#errHandler(err);
|
|
560
|
+
});
|
|
561
|
+
} else {
|
|
562
|
+
const statusMessage = headers[":status-text"] || "Unknown";
|
|
563
|
+
throw new Error(`watch connect failed: ${statusCode} ${statusMessage}`);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
req.on("error", err => {
|
|
567
|
+
void this.#errHandler(err);
|
|
568
|
+
});
|
|
569
|
+
} catch (e) {
|
|
570
|
+
void this.#errHandler(e);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
440
574
|
/** Clear the resync timer and schedule a new one. */
|
|
441
575
|
#checkResync = () => {
|
|
442
576
|
// Ignore if the last seen time is not set
|
|
@@ -468,7 +602,9 @@ export class Watcher<T extends GenericClass> {
|
|
|
468
602
|
this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
|
|
469
603
|
this.#streamCleanup();
|
|
470
604
|
|
|
471
|
-
|
|
605
|
+
if (!this.#useHTTP2) {
|
|
606
|
+
void this.#watch();
|
|
607
|
+
}
|
|
472
608
|
}
|
|
473
609
|
} else {
|
|
474
610
|
// Otherwise, call the finally function if it exists
|
|
@@ -516,5 +652,8 @@ export class Watcher<T extends GenericClass> {
|
|
|
516
652
|
this.#stream.removeAllListeners();
|
|
517
653
|
this.#stream.destroy();
|
|
518
654
|
}
|
|
655
|
+
if (this.#useHTTP2) {
|
|
656
|
+
void this.#http2Watch();
|
|
657
|
+
}
|
|
519
658
|
};
|
|
520
659
|
}
|