Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: loading in namespaces under reduced RBAC #2130

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/common/utils/common-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Disposer = () => void;
1 change: 1 addition & 0 deletions src/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export * from "./downloadFile";
export * from "./escapeRegExp";
export * from "./tar";
export * from "./delay";
export * from "./common-types";
16 changes: 16 additions & 0 deletions src/main/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface ClusterState {
allowedNamespaces: string[]
allowedResources: string[]
isGlobalWatchEnabled: boolean;
listedNamespaces: boolean;
}

/**
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this when we have already cluster.accessibleNamespaces and cluster.allowedNamespaces?
If accessibleNamespaces is provided by user it must be used first.

Otherwise maybe better to use this as I understand it correct it serves same purpose?

  @computed get listedNamespaces(): boolean {
    return !this.accessibleNamespaces.length;
  }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because allowedNamespaces might also be from the context. See line 700

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tought allowedNamespaces must be only from the context? No?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or by listing via the kube api ( which I believe is the most common)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also there are some inconsistency between 2 methods imho:

  protected async getAllowedNamespaces() { // get allowed?
    if (this.accessibleNamespaces.length) {
      return this.accessibleNamespaces; // No, get accessible!
    }

  protected async getAllowedResources() {
    try {
      if (!this.allowedNamespaces.length) { // why not checking accessible as well?
        return [];
      }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, but if getAllowedResources is only ever called after getAllowedNamespaces it "just works"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are lots of things I would like to change to cluster.ts and the above it part of it but I don't think that it is in scope for this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like the idea of yet-another-public-observable because it will go to the extension API and is really hard to take away later. Any possibility that we could resolve this from the existing information (as we did before)?


/**
* List of allowed resources
*
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -669,6 +680,8 @@ export class Cluster implements ClusterModel, ClusterState {

protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) {
this.listedNamespaces = false;

return this.accessibleNamespaces;
}

Expand All @@ -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];
Expand Down
63 changes: 42 additions & 21 deletions src/renderer/api/kube-watch-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the correct typing of this value. I assume that because we aren't using strict: true it was typed as only ReturnType<typeof 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());
}
}
Comment on lines +74 to +78
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail quickly if we shouldn't load the stores

};

let subscribing: Promise<void> = Promise.resolve();

if (preloading) {
if (waitUntilLoaded) {
preloading.loading.then(subscribe, error => {
this.log({
message: new Error("Loading stores has failed"),
meta: { stores, error, options: opts },
subscribing = preloading.loading
.then(() => subscribe(ac.signal))
.catch(error => {
this.log({
message: new Error("Loading stores has failed"),
meta: { stores, error, options: opts },
});
});
});
} else {
subscribe();
subscribing = 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();
}
Comment on lines +99 to +101
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (typeof preloading === "object") {
preloading.cancelLoading();
}
preloading?.cancelLoading();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my note about the actually typing of preloading and why the old version was actually wrong.

ac.abort();
subscribing.then(() => {
unsubscribeList.forEach(unsubscribe => unsubscribe());
unsubscribeList.length = 0;
Comment on lines +104 to +105
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is only "full" after subscribeP has "finished"


ac = new AbortController();
preloading = load(namespaces);
preloading.loading
.then(() => subscribing = subscribe(ac.signal));
});
}, {
equals: comparer.shallow,
});
Expand All @@ -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();
subscribing.then(() => {
unsubscribeList.forEach(unsubscribe => unsubscribe());
unsubscribeList.length = 0;
});
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/+apps-releases/release.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 5 additions & 3 deletions src/renderer/components/+namespaces/namespace.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,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) {
Expand All @@ -108,9 +108,11 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return namespaces;
}

getSubscribeApis() {
async getSubscribeApis() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why it's became async?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we need to wait for contextReady

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) {
Comment on lines +111 to +115
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this to be actually correct we need to always wait for the context to be ready. Otherwise we would return an API if this function was called before the context was ready.

return [];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { apiManager } from "../../api/api-manager";
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
api = clusterRoleBindingApi;

getSubscribeApis() {
async getSubscribeApis() {
return [clusterRoleBindingApi, roleBindingApi];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { apiManager } from "../../api/api-manager";
export class RolesStore extends KubeObjectStore<Role> {
api = clusterRoleApi;

getSubscribeApis() {
async getSubscribeApis() {
return [roleApi, clusterRoleApi];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/+workloads-overview/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class WorkloadsOverview extends React.Component<Props> {
jobStore, cronJobStore, eventStore,
], {
preload: true,
namespaces: clusterContext.contextNamespaces,
namespaces: clusterContext.selectedNamespaces,
}),
]);
}
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can revert this change if wanted. But contextNamespaces is a very not descriptive name IMO.

}

export const clusterContext: ClusterContext = {
Expand All @@ -17,7 +17,7 @@ export const clusterContext: ClusterContext = {
return this.cluster?.allowedNamespaces ?? [];
},

get contextNamespaces(): string[] {
return namespaceStore.contextNamespaces ?? [];
get selectedNamespaces(): string[] {
return namespaceStore.selectedNamespaces ?? [];
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class KubeObjectListLayout extends React.Component<KubeObjectListLayoutPr
disposeOnUnmount(this, [
kubeWatchApi.subscribeStores(stores, {
preload: true,
namespaces: clusterContext.contextNamespaces,
namespaces: clusterContext.selectedNamespaces,
})
]);
}
Expand Down
14 changes: 9 additions & 5 deletions src/renderer/kube-object.store.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why moving to async?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my note


await this.contextReady;
apis ??= await this.getSubscribeApis();

const abortController = new AbortController();
const namespaces = [...this.loadedNamespaces];

Expand Down