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

Fix: loading in namespaces under reduced RBAC

- don't assume that loading "all namespaces" that lens knows about is
  the same as loading "all namespaces" that exist on the cluster

- Track when actually listing namespaces

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-02-11 11:05:22 -05:00
parent f14f3b3287
commit aed51c1ab4
14 changed files with 90 additions and 41 deletions

View File

@ -49,6 +49,7 @@ export interface ClusterState {
allowedNamespaces: string[] allowedNamespaces: string[]
allowedResources: string[] allowedResources: string[]
isGlobalWatchEnabled: boolean; isGlobalWatchEnabled: boolean;
listedNamespaces: boolean;
} }
/** /**
@ -201,6 +202,15 @@ export class Cluster implements ClusterModel, ClusterState {
* @observable * @observable
*/ */
@observable allowedNamespaces: string[] = []; @observable allowedNamespaces: string[] = [];
/**
* Is true if `allowedNamespaces` was filled by a successful kubeAPI list
* namespaces request.
*
* @observable
*/
@observable listedNamespaces = false;
/** /**
* List of allowed resources * List of allowed resources
* *
@ -630,6 +640,7 @@ export class Cluster implements ClusterModel, ClusterState {
allowedNamespaces: this.allowedNamespaces, allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources, allowedResources: this.allowedResources,
isGlobalWatchEnabled: this.isGlobalWatchEnabled, isGlobalWatchEnabled: this.isGlobalWatchEnabled,
listedNamespaces: this.listedNamespaces,
}; };
return toJS(state, { return toJS(state, {
@ -669,6 +680,8 @@ export class Cluster implements ClusterModel, ClusterState {
protected async getAllowedNamespaces() { protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) { if (this.accessibleNamespaces.length) {
this.listedNamespaces = false;
return this.accessibleNamespaces; return this.accessibleNamespaces;
} }
@ -677,8 +690,11 @@ export class Cluster implements ClusterModel, ClusterState {
try { try {
const namespaceList = await api.listNamespace(); const namespaceList = await api.listNamespace();
this.listedNamespaces = true;
return namespaceList.body.items.map(ns => ns.metadata.name); return namespaceList.body.items.map(ns => ns.metadata.name);
} catch (error) { } catch (error) {
this.listedNamespaces = false;
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName); const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
if (ctx.namespace) return [ctx.namespace]; if (ctx.namespace) return [ctx.namespace];

View File

@ -64,36 +64,51 @@ export class KubeWatchApi {
let isUnsubscribed = false; let isUnsubscribed = false;
const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce }); const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce });
let preloading = preload && load(); let preloading: boolean | ReturnType<typeof load> = preload && load();
let cancelReloading: IReactionDisposer = noop; let cancelReloading: IReactionDisposer = noop;
let ac = new AbortController();
const subscribe = () => { const subscribe = async (signal: AbortSignal) => {
if (isUnsubscribed) return; if (isUnsubscribed || signal.aborted) return;
stores.forEach((store) => { for (const store of stores) {
unsubscribeList.push(store.subscribe()); if (!signal.aborted) {
}); unsubscribeList.push(await store.subscribe());
}
}
}; };
let subscribeP: Promise<void>;
if (preloading) { if (preloading) {
if (waitUntilLoaded) { if (waitUntilLoaded) {
preloading.loading.then(subscribe, error => { subscribeP = preloading.loading
this.log({ .then(() => subscribe(ac.signal))
message: new Error("Loading stores has failed"), .catch(error => {
meta: { stores, error, options: opts }, this.log({
message: new Error("Loading stores has failed"),
meta: { stores, error, options: opts },
});
}); });
});
} else { } else {
subscribe(); subscribeP = subscribe(ac.signal);
} }
// reload stores only for context namespaces change // reload stores only for context namespaces change
cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => { cancelReloading = reaction(() => this.context?.selectedNamespaces, namespaces => {
preloading?.cancelLoading(); if (typeof preloading === "object") {
unsubscribeList.forEach(unsubscribe => unsubscribe()); preloading.cancelLoading();
unsubscribeList.length = 0; }
preloading = load(namespaces); ac.abort();
preloading.loading.then(subscribe); subscribeP.then(() => {
unsubscribeList.forEach(unsubscribe => unsubscribe());
unsubscribeList.length = 0;
ac = new AbortController();
preloading = load(namespaces);
preloading.loading
.then(() => subscribeP = subscribe(ac.signal));
});
}, { }, {
equals: comparer.shallow, equals: comparer.shallow,
}); });
@ -104,9 +119,15 @@ export class KubeWatchApi {
if (isUnsubscribed) return; if (isUnsubscribed) return;
isUnsubscribed = true; isUnsubscribed = true;
cancelReloading(); cancelReloading();
preloading?.cancelLoading();
unsubscribeList.forEach(unsubscribe => unsubscribe()); if (typeof preloading === "object") {
unsubscribeList.length = 0; preloading.cancelLoading();
}
ac.abort();
subscribeP.then(() => {
unsubscribeList.forEach(unsubscribe => unsubscribe());
unsubscribeList.length = 0;
});
}; };
} }

View File

@ -74,7 +74,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
} }
async loadFromContextNamespaces(): Promise<void> { async loadFromContextNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.contextNamespaces); return this.loadAll(namespaceStore.selectedNamespaces);
} }
async loadItems(namespaces: string[]) { async loadItems(namespaces: string[]) {

View File

@ -13,7 +13,7 @@ import { namespaceStore } from "./namespace.store";
const Placeholder = observer((props: PlaceholderProps<any>) => { const Placeholder = observer((props: PlaceholderProps<any>) => {
const getPlaceholder = (): React.ReactNode => { const getPlaceholder = (): React.ReactNode => {
const namespaces = namespaceStore.contextNamespaces; const namespaces = namespaceStore.selectedNamespaces;
switch (namespaces.length) { switch (namespaces.length) {
case 0: case 0:

View File

@ -65,7 +65,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
} }
private autoLoadAllowedNamespaces(): IReactionDisposer { private autoLoadAllowedNamespaces(): IReactionDisposer {
return reaction(() => this.allowedNamespaces, namespaces => this.loadAll({ namespaces }), { return reaction(() => this.allowedNamespaces, namespaces => {
console.log("autoLoadAllowedNamespaces", namespaces);
return this.loadAll({ namespaces });
}, {
fireImmediately: true, fireImmediately: true,
equals: comparer.shallow, equals: comparer.shallow,
}); });
@ -98,7 +102,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
].flat())); ].flat()));
} }
@computed get contextNamespaces(): string[] { @computed get selectedNamespaces(): string[] {
const namespaces = Array.from(this.contextNs); const namespaces = Array.from(this.contextNs);
if (!namespaces.length) { if (!namespaces.length) {
@ -108,9 +112,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return namespaces; return namespaces;
} }
getSubscribeApis() { async getSubscribeApis() {
await this.contextReady;
// if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted // if user has given static list of namespaces let's not start watches because watch adds stuff that's not wanted
if (this.context?.cluster.accessibleNamespaces.length > 0) { if (this.context.cluster.accessibleNamespaces.length > 0) {
return []; return [];
} }

View File

@ -9,7 +9,7 @@ import { apiManager } from "../../api/api-manager";
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> { export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
api = clusterRoleBindingApi; api = clusterRoleBindingApi;
getSubscribeApis() { async getSubscribeApis() {
return [clusterRoleBindingApi, roleBindingApi]; return [clusterRoleBindingApi, roleBindingApi];
} }

View File

@ -7,7 +7,7 @@ import { apiManager } from "../../api/api-manager";
export class RolesStore extends KubeObjectStore<Role> { export class RolesStore extends KubeObjectStore<Role> {
api = clusterRoleApi; api = clusterRoleApi;
getSubscribeApis() { async getSubscribeApis() {
return [roleApi, clusterRoleApi]; return [roleApi, clusterRoleApi];
} }

View File

@ -26,7 +26,7 @@ export class OverviewStatuses extends React.Component {
@autobind() @autobind()
renderWorkload(resource: KubeResource): React.ReactElement { renderWorkload(resource: KubeResource): React.ReactElement {
const store = workloadStores[resource]; const store = workloadStores[resource];
const items = store.getAllByNs(namespaceStore.contextNamespaces); const items = store.getAllByNs(namespaceStore.selectedNamespaces);
return ( return (
<div className="workload" key={resource}> <div className="workload" key={resource}>

View File

@ -30,7 +30,7 @@ export class WorkloadsOverview extends React.Component<Props> {
jobStore, cronJobStore, eventStore, jobStore, cronJobStore, eventStore,
], { ], {
preload: true, preload: true,
namespaces: clusterContext.contextNamespaces, namespaces: clusterContext.selectedNamespaces,
}), }),
]); ]);
} }

View File

@ -5,7 +5,7 @@ import { namespaceStore } from "./+namespaces/namespace.store";
export interface ClusterContext { export interface ClusterContext {
cluster?: Cluster; cluster?: Cluster;
allNamespaces?: string[]; // available / allowed namespaces from cluster.ts allNamespaces?: string[]; // available / allowed namespaces from cluster.ts
contextNamespaces?: string[]; // selected by user (see: namespace-select.tsx) selectedNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
} }
export const clusterContext: ClusterContext = { export const clusterContext: ClusterContext = {
@ -17,7 +17,7 @@ export const clusterContext: ClusterContext = {
return this.cluster?.allowedNamespaces ?? []; return this.cluster?.allowedNamespaces ?? [];
}, },
get contextNamespaces(): string[] { get selectedNamespaces(): string[] {
return namespaceStore.contextNamespaces ?? []; return namespaceStore.selectedNamespaces ?? [];
}, },
}; };

View File

@ -140,7 +140,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
const stores = Array.from(new Set([store, ...dependentStores])); const stores = Array.from(new Set([store, ...dependentStores]));
// load context namespaces by default (see also: `<NamespaceSelectFilter/>`) // load context namespaces by default (see also: `<NamespaceSelectFilter/>`)
stores.forEach(store => store.loadAll(namespaceStore.contextNamespaces)); stores.forEach(store => store.loadAll(namespaceStore.selectedNamespaces));
} }
private filterCallbacks: { [type: string]: ItemsFilter } = { private filterCallbacks: { [type: string]: ItemsFilter } = {

View File

@ -28,7 +28,7 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
disposeOnUnmount(this, [ disposeOnUnmount(this, [
kubeWatchApi.subscribeStores(stores, { kubeWatchApi.subscribeStores(stores, {
preload: true, preload: true,
namespaces: clusterContext.contextNamespaces, namespaces: clusterContext.selectedNamespaces,
}) })
]); ]);
} }

View File

@ -1,7 +1,7 @@
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, Disposer } 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";
@ -35,7 +35,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
} }
@computed get contextItems(): T[] { @computed get contextItems(): T[] {
const namespaces = this.context?.contextNamespaces ?? []; const namespaces = this.context?.selectedNamespaces ?? [];
return this.items.filter(item => { return this.items.filter(item => {
const itemNamespace = item.getNs(); const itemNamespace = item.getNs();
@ -111,7 +111,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
const isLoadingAll = this.context.allNamespaces.every(ns => namespaces.includes(ns)); const isLoadingAll = this.context.allNamespaces.every(ns => namespaces.includes(ns));
if (isLoadingAll) { if (isLoadingAll && this.context.cluster?.listedNamespaces) {
this.loadedNamespaces = []; this.loadedNamespaces = [];
return api.list({}, this.query); return api.list({}, this.query);
@ -264,11 +264,15 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}); });
} }
getSubscribeApis(): KubeApi[] { async getSubscribeApis(): Promise<KubeApi[]> {
return [this.api]; return [this.api];
} }
subscribe(apis = this.getSubscribeApis()) { async subscribe(apis?: KubeApi[]): Promise<Disposer> {
await this.contextReady;
apis ??= await this.getSubscribeApis();
const abortController = new AbortController(); const abortController = new AbortController();
const namespaces = [...this.loadedNamespaces]; const namespaces = [...this.loadedNamespaces];

View File

@ -2,6 +2,8 @@
export const isElectron = !!navigator.userAgent.match(/Electron/); export const isElectron = !!navigator.userAgent.match(/Electron/);
export type Disposer = () => void;
export * from "../../common/utils"; export * from "../../common/utils";
export * from "./cssVar"; export * from "./cssVar";