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.
*/
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<string, KubeApi<KubeObject>>();
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
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<KubeObject>) {
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<K extends KubeObject>(api?: string | KubeApi<K>): KubeApi<K> | undefined {
if (!api) {
return undefined;
}
if (typeof api === "string") {
return this.getApi(api) as KubeApi<K>;
}
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<KubeObject>, apis: KubeApi<KubeObject>[] = [store.api]) {
apis.forEach(api => {
registerStore<T extends KubeObject>(storeConstructor: KubeObjectStoreConstructor<T>, apis?: KubeApi<KubeObject>[]) {
const store = new storeConstructor(this.cluster);
(apis ?? [store.api]).forEach(api => {
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;
}
}
export const apiManager = new ApiManager();

View File

@ -98,13 +98,14 @@ export interface KubeObjectStatus {
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 {
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<Metadata extends KubeObjectMetadata = KubeObjectMetadata
return new KubeObject(data);
}
static isNonSystem(item: KubeJsonApiData | KubeObject) {
static isNonSystem(item: KubeJsonApiData | BaseKubeObject) {
return !item.metadata.name.startsWith("system:");
}

View File

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

View File

@ -94,6 +94,8 @@ export class App extends React.Component {
const cluster = ClusterStore.getInstance().getById(App.clusterId);
ApiManager.createInstance(cluster);
await cluster.whenReady; // cluster.activate() is done at this point
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.
*/
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<K extends KubeObject> {
export interface KubeObjectStoreLoadingParams<T extends KubeObject> {
namespaces: string[];
api?: KubeApi<K>;
api?: KubeApi<T>;
reqInit?: RequestInit;
}
export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T> {
static defaultContext = observable.box<ClusterContext>(); // TODO: support multiple cluster contexts
export type KubeObjectStoreConstructor<T extends KubeObject> = new (cluster: Cluster) => KubeObjectStore<T>;
export abstract class KubeObjectStore<T extends KubeObject = KubeObject> extends ItemStore<T> {
abstract api: KubeApi<T>;
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<T extends KubeObject> extends ItemStore<T>
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<T extends KubeObject> extends ItemStore<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) {
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<T extends KubeObject> extends ItemStore<T>
@action
async loadAll(options: { namespaces?: string[], merge?: boolean, reqInit?: RequestInit } = {}): Promise<void | T[]> {
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<T extends KubeObject> extends ItemStore<T>
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<T extends KubeObject> extends ItemStore<T>
}
async update(item: T, data: Partial<T>): Promise<T> {
const newItem = await item.update(data);
ensureObjectSelfLink(this.api, newItem);
const newItem = await item.update<T>(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<T extends KubeObject> extends ItemStore<T>
});
}
subscribe() {
const abortController = new AbortController();
getSubscribeApis(): KubeApi<T>[] {
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<T>, 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<T extends KubeObject> extends ItemStore<T>
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 (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<T extends KubeObject> extends ItemStore<T>
@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":