1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-08-03 12:57:21 -04:00
parent 77077ef72c
commit f1360d602d
6 changed files with 127 additions and 90 deletions

View File

@ -19,18 +19,21 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { KubeObjectStore } from "../kube-object.store"; import { action, makeObservable, observable } from "mobx";
import { autoBind, Singleton } from "../utils";
import { action, observable, makeObservable } from "mobx"; import { parseKubeApi } from "./kube-api";
import { autoBind, iter } from "../utils"; import type { KubeObjectStoreConstructor, KubeObjectStore } from "../kube-object.store";
import { KubeApi, parseKubeApi } from "./kube-api"; import type { KubeApi } from "./kube-api";
import type { Cluster } from "../../main/cluster";
import type { KubeObject } from "./kube-object"; 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<string, KubeApi<KubeObject>>(); private apis = observable.map<string, KubeApi<KubeObject>>();
private stores = observable.map<string, KubeObjectStore<KubeObject>>(); private stores = observable.map<string, KubeObjectStore<KubeObject>>();
constructor() { constructor(public cluster: Cluster) {
super();
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
} }
@ -40,17 +43,17 @@ export class ApiManager {
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase); 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) { 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<KubeObject>) { registerApi(apiBase: string, api: KubeApi<KubeObject>) {
if (!this.apis.has(apiBase)) { if (!this.apis.has(apiBase)) {
this.stores.forEach((store) => { this.stores.forEach((store) => {
if (store.api === api) { if(store.api === api) {
this.stores.set(apiBase, store); this.stores.set(apiBase, store);
} }
}); });
@ -59,14 +62,8 @@ export class ApiManager {
} }
} }
protected resolveApi<K extends KubeObject>(api?: string | KubeApi<K>): KubeApi<K> | undefined { protected resolveApi(api: string | ApiSpecifier): ApiSpecifier {
if (!api) { if (typeof api === "string") return this.getApi(api);
return undefined;
}
if (typeof api === "string") {
return this.getApi(api) as KubeApi<K>;
}
return api; return api;
} }
@ -82,15 +79,15 @@ export class ApiManager {
} }
@action @action
registerStore(store: KubeObjectStore<KubeObject>, apis: KubeApi<KubeObject>[] = [store.api]) { registerStore<T extends KubeObject>(storeConstructor: KubeObjectStoreConstructor<T>, apis?: KubeApi<KubeObject>[]) {
apis.forEach(api => { const store = new storeConstructor(this.cluster);
(apis ?? [store.api]).forEach(api => {
this.stores.set(api.apiBase, store); this.stores.set(api.apiBase, store);
}); });
} }
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined { getStore<S extends KubeObjectStore<KubeObject>>(api: string | ApiSpecifier): S {
return this.stores.get(this.resolveApi(api)?.apiBase) as S; return this.stores.get(this.resolveApi(api)?.apiBase) as S;
} }
} }
export const apiManager = new ApiManager();

View File

@ -98,13 +98,14 @@ export interface KubeObjectStatus {
export type KubeMetaField = keyof KubeObjectMetadata; export type KubeMetaField = keyof KubeObjectMetadata;
export type BaseKubeObject = KubeObject<KubeObjectMetadata, KubeStatus, any>;
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject { export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
static readonly kind: string; static readonly kind: string;
static readonly namespaced: boolean; static readonly namespaced: boolean;
apiVersion: string; apiVersion: string;
kind: string; kind: string;
metadata: Metadata; metadata?: Metadata;
status?: Status; status?: Status;
spec?: Spec; spec?: Spec;
managedFields?: any; managedFields?: any;
@ -113,7 +114,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
return new KubeObject(data); return new KubeObject(data);
} }
static isNonSystem(item: KubeJsonApiData | KubeObject) { static isNonSystem(item: KubeJsonApiData | BaseKubeObject) {
return !item.metadata.name.startsWith("system:"); return !item.metadata.name.startsWith("system:");
} }

View File

@ -51,6 +51,10 @@ export interface IKubeWatchLog {
cssStyle?: string; cssStyle?: string;
} }
export interface ApiSpecifier {
apiBase: string;
}
export class KubeWatchApi { export class KubeWatchApi {
@observable context: ClusterContext = null; @observable context: ClusterContext = null;

View File

@ -94,6 +94,8 @@ export class App extends React.Component {
const cluster = ClusterStore.getInstance().getById(App.clusterId); const cluster = ClusterStore.getInstance().getById(App.clusterId);
ApiManager.createInstance(cluster);
await cluster.whenReady; // cluster.activate() is done at this point await cluster.whenReady; // cluster.activate() is done at this point
const activeEntityDisposer = reaction(() => catalogEntityRegistry.getById(App.clusterId), (entity) => { const activeEntityDisposer = reaction(() => catalogEntityRegistry.getById(App.clusterId), (entity) => {

View File

@ -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));
}

View File

@ -19,53 +19,43 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * 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 { 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 { KubeObject, KubeStatus } from "./api/kube-object";
import type { IKubeWatchEvent } from "./api/kube-watch-api"; import type { 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 { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api"; import { IKubeApiQueryParams, KubeApi, parseKubeApi } from "./api/kube-api";
import type { KubeJsonApiData } from "./api/kube-json-api"; import type { KubeJsonApiData } from "./api/kube-json-api";
import { Notifications } from "./components/notifications"; import { Notifications } from "./components/notifications";
import { allNamespaces, contextNamespaces, isLoadingAll } from "./components/namespace-helpers";
import type { Cluster } from "../main/cluster";
export interface KubeObjectStoreLoadingParams<K extends KubeObject> { export interface KubeObjectStoreLoadingParams<T extends KubeObject> {
namespaces: string[]; namespaces: string[];
api?: KubeApi<K>; api?: KubeApi<T>;
reqInit?: RequestInit; reqInit?: RequestInit;
} }
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> { export type KubeObjectStoreConstructor<T extends KubeObject> = new (cluster: Cluster) => KubeObjectStore<T>;
static defaultContext = observable.box<ClusterContext>(); // TODO: support multiple cluster contexts
export abstract class KubeObjectStore<T extends KubeObject = KubeObject> extends ItemStore<T> {
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;
@observable private loadedNamespaces?: string[]; @observable private loadedNamespaces?: string[];
get contextReady() { namespacesReady = when(() => Boolean(this.loadedNamespaces));
return when(() => Boolean(this.context));
}
get namespacesReady() { constructor(protected cluster: Cluster) {
return when(() => Boolean(this.loadedNamespaces));
}
constructor() {
super(); super();
makeObservable(this); makeObservable(this);
autoBind(this); autoBind(this);
this.bindWatchEventsUpdater(); this.bindWatchEventsUpdater();
} }
get context(): ClusterContext {
return KubeObjectStore.defaultContext.get();
}
@computed get contextItems(): T[] { @computed get contextItems(): T[] {
const namespaces = this.context?.contextNamespaces ?? []; const namespaces = contextNamespaces();
return this.items.filter(item => { return this.items.filter(item => {
const itemNamespace = item.getNs(); const itemNamespace = item.getNs();
@ -95,9 +85,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
if (namespaces.length) { if (namespaces.length) {
return this.items.filter(item => namespaces.includes(item.getNs())); return this.items.filter(item => namespaces.includes(item.getNs()));
} } else if (!strict) {
if (!strict) {
return this.items; return this.items;
} }
@ -138,26 +126,22 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams<T>): Promise<T[]> { protected async loadItems({ namespaces, api, reqInit }: KubeObjectStoreLoadingParams<T>): Promise<T[]> {
if (this.context?.cluster.isAllowedResource(api.kind)) { if (this.cluster.isAllowedResource(api.kind)) {
if (!api.isNamespaced) { if (!api.isNamespaced) {
return api.list({ reqInit }, this.query); return api.list({ reqInit }, this.query);
} }
const isLoadingAll = this.context.allNamespaces?.length > 1 if (isLoadingAll(this.cluster, namespaces)) {
&& this.context.cluster.accessibleNamespaces.length === 0
&& this.context.allNamespaces.every(ns => namespaces.includes(ns));
if (isLoadingAll) {
this.loadedNamespaces = []; this.loadedNamespaces = [];
return api.list({ reqInit }, this.query); 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 []; return [];
@ -169,12 +153,11 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
@action @action
async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise<void | T[]> { async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise<void | T[]> {
await this.contextReady;
this.isLoading = true; this.isLoading = true;
try { try {
const { 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 merge = true, // merge loaded items or return as result
reqInit, reqInit,
} = options; } = options;
@ -195,7 +178,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
if (error.message) { if (error.message) {
Notifications.error(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.resetOnError(error);
this.failedLoading = true; this.failedLoading = true;
} finally { } finally {
@ -279,10 +262,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
async update(item: T, data: Partial<T>): Promise<T> { async update(item: T, data: Partial<T>): Promise<T> {
const newItem = await item.update(data); const newItem = await item.update<T>(data);
ensureObjectSelfLink(this.api, newItem);
const index = this.items.findIndex(item => item.getId() === newItem.getId()); const index = this.items.findIndex(item => item.getId() === newItem.getId());
this.items.splice(index, 1, newItem); this.items.splice(index, 1, newItem);
@ -309,35 +289,51 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}); });
} }
subscribe() { getSubscribeApis(): KubeApi<T>[] {
const abortController = new AbortController(); return [this.api];
}
if (this.api.isNamespaced) { subscribe(apis = this.getSubscribeApis()) {
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])]) 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(() => { .then(() => {
if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) { if (
return this.watchNamespace("", abortController); store.cluster.isGlobalWatchEnabled
&& store.loadedNamespaces.length === 0
) {
return store.watchNamespace(api, "", abortController);
} }
for (const namespace of this.loadedNamespaces) { for (const namespace of this.loadedNamespaces) {
this.watchNamespace(namespace, abortController); store.watchNamespace(api, namespace, abortController);
} }
}) })
.catch(noop); // ignore DOMExceptions .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) { private watchNamespace(api: KubeApi<T>, namespace: string, abortController: AbortController) {
if (!this.api.getResourceVersion(namespace)) {
return;
}
let timedRetry: NodeJS.Timeout; let timedRetry: NodeJS.Timeout;
const watch = () => this.api.watch({ const watch = () => api.watch({
namespace, namespace,
abortController, abortController,
callback callback
@ -345,12 +341,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
const { signal } = abortController; const { signal } = abortController;
const callback = (data: IKubeWatchEvent<T>, error: any) => { const callback = (data: IKubeWatchEvent<KubeJsonApiData>, error: any) => {
if (!this.isLoaded || error instanceof DOMException) return; if (!this.isLoaded || error instanceof DOMException) return;
if (error instanceof Response) { if (error instanceof Response) {
if (error.status === 404 || error.status === 401) { if (error.status === 404) {
// api has gone, or credentials are not permitted, let's not retry // api has gone, let's not retry
return; return;
} }
@ -383,12 +379,12 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
@action @action
protected updateFromEventsBuffer() { protected updateFromEventsBuffer() {
const items = this.getItems(); const items = toJS(this.items);
for (const { type, object } of this.eventsBuffer.clear()) { for (const { type, object } of this.eventsBuffer.clear()) {
const index = items.findIndex(item => item.getId() === object.metadata?.uid); const index = items.findIndex(item => item.getId() === object.metadata?.uid);
const item = items[index]; const item = items[index];
const api = apiManager.getApiByKind(object.kind, object.apiVersion); const api = ApiManager.getInstance().getApiByKind(object.kind, object.apiVersion);
switch (type) { switch (type) {
case "ADDED": case "ADDED":