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:
parent
f14f3b3287
commit
aed51c1ab4
@ -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];
|
||||
|
||||
@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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[]) {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { apiManager } from "../../api/api-manager";
|
||||
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||
api = clusterRoleBindingApi;
|
||||
|
||||
getSubscribeApis() {
|
||||
async getSubscribeApis() {
|
||||
return [clusterRoleBindingApi, roleBindingApi];
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import { apiManager } from "../../api/api-manager";
|
||||
export class RolesStore extends KubeObjectStore<Role> {
|
||||
api = clusterRoleApi;
|
||||
|
||||
getSubscribeApis() {
|
||||
async getSubscribeApis() {
|
||||
return [roleApi, clusterRoleApi];
|
||||
}
|
||||
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -30,7 +30,7 @@ export class WorkloadsOverview extends React.Component<Props> {
|
||||
jobStore, cronJobStore, eventStore,
|
||||
], {
|
||||
preload: true,
|
||||
namespaces: clusterContext.contextNamespaces,
|
||||
namespaces: clusterContext.selectedNamespaces,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -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 ?? [];
|
||||
},
|
||||
};
|
||||
|
||||
@ -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 } = {
|
||||
|
||||
@ -28,7 +28,7 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores(stores, {
|
||||
preload: true,
|
||||
namespaces: clusterContext.contextNamespaces,
|
||||
namespaces: clusterContext.selectedNamespaces,
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
export const isElectron = !!navigator.userAgent.match(/Electron/);
|
||||
|
||||
export type Disposer = () => void;
|
||||
|
||||
export * from "../../common/utils";
|
||||
|
||||
export * from "./cssVar";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user