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

View File

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

View File

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

View File

@ -65,7 +65,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}
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,
equals: comparer.shallow,
});
@ -98,7 +102,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
].flat()));
}
@computed get contextNamespaces(): string[] {
@computed get selectedNamespaces(): string[] {
const namespaces = Array.from(this.contextNs);
if (!namespaces.length) {
@ -108,9 +112,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
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 (this.context?.cluster.accessibleNamespaces.length > 0) {
if (this.context.cluster.accessibleNamespaces.length > 0) {
return [];
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ 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)
selectedNamespaces?: string[]; // selected by user (see: namespace-select.tsx)
}
export const clusterContext: ClusterContext = {
@ -17,7 +17,7 @@ export const clusterContext: ClusterContext = {
return this.cluster?.allowedNamespaces ?? [];
},
get contextNamespaces(): string[] {
return namespaceStore.contextNamespaces ?? [];
get selectedNamespaces(): string[] {
return namespaceStore.selectedNamespaces ?? [];
},
};

View File

@ -140,7 +140,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
const stores = Array.from(new Set([store, ...dependentStores]));
// 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 } = {

View File

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

View File

@ -1,7 +1,7 @@
import type { ClusterContext } from "./components/context";
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 { IKubeWatchEvent } from "./api/kube-watch-api";
import { ItemStore } from "./item.store";
@ -35,7 +35,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
}
@computed get contextItems(): T[] {
const namespaces = this.context?.contextNamespaces ?? [];
const namespaces = this.context?.selectedNamespaces ?? [];
return this.items.filter(item => {
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));
if (isLoadingAll) {
if (isLoadingAll && this.context.cluster?.listedNamespaces) {
this.loadedNamespaces = [];
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];
}
subscribe(apis = this.getSubscribeApis()) {
async subscribe(apis?: KubeApi[]): Promise<Disposer> {
await this.contextReady;
apis ??= await this.getSubscribeApis();
const abortController = new AbortController();
const namespaces = [...this.loadedNamespaces];

View File

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