From f1360d602d75e95b98ec68e95a4b48b06af463e3 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 3 Aug 2021 12:57:21 -0400 Subject: [PATCH] poc Signed-off-by: Sebastian Malton --- src/renderer/api/api-manager.ts | 43 +++---- src/renderer/api/kube-object.ts | 5 +- src/renderer/api/kube-watch-api.ts | 4 + src/renderer/components/app.tsx | 2 + src/renderer/components/namespace-helpers.ts | 37 ++++++ src/renderer/kube-object.store.ts | 126 +++++++++---------- 6 files changed, 127 insertions(+), 90 deletions(-) create mode 100755 src/renderer/components/namespace-helpers.ts diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 1e01bd4cfb..344a23be3b 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -19,18 +19,21 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { KubeObjectStore } from "../kube-object.store"; - -import { action, observable, makeObservable } from "mobx"; -import { autoBind, iter } from "../utils"; -import { KubeApi, parseKubeApi } from "./kube-api"; +import { action, makeObservable, observable } from "mobx"; +import { autoBind, Singleton } from "../utils"; +import { parseKubeApi } from "./kube-api"; +import type { KubeObjectStoreConstructor, KubeObjectStore } from "../kube-object.store"; +import type { KubeApi } from "./kube-api"; +import type { Cluster } from "../../main/cluster"; import type { KubeObject } from "./kube-object"; +import type { ApiSpecifier } from "./kube-watch-api"; -export class ApiManager { +export class ApiManager extends Singleton { private apis = observable.map>(); private stores = observable.map>(); - constructor() { + constructor(public cluster: Cluster) { + super(); makeObservable(this); autoBind(this); } @@ -40,17 +43,17 @@ export class ApiManager { return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase); } - return iter.find(this.apis.values(), pathOrCallback ?? (() => true)); + return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true)); } getApiByKind(kind: string, apiVersion: string) { - return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion); + return Array.from(this.apis.values()).find((api) => api.kind === kind && api.apiVersionWithGroup === apiVersion); } registerApi(apiBase: string, api: KubeApi) { if (!this.apis.has(apiBase)) { this.stores.forEach((store) => { - if (store.api === api) { + if(store.api === api) { this.stores.set(apiBase, store); } }); @@ -59,14 +62,8 @@ export class ApiManager { } } - protected resolveApi(api?: string | KubeApi): KubeApi | undefined { - if (!api) { - return undefined; - } - - if (typeof api === "string") { - return this.getApi(api) as KubeApi; - } + protected resolveApi(api: string | ApiSpecifier): ApiSpecifier { + if (typeof api === "string") return this.getApi(api); return api; } @@ -82,15 +79,15 @@ export class ApiManager { } @action - registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) { - apis.forEach(api => { + registerStore(storeConstructor: KubeObjectStoreConstructor, apis?: KubeApi[]) { + const store = new storeConstructor(this.cluster); + + (apis ?? [store.api]).forEach(api => { this.stores.set(api.apiBase, store); }); } - getStore>(api: string | KubeApi): S | undefined { + getStore>(api: string | ApiSpecifier): S { return this.stores.get(this.resolveApi(api)?.apiBase) as S; } } - -export const apiManager = new ApiManager(); diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index 97b6442a02..f8d82a3732 100644 --- a/src/renderer/api/kube-object.ts +++ b/src/renderer/api/kube-object.ts @@ -98,13 +98,14 @@ export interface KubeObjectStatus { export type KubeMetaField = keyof KubeObjectMetadata; +export type BaseKubeObject = KubeObject; export class KubeObject implements ItemObject { static readonly kind: string; static readonly namespaced: boolean; apiVersion: string; kind: string; - metadata: Metadata; + metadata?: Metadata; status?: Status; spec?: Spec; managedFields?: any; @@ -113,7 +114,7 @@ export class KubeObject catalogEntityRegistry.getById(App.clusterId), (entity) => { diff --git a/src/renderer/components/namespace-helpers.ts b/src/renderer/components/namespace-helpers.ts new file mode 100755 index 0000000000..05418a8803 --- /dev/null +++ b/src/renderer/components/namespace-helpers.ts @@ -0,0 +1,37 @@ +import type { Cluster } from "../../main/cluster"; +import { ApiManager } from "../api/api-manager"; +import { Namespace } from "../api/endpoints"; + +export function allNamespaces(cluster: Cluster | null): string[] { + if (!cluster) { + return []; + } + + // user given list of namespaces + if (cluster?.accessibleNamespaces.length) { + return cluster.accessibleNamespaces; + } + + const namespaceStore = ApiManager.getInstance().getStore(Namespace.apiBase); + + if (namespaceStore.items.length > 0) { + // namespaces from kubernetes api + return namespaceStore.items.map((namespace) => namespace.getName()); + } else { + // fallback to cluster resolved namespaces because we could not load list + return cluster.allowedNamespaces || []; + } +} + +export function contextNamespaces(): string[] { + // TODO: will remove when refactoring this sort of thing + return (ApiManager.getInstance().getStore(Namespace.apiBase) as any).contextNamespaces ?? []; +} + +export function isLoadingAll(cluster: Cluster, namespaces: string[]): boolean { + const allNs = allNamespaces(cluster); + + return allNs.length > 1 + && cluster.accessibleNamespaces.length === 0 + && allNs.every(ns => namespaces.includes(ns)); +} diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 14dbbc6152..524ed66cc5 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -19,53 +19,43 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { ClusterContext } from "./components/context"; - import { action, computed, makeObservable, observable, reaction, when } from "mobx"; -import { autoBind, noop, rejectPromiseBy } from "./utils"; +import { autoBind, bifurcateArray, noop, rejectPromiseBy, toJS } from "./utils"; import { KubeObject, KubeStatus } from "./api/kube-object"; import type { IKubeWatchEvent } from "./api/kube-watch-api"; import { ItemStore } from "./item.store"; -import { apiManager } from "./api/api-manager"; -import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api"; +import { ApiManager } from "./api/api-manager"; +import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api"; import type { KubeJsonApiData } from "./api/kube-json-api"; import { Notifications } from "./components/notifications"; +import { allNamespaces, contextNamespaces, isLoadingAll } from "./components/namespace-helpers"; +import type { Cluster } from "../main/cluster"; -export interface KubeObjectStoreLoadingParams { +export interface KubeObjectStoreLoadingParams { namespaces: string[]; - api?: KubeApi; + api?: KubeApi; reqInit?: RequestInit; } -export abstract class KubeObjectStore extends ItemStore { - static defaultContext = observable.box(); // TODO: support multiple cluster contexts +export type KubeObjectStoreConstructor = new (cluster: Cluster) => KubeObjectStore; +export abstract class KubeObjectStore extends ItemStore { abstract api: KubeApi; public readonly limit?: number; public readonly bufferSize: number = 50000; @observable private loadedNamespaces?: string[]; - get contextReady() { - return when(() => Boolean(this.context)); - } + namespacesReady = when(() => Boolean(this.loadedNamespaces)); - get namespacesReady() { - return when(() => Boolean(this.loadedNamespaces)); - } - - constructor() { + constructor(protected cluster: Cluster) { super(); makeObservable(this); autoBind(this); this.bindWatchEventsUpdater(); } - get context(): ClusterContext { - return KubeObjectStore.defaultContext.get(); - } - @computed get contextItems(): T[] { - const namespaces = this.context?.contextNamespaces ?? []; + const namespaces = contextNamespaces(); return this.items.filter(item => { const itemNamespace = item.getNs(); @@ -95,9 +85,7 @@ export abstract class KubeObjectStore extends ItemStore if (namespaces.length) { return this.items.filter(item => namespaces.includes(item.getNs())); - } - - if (!strict) { + } else if (!strict) { return this.items; } @@ -138,26 +126,22 @@ export abstract class KubeObjectStore extends ItemStore } protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams): Promise { - if (this.context?.cluster.isAllowedResource(api.kind)) { + if (this.cluster.isAllowedResource(api.kind)) { if (!api.isNamespaced) { return api.list({ reqInit }, this.query); } - const isLoadingAll = this.context.allNamespaces?.length > 1 - && this.context.cluster.accessibleNamespaces.length === 0 - && this.context.allNamespaces.every(ns => namespaces.includes(ns)); - - if (isLoadingAll) { + if (isLoadingAll(this.cluster, namespaces)) { this.loadedNamespaces = []; return api.list({ reqInit }, this.query); - } else { - this.loadedNamespaces = namespaces; - - return Promise // load resources per namespace - .all(namespaces.map(namespace => api.list({ namespace, reqInit }))) - .then(items => items.flat().filter(Boolean)); } + + this.loadedNamespaces = namespaces; + + return Promise // load resources per namespace + .all(namespaces.map(namespace => api.list({ namespace, reqInit }))) + .then(items => items.flat().filter(Boolean)); } return []; @@ -169,12 +153,11 @@ export abstract class KubeObjectStore extends ItemStore @action async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise { - await this.contextReady; this.isLoading = true; try { const { - namespaces = this.context.allNamespaces, // load all namespaces by default + namespaces = allNamespaces(this.cluster), // load all namespaces by default merge = true, // merge loaded items or return as result reqInit, } = options; @@ -195,7 +178,7 @@ export abstract class KubeObjectStore extends ItemStore if (error.message) { Notifications.error(error.message); } - console.warn("[KubeObjectStore] loadAll failed", this.api.apiBase, error); + console.error("Loading store items failed", { error }); this.resetOnError(error); this.failedLoading = true; } finally { @@ -279,10 +262,7 @@ export abstract class KubeObjectStore extends ItemStore } async update(item: T, data: Partial): Promise { - const newItem = await item.update(data); - - ensureObjectSelfLink(this.api, newItem); - + const newItem = await item.update(data); const index = this.items.findIndex(item => item.getId() === newItem.getId()); this.items.splice(index, 1, newItem); @@ -309,35 +289,51 @@ export abstract class KubeObjectStore extends ItemStore }); } - subscribe() { - const abortController = new AbortController(); + getSubscribeApis(): KubeApi[] { + return [this.api]; + } - if (this.api.isNamespaced) { - Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])]) + subscribe(apis = this.getSubscribeApis()) { + const abortController = new AbortController(); + const [clusterScopedApis, namespaceScopedApis] = bifurcateArray(apis, api => api.isNamespaced); + + for (const api of namespaceScopedApis) { + const store = ApiManager.getInstance().getStore(api); + + // 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([store.namespacesReady])]) .then(() => { - if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) { - return this.watchNamespace("", abortController); + if ( + store.cluster.isGlobalWatchEnabled + && store.loadedNamespaces.length === 0 + ) { + return store.watchNamespace(api, "", abortController); } for (const namespace of this.loadedNamespaces) { - this.watchNamespace(namespace, abortController); + store.watchNamespace(api, namespace, abortController); } }) .catch(noop); // ignore DOMExceptions - } else { - this.watchNamespace("", abortController); } - return () => abortController.abort(); + for (const api of clusterScopedApis) { + /** + * if the api is cluster scoped then we will never assign to `loadedNamespaces` + * and thus `store.namespacesReady` will never resolve. Futhermore, we don't care + * about watching namespaces. + */ + ApiManager.getInstance().getStore(api).watchNamespace(api, "", abortController); + } + + return () => { + abortController.abort(); + }; } - private watchNamespace(namespace: string, abortController: AbortController) { - if (!this.api.getResourceVersion(namespace)) { - return; - } - + private watchNamespace(api: KubeApi, namespace: string, abortController: AbortController) { let timedRetry: NodeJS.Timeout; - const watch = () => this.api.watch({ + const watch = () => api.watch({ namespace, abortController, callback @@ -345,12 +341,12 @@ export abstract class KubeObjectStore extends ItemStore const { signal } = abortController; - const callback = (data: IKubeWatchEvent, error: any) => { + const callback = (data: IKubeWatchEvent, error: any) => { if (!this.isLoaded || error instanceof DOMException) return; if (error instanceof Response) { - if (error.status === 404 || error.status === 401) { - // api has gone, or credentials are not permitted, let's not retry + if (error.status === 404) { + // api has gone, let's not retry return; } @@ -383,12 +379,12 @@ export abstract class KubeObjectStore extends ItemStore @action protected updateFromEventsBuffer() { - const items = this.getItems(); + const items = toJS(this.items); for (const { type, object } of this.eventsBuffer.clear()) { const index = items.findIndex(item => item.getId() === object.metadata?.uid); const item = items[index]; - const api = apiManager.getApiByKind(object.kind, object.apiVersion); + const api = ApiManager.getInstance().getApiByKind(object.kind, object.apiVersion); switch (type) { case "ADDED":