1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix KubeObjectStore not correctly tracking loading of namespaces (#2266)

This commit is contained in:
Sebastian Malton 2021-04-29 08:32:29 -04:00 committed by GitHub
parent 2c3750c240
commit 082885cabf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 73 additions and 54 deletions

View File

@ -11,10 +11,12 @@ export * from "./debouncePromise";
export * from "./defineGlobal"; export * from "./defineGlobal";
export * from "./delay"; export * from "./delay";
export * from "./disposer"; export * from "./disposer";
export * from "./disposer";
export * from "./downloadFile"; export * from "./downloadFile";
export * from "./escapeRegExp"; export * from "./escapeRegExp";
export * from "./getRandId"; export * from "./getRandId";
export * from "./openExternal"; export * from "./openExternal";
export * from "./reject-promise";
export * from "./saveToAppFiles"; export * from "./saveToAppFiles";
export * from "./singleton"; export * from "./singleton";
export * from "./splitArray"; export * from "./splitArray";

View File

@ -0,0 +1,13 @@
import "abort-controller/polyfill";
/**
* Creates a new promise that will be rejected when the signal rejects.
*
* Useful for `Promise.race()` applications.
* @param signal The AbortController's signal to reject with
*/
export function rejectPromiseBy(signal: AbortSignal): Promise<void> {
return new Promise((_, reject) => {
signal.addEventListener("abort", reject);
});
}

View File

@ -50,6 +50,11 @@ export interface IKubeApiQueryParams {
fieldSelector?: string | string[]; // restrict list of objects by their fields, e.g. fieldSelector: "field=name" fieldSelector?: string | string[]; // restrict list of objects by their fields, e.g. fieldSelector: "field=name"
} }
export interface KubeApiListOptions {
namespace?: string;
reqInit?: RequestInit;
}
export interface IKubePreferredVersion { export interface IKubePreferredVersion {
preferredVersion?: { preferredVersion?: {
version: string; version: string;

View File

@ -1,17 +1,19 @@
import type { ClusterContext } from "./components/context"; import type { ClusterContext } from "./components/context";
import { action, computed, observable, reaction, when } from "mobx"; import { action, computed, observable, reaction, when } from "mobx";
import { autobind } from "./utils"; import { autobind, noop, rejectPromiseBy } from "./utils";
import { KubeObject, KubeStatus } from "./api/kube-object"; import { KubeObject, KubeStatus } from "./api/kube-object";
import { IKubeWatchEvent } from "./api/kube-watch-api"; import { IKubeWatchEvent } from "./api/kube-watch-api";
import { ItemStore } from "./item.store"; import { ItemStore } from "./item.store";
import { apiManager } from "./api/api-manager"; import { apiManager } from "./api/api-manager";
import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api"; import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api";
import { KubeJsonApiData } from "./api/kube-json-api"; import { KubeJsonApiData } from "./api/kube-json-api";
import { Notifications } from "./components/notifications";
export interface KubeObjectStoreLoadingParams { export interface KubeObjectStoreLoadingParams {
namespaces: string[]; namespaces: string[];
api?: KubeApi; api?: KubeApi;
reqInit?: RequestInit;
} }
@autobind() @autobind()
@ -21,9 +23,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
abstract api: KubeApi<T>; abstract api: KubeApi<T>;
public readonly limit?: number; public readonly limit?: number;
public readonly bufferSize: number = 50000; public readonly bufferSize: number = 50000;
private loadedNamespaces: string[] = []; @observable private loadedNamespaces?: string[];
contextReady = when(() => Boolean(this.context)); contextReady = when(() => Boolean(this.context));
namespacesReady = when(() => Boolean(this.loadedNamespaces));
constructor() { constructor() {
super(); super();
@ -103,10 +106,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
} }
} }
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> { protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams): Promise<T[]> {
if (this.context?.cluster.isAllowedResource(api.kind)) { if (this.context?.cluster.isAllowedResource(api.kind)) {
if (!api.isNamespaced) { if (!api.isNamespaced) {
return api.list({}, this.query); return api.list({ reqInit }, this.query);
} }
const isLoadingAll = this.context.allNamespaces?.length > 1 const isLoadingAll = this.context.allNamespaces?.length > 1
@ -116,13 +119,13 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
if (isLoadingAll) { if (isLoadingAll) {
this.loadedNamespaces = []; this.loadedNamespaces = [];
return api.list({}, this.query); return api.list({ reqInit }, this.query);
} else { } else {
this.loadedNamespaces = namespaces; this.loadedNamespaces = namespaces;
return Promise // load resources per namespace return Promise // load resources per namespace
.all(namespaces.map(namespace => api.list({ namespace }))) .all(namespaces.map(namespace => api.list({ namespace, reqInit })))
.then(items => items.flat()); .then(items => items.flat().filter(Boolean));
} }
} }
@ -134,7 +137,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
} }
@action @action
async loadAll(options: { namespaces?: string[], merge?: boolean } = {}): Promise<void | T[]> { async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise<void | T[]> {
await this.contextReady; await this.contextReady;
this.isLoading = true; this.isLoading = true;
@ -142,9 +145,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
const { const {
namespaces = this.context.allNamespaces, // load all namespaces by default namespaces = this.context.allNamespaces, // load all namespaces by default
merge = true, // merge loaded items or return as result merge = true, // merge loaded items or return as result
reqInit,
} = options; } = options;
const items = await this.loadItems({ namespaces, api: this.api }); const items = await this.loadItems({ namespaces, api: this.api, reqInit });
if (merge) { if (merge) {
this.mergeItems(items, { replace: false }); this.mergeItems(items, { replace: false });
@ -157,7 +161,10 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
return items; return items;
} catch (error) { } catch (error) {
console.error("Loading store items failed", { error, store: this }); if (error.message) {
Notifications.error(error.message);
}
console.error("Loading store items failed", { error });
this.resetOnError(error); this.resetOnError(error);
this.failedLoading = true; this.failedLoading = true;
} finally { } finally {
@ -274,9 +281,11 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
subscribe(apis = this.getSubscribeApis()) { subscribe(apis = this.getSubscribeApis()) {
const abortController = new AbortController(); const abortController = new AbortController();
const namespaces = [...this.loadedNamespaces];
if (this.context.cluster?.isGlobalWatchEnabled && namespaces.length === 0) { // This waits for the context and namespaces to be ready or fails fast if the disposer is called
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
.then(() => {
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) {
apis.forEach(api => this.watchNamespace(api, "", abortController)); apis.forEach(api => this.watchNamespace(api, "", abortController));
} else { } else {
apis.forEach(api => { apis.forEach(api => {
@ -285,6 +294,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}); });
}); });
} }
})
.catch(noop); // ignore DOMExceptions
return () => { return () => {
abortController.abort(); abortController.abort();
@ -293,48 +304,39 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
private watchNamespace(api: KubeApi<T>, namespace: string, abortController: AbortController) { private watchNamespace(api: KubeApi<T>, namespace: string, abortController: AbortController) {
let timedRetry: NodeJS.Timeout; let timedRetry: NodeJS.Timeout;
const watch = () => api.watch({
namespace,
abortController,
callback
});
abortController.signal.addEventListener("abort", () => clearTimeout(timedRetry)); const { signal } = abortController;
const callback = (data: IKubeWatchEvent, error: any) => { const callback = (data: IKubeWatchEvent, error: any) => {
if (!this.isLoaded || abortController.signal.aborted) return; if (!this.isLoaded || error instanceof DOMException) return;
if (error instanceof Response) { if (error instanceof Response) {
if (error.status === 404) { if (error.status === 404) {
// api has gone, let's not retry // api has gone, let's not retry
return; return;
} else { // not sure what to do, best to retry
if (timedRetry) clearTimeout(timedRetry);
timedRetry = setTimeout(() => {
api.watch({
namespace,
abortController,
callback
});
}, 5000);
} }
// not sure what to do, best to retry
clearTimeout(timedRetry);
timedRetry = setTimeout(watch, 5000);
} else if (error instanceof KubeStatus && error.code === 410) { } else if (error instanceof KubeStatus && error.code === 410) {
if (timedRetry) clearTimeout(timedRetry); clearTimeout(timedRetry);
// resourceVersion has gone, let's try to reload // resourceVersion has gone, let's try to reload
timedRetry = setTimeout(() => { timedRetry = setTimeout(() => {
(namespace === "" ? this.loadAll({ merge: false }) : this.loadAll({namespaces: [namespace]})).then(() => { (
api.watch({ namespace
namespace, ? this.loadAll({ namespaces: [namespace], reqInit: { signal } })
abortController, : this.loadAll({ merge: false, reqInit: { signal } })
callback ).then(watch);
});
});
}, 1000); }, 1000);
} else if (error) { // not sure what to do, best to retry } else if (error) { // not sure what to do, best to retry
if (timedRetry) clearTimeout(timedRetry); clearTimeout(timedRetry);
timedRetry = setTimeout(watch, 5000);
timedRetry = setTimeout(() => {
api.watch({
namespace,
abortController,
callback
});
}, 5000);
} }
if (data) { if (data) {
@ -342,11 +344,8 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
} }
}; };
api.watch({ signal.addEventListener("abort", () => clearTimeout(timedRetry));
namespace, watch();
abortController,
callback: (data, error) => callback(data, error)
});
} }
@action @action