Skip to content

Commit 0cc04fe

Browse files
committed
Add a CONNECT event to informer.
1 parent d18e69d commit 0cc04fe

File tree

3 files changed

+141
-27
lines changed

3 files changed

+141
-27
lines changed

src/cache.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
import { ADD, CHANGE, DELETE, ERROR, Informer, ListPromise, ObjectCallback, UPDATE } from './informer';
1+
import {
2+
ADD,
3+
CHANGE,
4+
CONNECT,
5+
DELETE,
6+
ERROR,
7+
Informer,
8+
ListPromise,
9+
ObjectCallback,
10+
UPDATE,
11+
} from './informer';
212
import { KubernetesObject } from './types';
313
import { RequestResult, Watch } from './watch';
414

@@ -25,6 +35,7 @@ export class ListWatch<T extends KubernetesObject> implements ObjectCache<T>, In
2535
this.callbackCache[UPDATE] = [];
2636
this.callbackCache[DELETE] = [];
2737
this.callbackCache[ERROR] = [];
38+
this.callbackCache[CONNECT] = [];
2839
this.resourceVersion = '';
2940
if (autoStart) {
3041
this.doneHandler(null);
@@ -102,13 +113,14 @@ export class ListWatch<T extends KubernetesObject> implements ObjectCache<T>, In
102113
private async doneHandler(err: any): Promise<any> {
103114
this._stop();
104115
if (err) {
105-
this.callbackCache[ERROR].forEach((elt: ObjectCallback<T>) => elt(err));
116+
this.callbackCache[ERROR].forEach((elt: ObjectCallback<T>) => elt(undefined, err));
106117
return;
107118
}
108119
if (this.stopped) {
109120
// do not auto-restart
110121
return;
111122
}
123+
this.callbackCache[CONNECT].forEach((elt: ObjectCallback<T>) => elt());
112124
// TODO: Don't always list here for efficiency
113125
// try to restart the watch from resourceVersion, but detect 410 GONE and relist in that case.
114126
// Or if resourceVersion is empty.

src/cache_test.ts

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { EventEmitter } from 'ws';
99

1010
import { V1Namespace, V1NamespaceList, V1ObjectMeta, V1Pod, V1ListMeta } from './api';
1111
import { deleteObject, ListWatch, deleteItems } from './cache';
12-
import { ADD, UPDATE, DELETE, ERROR, ListPromise, CHANGE } from './informer';
12+
import { ADD, UPDATE, DELETE, ERROR, ListPromise, CHANGE, CONNECT } from './informer';
1313

1414
use(chaiAsPromised);
1515

@@ -44,7 +44,7 @@ describe('ListWatchCache', () => {
4444
};
4545
const lw = new ListWatch('/some/path', fake, listFn);
4646
const verb = 'FOOBAR';
47-
expect(() => lw.on(verb, (obj: V1Namespace) => {})).to.throw(`Unknown verb: ${verb}`);
47+
expect(() => lw.on(verb, (obj?: V1Namespace) => {})).to.throw(`Unknown verb: ${verb}`);
4848
});
4949

5050
it('should perform basic caching', async () => {
@@ -216,19 +216,19 @@ describe('ListWatchCache', () => {
216216
expect(pathOut).to.equal('/some/path');
217217

218218
const addPromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
219-
informer.on(ADD, (obj: V1Namespace) => {
219+
informer.on(ADD, (obj?: V1Namespace) => {
220220
resolve(obj);
221221
});
222222
});
223223

224224
const updatePromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
225-
informer.on(UPDATE, (obj: V1Namespace) => {
225+
informer.on(UPDATE, (obj?: V1Namespace) => {
226226
resolve(obj);
227227
});
228228
});
229229

230230
const deletePromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
231-
informer.on(DELETE, (obj: V1Namespace) => {
231+
informer.on(DELETE, (obj?: V1Namespace) => {
232232
resolve(obj);
233233
});
234234
});
@@ -310,7 +310,7 @@ describe('ListWatchCache', () => {
310310

311311
let count = 0;
312312
const changePromise = new Promise<boolean>((resolve: (V1Namespace) => void) => {
313-
informer.on(CHANGE, (obj: V1Namespace) => {
313+
informer.on(CHANGE, (obj?: V1Namespace) => {
314314
count++;
315315
if (count == 3) {
316316
resolve(true);
@@ -372,13 +372,13 @@ describe('ListWatchCache', () => {
372372
expect(pathOut).to.equal('/some/path');
373373

374374
const addPromise = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
375-
informer.on(ADD, (obj: V1Namespace) => {
375+
informer.on(ADD, (obj?: V1Namespace) => {
376376
resolve(obj);
377377
});
378378
});
379379

380380
const addPromise2 = new Promise<V1Namespace>((resolve: (V1Namespace) => void) => {
381-
informer.on(ADD, (obj: V1Namespace) => {
381+
informer.on(ADD, (obj?: V1Namespace) => {
382382
resolve(obj);
383383
});
384384
});
@@ -442,9 +442,9 @@ describe('ListWatchCache', () => {
442442
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn, false);
443443

444444
const addObjects: V1Namespace[] = [];
445-
informer.on(ADD, (obj: V1Namespace) => addObjects.push(obj));
445+
informer.on(ADD, (obj?: V1Namespace) => addObjects.push(obj!));
446446
const updateObjects: V1Namespace[] = [];
447-
informer.on(UPDATE, (obj: V1Namespace) => updateObjects.push(obj));
447+
informer.on(UPDATE, (obj?: V1Namespace) => updateObjects.push(obj!));
448448

449449
informer.start();
450450
await promise;
@@ -518,11 +518,11 @@ describe('ListWatchCache', () => {
518518
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn, false);
519519

520520
const addObjects: V1Namespace[] = [];
521-
informer.on(ADD, (obj: V1Namespace) => addObjects.push(obj));
521+
informer.on(ADD, (obj?: V1Namespace) => addObjects.push(obj!));
522522
const updateObjects: V1Namespace[] = [];
523-
informer.on(UPDATE, (obj: V1Namespace) => updateObjects.push(obj));
523+
informer.on(UPDATE, (obj?: V1Namespace) => updateObjects.push(obj!));
524524
const deleteObjects: V1Namespace[] = [];
525-
informer.on(DELETE, (obj: V1Namespace) => deleteObjects.push(obj));
525+
informer.on(DELETE, (obj?: V1Namespace) => deleteObjects.push(obj!));
526526
informer.start();
527527
await promise;
528528
const [pathOut, , , doneHandler] = mock.capture(fakeWatch.watch).last();
@@ -716,12 +716,12 @@ describe('ListWatchCache', () => {
716716
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
717717

718718
const addedList1: V1Namespace[] = [];
719-
const addToList1Fn = function(obj: V1Namespace) {
720-
addedList1.push(obj);
719+
const addToList1Fn = function(obj?: V1Namespace) {
720+
addedList1.push(obj!);
721721
};
722722
const addedList2: V1Namespace[] = [];
723-
const addToList2Fn = function(obj: V1Namespace) {
724-
addedList2.push(obj);
723+
const addToList2Fn = function(obj?: V1Namespace) {
724+
addedList2.push(obj!);
725725
};
726726

727727
informer.start();
@@ -775,8 +775,8 @@ describe('ListWatchCache', () => {
775775
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
776776

777777
const addedList: V1Namespace[] = [];
778-
const addToListFn = function(obj: V1Namespace) {
779-
addedList.push(obj);
778+
const addToListFn = function(obj?: V1Namespace) {
779+
addedList.push(obj!);
780780
};
781781
const removeSelf = function() {
782782
informer.off(ADD, removeSelf);
@@ -863,12 +863,12 @@ describe('ListWatchCache', () => {
863863
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
864864

865865
const addedList1: V1Namespace[] = [];
866-
const addToList1Fn = function(obj: V1Namespace) {
867-
addedList1.push(obj);
866+
const addToList1Fn = function(obj?: V1Namespace) {
867+
addedList1.push(obj!);
868868
};
869869
const addedList2: V1Namespace[] = [];
870-
const addToList2Fn = function(obj: V1Namespace) {
871-
addedList2.push(obj);
870+
const addToList2Fn = function(obj?: V1Namespace) {
871+
addedList2.push(obj!);
872872
};
873873

874874
informer.start();
@@ -1106,7 +1106,104 @@ describe('delete items', () => {
11061106
];
11071107
const pods: V1Pod[] = [];
11081108

1109-
deleteItems(listA, listB, [(obj: V1Pod) => pods.push(obj)]);
1109+
deleteItems(listA, listB, [(obj?: V1Pod) => pods.push(obj!)]);
11101110
expect(pods).to.deep.equal(expected);
11111111
});
1112+
1113+
it('should call the connect handler', async () => {
1114+
const fakeWatch = mock.mock(Watch);
1115+
const listObj = {
1116+
metadata: {
1117+
resourceVersion: '12345',
1118+
} as V1ListMeta,
1119+
items: [],
1120+
} as V1NamespaceList;
1121+
1122+
const listFn: ListPromise<V1Namespace> = function(): Promise<{
1123+
response: http.IncomingMessage;
1124+
body: V1NamespaceList;
1125+
}> {
1126+
return new Promise<{ response: http.IncomingMessage; body: V1NamespaceList }>(
1127+
(resolve, reject) => {
1128+
resolve({ response: {} as http.IncomingMessage, body: listObj });
1129+
},
1130+
);
1131+
};
1132+
const informer = new ListWatch('/some/path', mock.instance(fakeWatch), listFn, false);
1133+
const connectPromise = new Promise<boolean>((resolve: (boolean) => void) => {
1134+
informer.on(CONNECT, (obj?: V1Namespace) => {
1135+
resolve(true);
1136+
});
1137+
});
1138+
informer.start();
1139+
1140+
expect(connectPromise).to.eventually.be.true;
1141+
});
1142+
1143+
it('does calls connect after a restart after an error', async () => {
1144+
const fakeWatch = mock.mock(Watch);
1145+
const list: V1Pod[] = [
1146+
{
1147+
metadata: {
1148+
name: 'name1',
1149+
namespace: 'ns1',
1150+
} as V1ObjectMeta,
1151+
} as V1Pod,
1152+
{
1153+
metadata: {
1154+
name: 'name2',
1155+
namespace: 'ns2',
1156+
} as V1ObjectMeta,
1157+
} as V1Pod,
1158+
];
1159+
const listObj = {
1160+
metadata: {
1161+
resourceVersion: '12345',
1162+
} as V1ListMeta,
1163+
items: list,
1164+
} as V1NamespaceList;
1165+
1166+
const listFn: ListPromise<V1Namespace> = function(): Promise<{
1167+
response: http.IncomingMessage;
1168+
body: V1NamespaceList;
1169+
}> {
1170+
return new Promise<{ response: http.IncomingMessage; body: V1NamespaceList }>(
1171+
(resolve, reject) => {
1172+
resolve({ response: {} as http.IncomingMessage, body: listObj });
1173+
},
1174+
);
1175+
};
1176+
let promise = new Promise((resolve) => {
1177+
mock.when(
1178+
fakeWatch.watch(mock.anything(), mock.anything(), mock.anything(), mock.anything()),
1179+
).thenCall(() => {
1180+
resolve(new FakeRequest());
1181+
});
1182+
});
1183+
1184+
const cache = new ListWatch('/some/path', mock.instance(fakeWatch), listFn);
1185+
await promise;
1186+
1187+
let errorEmitted = false;
1188+
cache.on(ERROR, () => (errorEmitted = true));
1189+
1190+
const [, , , doneHandler] = mock.capture(fakeWatch.watch).last();
1191+
1192+
const error = new Error('testing');
1193+
await doneHandler(error);
1194+
1195+
mock.verify(
1196+
fakeWatch.watch(mock.anything(), mock.anything(), mock.anything(), mock.anything()),
1197+
).once();
1198+
expect(errorEmitted).to.equal(true);
1199+
1200+
const connectPromise = new Promise<boolean>((resolve: (boolean) => void) => {
1201+
cache.on(CONNECT, (obj?: V1Namespace) => {
1202+
resolve(true);
1203+
});
1204+
});
1205+
cache.start();
1206+
1207+
expect(connectPromise).to.eventually.be.true;
1208+
});
11121209
});

src/informer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@ import { Watch } from './watch';
55

66
import http = require('http');
77

8-
export type ObjectCallback<T extends KubernetesObject> = (obj: T) => void;
8+
export type ObjectCallback<T extends KubernetesObject> = (obj?: T, err?: any) => void;
99
export type ListCallback<T extends KubernetesObject> = (list: T[], ResourceVersion: string) => void;
1010
export type ListPromise<T extends KubernetesObject> = () => Promise<{
1111
response: http.IncomingMessage;
1212
body: KubernetesListObject<T>;
1313
}>;
1414

15+
// These are issued per object
1516
export const ADD: string = 'add';
1617
export const UPDATE: string = 'update';
1718
export const CHANGE: string = 'change';
1819
export const DELETE: string = 'delete';
20+
21+
// This is issued when a watch connects or reconnects
22+
export const CONNECT: string = 'connect';
23+
// This is issued when there is an error
1924
export const ERROR: string = 'error';
2025

2126
export interface Informer<T> {

0 commit comments

Comments
 (0)