Cleanup 'Cluster' to remove environment specific details (#6951)

- requestNamespaceListPermissions is infallable so no need to have the extra try/catch

- Refactor isMetricHidden method away from Cluster

- Refactor shouldShowResource out of Cluster

- Refactor isInLocalKubeconfig out of Cluster

- Remove depecrated and unused workspace from Cluster

- Refactor out kubectl as a dependency of Cluster

- Remove from cluster getter used only once

- Split out ClusterConnection from Cluster

- Also split out KubeAuthProxyServer from ContextHandler

- Rename ContextHandler to PrometheusHandler

- Cleanup onNetworkOffline/Online impls within ClusterManager

- Remove annotations from ClusterConnection

- Remove mobx annotations from Cluster

- Rename loadConfigFromFileInjectable

- Remove all uses of dead createClusterInjectionToken

- Fix type errors related to broadcastConnectionUpdate

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton authored and GitHub committed 2023-03-10 09:37:39 +02:00
1 parent e6d6d1d8f7
commit ad9bafe2a5
133 files changed
+2544 -1961

No files matched your search

@@ -3,20 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import type { Cluster } from "../../../../common/cluster/cluster";
import catalogEntityRegistryInjectable from "./registry.injectable";
export type GetActiveClusterEntity = () => Cluster | undefined;
const getActiveClusterEntityInjectable = getInjectable({
id: "get-active-cluster-entity",
instantiate: (di): GetActiveClusterEntity => {
const activeEntityInternalClusterInjectable = getInjectable({
id: "active-entity-internal-cluster",
instantiate: (di) => {
const store = di.inject(clusterStoreInjectable);
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
return () => store.getById(entityRegistry.activeEntity?.getId());
return computed(() => store.getById(entityRegistry.activeEntity?.getId()));
},
});
export default getActiveClusterEntityInjectable;
export default activeEntityInternalClusterInjectable;
@@ -5,14 +5,22 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import type { ClusterMetricsResourceType } from "../../../../common/cluster-types";
import getActiveClusterEntityInjectable from "./get-active-cluster-entity.injectable";
import activeEntityInternalClusterInjectable from "./get-active-cluster-entity.injectable";
const enabledMetricsInjectable = getInjectable({
id: "enabled-metrics",
instantiate: (di, kind) => {
const getActiveClusterEntity = di.inject(getActiveClusterEntityInjectable);
const activeEntityInternalCluster = di.inject(activeEntityInternalClusterInjectable);
return computed(() => !getActiveClusterEntity()?.isMetricHidden(kind));
return computed(() => {
const cluster = activeEntityInternalCluster.get();
if (!cluster?.preferences.hiddenMetrics) {
return false;
}
return cluster.preferences.hiddenMetrics.includes(kind);
});
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, kind: ClusterMetricsResourceType) => kind,
@@ -5,8 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import readFileInjectable from "../../../common/fs/read-file.injectable";
import { loadConfigFromString } from "../../../common/kube-helpers";
import loadKubeconfigInjectable from "../../../common/cluster/load-kubeconfig.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import openDeleteClusterDialogInjectable from "../../components/delete-cluster-dialog/open.injectable";
import { beforeFrameStartsSecondInjectionToken } from "../tokens";
@@ -18,7 +17,6 @@ const setupKubernetesClusterContextMenuOpenInjectable = getInjectable({
run: () => {
const catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable);
const openDeleteClusterDialog = di.inject(openDeleteClusterDialogInjectable);
const readFile = di.inject(readFileInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
const logger = di.inject(loggerInjectable);
@@ -37,7 +35,9 @@ const setupKubernetesClusterContextMenuOpenInjectable = getInjectable({
return logger.warn("[KUBERNETES-CLUSTER]: cannot delete cluster, does not exist in store", { clusterId });
}
const result = loadConfigFromString(await readFile(cluster.kubeConfigPath));
const loadKubeconfig = di.inject(loadKubeconfigInjectable, cluster);
const result = await loadKubeconfig(true);
if (result.error) {
logger.error("[KUBERNETES-CLUSTER]: failed to parse kubeconfig file", result.error);
@@ -55,7 +55,7 @@ const clusterFrameContextForNamespacedResourcesInjectable = getInjectable({
&& cluster.accessibleNamespaces.length === 0
&& allNamespaces.get().every(ns => namespaces.includes(ns))
),
isGlobalWatchEnabled: () => cluster.isGlobalWatchEnabled,
isGlobalWatchEnabled: () => cluster.isGlobalWatchEnabled.get(),
get allNamespaces() {
return allNamespaces.get();
},
@@ -15,7 +15,7 @@ const shouldShowResourceInjectable = getInjectable({
const cluster = di.inject(hostedClusterInjectable);
return cluster
? computed(() => cluster.shouldShowResource(resource))
? computed(() => cluster.resourcesToShow.has(formatKubeApiResource(resource)))
: computed(() => false);
},
injectionToken: shouldShowResourceInjectionToken,
@@ -1,46 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterDependencies } from "../../common/cluster/cluster";
import { Cluster } from "../../common/cluster/cluster";
import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import loggerInjectable from "../../common/logger.injectable";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable";
const createClusterInjectable = getInjectable({
id: "create-cluster",
instantiate: (di) => {
const dependencies: ClusterDependencies = {
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
logger: di.inject(loggerInjectable),
broadcastMessage: di.inject(broadcastMessageInjectable),
loadConfigfromFile: di.inject(loadConfigfromFileInjectable),
// TODO: Dismantle wrong abstraction
// Note: "as never" to get around strictness in unnatural scenario
createKubeconfigManager: () => undefined as never,
createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");},
createContextHandler: () => undefined as never,
createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
requestNamespaceListPermissionsFor: () => { throw new Error("Tried to access back-end feature in front-end."); },
createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); },
requestApiResources: () => { throw new Error("Tried to access back-end feature in front-end."); },
detectClusterMetadata: () => { throw new Error("Tried to access back-end feature in front-end."); },
clusterVersionDetector: {
detect: () => { throw new Error("Tried to access back-end feature in front-end."); },
key: "irrelavent",
},
};
return (model, configData) => new Cluster(dependencies, model, configData);
},
injectionToken: createClusterInjectionToken,
});
export default createClusterInjectable;
@@ -6,6 +6,7 @@
import styles from "./cluster-overview.module.scss";
import React from "react";
import type { IComputedValue } from "mobx";
import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import type { NodeStore } from "../+nodes/store";
@@ -22,32 +23,30 @@ import type { EventStore } from "../+events/store";
import { withInjectables } from "@ogre-tools/injectable-react";
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api";
import type { Cluster } from "../../../common/cluster/cluster";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import assert from "assert";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import podStoreInjectable from "../+workloads-pods/store.injectable";
import eventStoreInjectable from "../+events/store.injectable";
import nodeStoreInjectable from "../+nodes/store.injectable";
import enabledMetricsInjectable from "../../api/catalog/entity/metrics-enabled.injectable";
interface Dependencies {
subscribeStores: SubscribeStores;
clusterOverviewStore: ClusterOverviewStore;
hostedCluster: Cluster;
podStore: PodStore;
eventStore: EventStore;
nodeStore: NodeStore;
clusterMetricsAreVisible: IComputedValue<boolean>;
}
@observer
class NonInjectedClusterOverview extends React.Component<Dependencies> {
private metricPoller = interval(60, () => this.loadMetrics());
loadMetrics() {
if (this.props.hostedCluster.available) {
this.props.clusterOverviewStore.loadMetrics();
private readonly metricPoller = interval(60, async () => {
try {
await this.props.clusterOverviewStore.loadMetrics();
} catch {
// ignore
}
}
});
componentDidMount() {
this.metricPoller.start(true);
@@ -97,14 +96,13 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
}
render() {
const { eventStore, nodeStore, hostedCluster } = this.props;
const { eventStore, nodeStore, clusterMetricsAreVisible } = this.props;
const isLoaded = nodeStore.isLoaded && eventStore.isLoaded;
const isMetricHidden = hostedCluster.isMetricHidden(ClusterMetricsResourceType.Cluster);
return (
<TabLayout scrollable>
<div className={styles.ClusterOverview} data-testid="cluster-overview-page">
{this.renderClusterOverview(isLoaded, isMetricHidden)}
{this.renderClusterOverview(isLoaded, clusterMetricsAreVisible.get())}
</div>
</TabLayout>
);
@@ -112,18 +110,12 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
}
export const ClusterOverview = withInjectables<Dependencies>(NonInjectedClusterOverview, {
getProps: (di) => {
const hostedCluster = di.inject(hostedClusterInjectable);
assert(hostedCluster, "Only allowed to renderer ClusterOverview within cluster frame");
return {
subscribeStores: di.inject(subscribeStoresInjectable),
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
hostedCluster,
podStore: di.inject(podStoreInjectable),
eventStore: di.inject(eventStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable),
};
},
getProps: (di) => ({
subscribeStores: di.inject(subscribeStoresInjectable),
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
clusterMetricsAreVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Cluster),
podStore: di.inject(podStoreInjectable),
eventStore: di.inject(eventStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable),
}),
});
@@ -11,8 +11,8 @@ import { renderFor } from "../../test-utils/renderFor";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import hostedClusterInjectable from "../../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../../cluster/create-cluster.injectable";
import directoryForKubeConfigsInjectable from "../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { Cluster } from "../../../../common/cluster/cluster";
jest.mock("../../kube-object-meta/kube-object-meta", () => ({
KubeObjectMeta: () => null,
@@ -27,9 +27,7 @@ describe("SecretDetails tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -11,12 +11,12 @@ import { fireEvent } from "@testing-library/react";
import React from "react";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Cluster } from "../../../common/cluster/cluster";
import type { Fetch } from "../../../common/fetch/fetch.injectable";
import fetchInjectable from "../../../common/fetch/fetch.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import { createMockResponseFromString } from "../../../test-utils/mock-responses";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
@@ -58,9 +58,7 @@ describe("<NamespaceSelectFilter />", () => {
fetchMock = asyncFn();
di.override(fetchInjectable, () => fetchMock);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -7,9 +7,9 @@ import type { DiContainer } from "@ogre-tools/injectable";
import { observable } from "mobx";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Cluster } from "../../../common/cluster/cluster";
import { Namespace } from "../../../common/k8s-api/endpoints";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { NamespaceStore } from "./store";
@@ -111,9 +111,7 @@ describe("NamespaceStore", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -15,11 +15,11 @@ import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be-
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../../../cluster/create-cluster.injectable";
import type { CloseClusterRoleBindingDialog } from "../dialog/close.injectable";
import closeClusterRoleBindingDialogInjectable from "../dialog/close.injectable";
import type { OpenClusterRoleBindingDialog } from "../dialog/open.injectable";
import openClusterRoleBindingDialogInjectable from "../dialog/open.injectable";
import { Cluster } from "../../../../../common/cluster/cluster";
describe("ClusterRoleBindingDialog tests", () => {
let render: DiRender;
@@ -36,9 +36,7 @@ describe("ClusterRoleBindingDialog tests", () => {
closeClusterRoleBindingDialog = di.inject(closeClusterRoleBindingDialogInjectable);
openClusterRoleBindingDialog = di.inject(openClusterRoleBindingDialogInjectable);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -15,9 +15,9 @@ import clusterRoleStoreInjectable from "../../+cluster-roles/store.injectable";
import storesAndApisCanBeCreatedInjectable from "../../../../stores-apis-can-be-created.injectable";
import directoryForKubeConfigsInjectable from "../../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../../../cluster/create-cluster.injectable";
import type { OpenRoleBindingDialog } from "../dialog/open.injectable";
import openRoleBindingDialogInjectable from "../dialog/open.injectable";
import { Cluster } from "../../../../../common/cluster/cluster";
describe("RoleBindingDialog tests", () => {
let render: DiRender;
@@ -32,9 +32,7 @@ describe("RoleBindingDialog tests", () => {
openRoleBindingDialog = di.inject(openRoleBindingDialogInjectable);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -23,8 +23,6 @@ import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import daemonSetStoreInjectable from "./store.injectable";
import podStoreInjectable from "../+workloads-pods/store.injectable";
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
import requestPodMetricsForDaemonSetsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-pod-metrics-for-daemon-sets.injectable";
import loggerInjectable from "../../../common/logger.injectable";
export interface DaemonSetDetailsProps extends KubeObjectDetailsProps<DaemonSet> {
@@ -109,8 +107,6 @@ export const DaemonSetDetails = withInjectables<Dependencies, DaemonSetDetailsPr
subscribeStores: di.inject(subscribeStoresInjectable),
daemonSetStore: di.inject(daemonSetStoreInjectable),
podStore: di.inject(podStoreInjectable),
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
requestPodMetricsForDaemonSets: di.inject(requestPodMetricsForDaemonSetsInjectable),
logger: di.inject(loggerInjectable),
}),
});
@@ -22,8 +22,8 @@ import type { PortForwardStore } from "../../port-forward";
import { disposeOnUnmount, observer } from "mobx-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable";
import type { GetActiveClusterEntity } from "../../api/catalog/entity/get-active-cluster-entity.injectable";
import getActiveClusterEntityInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
import type { IComputedValue } from "mobx";
import enabledMetricsInjectable from "../../api/catalog/entity/metrics-enabled.injectable";
export interface PodDetailsContainerProps {
pod: Pod;
@@ -33,7 +33,7 @@ export interface PodDetailsContainerProps {
interface Dependencies {
portForwardStore: PortForwardStore;
getActiveClusterEntity: GetActiveClusterEntity;
containerMetricsVisible: IComputedValue<boolean>;
}
@observer
@@ -84,7 +84,7 @@ class NonInjectedPodDetailsContainer extends React.Component<PodDetailsContainer
}
render() {
const { pod, container, metrics, getActiveClusterEntity } = this.props;
const { pod, container, metrics, containerMetricsVisible } = this.props;
if (!pod || !container) return null;
const { name, image, imagePullPolicy, ports, volumeMounts, command, args } = container;
@@ -97,7 +97,7 @@ class NonInjectedPodDetailsContainer extends React.Component<PodDetailsContainer
const readiness = pod.getReadinessProbe(container);
const startup = pod.getStartupProbe(container);
const isInitContainer = !!pod.getInitContainers().find(c => c.name == name);
const isMetricHidden = getActiveClusterEntity()?.isMetricHidden(ClusterMetricsResourceType.Container);
const isMetricHidden = containerMetricsVisible.get();
return (
<div className="PodDetailsContainer">
@@ -217,6 +217,6 @@ export const PodDetailsContainer = withInjectables<Dependencies, PodDetailsConta
getProps: (di, props) => ({
...props,
portForwardStore: di.inject(portForwardStoreInjectable),
getActiveClusterEntity: di.inject(getActiveClusterEntityInjectable),
containerMetricsVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Container),
}),
});
@@ -10,7 +10,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
const scheduledCronJob = new CronJob({
apiVersion: "foo",
@@ -127,9 +127,7 @@ describe("CronJob Store tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
const runningDaemonSet = new DaemonSet({
apiVersion: "foo",
@@ -144,9 +144,7 @@ describe("DaemonSet Store tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -14,7 +14,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
const spec: PodSpec = {
containers: [{
@@ -216,9 +216,7 @@ describe("Deployment Store tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
const runningJob = new Job({
apiVersion: "foo",
@@ -181,9 +181,7 @@ describe("Job Store tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -11,7 +11,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
const runningPod = new Pod({
apiVersion: "foo",
@@ -127,9 +127,7 @@ describe("Pod Store tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
const runningReplicaSet = new ReplicaSet({
apiVersion: "foo",
@@ -144,9 +144,7 @@ describe("ReplicaSet Store tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -13,7 +13,7 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
const runningStatefulSet = new StatefulSet({
apiVersion: "foo",
@@ -144,9 +144,7 @@ describe("StatefulSet Store tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -56,7 +56,7 @@ export class ClusterFrameHandler {
const iframe = document.createElement("iframe");
iframe.id = `cluster-frame-${cluster.id}`;
iframe.name = cluster.contextName;
iframe.name = cluster.contextName.get();
iframe.setAttribute("src", getClusterFrameUrl(clusterId));
iframe.addEventListener("load", action(() => {
this.dependencies.logger.info(`[LENS-VIEW]: frame for clusterId=${clusterId} has loaded`);
@@ -71,19 +71,19 @@ export class ClusterFrameHandler {
this.dependencies.logger.info(`[LENS-VIEW]: waiting cluster to be ready, clusterId=${clusterId}`);
const dispose = when(
() => cluster.ready,
() => cluster.ready.get(),
() => this.dependencies.logger.info(`[LENS-VIEW]: cluster is ready, clusterId=${clusterId}`),
);
when(
// cluster.disconnect is set to `false` when the cluster starts to connect
() => !cluster.disconnected,
() => !cluster.disconnected.get(),
() => {
when(
() => {
const cluster = this.dependencies.getClusterById(clusterId);
return Boolean(!cluster || (cluster.disconnected && this.views.get(clusterId)?.isLoaded));
return Boolean(!cluster || (cluster.disconnected.get() && this.views.get(clusterId)?.isLoaded));
},
() => {
this.dependencies.logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
@@ -126,7 +126,7 @@ export class ClusterFrameHandler {
() => {
const view = this.views.get(clusterId);
if (cluster.available && cluster.ready && view?.isLoaded) {
if (cluster.available.get() && cluster.ready.get() && view?.isLoaded) {
return view;
}
@@ -158,7 +158,7 @@ class NonInjectedClusterStatus extends React.Component<ClusterStatusProps & Depe
return (
<div className={cssNames(styles.status, "flex column box center align-center justify-center", this.props.className)}>
<div className="flex items-center column gaps">
<h2>{this.entity?.getName() ?? this.cluster.name}</h2>
<h2>{this.entity?.getName() ?? this.cluster.name.get()}</h2>
{this.renderStatusIcon()}
{this.renderAuthenticationOutput()}
{this.renderReconnectionHelp()}
@@ -53,7 +53,11 @@ class NonInjectedClusterView extends React.Component<Dependencies> {
@computed get isReady(): boolean {
const { cluster } = this;
return (cluster?.ready && cluster?.available && this.isViewLoaded.get()) ?? false;
if (!cluster) {
return false;
}
return cluster.ready.get() && cluster.available.get() && this.isViewLoaded.get();
}
componentDidMount() {
@@ -8,17 +8,19 @@ import { waitFor } from "@testing-library/react";
import { ClusterLocalTerminalSetting } from "../local-terminal-settings";
import userEvent from "@testing-library/user-event";
import type { Stats } from "fs";
import type { Cluster } from "../../../../common/cluster/cluster";
import { Cluster } from "../../../../common/cluster/cluster";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import showErrorNotificationInjectable from "../../notifications/show-error-notification.injectable";
import statInjectable from "../../../../common/fs/stat.injectable";
import loadKubeconfigInjectable from "../../../../common/cluster/load-kubeconfig.injectable";
describe("ClusterLocalTerminalSettings", () => {
let render: DiRender;
let showErrorNotificationMock: jest.Mock;
let statMock: jest.Mock;
let loadKubeconfigMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting();
@@ -34,27 +36,28 @@ describe("ClusterLocalTerminalSettings", () => {
() => showErrorNotificationMock,
);
loadKubeconfigMock = jest.fn();
di.override(loadKubeconfigInjectable, () => loadKubeconfigMock);
render = renderFor(di);
jest.resetAllMocks();
});
it("should render without errors", () => {
const dom = render(<ClusterLocalTerminalSetting cluster={null as never}/>);
expect(dom.container).toBeInstanceOf(HTMLElement);
});
it("should render the current settings", async () => {
const cluster = {
loadKubeconfigMock.mockImplementation(() => ({
getContextObject: () => ({}),
}));
const cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
preferences: {
terminalCWD: "/foobar",
defaultNamespace: "kube-system",
},
getKubeconfig: jest.fn(() => ({
getContextObject: jest.fn(() => ({})),
})),
} as unknown as Cluster;
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
expect(await dom.findByDisplayValue("/foobar")).toBeDefined();
@@ -62,16 +65,21 @@ describe("ClusterLocalTerminalSettings", () => {
});
it("should change placeholder for 'Default Namespace' to be the namespace from the kubeconfig", async () => {
const cluster = {
loadKubeconfigMock.mockImplementation(() => ({
getContextObject: () => ({ namespace: "blat" }),
}));
const cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
preferences: {
terminalCWD: "/foobar",
},
getKubeconfig: jest.fn(() => ({
getContextObject: jest.fn(() => ({
namespace: "blat",
})),
})),
} as unknown as Cluster;
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
expect(await dom.findByDisplayValue("/foobar")).toBeDefined();
@@ -79,14 +87,20 @@ describe("ClusterLocalTerminalSettings", () => {
});
it("should save the new default namespace after clicking away", async () => {
const cluster = {
loadKubeconfigMock.mockImplementation(() => ({
getContextObject: () => ({}),
}));
const cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
preferences: {
terminalCWD: "/foobar",
},
getKubeconfig: jest.fn(() => ({
getContextObject: jest.fn(() => ({})),
})),
} as unknown as Cluster;
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
const dn = await dom.findByTestId("default-namespace");
@@ -107,11 +121,17 @@ describe("ClusterLocalTerminalSettings", () => {
} as Stats;
});
const cluster = {
getKubeconfig: jest.fn(() => ({
getContextObject: jest.fn(() => ({})),
})),
} as unknown as Cluster;
loadKubeconfigMock.mockImplementation(() => ({
getContextObject: () => ({}),
}));
const cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
const dn = await dom.findByTestId("working-directory");
@@ -133,11 +153,17 @@ describe("ClusterLocalTerminalSettings", () => {
} as Stats;
});
const cluster = {
getKubeconfig: jest.fn(() => ({
getContextObject: jest.fn(() => ({})),
})),
} as unknown as Cluster;
loadKubeconfigMock.mockImplementation(() => ({
getContextObject: () => ({}),
}));
const cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
const dn = await dom.findByTestId("working-directory");
@@ -19,7 +19,7 @@ export class ClusterKubeconfig extends React.Component<ClusterKubeconfigProps> {
openKubeconfig = () => {
const { cluster } = this.props;
shell.showItemInFolder(cluster.kubeConfigPath);
shell.showItemInFolder(cluster.kubeConfigPath.get());
};
render() {
@@ -27,7 +27,9 @@ export class ClusterKubeconfig extends React.Component<ClusterKubeconfigProps> {
<Notice className="mb-14 mt-3">
<SubTitle title="Kubeconfig" />
<span>
<a className="link value" onClick={this.openKubeconfig}>{this.props.cluster.kubeConfigPath}</a>
<a className="link value" onClick={this.openKubeconfig}>
{this.props.cluster.kubeConfigPath.get()}
</a>
</span>
</Notice>
);
@@ -0,0 +1,48 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { Cluster } from "../../../common/cluster/cluster";
import loadKubeconfigInjectable from "../../../common/cluster/load-kubeconfig.injectable";
export interface LocalTerminalSettingPresenter {
readonly directory: {
get: () => string;
set: (value: string) => void;
};
readonly defaultNamespace: {
get: () => string;
set: (value: string) => void;
};
readonly placeholderDefaultNamespace: string;
}
const localTerminalSettingPresenterInjectable = getInjectable({
id: "local-terminal-setting-presenter",
instantiate: async (di, cluster: Cluster): Promise<LocalTerminalSettingPresenter> => {
const loadKubeconfig = di.inject(loadKubeconfigInjectable, cluster);
const kubeconfig = await loadKubeconfig();
const directory = observable.box(cluster.preferences.terminalCWD || "");
const defaultNamespace = observable.box(cluster.preferences.defaultNamespace || "");
const placeholderDefaultNamespace = kubeconfig.getContextObject(cluster.contextName.get())?.namespace || "default";
return {
directory: {
get: () => directory.get(),
set: (value) => directory.set(value),
},
defaultNamespace: {
get: () => defaultNamespace.get(),
set: (value) => defaultNamespace.set(value),
},
placeholderDefaultNamespace,
};
},
lifecycle: lifecycleEnum.transient,
});
export default localTerminalSettingPresenterInjectable;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React, { useEffect, useState } from "react";
import React from "react";
import { observer } from "mobx-react";
import type { Cluster } from "../../../common/cluster/cluster";
import { Input } from "../input";
@@ -20,6 +20,10 @@ import Gutter from "../gutter/gutter";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import type { OpenPathPickingDialog } from "../../../features/path-picking-dialog/renderer/pick-paths.injectable";
import openPathPickingDialogInjectable from "../../../features/path-picking-dialog/renderer/pick-paths.injectable";
import type { LocalTerminalSettingPresenter } from "./local-terminal-setting-presenter.injectable";
import localTerminalSettingPresenterInjectable from "./local-terminal-setting-presenter.injectable";
import { Spinner } from "../spinner";
import { action, runInAction } from "mobx";
export interface ClusterLocalTerminalSettingProps {
cluster: Cluster;
@@ -30,70 +34,57 @@ interface Dependencies {
resolveTilde: ResolveTilde;
openPathPickingDialog: OpenPathPickingDialog;
isWindows: boolean;
presenter: LocalTerminalSettingPresenter;
}
const NonInjectedClusterLocalTerminalSetting = observer(({
cluster,
showErrorNotification,
validateDirectory,
resolveTilde,
isWindows,
openPathPickingDialog,
}: Dependencies & ClusterLocalTerminalSettingProps) => {
if (!cluster) {
return null;
}
const [directory, setDirectory] = useState<string>(cluster.preferences?.terminalCWD || "");
const [defaultNamespace, setDefaultNamespaces] = useState<string>(cluster.preferences?.defaultNamespace || "");
const [placeholderDefaultNamespace, setPlaceholderDefaultNamespace] = useState("default");
useEffect(() => {
(async () => {
const kubeconfig = await cluster.getKubeconfig();
const { namespace } = kubeconfig.getContextObject(cluster.contextName) ?? {};
if (namespace) {
setPlaceholderDefaultNamespace(namespace);
}
})();
setDirectory(cluster.preferences?.terminalCWD || "");
setDefaultNamespaces(cluster.preferences?.defaultNamespace || "");
}, [cluster]);
const NonInjectedClusterLocalTerminalSetting = observer((props: Dependencies & ClusterLocalTerminalSettingProps) => {
const {
cluster,
showErrorNotification,
validateDirectory,
resolveTilde,
isWindows,
openPathPickingDialog,
presenter,
} = props;
const commitDirectory = async (directory: string) => {
cluster.preferences ??= {};
if (!directory) {
cluster.preferences.terminalCWD = undefined;
} else {
const dir = resolveTilde(directory);
const result = await validateDirectory(dir);
runInAction(() => {
cluster.preferences.terminalCWD = undefined;
});
if (!result.callWasSuccessful) {
showErrorNotification(
<>
<b>Terminal Working Directory</b>
<p>
{"Your changes were not saved because "}
{result.error}
</p>
</>,
);
} else {
cluster.preferences.terminalCWD = dir;
setDirectory(dir);
}
return;
}
const dir = resolveTilde(directory);
const result = await validateDirectory(dir);
if (result.callWasSuccessful) {
runInAction(() => {
cluster.preferences.terminalCWD = dir;
presenter.directory.set(dir);
});
return;
}
showErrorNotification(
<>
<b>Terminal Working Directory</b>
<p>
{"Your changes were not saved because "}
{result.error}
</p>
</>,
);
};
const commitDefaultNamespace = () => {
cluster.preferences ??= {};
cluster.preferences.defaultNamespace = defaultNamespace || undefined;
};
const commitDefaultNamespace = action(() => {
cluster.preferences.defaultNamespace = presenter.defaultNamespace.get() || undefined;
});
const setAndCommitDirectory = (newPath: string) => {
setDirectory(newPath);
presenter.directory.set(newPath);
commitDirectory(newPath);
};
@@ -112,15 +103,15 @@ const NonInjectedClusterLocalTerminalSetting = observer(({
<SubTitle title="Working Directory"/>
<Input
theme="round-black"
value={directory}
value={presenter.directory.get()}
data-testid="working-directory"
onChange={setDirectory}
onBlur={() => commitDirectory(directory)}
onChange={value => presenter.directory.set(value)}
onBlur={() => commitDirectory(presenter.directory.get())}
placeholder={isWindows ? "$USERPROFILE" : "$HOME"}
iconRight={(
<>
{
directory && (
presenter.directory.get() && (
<Icon
material="close"
title="Clear"
@@ -151,10 +142,10 @@ const NonInjectedClusterLocalTerminalSetting = observer(({
<Input
theme="round-black"
data-testid="default-namespace"
value={defaultNamespace}
onChange={setDefaultNamespaces}
value={presenter.defaultNamespace.get()}
onChange={value => presenter.defaultNamespace.set(value)}
onBlur={commitDefaultNamespace}
placeholder={placeholderDefaultNamespace}
placeholder={presenter.placeholderDefaultNamespace}
/>
<small className="hint">
Default namespace used for kubectl.
@@ -165,12 +156,14 @@ const NonInjectedClusterLocalTerminalSetting = observer(({
});
export const ClusterLocalTerminalSetting = withInjectables<Dependencies, ClusterLocalTerminalSettingProps>(NonInjectedClusterLocalTerminalSetting, {
getProps: (di, props) => ({
getPlaceholder: () => <Spinner center />,
getProps: async (di, props) => ({
...props,
showErrorNotification: di.inject(showErrorNotificationInjectable),
validateDirectory: di.inject(validateDirectoryInjectable),
resolveTilde: di.inject(resolveTildeInjectable),
isWindows: di.inject(isWindowsInjectable),
openPathPickingDialog: di.inject(openPathPickingDialogInjectable),
presenter: await di.inject(localTerminalSettingPresenterInjectable, props.cluster),
}),
});
@@ -4,7 +4,7 @@
*/
import type { Cluster } from "../../../common/cluster/cluster";
import { makeObservable, observable } from "mobx";
import { makeObservable, observable, runInAction } from "mobx";
import { SubTitle } from "../layout/sub-title";
import React from "react";
import { Input } from "../input/input";
@@ -28,9 +28,10 @@ export class ClusterNodeShellSetting extends React.Component<ClusterNodeShellSet
}
componentWillUnmount() {
this.props.cluster.preferences ??= {};
this.props.cluster.preferences.nodeShellImage = this.nodeShellImage || undefined;
this.props.cluster.preferences.imagePullSecret = this.imagePullSecret || undefined;
runInAction(() => {
this.props.cluster.preferences.nodeShellImage = this.nodeShellImage || undefined;
this.props.cluster.preferences.imagePullSecret = this.imagePullSecret || undefined;
});
}
render() {
@@ -6,5 +6,5 @@ import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../../common/cluster/cluster";
export function isCurrentContext(config: KubeConfig, cluster: Cluster) {
return config.currentContext == cluster.contextName;
return config.currentContext == cluster.contextName.get();
}
@@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
export type IsInLocalKubeconfig = (cluster: Cluster) => boolean;
const isInLocalKubeconfigInjectable = getInjectable({
id: "is-in-local-kubeconfig",
instantiate: (di): IsInLocalKubeconfig => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
return cluster => cluster.kubeConfigPath.get().startsWith(directoryForKubeConfigs);
},
});
export default isInLocalKubeconfigInjectable;
@@ -30,6 +30,8 @@ import type { SaveKubeconfig } from "./save-kubeconfig.injectable";
import saveKubeconfigInjectable from "./save-kubeconfig.injectable";
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
import { isCurrentContext } from "./is-current-context";
import type { IsInLocalKubeconfig } from "./is-in-local-kubeconfig.injectable";
import isInLocalKubeconfigInjectable from "./is-in-local-kubeconfig.injectable";
interface Dependencies {
state: IObservableValue<DeleteClusterDialogState | undefined>;
@@ -39,6 +41,7 @@ interface Dependencies {
requestClearClusterAsDeleting: RequestClearClusterAsDeleting;
showErrorNotification: ShowNotification;
saveKubeconfig: SaveKubeconfig;
isInLocalKubeconfig: IsInLocalKubeconfig;
}
@observer
@@ -52,7 +55,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
this.props.state.set({
...state,
config: Object.assign(config, {
contexts: config.contexts.filter(item => item.name !== state.cluster.contextName),
contexts: config.contexts.filter(item => item.name !== state.cluster.contextName.get()),
currentContext: newCurrentContext && showContextSwitch
? newCurrentContext
: config.currentContext,
@@ -61,7 +64,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
});
try {
await this.props.saveKubeconfig(config, cluster.kubeConfigPath);
await this.props.saveKubeconfig(config, cluster.kubeConfigPath.get());
this.props.hotbarStore.removeAllHotbarItems(cluster.id);
await this.props.requestDeleteCluster(cluster.id);
} catch(error) {
@@ -73,7 +76,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
}
shouldDeleteBeDisabled({ cluster, config, newCurrentContext, showContextSwitch }: DeleteClusterDialogState): boolean {
const noContextsAvailable = config.contexts.filter(context => context.name !== cluster.contextName).length == 0;
const noContextsAvailable = config.contexts.filter(context => context.name !== cluster.contextName.get()).length == 0;
const newContextNotSelected = newCurrentContext === "";
if (noContextsAvailable) {
@@ -92,7 +95,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
const selectOptions = config
.contexts
.filter(context => context.name !== cluster.contextName)
.filter(context => context.name !== cluster.contextName.get())
.map(context => ({
value: context.name,
label: context.name,
@@ -121,7 +124,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
}
renderDeleteMessage({ cluster }: DeleteClusterDialogState) {
if (cluster.isInLocalKubeconfig()) {
if (this.props.isInLocalKubeconfig(cluster)) {
return (
<div>
{"Delete the "}
@@ -141,7 +144,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
</b>
{" context from "}
<b>
{cluster.kubeConfigPath}
{cluster.kubeConfigPath.get()}
</b>
?
</div>
@@ -149,7 +152,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
}
getWarningMessage({ cluster, config }: DeleteClusterDialogState) {
if (cluster.isInLocalKubeconfig()) {
if (this.props.isInLocalKubeconfig(cluster)) {
return (
<p data-testid="internal-kubeconfig-warning">
Are you sure you want to delete it? It can be re-added through the copy/paste mechanism.
@@ -157,7 +160,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
);
}
const contexts = config.contexts.filter(context => context.name !== cluster.contextName);
const contexts = config.contexts.filter(context => context.name !== cluster.contextName.get());
if (!contexts.length) {
return (
@@ -191,7 +194,7 @@ class NonInjectedDeleteClusterDialog extends React.Component<Dependencies> {
renderContents(state: DeleteClusterDialogState) {
const { config, cluster, showContextSwitch } = state;
const contexts = state.config.contexts.filter(context => context.name !== state.cluster.contextName);
const contexts = state.config.contexts.filter(context => context.name !== state.cluster.contextName.get());
const disableDelete = this.shouldDeleteBeDisabled(state);
const currentContext = isCurrentContext(config, cluster);
@@ -271,5 +274,6 @@ export const DeleteClusterDialog = withInjectables<Dependencies>(NonInjectedDele
requestDeleteCluster: di.inject(requestDeleteClusterInjectable),
saveKubeconfig: di.inject(saveKubeconfigInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable),
isInLocalKubeconfig: di.inject(isInLocalKubeconfigInjectable),
}),
});
@@ -20,7 +20,7 @@ import directoryForUserDataInjectable from "../../../common/app-paths/directory-
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import type { PodStore } from "../+workloads-pods/store";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import { Cluster } from "../../../common/cluster/cluster";
describe("kube-object-list-layout", () => {
let di: DiContainer;
@@ -34,9 +34,7 @@ describe("kube-object-list-layout", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectionToken);
di.override(hostedClusterInjectable, () => createCluster({
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
@@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import activeEntityInternalClusterInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
const clusterNameInjectable = getInjectable({
id: "cluster-name",
instantiate: (di) => {
const activeEntityInternalCluster = di.inject(activeEntityInternalClusterInjectable);
return computed(() => activeEntityInternalCluster.get()?.name.get());
},
});
export default clusterNameInjectable;
@@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import clusterInjectable from "./cluster.injectable";
const clusterNameInjectable = getInjectable({
id: "cluster-name",
instantiate: (di) => {
const cluster = di.inject(clusterInjectable);
return cluster?.name;
},
lifecycle: lifecycleEnum.transient,
});
export default clusterNameInjectable;
@@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import getActiveClusterEntityInjectable from "../../../api/catalog/entity/get-active-cluster-entity.injectable";
const clusterInjectable = getInjectable({
id: "cluster",
instantiate: (di) => {
const getActiveClusterEntity = di.inject(getActiveClusterEntityInjectable);
return getActiveClusterEntity();
},
lifecycle: lifecycleEnum.transient,
});
export default clusterInjectable;
@@ -15,7 +15,6 @@ import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { computed, runInAction } from "mobx";
import clusterInjectable from "./dependencies/cluster.injectable";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import type { Cluster } from "../../../common/cluster/cluster";
@@ -25,6 +24,7 @@ import { KubeObjectMenu } from "./index";
import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable";
import hideDetailsInjectable from "../kube-detail-params/hide-details.injectable";
import { kubeObjectMenuItemInjectionToken } from "./kube-object-menu-item-injection-token";
import activeEntityInternalClusterInjectable from "../../api/catalog/entity/get-active-cluster-entity.injectable";
// TODO: make `animated={false}` not required to make tests deterministic
describe("kube-object-menu", () => {
@@ -45,11 +45,10 @@ describe("kube-object-menu", () => {
render = renderFor(di);
di.override(
clusterInjectable,
() =>
({
name: "Some name",
} as Cluster),
activeEntityInternalClusterInjectable,
() => computed(() => ({
name: computed(() => "Some name"),
} as Cluster)),
);
di.override(
@@ -66,7 +65,11 @@ describe("kube-object-menu", () => {
});
it("given no cluster, does not crash", () => {
di.override(clusterInjectable, () => null);
di.override(
activeEntityInternalClusterInjectable,
() => computed(() => undefined),
);
expect(() => {
render(<KubeObjectMenu object={null as never} toolbar={true} />);
@@ -11,7 +11,7 @@ import { MenuItem, MenuActions } from "../menu";
import identity from "lodash/identity";
import type { ApiManager } from "../../../common/k8s-api/api-manager";
import { withInjectables } from "@ogre-tools/injectable-react";
import clusterNameInjectable from "./dependencies/cluster-name.injectable";
import clusterNameInjectable from "./cluster-name.injectable";
import createEditResourceTabInjectable from "../dock/edit-resource/edit-resource-tab.injectable";
import kubeObjectMenuItemsInjectable from "./kube-object-menu-items.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
@@ -38,7 +38,7 @@ export interface KubeObjectMenuProps<TKubeObject extends KubeObject> extends Men
interface Dependencies {
apiManager: ApiManager;
kubeObjectMenuItems: IComputedValue<React.ElementType[]>;
clusterName: string | undefined;
clusterName: IComputedValue<string | undefined>;
hideDetails: HideDetails;
createEditResourceTab: (kubeObject: KubeObject) => void;
onContextMenuOpen: OnKubeObjectContextMenuOpen;
@@ -65,7 +65,7 @@ class NonInjectedKubeObjectMenu<Kube extends KubeObject> extends React.Component
{`Remove ${object.kind} `}
<b>{breadcrumb}</b>
{" from "}
<b>{this.props.clusterName}</b>
<b>{this.props.clusterName.get()}</b>
?
</p>
);
@@ -22,7 +22,7 @@ import navigateToPreferencesInjectable from "../../../features/preferences/commo
import type { NavigateToHelmCharts } from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import { Cluster } from "../../../common/cluster/cluster";
import type { NamespaceStore } from "../+namespaces/store";
import historyInjectable from "../../navigation/history.injectable";
import type { MinimalTrayMenuItem } from "../../../main/tray/electron-tray/electron-tray.injectable";
@@ -228,8 +228,6 @@ export const getApplicationBuilder = () => {
},
}));
const allowedResourcesState = observable.set<string>();
const windowHelpers = new Map<string, { di: DiContainer; getRendered: () => RenderResult }>();
const createElectronWindowFake: CreateElectronWindow = (configuration) => {
@@ -526,19 +524,20 @@ export const getApplicationBuilder = () => {
environment = environments.clusterFrame;
builder.beforeWindowStart((windowDi) => {
const clusterStub = {
const cluster = new Cluster({
id: "some-cluster-id",
accessibleNamespaces: observable.array(),
allowedNamespaces: observable.array(),
shouldShowResource: (kind) => allowedResourcesState.has(formatKubeApiResource(kind)),
} as Partial<Cluster> as Cluster;
contextName: "some-context-name",
kubeConfigPath: "/some-path-to-kube-config",
}, {
clusterServerUrl: "https://localhost:12345",
});
windowDi.override(activeKubernetesClusterInjectable, () =>
computed(() => catalogEntityFromCluster(clusterStub)),
computed(() => catalogEntityFromCluster(cluster)),
);
windowDi.override(hostedClusterIdInjectable, () => clusterStub.id);
windowDi.override(hostedClusterInjectable, () => clusterStub);
windowDi.override(hostedClusterIdInjectable, () => cluster.id);
windowDi.override(hostedClusterInjectable, () => cluster);
// TODO: Figure out a way to remove this stub.
windowDi.override(namespaceStoreInjectable, () => ({
@@ -645,8 +644,11 @@ export const getApplicationBuilder = () => {
allowKubeResource: (resource) => {
environment.onAllowKubeResource();
const windowDi = builder.applicationWindow.only.di;
const cluster = windowDi.inject(hostedClusterInjectable);
runInAction(() => {
allowedResourcesState.add(formatKubeApiResource(resource));
cluster?.resourcesToShow.add(formatKubeApiResource(resource));
});
return builder;
@@ -14,8 +14,7 @@ import { DefaultProps } from "../../mui-base-theme";
import { ClusterFrame } from "./cluster-frame";
import historyInjectable from "../../navigation/history.injectable";
import { computed } from "mobx";
import type { Cluster } from "../../../common/cluster/cluster";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { Cluster } from "../../../common/cluster/cluster";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
@@ -49,9 +48,7 @@ describe("<ClusterFrame />", () => {
testUsingFakeTime("2000-01-01 12:00:00am");
const createCluster = di.inject(createClusterInjectable);
cluster = createCluster(
cluster = new Cluster(
{
contextName: "my-cluster",
id: "123456",
@@ -68,8 +65,7 @@ describe("<ClusterFrame />", () => {
describe("given cluster with list nodes and namespaces permissions", () => {
beforeEach(() => {
// TODO: replace with not using private info
(cluster as unknown as { readonly allowedResources: Cluster["allowedResources"] }).allowedResources.replace(["nodes", "namespaces"]);
cluster.resourcesToShow.replace(["nodes", "namespaces"]);
});
it("renders", () => {
@@ -110,7 +106,7 @@ describe("<ClusterFrame />", () => {
describe("given cluster without list nodes, but with namespaces permissions", () => {
beforeEach(() => {
(cluster as unknown as { readonly allowedResources: Cluster["allowedResources"] }).allowedResources.replace(["namespaces"]);
cluster.resourcesToShow.replace(["namespaces"]);
});
it("renders", () => {
@@ -40,7 +40,7 @@ export const initClusterFrame = ({
);
await requestSetClusterFrameId(hostedCluster.id);
await hostedCluster.whenReady; // cluster.activate() is done at this point
await when(() => hostedCluster.ready.get()); // cluster.activate() is done at this point
catalogEntityRegistry.activeEntity = hostedCluster.id;
@@ -55,7 +55,7 @@ const listNamespacesForbiddenHandlerInjectable = getInjectable({
<b>Add Accessible Namespaces</b>
<p>
{"Cluster "}
<b>{getClusterById(clusterId)?.name ?? "<unknown cluster>"}</b>
<b>{getClusterById(clusterId)?.name.get() ?? "<unknown cluster>"}</b>
{" does not have permissions to list namespaces. Please add the namespaces you have access to."}
</p>
<div className="flex gaps row align-left box grow">