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

refactoring, detaching NamespaceStore from KubeObjectStore

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-02-05 14:28:46 +02:00
parent 5b86ae2f5d
commit 77e3a7fb83
18 changed files with 117 additions and 115 deletions

View File

@ -1,14 +1,14 @@
// Kubernetes watch-api client
// API: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams
import type { Cluster } from "../../main/cluster";
import type { IKubeWatchEvent, IKubeWatchEventStreamEnd, IWatchRoutePayload } from "../../main/routes/watch-route";
import type { KubeObject } from "./kube-object";
import type { KubeObjectStore } from "../kube-object.store";
import type { ClusterContext } from "../components/context";
import plimit from "p-limit";
import debounce from "lodash/debounce";
import { autorun, comparer, computed, IReactionDisposer, observable, reaction } from "mobx";
import { comparer, computed, IReactionDisposer, observable, reaction, when } from "mobx";
import { autobind, EventEmitter, noop } from "../utils";
import { ensureObjectSelfLink, KubeApi, parseKubeApi } from "./kube-api";
import { KubeJsonApiData, KubeJsonApiError } from "./kube-json-api";
@ -48,21 +48,18 @@ export class KubeWatchApi {
private reader: ReadableStreamReader<string>;
public onMessage = new EventEmitter<[IKubeWatchMessage]>();
@observable.ref private cluster: Cluster;
@observable.ref private namespaces: string[] = [];
@observable context: ClusterContext = null;
@observable subscribers = observable.map<KubeApi, number>();
@observable isConnected = false;
@computed get isReady(): boolean {
return Boolean(this.cluster && this.namespaces);
}
contextReady = when(() => Boolean(this.context));
@computed get isActive(): boolean {
return this.apis.length > 0;
}
@computed get apis(): string[] {
if (!this.isReady) {
if (!this.context) {
return [];
}
@ -72,22 +69,20 @@ export class KubeWatchApi {
}
// TODO: optimize - check when all namespaces are selected and then request all in one
if (api.isNamespaced && !this.cluster.isGlobalWatchEnabled) {
return this.namespaces.map(namespace => api.getWatchUrl(namespace));
if (api.isNamespaced && !this.context.cluster.isGlobalWatchEnabled) {
return this.context.contextNamespaces.map(namespace => api.getWatchUrl(namespace));
}
return api.getWatchUrl();
}).flat();
}
async init({ getCluster, getNamespaces }: {
getCluster: () => Cluster,
getNamespaces: () => string[],
}): Promise<void> {
autorun(() => {
this.cluster = getCluster();
this.namespaces = getNamespaces();
});
constructor() {
this.init();
}
private async init() {
await this.contextReady;
this.bindAutoConnect();
}
@ -109,7 +104,7 @@ export class KubeWatchApi {
}
isAllowedApi(api: KubeApi): boolean {
return Boolean(this?.cluster.isAllowedResource(api.kind));
return Boolean(this.context?.cluster.isAllowedResource(api.kind));
}
subscribeApi(api: KubeApi | KubeApi[]): () => void {
@ -130,15 +125,15 @@ export class KubeWatchApi {
};
}
preloadStores(stores: KubeObjectStore[], { loadOnce = false } = {}) {
preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) {
const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages
const preloading: Promise<any>[] = [];
for (const store of stores) {
preloading.push(limitRequests(async () => {
if (store.isLoaded && loadOnce) return; // skip
if (store.isLoaded && opts.loadOnce) return; // skip
return store.loadAll(this.namespaces);
return store.loadAll({ namespaces: opts.namespaces });
}));
}
@ -154,7 +149,7 @@ export class KubeWatchApi {
const unsubscribeList: (() => void)[] = [];
let isUnsubscribed = false;
const load = () => this.preloadStores(stores, { loadOnce });
const load = (namespaces?: string[]) => this.preloadStores(stores, { namespaces, loadOnce });
let preloading = preload && load();
let cancelReloading: IReactionDisposer = noop;
@ -175,10 +170,10 @@ export class KubeWatchApi {
subscribe();
}
// reload when context namespaces changes
cancelReloading = reaction(() => this.namespaces, () => {
// partial reload only selected namespaces
cancelReloading = reaction(() => this.context.contextNamespaces, namespaces => {
preloading?.cancelLoading();
preloading = load();
preloading = load(namespaces);
}, {
equals: comparer.shallow,
});
@ -291,7 +286,7 @@ export class KubeWatchApi {
}
// skip updates from non-watching resources context
if (!namespace || this.namespaces.includes(namespace)) {
if (!namespace || this.context?.contextNamespaces.includes(namespace)) {
this.onMessage.emit(message);
}
} catch (error) {

View File

@ -30,7 +30,7 @@ export class CrdResources extends React.Component<Props> {
const { store } = this;
if (store && !store.isLoading && !store.isLoaded) {
store.loadAllFromContextNamespaces();
store.reloadAll();
}
})
]);
@ -97,7 +97,7 @@ export class CrdResources extends React.Component<Props> {
...extraColumns.map((column) => {
let value = jsonPath.value(crdInstance, parseJsonPath(column.jsonPath.slice(1)));
if (Array.isArray(value) || typeof value === "object") {
if (Array.isArray(value) || typeof value === "object") {
value = JSON.stringify(value);
}

View File

@ -14,7 +14,7 @@ export interface KubeEventDetailsProps {
@observer
export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
async componentDidMount() {
eventStore.loadAllFromContextNamespaces();
eventStore.reloadAll();
}
render() {

View File

@ -32,8 +32,8 @@ export class NamespaceDetails extends React.Component<Props> {
}
componentDidMount() {
resourceQuotaStore.loadAllFromContextNamespaces();
limitRangeStore.loadAllFromContextNamespaces();
resourceQuotaStore.reloadAll();
limitRangeStore.reloadAll();
}
render() {

View File

@ -41,8 +41,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
private async init() {
await this.resolveCluster();
if (!this.cluster) return; // skip for non-cluster context window
if (!this.context) return; // skip for non-cluster context window
this.setContext(this.initialNamespaces);
this.autoLoadAllowedNamespaces();
@ -66,7 +65,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
private autoLoadAllowedNamespaces(): IReactionDisposer {
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll(namespaces), {
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll({ namespaces }), {
fireImmediately: true,
equals: comparer.shallow,
});
@ -94,8 +93,8 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
@computed get allowedNamespaces(): string[] {
return Array.from(new Set([
...(this.cluster?.allowedNamespaces ?? []), // loaded names from main, updating every 30s and thus might be stale
...this.items.map(item => item.getName()), // loaded names from hosted cluster
...(this.context?.allNamespaces ?? []), // allowed namespaces from cluster (main), updating every 30s
...this.items.map(item => item.getName()), // loaded namespaces from k8s api
].flat()));
}
@ -111,7 +110,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
getSubscribeApis() {
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
if (this.cluster?.accessibleNamespaces.length > 0) {
if (this.context?.cluster.accessibleNamespaces.length > 0) {
return [];
}

View File

@ -29,7 +29,7 @@ export class NodeDetails extends React.Component<Props> {
});
async componentDidMount() {
podsStore.loadAllFromContextNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {

View File

@ -7,7 +7,7 @@ import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Select, SelectOption } from "../select";
import { SubTitle } from "../layout/sub-title";
import { IRoleBindingSubject, RoleBinding, ServiceAccount, Role } from "../../api/endpoints";
import { IRoleBindingSubject, Role, RoleBinding, ServiceAccount } from "../../api/endpoints";
import { Icon } from "../icon";
import { Input } from "../input";
import { NamespaceSelect } from "../+namespaces/namespace-select";
@ -19,6 +19,7 @@ import { namespaceStore } from "../+namespaces/namespace.store";
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
import { roleBindingsStore } from "./role-bindings.store";
import { showDetails } from "../kube-object";
import { KubeObjectStore } from "../../kube-object.store";
interface BindingSelectOption extends SelectOption {
value: string; // binding name
@ -73,14 +74,14 @@ export class AddRoleBindingDialog extends React.Component<Props> {
};
async loadData() {
const stores = [
const stores: KubeObjectStore[] = [
namespaceStore,
rolesStore,
serviceAccountsStore,
];
this.isLoading = true;
await Promise.all(stores.map(store => store.loadAllFromContextNamespaces()));
await Promise.all(stores.map(store => store.reloadAll()));
this.isLoading = false;
}
@ -136,8 +137,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
roleBinding: this.roleBinding,
addSubjects: subjects,
});
}
else {
} else {
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
roleBinding = await roleBindingsStore.create({ name, namespace }, {
@ -265,7 +265,7 @@ export class AddRoleBindingDialog extends React.Component<Props> {
</h5>
);
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
const nextLabel = isEditing ? "Update" : "Create";
const nextLabel = isEditing ? "Update" : "Create";
return (
<Dialog

View File

@ -20,7 +20,7 @@ interface Props extends KubeObjectDetailsProps<CronJob> {
@observer
export class CronJobDetails extends React.Component<Props> {
async componentDidMount() {
jobStore.loadAllFromContextNamespaces();
jobStore.reloadAll();
}
render() {

View File

@ -30,7 +30,7 @@ export class DaemonSetDetails extends React.Component<Props> {
});
componentDidMount() {
podsStore.loadAllFromContextNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {

View File

@ -31,7 +31,7 @@ export class DeploymentDetails extends React.Component<Props> {
});
componentDidMount() {
podsStore.loadAllFromContextNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {

View File

@ -25,7 +25,7 @@ interface Props extends KubeObjectDetailsProps<Job> {
@observer
export class JobDetails extends React.Component<Props> {
async componentDidMount() {
podsStore.loadAllFromContextNamespaces();
podsStore.reloadAll();
}
render() {

View File

@ -29,7 +29,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
});
async componentDidMount() {
podsStore.loadAllFromContextNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {

View File

@ -30,7 +30,7 @@ export class StatefulSetDetails extends React.Component<Props> {
});
componentDidMount() {
podsStore.loadAllFromContextNamespaces();
podsStore.reloadAll();
}
componentWillUnmount() {

View File

@ -43,12 +43,13 @@ import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../exte
import { TabLayout, TabLayoutRoute } from "./layout/tab-layout";
import { StatefulSetScaleDialog } from "./+workloads-statefulsets/statefulset-scale-dialog";
import { eventStore } from "./+events/event.store";
import { namespaceStore } from "./+namespaces/namespace.store";
import { nodesStore } from "./+nodes/nodes.store";
import { podsStore } from "./+workloads-pods/pods.store";
import { kubeWatchApi } from "../api/kube-watch-api";
import { ReplicaSetScaleDialog } from "./+workloads-replicasets/replicaset-scale-dialog";
import { CommandContainer } from "./command-palette/command-container";
import { KubeObjectStore } from "../kube-object.store";
import { clusterContext } from "./context";
@observer
export class App extends React.Component {
@ -76,10 +77,9 @@ export class App extends React.Component {
});
whatInput.ask(); // Start to monitor user input device
await kubeWatchApi.init({
getCluster: () => getHostedCluster(),
getNamespaces: () => namespaceStore.contextNamespaces,
});
// Setup hosted cluster context
KubeObjectStore.defaultContext = clusterContext;
kubeWatchApi.context = clusterContext;
}
componentDidMount() {
@ -162,9 +162,9 @@ export class App extends React.Component {
const tabRoutes = this.getTabLayoutRoutes(menu);
if (tabRoutes.length > 0) {
const pageComponent = () => <TabLayout tabs={tabRoutes} />;
const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
route = <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)} />;
route = <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
this.extensionRoutes.set(menu, route);
} else {
const page = clusterPageRegistry.getByPageTarget(menu.target);
@ -228,7 +228,7 @@ export class App extends React.Component {
<StatefulSetScaleDialog/>
<ReplicaSetScaleDialog/>
<CronJobTriggerDialog/>
<CommandContainer cluster={cluster} />
<CommandContainer cluster={cluster}/>
</ErrorBoundary>
</Router>
);

View File

@ -0,0 +1,23 @@
import type { Cluster } from "../../main/cluster";
import { getHostedCluster } from "../../common/cluster-store";
import { namespaceStore } from "./+namespaces/namespace.store";
export interface ClusterContext {
cluster?: Cluster;
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
}
export const clusterContext: ClusterContext = {
get cluster(): Cluster | null {
return getHostedCluster();
},
get allNamespaces(): string[] {
return this.cluster?.allowedNamespaces ?? [];
},
get contextNamespaces(): string[] {
return namespaceStore.contextNamespaces ?? [];
},
};

View File

@ -40,7 +40,7 @@ interface Props {
@observer
export class Sidebar extends React.Component<Props> {
async componentDidMount() {
crdStore.loadAllFromContextNamespaces();
crdStore.reloadAll();
}
renderCustomResources() {

View File

@ -9,7 +9,7 @@ export interface ItemObject {
@autobind()
export abstract class ItemStore<T extends ItemObject = ItemObject> {
abstract loadAll(...args: any[]): Promise<void>;
abstract loadAll(...args: any[]): Promise<void | T[]>;
protected defaultSorting = (item: T) => item.getName();

View File

@ -1,7 +1,6 @@
import type { Cluster } from "../main/cluster";
import type { NamespaceStore } from "./components/+namespaces/namespace.store";
import type { ClusterContext } from "./components/context";
import { action, computed, observable, reaction } from "mobx";
import { action, observable, reaction, when } from "mobx";
import { autobind } from "./utils";
import { KubeObject } from "./api/kube-object";
import { IKubeWatchEvent, IKubeWatchMessage, kubeWatchApi } from "./api/kube-watch-api";
@ -17,44 +16,23 @@ export interface KubeObjectStoreLoadingParams {
@autobind()
export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemStore<T> {
@observable static defaultContext: ClusterContext; // TODO: support multiple cluster contexts
abstract api: KubeApi<T>;
public readonly limit?: number;
public readonly bufferSize: number = 50000;
@observable.ref protected cluster: Cluster;
contextReady = when(() => Boolean(this.context));
get context(): ClusterContext {
return KubeObjectStore.defaultContext;
}
constructor() {
super();
this.bindWatchEventsUpdater();
}
// TODO: detach / remove circular dependency
@observable.ref private namespaceStore: NamespaceStore;
protected async resolveNamespaceStore(): Promise<NamespaceStore> {
const { namespaceStore } = await import("./components/+namespaces/namespace.store");
this.namespaceStore = namespaceStore;
return namespaceStore;
}
protected async resolveCluster(): Promise<Cluster> {
const { getHostedCluster, clusterStore } = await import("../common/cluster-store");
await clusterStore.whenLoaded;
this.cluster = getHostedCluster();
await this.cluster.whenReady;
return this.cluster;
}
// TODO: figure out how to transparently replace with this.items
@computed get contextItems(): T[] {
const contextNamespaces = this.namespaceStore?.contextNamespaces ?? []; // not loaded
return this.items.filter((item: T) => !item.getNs() || contextNamespaces.includes(item.getId()));
}
get query(): IKubeApiQueryParams {
const { limit } = this;
@ -111,9 +89,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
const cluster = await this.resolveCluster();
if (cluster.isAllowedResource(api.kind)) {
if (this.context?.cluster.isAllowedResource(api.kind)) {
if (api.isNamespaced) {
return Promise
.all(namespaces.map(namespace => api.list({ namespace })))
@ -131,21 +107,24 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
@action
async loadAll(namespaces?: string[], { replace = false /*partial update*/ } = {}): Promise<void> {
async loadAll({ namespaces = [], merge = true } = {}): Promise<void | T[]> {
await this.contextReady;
this.isLoading = true;
try {
// load all available namespaces by default
if (!namespaces?.length) {
const namespaceStore = await this.resolveNamespaceStore();
namespaces = namespaceStore.allowedNamespaces; // load all by default if list not provided
if (!namespaces.length) {
namespaces = this.context.allNamespaces; // load all available namespaces by default
}
const items = await this.loadItems({ namespaces, api: this.api });
this.mergeItems(items, { replace });
this.isLoaded = true;
if (merge) {
this.mergeItems(items);
} else {
return items;
}
} catch (error) {
console.error("Loading store items failed", { error, store: this });
this.resetOnError(error);
@ -155,18 +134,28 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
@action
mergeItems(partialItems: T[], { replace = false, updateStore = true, sort = true, filter = true } = {}): T[] {
reloadAll(opts: { namespaces?: string[], merge?: boolean, force?: boolean } = {}) {
const { force = false, ...loadingOptions } = opts;
if (this.isLoading || (this.isLoaded && !force)) {
return;
}
return this.loadAll(loadingOptions);
}
@action
mergeItems(partialItems: T[], { replace = true, updateStore = true, sort = true, filter = true } = {}): T[] {
let items = partialItems;
// update existing items
if (!replace) {
items = this.items.toJS();
const partialIds = partialItems.map(item => item.getId());
partialItems.forEach(item => {
const index = items.findIndex(i => i.getId() === item.getId());
if (index < 0) items.push(item); // add
else items[index] = item; // update
});
items = [
...this.items.filter(existingItem => !partialIds.includes(existingItem.getId())),
...partialItems,
];
}
if (filter) items = this.filterItemsOnLoad(items);
@ -176,10 +165,6 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
return items;
}
async loadAllFromContextNamespaces(): Promise<void> {
return this.loadAll(this.namespaceStore?.contextNamespaces);
}
protected resetOnError(error: any) {
if (error) this.reset();
}