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

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 2023-03-10 02:37:39 -05:00 committed by GitHub
parent e6d6d1d8f7
commit ad9bafe2a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 2544 additions and 1961 deletions

286
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,6 @@ import type { GetCustomKubeConfigFilePath } from "../app-paths/get-custom-kube-c
import getCustomKubeConfigFilePathInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; import getCustomKubeConfigFilePathInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import assert from "assert"; import assert from "assert";
@ -27,6 +25,7 @@ import type { WriteFileSync } from "../fs/write-file-sync.injectable";
import writeFileSyncInjectable from "../fs/write-file-sync.injectable"; import writeFileSyncInjectable from "../fs/write-file-sync.injectable";
import type { WriteBufferSync } from "../fs/write-buffer-sync.injectable"; import type { WriteBufferSync } from "../fs/write-buffer-sync.injectable";
import writeBufferSyncInjectable from "../fs/write-buffer-sync.injectable"; import writeBufferSyncInjectable from "../fs/write-buffer-sync.injectable";
import { Cluster } from "../cluster/cluster";
// NOTE: this is intended to read the actual file system // NOTE: this is intended to read the actual file system
const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png"); const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png");
@ -58,7 +57,6 @@ users:
describe("cluster-store", () => { describe("cluster-store", () => {
let di: DiContainer; let di: DiContainer;
let clusterStore: ClusterStore; let clusterStore: ClusterStore;
let createCluster: CreateCluster;
let writeJsonSync: WriteJsonSync; let writeJsonSync: WriteJsonSync;
let writeFileSync: WriteFileSync; let writeFileSync: WriteFileSync;
let writeBufferSync: WriteBufferSync; let writeBufferSync: WriteBufferSync;
@ -74,7 +72,6 @@ describe("cluster-store", () => {
di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin"); di.override(normalizedPlatformInjectable, () => "darwin");
writeJsonSync = di.inject(writeJsonSyncInjectable); writeJsonSync = di.inject(writeJsonSyncInjectable);
writeFileSync = di.inject(writeFileSyncInjectable); writeFileSync = di.inject(writeFileSyncInjectable);
writeBufferSync = di.inject(writeBufferSyncInjectable); writeBufferSync = di.inject(writeBufferSyncInjectable);
@ -84,7 +81,6 @@ describe("cluster-store", () => {
describe("empty config", () => { describe("empty config", () => {
beforeEach(async () => { beforeEach(async () => {
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {}); writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {});
@ -94,7 +90,7 @@ describe("cluster-store", () => {
describe("with foo cluster added", () => { describe("with foo cluster added", () => {
beforeEach(() => { beforeEach(() => {
const cluster = createCluster({ const cluster = new Cluster({
id: "foo", id: "foo",
contextName: "foo", contextName: "foo",
preferences: { preferences: {
@ -201,7 +197,6 @@ describe("cluster-store", () => {
], ],
}); });
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
clusterStore = di.inject(clusterStoreInjectable); clusterStore = di.inject(clusterStoreInjectable);
@ -256,7 +251,6 @@ describe("cluster-store", () => {
], ],
}); });
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
clusterStore = di.inject(clusterStoreInjectable); clusterStore = di.inject(clusterStoreInjectable);
@ -274,7 +268,6 @@ describe("cluster-store", () => {
beforeEach(() => { beforeEach(() => {
di.override(storeMigrationVersionInjectable, () => "3.6.0"); di.override(storeMigrationVersionInjectable, () => "3.6.0");
createCluster = di.inject(createClusterInjectionToken);
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable); getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", { writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {
@ -302,9 +295,9 @@ describe("cluster-store", () => {
}); });
it("migrates to modern format with kubeconfig in a file", async () => { it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath; const configPath = clusterStore.clustersList[0].kubeConfigPath.get();
expect(readFileSync(config)).toBe(minimalValidKubeConfig); expect(readFileSync(configPath)).toBe(minimalValidKubeConfig);
}); });
it("migrates to modern format with icon not in file", async () => { it("migrates to modern format with icon not in file", async () => {

View File

@ -13,6 +13,7 @@ import { requestClusterActivation, requestClusterDisconnection } from "../../ren
import KubeClusterCategoryIcon from "./icons/kubernetes.svg"; import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
import getClusterByIdInjectable from "../cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../cluster-store/get-by-id.injectable";
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import clusterConnectionInjectable from "../../main/cluster/cluster-connection.injectable";
export interface KubernetesClusterPrometheusMetrics { export interface KubernetesClusterPrometheusMetrics {
address?: { address?: {
@ -79,8 +80,15 @@ export class KubernetesCluster<
if (app) { if (app) {
const di = getLegacyGlobalDiForExtensionApi(); const di = getLegacyGlobalDiForExtensionApi();
const getClusterById = di.inject(getClusterByIdInjectable); const getClusterById = di.inject(getClusterByIdInjectable);
const cluster = getClusterById(this.getId());
await getClusterById(this.getId())?.activate(); if (!cluster) {
return;
}
const connectionCluster = di.inject(clusterConnectionInjectable, cluster);
await connectionCluster.activate();
} else { } else {
await requestClusterActivation(this.getId(), false); await requestClusterActivation(this.getId(), false);
} }
@ -90,8 +98,15 @@ export class KubernetesCluster<
if (app) { if (app) {
const di = getLegacyGlobalDiForExtensionApi(); const di = getLegacyGlobalDiForExtensionApi();
const getClusterById = di.inject(getClusterByIdInjectable); const getClusterById = di.inject(getClusterByIdInjectable);
const cluster = getClusterById(this.getId());
getClusterById(this.getId())?.disconnect(); if (!cluster) {
return;
}
const connectionCluster = di.inject(clusterConnectionInjectable, cluster);
connectionCluster.disconnect();
} else { } else {
await requestClusterDisconnection(this.getId(), false); await requestClusterDisconnection(this.getId(), false);
} }

View File

@ -241,7 +241,7 @@ export interface CatalogEntityMetadata extends EntityMetadataObject {
shortName?: string; shortName?: string;
description?: string; description?: string;
source?: string; source?: string;
labels: Record<string, string>; labels: Partial<Record<string, string>>;
} }
export interface CatalogEntityStatus { export interface CatalogEntityStatus {

View File

@ -4,7 +4,6 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store"; import { ClusterStore } from "./cluster-store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable"; import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import emitAppEventInjectable from "../app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../app-event-bus/emit-event.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
@ -23,7 +22,6 @@ const clusterStoreInjectable = getInjectable({
id: "cluster-store", id: "cluster-store",
instantiate: (di) => new ClusterStore({ instantiate: (di) => new ClusterStore({
createCluster: di.inject(createClusterInjectionToken),
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable), readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
emitAppEvent: di.inject(emitAppEventInjectable), emitAppEvent: di.inject(emitAppEventInjectable),
directoryForUserData: di.inject(directoryForUserDataInjectable), directoryForUserData: di.inject(directoryForUserDataInjectable),

View File

@ -10,7 +10,6 @@ import { BaseStore } from "../base-store/base-store";
import { Cluster } from "../cluster/cluster"; import { Cluster } from "../cluster/cluster";
import { toJS } from "../utils"; import { toJS } from "../utils";
import type { ClusterModel, ClusterId } from "../cluster-types"; import type { ClusterModel, ClusterId } from "../cluster-types";
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable"; import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable"; import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
@ -19,7 +18,6 @@ export interface ClusterStoreModel {
} }
interface Dependencies extends BaseStoreDependencies { interface Dependencies extends BaseStoreDependencies {
createCluster: CreateCluster;
readClusterConfigSync: ReadClusterConfigSync; readClusterConfigSync: ReadClusterConfigSync;
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
} }
@ -64,7 +62,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
const cluster = clusterOrModel instanceof Cluster const cluster = clusterOrModel instanceof Cluster
? clusterOrModel ? clusterOrModel
: this.dependencies.createCluster( : new Cluster(
clusterOrModel, clusterOrModel,
this.dependencies.readClusterConfigSync(clusterOrModel), this.dependencies.readClusterConfigSync(clusterOrModel),
); );
@ -87,7 +85,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
if (cluster) { if (cluster) {
cluster.updateModel(clusterModel); cluster.updateModel(clusterModel);
} else { } else {
cluster = this.dependencies.createCluster( cluster = new Cluster(
clusterModel, clusterModel,
this.dependencies.readClusterConfigSync(clusterModel), this.dependencies.readClusterConfigSync(clusterModel),
); );

View File

@ -39,10 +39,6 @@ export const updateClusterModelChecker = Joi.object<UpdateClusterModel>({
contextName: Joi.string() contextName: Joi.string()
.required() .required()
.min(1), .min(1),
workspace: Joi.string()
.optional(),
workspaces: Joi.array()
.items(Joi.string()),
preferences: Joi.object(), preferences: Joi.object(),
metadata: Joi.object(), metadata: Joi.object(),
accessibleNamespaces: Joi.array() accessibleNamespaces: Joi.array()
@ -70,18 +66,6 @@ export interface ClusterModel {
/** Path to cluster kubeconfig */ /** Path to cluster kubeconfig */
kubeConfigPath: string; kubeConfigPath: string;
/**
* Workspace id
*
* @deprecated
*/
workspace?: string;
/**
* @deprecated this is used only for hotbar migrations from 4.2.X
*/
workspaces?: string[];
/** User context in kubeconfig */ /** User context in kubeconfig */
contextName: string; contextName: string;
@ -97,7 +81,7 @@ export interface ClusterModel {
/** /**
* Labels for the catalog entity * Labels for the catalog entity
*/ */
labels?: Record<string, string>; labels?: Partial<Record<string, string>>;
} }
/** /**
@ -206,6 +190,6 @@ export interface ClusterState {
ready: boolean; ready: boolean;
isAdmin: boolean; isAdmin: boolean;
allowedNamespaces: string[]; allowedNamespaces: string[];
allowedResources: string[]; resourcesToShow: string[];
isGlobalWatchEnabled: boolean; isGlobalWatchEnabled: boolean;
} }

View File

@ -6,7 +6,6 @@
import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node"; import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
/** /**
@ -19,13 +18,13 @@ export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean
/** /**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/ */
export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI; export type CreateAuthorizationReview = (proxyConfig: KubeConfig) => CanI;
interface Dependencies { const createAuthorizationReviewInjectable = getInjectable({
logger: Logger; id: "authorization-review",
} instantiate: (di): CreateAuthorizationReview => {
const logger = di.inject(loggerInjectable);
const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => {
return (proxyConfig) => { return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api); const api = proxyConfig.makeApiClient(AuthorizationV1Api);
@ -45,15 +44,7 @@ const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => {
} }
}; };
}; };
};
const authorizationReviewInjectable = getInjectable({
id: "authorization-review",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return authorizationReview({ logger });
}, },
}); });
export default authorizationReviewInjectable; export default createAuthorizationReviewInjectable;

View File

@ -3,164 +3,74 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { action, comparer, computed, makeObservable, observable, reaction, runInAction, when } from "mobx"; import { computed, observable, toJS, runInAction } from "mobx";
import type { ClusterContextHandler } from "../../main/context-handler/context-handler"; import type { KubeApiResource } from "../rbac";
import type { KubeConfig } from "@kubernetes/client-node"; import type { ClusterState, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, ClusterConfigData } from "../cluster-types";
import { HttpError } from "@kubernetes/client-node"; import { ClusterMetadataKey, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
import type { Kubectl } from "../../main/kubectl/kubectl"; import type { IObservableValue } from "mobx";
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager"; import { replaceObservableObject } from "../utils/replace-observable-object";
import type { KubeApiResource, KubeApiResourceDescriptor } from "../rbac"; import { pick } from "lodash";
import { formatKubeApiResource } from "../rbac";
import plimit from "p-limit";
import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
import { disposer, isDefined, isRequestError, toJS } from "../utils";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
import type { CanI } from "./authorization-review.injectable";
import type { ListNamespaces } from "./list-namespaces.injectable";
import assert from "assert";
import type { Logger } from "../logger";
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable";
import type { CanListResource, RequestNamespaceListPermissions, RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable";
import type { RequestApiResources } from "../../main/cluster/request-api-resources.injectable";
import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable";
import type { FalibleOnlyClusterMetadataDetector } from "../../main/cluster-detectors/token";
export interface ClusterDependencies {
readonly directoryForKubeConfigs: string;
readonly logger: Logger;
readonly clusterVersionDetector: FalibleOnlyClusterMetadataDetector;
detectClusterMetadata: DetectClusterMetadata;
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI;
requestApiResources: RequestApiResources;
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor;
createListNamespaces: (config: KubeConfig) => ListNamespaces;
broadcastMessage: BroadcastMessage;
loadConfigfromFile: LoadConfigfromFile;
}
export class Cluster {
/** /**
* Cluster * Unique id for a cluster
*
* @beta
*/ */
export class Cluster implements ClusterModel { readonly id: ClusterId;
/** Unique id for a cluster */
public readonly id: ClusterId;
private kubeCtl: Kubectl | undefined;
/**
* Context handler
*
* @internal
*/
protected readonly _contextHandler: ClusterContextHandler | undefined;
protected readonly _proxyKubeconfigManager: KubeconfigManager | undefined;
protected readonly eventsDisposer = disposer();
protected activated = false;
public get contextHandler() {
// TODO: remove these once main/renderer are seperate classes
assert(this._contextHandler, "contextHandler is only defined in the main environment");
return this._contextHandler;
}
protected get proxyKubeconfigManager() {
// TODO: remove these once main/renderer are seperate classes
assert(this._proxyKubeconfigManager, "proxyKubeconfigManager is only defined in the main environment");
return this._proxyKubeconfigManager;
}
get whenReady() {
return when(() => this.ready);
}
/** /**
* Kubeconfig context name * Kubeconfig context name
*
* @observable
*/ */
@observable contextName!: string; readonly contextName = observable.box() as IObservableValue<string>;
/** /**
* Path to kubeconfig * Path to kubeconfig
*
* @observable
*/ */
@observable kubeConfigPath!: string; readonly kubeConfigPath = observable.box() as IObservableValue<string>;
/**
* @deprecated
*/
@observable workspace?: string;
/**
* @deprecated
*/
@observable workspaces?: string[];
/** /**
* Kubernetes API server URL * Kubernetes API server URL
*
* @observable
*/ */
@observable apiUrl: string; // cluster server url readonly apiUrl: IObservableValue<string>;
/** /**
* Is cluster online * Describes if we can detect that cluster is online
*
* @observable
*/ */
@observable online = false; // describes if we can detect that cluster is online readonly online = observable.box(false);
/** /**
* Can user access cluster resources * Describes if user is able to access cluster resources
*
* @observable
*/ */
@observable accessible = false; // if user is able to access cluster resources readonly accessible = observable.box(false);
/** /**
* Is cluster instance in usable state * Is cluster instance in usable state
*
* @observable
*/ */
@observable ready = false; // cluster is in usable state readonly ready = observable.box(false);
/**
* Is cluster currently reconnecting
*
* @observable
*/
@observable reconnecting = false;
/** /**
* Is cluster disconnected. False if user has selected to connect. * Is cluster disconnected. False if user has selected to connect.
*
* @observable
*/ */
@observable disconnected = true; readonly disconnected = observable.box(true);
/** /**
* Does user have admin like access * Does user have admin like access
*
* @observable
*/ */
@observable isAdmin = false; readonly isAdmin = observable.box(false);
/** /**
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1" * Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
*
* @observable
*/ */
@observable isGlobalWatchEnabled = false; readonly isGlobalWatchEnabled = observable.box(false);
/** /**
* Preferences * Preferences
*
* @observable
*/ */
@observable preferences: ClusterPreferences = {}; readonly preferences = observable.object<ClusterPreferences>({});
/** /**
* Metadata * Metadata
*
* @observable
*/ */
@observable metadata: ClusterMetadata = {}; readonly metadata = observable.object<ClusterMetadata>({});
/** /**
* List of allowed namespaces verified via K8S::SelfSubjectAccessReview api * List of allowed namespaces verified via K8S::SelfSubjectAccessReview api
@ -172,73 +82,47 @@ export class Cluster implements ClusterModel {
*/ */
readonly accessibleNamespaces = observable.array<string>(); readonly accessibleNamespaces = observable.array<string>();
private readonly knownResources = observable.array<KubeApiResource>(); /**
* The list of all known resources associated with this cluster
*/
readonly knownResources = observable.array<KubeApiResource>();
// The formatting of this is `group.name` or `name` (if in core) /**
private readonly allowedResources = observable.set<string>(); * The formatting of this is `group.name` or `name` (if in core)
*/
readonly resourcesToShow = observable.set<string>();
/** /**
* Labels for the catalog entity * Labels for the catalog entity
*/ */
@observable labels: Record<string, string> = {}; readonly labels = observable.object<Partial<Record<string, string>>>({});
/** /**
* Is cluster available * Is cluster available
*
* @computed
*/ */
@computed get available() { readonly available = computed(() => this.accessible.get() && !this.disconnected.get());
return this.accessible && !this.disconnected;
}
/** /**
* Cluster name * Cluster name
*
* @computed
*/ */
@computed get name() { readonly name = computed(() => this.preferences.clusterName || this.contextName.get());
return this.preferences.clusterName || this.contextName;
}
/** /**
* The detected kubernetes distribution * The detected kubernetes distribution
*/ */
@computed get distribution(): string { readonly distribution = computed(() => this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown");
return this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown";
}
/** /**
* The detected kubernetes version * The detected kubernetes version
*/ */
@computed get version(): string { readonly version = computed(() => this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown");
return this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown";
}
/** /**
* Prometheus preferences * Prometheus preferences
*
* @computed
* @internal
*/ */
@computed get prometheusPreferences(): ClusterPrometheusPreferences { readonly prometheusPreferences = computed(() => pick(toJS(this.preferences), "prometheus", "prometheusProvider") as ClusterPrometheusPreferences);
const { prometheus, prometheusProvider } = this.preferences;
return toJS({ prometheus, prometheusProvider });
}
/**
* defaultNamespace preference
*
* @computed
* @internal
*/
@computed get defaultNamespace(): string | undefined {
return this.preferences.defaultNamespace;
}
constructor(private readonly dependencies: ClusterDependencies, { id, ...model }: ClusterModel, configData: ClusterConfigData) {
makeObservable(this);
constructor({ id, ...model }: ClusterModel, configData: ClusterConfigData) {
const { error } = clusterModelIdChecker.validate({ id }); const { error } = clusterModelIdChecker.validate({ id });
if (error) { if (error) {
@ -247,16 +131,7 @@ export class Cluster implements ClusterModel {
this.id = id; this.id = id;
this.updateModel(model); this.updateModel(model);
this.apiUrl = configData.clusterServerUrl; this.apiUrl = observable.box(configData.clusterServerUrl);
// for the time being, until renderer gets its own cluster type
this._contextHandler = this.dependencies.createContextHandler(this);
this._proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
this.dependencies.logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id,
context: this.contextName,
apiUrl: this.apiUrl,
});
} }
/** /**
@ -264,7 +139,7 @@ export class Cluster implements ClusterModel {
* *
* @param model * @param model
*/ */
@action updateModel(model: UpdateClusterModel) { updateModel(model: UpdateClusterModel) {
// Note: do not assign ID as that should never be updated // Note: do not assign ID as that should never be updated
const { error } = updateClusterModelChecker.validate(model, { allowUnknown: true }); const { error } = updateClusterModelChecker.validate(model, { allowUnknown: true });
@ -273,23 +148,16 @@ export class Cluster implements ClusterModel {
throw error; throw error;
} }
this.kubeConfigPath = model.kubeConfigPath; runInAction(() => {
this.contextName = model.contextName; this.kubeConfigPath.set(model.kubeConfigPath);
this.contextName.set(model.contextName);
if (model.workspace) {
this.workspace = model.workspace;
}
if (model.workspaces) {
this.workspaces = model.workspaces;
}
if (model.preferences) { if (model.preferences) {
this.preferences = model.preferences; replaceObservableObject(this.preferences, model.preferences);
} }
if (model.metadata) { if (model.metadata) {
this.metadata = model.metadata; replaceObservableObject(this.metadata, model.metadata);
} }
if (model.accessibleNamespaces) { if (model.accessibleNamespaces) {
@ -297,430 +165,66 @@ export class Cluster implements ClusterModel {
} }
if (model.labels) { if (model.labels) {
this.labels = model.labels; replaceObservableObject(this.labels, model.labels);
} }
}
/**
* @internal
*/
protected bindEvents() {
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes
this.eventsDisposer.push(
reaction(
() => this.prometheusPreferences,
prefs => this.contextHandler.setupPrometheus(prefs),
{ equals: comparer.structural },
),
() => clearInterval(refreshTimer),
() => clearInterval(refreshMetadataTimer),
reaction(() => this.defaultNamespace, () => this.recreateProxyKubeconfig()),
);
}
/**
* @internal
*/
protected async recreateProxyKubeconfig() {
this.dependencies.logger.info("[CLUSTER]: Recreating proxy kubeconfig");
try {
await this.proxyKubeconfigManager.clear();
await this.getProxyKubeconfig();
} catch (error) {
this.dependencies.logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
}
}
/**
* @param force force activation
* @internal
*/
@action
async activate(force = false) {
if (this.activated && !force) {
return;
}
this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta());
if (!this.eventsDisposer.length) {
this.bindEvents();
}
if (this.disconnected || !this.accessible) {
try {
this.broadcastConnectUpdate("Starting connection ...");
await this.reconnect();
} catch (error) {
this.broadcastConnectUpdate(`Failed to start connection: ${error}`, "error");
return;
}
}
try {
this.broadcastConnectUpdate("Refreshing connection status ...");
await this.refreshConnectionStatus();
} catch (error) {
this.broadcastConnectUpdate(`Failed to connection status: ${error}`, "error");
return;
}
if (this.accessible) {
try {
this.broadcastConnectUpdate("Refreshing cluster accessibility ...");
await this.refreshAccessibility();
} catch (error) {
this.broadcastConnectUpdate(`Failed to refresh accessibility: ${error}`, "error");
return;
}
// download kubectl in background, so it's not blocking dashboard
this.ensureKubectl()
.catch(error => this.dependencies.logger.warn(`[CLUSTER]: failed to download kubectl for clusterId=${this.id}`, error));
this.broadcastConnectUpdate("Connected, waiting for view to load ...");
}
this.activated = true;
}
/**
* @internal
*/
async ensureKubectl() {
this.kubeCtl ??= this.dependencies.createKubectl(this.version);
await this.kubeCtl.ensureKubectl();
return this.kubeCtl;
}
/**
* @internal
*/
@action
async reconnect() {
this.dependencies.logger.info(`[CLUSTER]: reconnect`, this.getMeta());
await this.contextHandler?.restartServer();
this.disconnected = false;
}
/**
* @internal
*/
@action disconnect(): void {
if (this.disconnected) {
return void this.dependencies.logger.debug("[CLUSTER]: already disconnected", { id: this.id });
}
this.dependencies.logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
this.eventsDisposer();
this.contextHandler?.stopServer();
this.disconnected = true;
this.online = false;
this.accessible = false;
this.ready = false;
this.activated = false;
this.allowedNamespaces.clear();
this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id });
}
/**
* @internal
*/
@action
async refresh() {
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.refreshConnectionStatus();
}
/**
* @internal
*/
@action
async refreshAccessibilityAndMetadata() {
await this.refreshAccessibility();
await this.refreshMetadata();
}
/**
* @internal
*/
async refreshMetadata() {
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const newMetadata = await this.dependencies.detectClusterMetadata(this);
runInAction(() => {
this.metadata = {
...this.metadata,
...newMetadata,
};
}); });
} }
/**
* @internal
*/
private async refreshAccessibility(): Promise<void> {
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta());
const proxyConfig = await this.getProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig);
this.isAdmin = await canI({
namespace: "kube-system",
resource: "*",
verb: "create",
});
this.isGlobalWatchEnabled = await canI({
verb: "watch",
resource: "*",
});
this.allowedNamespaces.replace(await this.requestAllowedNamespaces(proxyConfig));
const knownResources = await this.dependencies.requestApiResources(this);
if (knownResources.callWasSuccessful) {
this.knownResources.replace(knownResources.response);
} else if (this.knownResources.length > 0) {
this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources, sticking with previous list`);
} else {
this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources for the first time, blocking connection to cluster...`);
this.broadcastConnectUpdate("Failed to list kube API resources, please reconnect...", "error");
}
this.allowedResources.replace(await this.getAllowedResources(requestNamespaceListPermissions));
this.ready = this.knownResources.length > 0;
this.dependencies.logger.debug(`[CLUSTER]: refreshed accessibility data`, this.getState());
}
/**
* @internal
*/
@action
async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
}
async getKubeconfig(): Promise<KubeConfig> {
const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath);
return config;
}
/**
* @internal
*/
async getProxyKubeconfig(): Promise<KubeConfig> {
const proxyKCPath = await this.getProxyKubeconfigPath();
const { config } = await this.dependencies.loadConfigfromFile(proxyKCPath);
return config;
}
/**
* @internal
*/
async getProxyKubeconfigPath(): Promise<string> {
return this.proxyKubeconfigManager.getPath();
}
protected async getConnectionStatus(): Promise<ClusterStatus> {
try {
const versionData = await this.dependencies.clusterVersionDetector.detect(this);
this.metadata.version = versionData.value;
return ClusterStatus.AccessGranted;
} catch (error) {
this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.contextName}": ${error}`);
if (isRequestError(error)) {
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.broadcastConnectUpdate("Invalid credentials", "error");
return ClusterStatus.AccessDenied;
}
const message = String(error.error || error.message) || String(error);
this.broadcastConnectUpdate(message, "error");
return ClusterStatus.Offline;
}
if (error.failed === true) {
if (error.timedOut === true) {
this.broadcastConnectUpdate("Connection timed out", "error");
return ClusterStatus.Offline;
}
this.broadcastConnectUpdate("Failed to fetch credentials", "error");
return ClusterStatus.AccessDenied;
}
const message = String(error.error || error.message) || String(error);
this.broadcastConnectUpdate(message, "error");
} else if (error instanceof Error || typeof error === "string") {
this.broadcastConnectUpdate(`${error}`, "error");
} else {
this.broadcastConnectUpdate("Unknown error has occurred", "error");
}
return ClusterStatus.Offline;
}
}
toJSON(): ClusterModel { toJSON(): ClusterModel {
return toJS({ return {
id: this.id, id: this.id,
contextName: this.contextName, contextName: this.contextName.get(),
kubeConfigPath: this.kubeConfigPath, kubeConfigPath: this.kubeConfigPath.get(),
workspace: this.workspace, preferences: toJS(this.preferences),
workspaces: this.workspaces, metadata: toJS(this.metadata),
preferences: this.preferences, accessibleNamespaces: this.accessibleNamespaces.toJSON(),
metadata: this.metadata, labels: toJS(this.labels),
accessibleNamespaces: this.accessibleNamespaces, };
labels: this.labels,
});
} }
/** /**
* Serializable cluster-state used for sync btw main <-> renderer * Serializable cluster-state used for sync btw main <-> renderer
*/ */
getState(): ClusterState { getState(): ClusterState {
return toJS({ return {
apiUrl: this.apiUrl, apiUrl: this.apiUrl.get(),
online: this.online, online: this.online.get(),
ready: this.ready, ready: this.ready.get(),
disconnected: this.disconnected, disconnected: this.disconnected.get(),
accessible: this.accessible, accessible: this.accessible.get(),
isAdmin: this.isAdmin, isAdmin: this.isAdmin.get(),
allowedNamespaces: this.allowedNamespaces, allowedNamespaces: this.allowedNamespaces.toJSON(),
allowedResources: [...this.allowedResources], resourcesToShow: this.resourcesToShow.toJSON(),
isGlobalWatchEnabled: this.isGlobalWatchEnabled, isGlobalWatchEnabled: this.isGlobalWatchEnabled.get(),
}); };
} }
/** /**
* @internal
* @param state cluster state * @param state cluster state
*/ */
@action setState(state: ClusterState) { setState(state: ClusterState) {
this.accessible = state.accessible; runInAction(() => {
this.accessible.set(state.accessible);
this.allowedNamespaces.replace(state.allowedNamespaces); this.allowedNamespaces.replace(state.allowedNamespaces);
this.allowedResources.replace(state.allowedResources); this.resourcesToShow.replace(state.resourcesToShow);
this.apiUrl = state.apiUrl; this.apiUrl.set(state.apiUrl);
this.disconnected = state.disconnected; this.disconnected.set(state.disconnected);
this.isAdmin = state.isAdmin; this.isAdmin.set(state.isAdmin);
this.isGlobalWatchEnabled = state.isGlobalWatchEnabled; this.isGlobalWatchEnabled.set(state.isGlobalWatchEnabled);
this.online = state.online; this.online.set(state.online);
this.ready = state.ready; this.ready.set(state.ready);
});
} }
// get cluster system meta, e.g. use in "logger" // get cluster system meta, e.g. use in "logger"
getMeta() { getMeta() {
return { return {
id: this.id, id: this.id,
name: this.contextName, name: this.contextName.get(),
ready: this.ready, ready: this.ready.get(),
online: this.online, online: this.online.get(),
accessible: this.accessible, accessible: this.accessible.get(),
disconnected: this.disconnected, disconnected: this.disconnected.get(),
}; };
} }
/**
* broadcast an authentication update concerning this cluster
* @internal
*/
broadcastConnectUpdate(message: string, level: KubeAuthUpdate["level"] = "info"): void {
const update: KubeAuthUpdate = { message, level };
this.dependencies.logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: this.getMeta() });
this.dependencies.broadcastMessage(`cluster:${this.id}:connection-update`, update);
}
protected async requestAllowedNamespaces(proxyConfig: KubeConfig) {
if (this.accessibleNamespaces.length) {
return this.accessibleNamespaces;
}
try {
const listNamespaces = this.dependencies.createListNamespaces(proxyConfig);
return await listNamespaces();
} catch (error) {
const ctx = proxyConfig.getContextObject(this.contextName);
const namespaceList = [ctx?.namespace].filter(isDefined);
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
const { response } = error as HttpError & { response: { body: unknown }};
this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error: response.body });
this.dependencies.broadcastMessage(clusterListNamespaceForbiddenChannel, this.id);
}
return namespaceList;
}
}
protected async getAllowedResources(requestNamespaceListPermissions: RequestNamespaceListPermissions) {
if (!this.allowedNamespaces.length || !this.knownResources.length) {
return [];
}
try {
const apiLimit = plimit(5); // 5 concurrent api requests
const canListResourceCheckers = await Promise.all((
this.allowedNamespaces.map(namespace => apiLimit(() => requestNamespaceListPermissions(namespace)))
));
const canListNamespacedResource: CanListResource = (resource) => canListResourceCheckers.some(fn => fn(resource));
return this.knownResources
.filter(canListNamespacedResource)
.map(formatKubeApiResource);
} catch (error) {
return [];
}
}
shouldShowResource(resource: KubeApiResourceDescriptor): boolean {
if (this.allowedResources.size === 0) {
// better to show than hide everything
return true;
}
return this.allowedResources.has(formatKubeApiResource(resource));
}
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
}
get nodeShellImage(): string {
return this.preferences?.nodeShellImage || initialNodeShellImage;
}
get imagePullSecret(): string | undefined {
return this.preferences?.imagePullSecret;
}
isInLocalKubeconfig() {
return this.kubeConfigPath.startsWith(this.dependencies.directoryForKubeConfigs);
}
} }

View File

@ -1,13 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterConfigData, ClusterModel } from "../cluster-types";
import type { Cluster } from "./cluster";
export type CreateCluster = (model: ClusterModel, configData: ClusterConfigData) => Cluster;
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
id: "create-cluster-token",
});

View File

@ -9,7 +9,11 @@ import { isDefined } from "../utils";
export type ListNamespaces = () => Promise<string[]>; export type ListNamespaces = () => Promise<string[]>;
export function listNamespaces(config: KubeConfig): ListNamespaces { export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces;
const createListNamespacesInjectable = getInjectable({
id: "create-list-namespaces",
instantiate: (): CreateListNamespaces => (config) => {
const coreApi = config.makeApiClient(CoreV1Api); const coreApi = config.makeApiClient(CoreV1Api);
return async () => { return async () => {
@ -19,11 +23,7 @@ export function listNamespaces(config: KubeConfig): ListNamespaces {
.map(ns => ns.metadata?.name) .map(ns => ns.metadata?.name)
.filter(isDefined); .filter(isDefined);
}; };
} },
const listNamespacesInjectable = getInjectable({
id: "list-namespaces",
instantiate: () => listNamespaces,
}); });
export default listNamespacesInjectable; export default createListNamespacesInjectable;

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "./cluster";
import loadConfigFromFileInjectable from "../kube-helpers/load-config-from-file.injectable";
import type { ConfigResult } from "../kube-helpers";
export interface LoadKubeconfig {
(fullResult?: false): Promise<KubeConfig>;
(fullResult: true): Promise<ConfigResult>;
}
const loadKubeconfigInjectable = getInjectable({
id: "load-kubeconfig",
instantiate: (di, cluster) => {
const loadConfigFromFile = di.inject(loadConfigFromFileInjectable);
return (async (fullResult = false) => {
const result = await loadConfigFromFile(cluster.kubeConfigPath.get());
if (fullResult) {
return result;
}
return result.config;
}) as LoadKubeconfig;
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default loadKubeconfigInjectable;

View File

@ -17,10 +17,10 @@ import { KubeApi } from "../kube-api";
import { KubeObject } from "../kube-object"; import { KubeObject } from "../kube-object";
import { KubeObjectStore } from "../kube-object.store"; import { KubeObjectStore } from "../kube-object.store";
import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api";
import { Cluster } from "../../cluster/cluster";
class TestApi extends KubeApi<KubeObject> { class TestApi extends KubeApi<KubeObject> {
protected async checkPreferredVersion() { protected async checkPreferredVersion() {
@ -43,9 +43,7 @@ describe("ApiManager", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true); di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectionToken); di.override(hostedClusterInjectable, () => new Cluster({
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",

View File

@ -22,7 +22,7 @@ import type { DiContainer } from "@ogre-tools/injectable";
import ingressApiInjectable from "../endpoints/ingress.api.injectable"; import ingressApiInjectable from "../endpoints/ingress.api.injectable";
import loggerInjectable from "../../logger.injectable"; import loggerInjectable from "../../logger.injectable";
import maybeKubeApiInjectable from "../maybe-kube-api.injectable"; import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token"; import { Cluster } from "../../cluster/cluster";
describe("KubeApi", () => { describe("KubeApi", () => {
let fetchMock: AsyncFnMock<Fetch>; let fetchMock: AsyncFnMock<Fetch>;
@ -39,9 +39,7 @@ describe("KubeApi", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true); di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectionToken); di.override(hostedClusterInjectable, () => new Cluster({
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",

View File

@ -35,7 +35,7 @@ import namespaceApiInjectable from "../endpoints/namespace.api.injectable";
// NOTE: this is fine because we are testing something that only exported // NOTE: this is fine because we are testing something that only exported
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { PodsApi } from "../../../extensions/common-api/k8s-api"; import { PodsApi } from "../../../extensions/common-api/k8s-api";
import { createClusterInjectionToken } from "../../cluster/create-cluster-injection-token"; import { Cluster } from "../../cluster/cluster";
describe("createKubeApiForRemoteCluster", () => { describe("createKubeApiForRemoteCluster", () => {
let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster; let createKubeApiForRemoteCluster: CreateKubeApiForRemoteCluster;
@ -48,9 +48,7 @@ describe("createKubeApiForRemoteCluster", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true); di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectionToken); di.override(hostedClusterInjectable, () => new Cluster({
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",
@ -154,10 +152,9 @@ describe("KubeApi", () => {
fetchMock = asyncFn(); fetchMock = asyncFn();
di.override(fetchInjectable, () => fetchMock); di.override(fetchInjectable, () => fetchMock);
const createCluster = di.inject(createClusterInjectionToken);
const createKubeJsonApi = di.inject(createKubeJsonApiInjectable); const createKubeJsonApi = di.inject(createKubeJsonApiInjectable);
di.override(hostedClusterInjectable, () => createCluster({ di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",

View File

@ -8,11 +8,11 @@ import type { ConfigResult } from "../kube-helpers";
import { loadConfigFromString } from "../kube-helpers"; import { loadConfigFromString } from "../kube-helpers";
import resolveTildeInjectable from "../path/resolve-tilde.injectable"; import resolveTildeInjectable from "../path/resolve-tilde.injectable";
export type LoadConfigfromFile = (filePath: string) => Promise<ConfigResult>; export type LoadConfigFromFile = (filePath: string) => Promise<ConfigResult>;
const loadConfigfromFileInjectable = getInjectable({ const loadConfigFromFileInjectable = getInjectable({
id: "load-configfrom-file", id: "load-config-from-file",
instantiate: (di): LoadConfigfromFile => { instantiate: (di): LoadConfigFromFile => {
const readFile = di.inject(readFileInjectable); const readFile = di.inject(readFileInjectable);
const resolveTilde = di.inject(resolveTildeInjectable); const resolveTilde = di.inject(resolveTildeInjectable);
@ -20,4 +20,4 @@ const loadConfigfromFileInjectable = getInjectable({
}, },
}); });
export default loadConfigfromFileInjectable; export default loadConfigFromFileInjectable;

View File

@ -25,7 +25,7 @@ export interface BackoffCallerOptions<E> {
maxAttempts?: number; maxAttempts?: number;
/** /**
* In miliseconds * In milliseconds
* @default 1000 * @default 1000
*/ */
initialTimeout?: number; initialTimeout?: number;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { runInAction } from "mobx";
export function replaceObservableObject<Key extends string | number | symbol>(target: Partial<Record<Key, unknown>>, source: Partial<Record<Key, unknown>>): void {
runInAction(() => {
for (const key in target) {
if (!(key in source)) {
delete target[key];
}
}
Object.assign(target, source);
});
}

View File

@ -7,11 +7,10 @@ import type { DiContainer } from "@ogre-tools/injectable";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import { KubernetesCluster, WebLink } from "../../common/catalog-entities"; import { KubernetesCluster, WebLink } from "../../common/catalog-entities";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time"; import { advanceFakeTime, testUsingFakeTime } from "../../common/test-utils/use-fake-time";
import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable";
import createClusterInjectable from "../../renderer/cluster/create-cluster.injectable";
import showEntityDetailsInjectable from "../../renderer/components/+catalog/entity-details/show.injectable"; import showEntityDetailsInjectable from "../../renderer/components/+catalog/entity-details/show.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
@ -41,8 +40,6 @@ describe("opening catalog entity details panel", () => {
testUsingFakeTime(); testUsingFakeTime();
builder.afterWindowStart((windowDi) => { builder.afterWindowStart((windowDi) => {
const createCluster = windowDi.inject(createClusterInjectable);
clusterEntity = new KubernetesCluster({ clusterEntity = new KubernetesCluster({
metadata: { metadata: {
labels: {}, labels: {},
@ -85,7 +82,7 @@ describe("opening catalog entity details panel", () => {
phase: "available", phase: "available",
}, },
}); });
cluster = createCluster({ cluster = new Cluster({
contextName: clusterEntity.spec.kubeconfigContext, contextName: clusterEntity.spec.kubeconfigContext,
id: clusterEntity.getId(), id: clusterEntity.getId(),
kubeConfigPath: clusterEntity.spec.kubeconfigPath, kubeConfigPath: clusterEntity.spec.kubeconfigPath,

View File

@ -5,16 +5,12 @@
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { KubeConfig } from "@kubernetes/client-node"; import { KubeConfig } from "@kubernetes/client-node";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import type { CreateCluster } from "../../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import createContextHandlerInjectable from "../../../main/context-handler/create-context-handler.injectable";
import createKubeconfigManagerInjectable from "../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable";
import normalizedPlatformInjectable from "../../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../../../main/kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../../main/kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../../../main/kubectl/normalized-arch.injectable";
import openDeleteClusterDialogInjectable, { type OpenDeleteClusterDialog } from "../../../renderer/components/delete-cluster-dialog/open.injectable"; import openDeleteClusterDialogInjectable, { type OpenDeleteClusterDialog } from "../../../renderer/components/delete-cluster-dialog/open.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import { type ApplicationBuilder, getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { Cluster } from "../../../common/cluster/cluster"; import { Cluster } from "../../../common/cluster/cluster";
import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable";
@ -73,7 +69,6 @@ users:
describe("Deleting a cluster", () => { describe("Deleting a cluster", () => {
let builder: ApplicationBuilder; let builder: ApplicationBuilder;
let openDeleteClusterDialog: OpenDeleteClusterDialog; let openDeleteClusterDialog: OpenDeleteClusterDialog;
let createCluster: CreateCluster;
let rendered: RenderResult; let rendered: RenderResult;
let config: KubeConfig; let config: KubeConfig;
@ -82,8 +77,6 @@ describe("Deleting a cluster", () => {
builder = getApplicationBuilder(); builder = getApplicationBuilder();
builder.beforeApplicationStart((mainDi) => { builder.beforeApplicationStart((mainDi) => {
mainDi.override(createContextHandlerInjectable, () => () => undefined as never);
mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as never);
mainDi.override(kubectlBinaryNameInjectable, () => "kubectl"); mainDi.override(kubectlBinaryNameInjectable, () => "kubectl");
mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); mainDi.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
mainDi.override(normalizedPlatformInjectable, () => "darwin"); mainDi.override(normalizedPlatformInjectable, () => "darwin");
@ -94,8 +87,6 @@ describe("Deleting a cluster", () => {
}); });
builder.afterWindowStart(windowDi => { builder.afterWindowStart(windowDi => {
createCluster = windowDi.inject(createClusterInjectionToken);
const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable); const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable);
navigateToCatalog(); navigateToCatalog();
@ -111,7 +102,7 @@ describe("Deleting a cluster", () => {
beforeEach(() => { beforeEach(() => {
config.loadFromString(multiClusterConfig); config.loadFromString(multiClusterConfig);
currentCluster = createCluster({ currentCluster = new Cluster({
id: "some-current-context-cluster", id: "some-current-context-cluster",
contextName: "some-current-context", contextName: "some-current-context",
preferences: { preferences: {
@ -121,7 +112,7 @@ describe("Deleting a cluster", () => {
}, { }, {
clusterServerUrl: currentClusterServerUrl, clusterServerUrl: currentClusterServerUrl,
}); });
nonCurrentCluster = createCluster({ nonCurrentCluster = new Cluster({
id: "some-non-current-context-cluster", id: "some-non-current-context-cluster",
contextName: "some-non-current-context", contextName: "some-non-current-context",
preferences: { preferences: {
@ -199,7 +190,7 @@ describe("Deleting a cluster", () => {
const directoryForKubeConfigs = builder.applicationWindow.only.di.inject(directoryForKubeConfigsInjectable); const directoryForKubeConfigs = builder.applicationWindow.only.di.inject(directoryForKubeConfigsInjectable);
const joinPaths = builder.applicationWindow.only.di.inject(joinPathsInjectable); const joinPaths = builder.applicationWindow.only.di.inject(joinPathsInjectable);
currentCluster = createCluster({ currentCluster = new Cluster({
id: "some-cluster", id: "some-cluster",
contextName: "some-context", contextName: "some-context",
preferences: { preferences: {
@ -235,7 +226,7 @@ describe("Deleting a cluster", () => {
beforeEach(() => { beforeEach(() => {
config.loadFromString(singleClusterConfig); config.loadFromString(singleClusterConfig);
currentCluster = createCluster({ currentCluster = new Cluster({
id: "some-cluster", id: "some-cluster",
contextName: "some-context", contextName: "some-context",
preferences: { preferences: {

View File

@ -9,6 +9,7 @@ import directoryForLensLocalStorageInjectable from "../../../../common/directory
import removePathInjectable from "../../../../common/fs/remove.injectable"; import removePathInjectable from "../../../../common/fs/remove.injectable";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import { noop } from "../../../../common/utils"; import { noop } from "../../../../common/utils";
import clusterConnectionInjectable from "../../../../main/cluster/cluster-connection.injectable";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens";
import { deleteClusterChannel } from "../common/delete-channel"; import { deleteClusterChannel } from "../common/delete-channel";
@ -31,7 +32,9 @@ const deleteClusterChannelListenerInjectable = getRequestChannelListenerInjectab
return; return;
} }
cluster.disconnect(); const clusterConnection = di.inject(clusterConnectionInjectable, cluster);
clusterConnection.disconnect();
clusterFrames.delete(cluster.id); clusterFrames.delete(cluster.id);
// Remove from the cluster store as well, this should clear any old settings // Remove from the cluster store as well, this should clear any old settings

View File

@ -64,11 +64,13 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock); windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock);
}); });
builder.afterWindowStart(() => {
builder.allowKubeResource({ builder.allowKubeResource({
apiName: "namespaces", apiName: "namespaces",
group: "", group: "",
}); });
}); });
});
describe("when navigating to namespaces", () => { describe("when navigating to namespaces", () => {
let rendered: RenderResult; let rendered: RenderResult;

View File

@ -35,11 +35,13 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
windowDi.override(callForResourceInjectable, () => callForNamespaceMock); windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
}); });
builder.afterWindowStart(() => {
builder.allowKubeResource({ builder.allowKubeResource({
apiName: "namespaces", apiName: "namespaces",
group: "", group: "",
}); });
}); });
});
describe("given tab was previously opened, when application is started", () => { describe("given tab was previously opened, when application is started", () => {
let rendered: RenderResult; let rendered: RenderResult;

View File

@ -9,20 +9,24 @@ import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/c
describe("workload overview", () => { describe("workload overview", () => {
let rendered: RenderResult; let rendered: RenderResult;
let applicationBuilder: ApplicationBuilder; let builder: ApplicationBuilder;
beforeEach(async () => { beforeEach(async () => {
applicationBuilder = getApplicationBuilder().setEnvironmentToClusterFrame(); builder = getApplicationBuilder().setEnvironmentToClusterFrame();
applicationBuilder.allowKubeResource({
builder.afterWindowStart(() => {
builder.allowKubeResource({
apiName: "pods", apiName: "pods",
group: "", group: "",
}); });
rendered = await applicationBuilder.render(); });
rendered = await builder.render();
}); });
describe("when navigating to workload overview", () => { describe("when navigating to workload overview", () => {
beforeEach(() => { beforeEach(() => {
applicationBuilder.navigateWith(navigateToWorkloadsOverviewInjectable); builder.navigateWith(navigateToWorkloadsOverviewInjectable);
}); });
it("renders", () => { it("renders", () => {

View File

@ -7,11 +7,10 @@ import type { DiContainer } from "@ogre-tools/injectable";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import { KubernetesCluster, WebLink } from "../../common/catalog-entities"; import { KubernetesCluster, WebLink } from "../../common/catalog-entities";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import navigateToEntitySettingsInjectable from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable"; import navigateToEntitySettingsInjectable from "../../common/front-end-routing/routes/entity-settings/navigate-to-entity-settings.injectable";
import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable"; import catalogEntityRegistryInjectable from "../../renderer/api/catalog/entity/registry.injectable";
import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { type ApplicationBuilder, getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import createClusterInjectable from "../../renderer/cluster/create-cluster.injectable";
describe("Showing correct entity settings", () => { describe("Showing correct entity settings", () => {
let builder: ApplicationBuilder; let builder: ApplicationBuilder;
@ -37,8 +36,6 @@ describe("Showing correct entity settings", () => {
}); });
builder.afterWindowStart((windowDi) => { builder.afterWindowStart((windowDi) => {
const createCluster = windowDi.inject(createClusterInjectable);
clusterEntity = new KubernetesCluster({ clusterEntity = new KubernetesCluster({
metadata: { metadata: {
labels: {}, labels: {},
@ -81,7 +78,7 @@ describe("Showing correct entity settings", () => {
phase: "available", phase: "available",
}, },
}); });
cluster = createCluster({ cluster = new Cluster({
contextName: clusterEntity.spec.kubeconfigContext, contextName: clusterEntity.spec.kubeconfigContext,
id: clusterEntity.getId(), id: clusterEntity.getId(),
kubeConfigPath: clusterEntity.spec.kubeconfigPath, kubeConfigPath: clusterEntity.spec.kubeconfigPath,

View File

@ -2,35 +2,29 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; import { Cluster } from "../../common/cluster/cluster";
import type { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl/kubectl"; import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token"; import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable"; import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import { parse } from "url";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable"; import type { ClusterConnection } from "../cluster/cluster-connection.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import clusterConnectionInjectable from "../cluster/cluster-connection.injectable";
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable"; import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable"; import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
describe("create clusters", () => { describe("create clusters", () => {
let cluster: Cluster; let cluster: Cluster;
let createCluster: CreateCluster; let clusterConnection: ClusterConnection;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
const di = getDiForUnitTesting(); const di = getDiForUnitTesting();
const clusterServerUrl = "https://192.168.64.3:8443"; const clusterServerUrl = "https://192.168.64.3:8443";
@ -39,65 +33,51 @@ describe("create clusters", () => {
di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin"); di.override(normalizedPlatformInjectable, () => "darwin");
di.override(broadcastMessageInjectable, () => async () => {}); di.override(broadcastConnectionUpdateInjectable, () => async () => {});
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true); di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true);
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(createContextHandlerInjectable, () => (cluster) => ({ di.override(prometheusHandlerInjectable, () => ({
restartServer: jest.fn(),
stopServer: jest.fn(),
clusterUrl: parse(cluster.apiUrl),
getApiTarget: jest.fn(),
getPrometheusDetails: jest.fn(), getPrometheusDetails: jest.fn(),
resolveAuthProxyCa: jest.fn(),
resolveAuthProxyUrl: jest.fn(),
setupPrometheus: jest.fn(), setupPrometheus: jest.fn(),
ensureServer: jest.fn(), }));
} as ClusterContextHandler));
di.override(pathExistsInjectable, () => () => { throw new Error("tried call pathExists without override"); });
di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); });
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
createCluster = di.inject(createClusterInjectionToken); di.override(kubeconfigManagerInjectable, () => ({
ensurePath: async () => "/some-proxy-kubeconfig-file",
} as Partial<KubeconfigManager> as KubeconfigManager));
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
cluster = createCluster({ cluster = new Cluster({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
}, { }, {
clusterServerUrl, clusterServerUrl,
}); });
});
afterEach(() => { clusterConnection = di.inject(clusterConnectionInjectable, cluster);
cluster.disconnect();
}); });
it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => { it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => {
expect(cluster.apiUrl).toBe("https://192.168.64.3:8443"); expect(cluster.apiUrl.get()).toBe("https://192.168.64.3:8443");
}); });
it("reconnect should not throw if contextHandler is missing", () => { it("reconnect should not throw if contextHandler is missing", () => {
expect(() => cluster.reconnect()).not.toThrowError(); expect(() => clusterConnection.reconnect()).not.toThrowError();
}); });
it("disconnect should not throw if contextHandler is missing", () => { it("disconnect should not throw if contextHandler is missing", () => {
expect(() => cluster.disconnect()).not.toThrowError(); expect(() => clusterConnection.disconnect()).not.toThrowError();
}); });
it("activating cluster should try to connect to cluster and do a refresh", async () => { it("activating cluster should try to connect to cluster and do a refresh", async () => {
jest.spyOn(cluster, "reconnect"); jest.spyOn(clusterConnection, "reconnect").mockImplementation(async () => {});
jest.spyOn(cluster, "refreshConnectionStatus"); jest.spyOn(clusterConnection, "refreshConnectionStatus").mockImplementation(async () => {});
await cluster.activate(); await clusterConnection.activate();
expect(cluster.reconnect).toBeCalled(); expect(clusterConnection.reconnect).toBeCalled();
expect(cluster.refreshConnectionStatus).toBeCalled(); expect(clusterConnection.refreshConnectionStatus).toBeCalled();
cluster.disconnect();
jest.resetAllMocks();
}); });
}); });

View File

@ -3,16 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { ClusterContextHandler } from "../context-handler/context-handler";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import { Cluster } from "../../common/cluster/cluster";
import type { Cluster } from "../../common/cluster/cluster";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { PrometheusProvider } from "../prometheus/provider"; import type { PrometheusProvider } from "../prometheus/provider";
import { prometheusProviderInjectionToken } from "../prometheus/provider"; import { prometheusProviderInjectionToken } from "../prometheus/provider";
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import loadProxyKubeconfigInjectable from "../cluster/load-proxy-kubeconfig.injectable";
import type { KubeConfig } from "@kubernetes/client-node";
enum ServiceResult { enum ServiceResult {
Success, Success,
@ -41,22 +45,30 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult):
}, },
}); });
const clusterStub = {
getProxyKubeconfig: () => ({
makeApiClient: (): void => undefined,
}),
apiUrl: "http://localhost:81",
} as unknown as Cluster;
describe("ContextHandler", () => { describe("ContextHandler", () => {
let createContextHandler: (cluster: Cluster) => ClusterContextHandler;
let di: DiContainer; let di: DiContainer;
let cluster: Cluster;
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
di.override(createKubeAuthProxyInjectable, () => ({} as any));
createContextHandler = di.inject(createContextHandlerInjectable); di.override(loadProxyKubeconfigInjectable, () => async () => ({
makeApiClient: () => ({} as any),
} as Partial<KubeConfig>));
di.override(createKubeAuthProxyInjectable, () => () => ({
run: async () => {},
} as KubeAuthProxy));
di.override(directoryForTempInjectable, () => "/some-directory-for-tmp");
di.inject(lensProxyPortInjectable).set(9968);
cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-kubeconfig-path",
}, {
clusterServerUrl: "https://some-website.com",
});
}); });
describe("getPrometheusService", () => { describe("getPrometheusService", () => {
@ -76,7 +88,7 @@ describe("ContextHandler", () => {
} }
}); });
expect(() => createContextHandler(clusterStub).getPrometheusDetails()).rejects.toThrowError(); expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError();
}); });
it.each([ it.each([
@ -107,7 +119,7 @@ describe("ContextHandler", () => {
} }
}); });
const details = await createContextHandler(clusterStub).getPrometheusDetails(); const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === `id_failure_${failures}`); expect(details.provider.kind === `id_failure_${failures}`);
}); });
@ -140,7 +152,7 @@ describe("ContextHandler", () => {
} }
}); });
const details = await createContextHandler(clusterStub).getPrometheusDetails(); const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === "id_failure_0"); expect(details.provider.kind === "id_failure_0");
}); });
@ -183,7 +195,7 @@ describe("ContextHandler", () => {
} }
}); });
const details = await createContextHandler(clusterStub).getPrometheusDetails(); const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === "id_success_0"); expect(details.provider.kind === "id_success_0");
}); });

View File

@ -4,7 +4,7 @@
*/ */
import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable"; import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { ChildProcess } from "child_process"; import type { ChildProcess } from "child_process";
import { Kubectl } from "../kubectl/kubectl"; import { Kubectl } from "../kubectl/kubectl";
@ -14,8 +14,6 @@ import type { Readable } from "stream";
import { EventEmitter } from "stream"; import { EventEmitter } from "stream";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import spawnInjectable from "../child-process/spawn.injectable"; import spawnInjectable from "../child-process/spawn.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
@ -31,7 +29,6 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab
const clusterServerUrl = "https://192.168.64.3:8443"; const clusterServerUrl = "https://192.168.64.3:8443";
describe("kube auth proxy tests", () => { describe("kube auth proxy tests", () => {
let createCluster: CreateCluster;
let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
let spawnMock: jest.Mock; let spawnMock: jest.Mock;
let waitUntilPortIsUsedMock: jest.Mock; let waitUntilPortIsUsedMock: jest.Mock;
@ -86,12 +83,11 @@ describe("kube auth proxy tests", () => {
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin"); di.override(normalizedPlatformInjectable, () => "darwin");
createCluster = di.inject(createClusterInjectionToken);
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
}); });
it("calling exit multiple times shouldn't throw", async () => { it("calling exit multiple times shouldn't throw", async () => {
const cluster = createCluster({ const cluster = new Cluster({
id: "foobar", id: "foobar",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
contextName: "minikube", contextName: "minikube",
@ -114,8 +110,11 @@ describe("kube auth proxy tests", () => {
beforeEach(async () => { beforeEach(async () => {
mockedCP = mockDeep<ChildProcess>(); mockedCP = mockDeep<ChildProcess>();
listeners = new EventEmitter(); listeners = new EventEmitter();
const stderr = mockedCP.stderr = mock<Readable>(); const stderr = mock<Readable>();
const stdout = mockedCP.stdout = mock<Readable>(); const stdout = mock<Readable>();
mockedCP.stderr = stderr as any;
mockedCP.stdout = stdout as any;
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
@ -124,32 +123,32 @@ describe("kube auth proxy tests", () => {
return mockedCP; return mockedCP;
}); });
mockedCP.stderr.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stderr.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.on(`stderr/${String(event)}`, listener); listeners.on(`stderr/${String(event)}`, listener);
return stderr; return stderr;
}); });
mockedCP.stderr.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stderr.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stderr/${String(event)}`, listener); listeners.off(`stderr/${String(event)}`, listener);
return stderr; return stderr;
}); });
mockedCP.stderr.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stderr.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stderr/${String(event)}`, listener); listeners.off(`stderr/${String(event)}`, listener);
return stderr; return stderr;
}); });
mockedCP.stderr.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stderr.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.once(`stderr/${String(event)}`, listener); listeners.once(`stderr/${String(event)}`, listener);
return stderr; return stderr;
}); });
mockedCP.stderr.removeAllListeners.mockImplementation((event?: string | symbol): Readable => { stderr.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
listeners.removeAllListeners(event ?? `stderr/${String(event)}`); listeners.removeAllListeners(event ?? `stderr/${String(event)}`);
return stderr; return stderr;
}); });
mockedCP.stdout.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stdout.on.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.on(`stdout/${String(event)}`, listener); listeners.on(`stdout/${String(event)}`, listener);
if (event === "data") { if (event === "data") {
@ -158,22 +157,22 @@ describe("kube auth proxy tests", () => {
return stdout; return stdout;
}); });
mockedCP.stdout.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stdout.once.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.once(`stdout/${String(event)}`, listener); listeners.once(`stdout/${String(event)}`, listener);
return stdout; return stdout;
}); });
mockedCP.stdout.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stdout.off.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stdout/${String(event)}`, listener); listeners.off(`stdout/${String(event)}`, listener);
return stdout; return stdout;
}); });
mockedCP.stdout.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => { stdout.removeListener.mockImplementation((event: string | symbol, listener: (message: any, sendHandle: any) => void): Readable => {
listeners.off(`stdout/${String(event)}`, listener); listeners.off(`stdout/${String(event)}`, listener);
return stdout; return stdout;
}); });
mockedCP.stdout.removeAllListeners.mockImplementation((event?: string | symbol): Readable => { stdout.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
listeners.removeAllListeners(event ?? `stdout/${String(event)}`); listeners.removeAllListeners(event ?? `stdout/${String(event)}`);
return stdout; return stdout;
@ -185,7 +184,7 @@ describe("kube auth proxy tests", () => {
}); });
waitUntilPortIsUsedMock.mockReturnValueOnce(Promise.resolve()); waitUntilPortIsUsedMock.mockReturnValueOnce(Promise.resolve());
const cluster = createCluster({ const cluster = new Cluster({
id: "foobar", id: "foobar",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
contextName: "minikube", contextName: "minikube",
@ -194,37 +193,38 @@ describe("kube auth proxy tests", () => {
}); });
proxy = createKubeAuthProxy(cluster, {}); proxy = createKubeAuthProxy(cluster, {});
await proxy.run();
}); });
it("should call spawn and broadcast errors", async () => { it("should call spawn and broadcast errors", () => {
await proxy.run();
listeners.emit("error", { message: "foobarbat" }); listeners.emit("error", { message: "foobarbat" });
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", level: "error" }); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", level: "error" });
}); });
it("should call spawn and broadcast exit", async () => { it("should call spawn and broadcast exit as error when exitCode != 0", () => {
await proxy.run(); listeners.emit("exit", 1);
listeners.emit("exit", 0);
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 0", level: "info" }); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited with code: 1", level: "error" });
}); });
it("should call spawn and broadcast errors from stderr", async () => { it("should call spawn and broadcast exit as info when exitCode == 0", () => {
await proxy.run(); listeners.emit("exit", 0);
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "proxy exited successfully", level: "info" });
});
it("should call spawn and broadcast errors from stderr", () => {
listeners.emit("stderr/data", "an error"); listeners.emit("stderr/data", "an error");
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", level: "error" }); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", level: "error" });
}); });
it("should call spawn and broadcast stdout serving info", async () => { it("should call spawn and broadcast stdout serving info", () => {
await proxy.run();
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", level: "info" }); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", level: "info" });
}); });
it("should call spawn and broadcast stdout other info", async () => { it("should call spawn and broadcast stdout other info", () => {
await proxy.run();
listeners.emit("stdout/data", "some info"); listeners.emit("stdout/data", "some info");
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", level: "info" }); expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "some info", level: "info" });

View File

@ -3,14 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import type { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import { parse } from "url";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
@ -30,12 +27,13 @@ import removePathInjectable from "../../common/fs/remove.injectable";
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable"; import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable";
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable"; import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable";
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable"; import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
const clusterServerUrl = "https://192.168.64.3:8443"; const clusterServerUrl = "https://192.168.64.3:8443";
describe("kubeconfig manager tests", () => { describe("kubeconfig manager tests", () => {
let clusterFake: Cluster; let clusterFake: Cluster;
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
let di: DiContainer; let di: DiContainer;
let loggerMock: jest.Mocked<Logger>; let loggerMock: jest.Mocked<Logger>;
let readFileMock: AsyncFnMock<ReadFile>; let readFileMock: AsyncFnMock<ReadFile>;
@ -56,6 +54,7 @@ describe("kubeconfig manager tests", () => {
di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); }); di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); });
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
di.inject(lensProxyPortInjectable).set(9191);
readFileMock = asyncFn(); readFileMock = asyncFn();
di.override(readFileInjectable, () => readFileMock); di.override(readFileInjectable, () => readFileMock);
@ -78,23 +77,15 @@ describe("kubeconfig manager tests", () => {
ensureServerMock = asyncFn(); ensureServerMock = asyncFn();
di.override(createContextHandlerInjectable, () => (cluster) => ({ di.override(kubeAuthProxyServerInjectable, () => ({
restartServer: jest.fn(), restart: jest.fn(),
stopServer: jest.fn(), stop: jest.fn(),
clusterUrl: parse(cluster.apiUrl),
getApiTarget: jest.fn(), getApiTarget: jest.fn(),
getPrometheusDetails: jest.fn(), ensureRunning: ensureServerMock,
resolveAuthProxyCa: jest.fn(), ensureAuthProxyUrl: jest.fn(),
resolveAuthProxyUrl: jest.fn(),
setupPrometheus: jest.fn(),
ensureServer: ensureServerMock,
})); }));
const createCluster = di.inject(createClusterInjectionToken); clusterFake = new Cluster({
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
clusterFake = createCluster({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "/minikube-config.yml", kubeConfigPath: "/minikube-config.yml",
@ -102,9 +93,7 @@ describe("kubeconfig manager tests", () => {
clusterServerUrl, clusterServerUrl,
}); });
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("https://127.0.0.1:9191/foo"); kubeConfManager = di.inject(kubeconfigManagerInjectable, clusterFake);
kubeConfManager = createKubeconfigManager(clusterFake);
}); });
describe("when calling clear", () => { describe("when calling clear", () => {
@ -123,7 +112,7 @@ describe("kubeconfig manager tests", () => {
let getPathPromise: Promise<string>; let getPathPromise: Promise<string>;
beforeEach(async () => { beforeEach(async () => {
getPathPromise = kubeConfManager.getPath(); getPathPromise = kubeConfManager.ensurePath();
}); });
it("should not call pathExists()", () => { it("should not call pathExists()", () => {
@ -234,7 +223,7 @@ describe("kubeconfig manager tests", () => {
let getPathPromise: Promise<string>; let getPathPromise: Promise<string>;
beforeEach(async () => { beforeEach(async () => {
getPathPromise = kubeConfManager.getPath(); getPathPromise = kubeConfManager.ensurePath();
}); });
it("should call pathExists", () => { it("should call pathExists", () => {

View File

@ -0,0 +1,213 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import { Cluster } from "../../common/cluster/cluster";
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { PrometheusProvider } from "../prometheus/provider";
import { prometheusProviderInjectionToken } from "../prometheus/provider";
import { runInAction } from "mobx";
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
import loadProxyKubeconfigInjectable from "../cluster/load-proxy-kubeconfig.injectable";
import { KubeConfig } from "@kubernetes/client-node";
enum ServiceResult {
Success,
Failure,
}
const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): PrometheusProvider => ({
kind,
name: "TestProvider1",
isConfigurable: false,
getQuery: () => {
throw new Error("getQuery is not implemented.");
},
getPrometheusService: async () => {
switch (alwaysFail) {
case ServiceResult.Success:
return {
kind,
namespace: "default",
port: 7000,
service: "",
};
case ServiceResult.Failure:
throw new Error("does fail");
}
},
});
describe("ContextHandler", () => {
let di: DiContainer;
let cluster: Cluster;
beforeEach(() => {
di = getDiForUnitTesting();
di.override(loadProxyKubeconfigInjectable, (di, cluster) => async () => {
const res = new KubeConfig();
res.addCluster({
name: "some-cluster-name",
server: cluster.apiUrl.get(),
skipTLSVerify: false,
});
res.addContext({
cluster: "some-cluster-name",
name: "some-context-name",
user: "some-user-name",
});
res.addUser({
name: "some-user-name",
});
res.setCurrentContext("some-context-name");
return res;
});
di.override(directoryForTempInjectable, () => "/some-temp-dir");
di.inject(lensProxyPortInjectable).set(12345);
cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
}, {
clusterServerUrl: "http://localhost:81",
});
});
describe("getPrometheusService", () => {
it.each([
[0],
[1],
[2],
[3],
])("should throw after %d failure(s)", async (failures) => {
runInAction(() => {
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
});
expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError();
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => {
runInAction(() => {
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
for (let i = 0; i < successes; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === `id_failure_${failures}`);
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => {
runInAction(() => {
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
for (let i = 0; i < successes; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === "id_failure_0");
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => {
const beforeSuccesses = Math.floor(successes / 2);
runInAction(() => {
for (let i = 0; i < beforeSuccesses; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
for (let i = beforeSuccesses; i < successes; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === "id_success_0");
});
});
});

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { observable, ObservableMap } from "mobx"; import { observable, ObservableMap, runInAction } from "mobx";
import type { CatalogEntity } from "../../../common/catalog"; import type { CatalogEntity } from "../../../common/catalog";
import { loadFromOptions } from "../../../common/kube-helpers"; import { loadFromOptions } from "../../../common/kube-helpers";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
@ -34,6 +34,8 @@ import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.inject
import pathExistsInjectable from "../../../common/fs/path-exists.injectable"; import pathExistsInjectable from "../../../common/fs/path-exists.injectable";
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable"; import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
import type { KubeconfigManager } from "../../kubeconfig-manager/kubeconfig-manager";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
describe("kubeconfig-sync.source tests", () => { describe("kubeconfig-sync.source tests", () => {
let computeKubeconfigDiff: ComputeKubeconfigDiff; let computeKubeconfigDiff: ComputeKubeconfigDiff;
@ -52,6 +54,10 @@ describe("kubeconfig-sync.source tests", () => {
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
di.override(kubeconfigManagerInjectable, () => ({
ensurePath: async () => "/some-proxy-kubeconfig-file",
} as Partial<KubeconfigManager> as KubeconfigManager));
clusters = new Map(); clusters = new Map();
di.override(getClusterByIdInjectable, () => id => clusters.get(id)); di.override(getClusterByIdInjectable, () => id => clusters.get(id));
@ -79,7 +85,7 @@ describe("kubeconfig-sync.source tests", () => {
const config = loadFromOptions({ const config = loadFromOptions({
clusters: [{ clusters: [{
name: "cluster-name", name: "cluster-name",
server: "1.2.3.4", server: "https://1.2.3.4",
skipTLSVerify: false, skipTLSVerify: false,
}], }],
users: [{ users: [{
@ -117,7 +123,7 @@ describe("kubeconfig-sync.source tests", () => {
clusters: [{ clusters: [{
name: "cluster-name", name: "cluster-name",
cluster: { cluster: {
server: "1.2.3.4", server: "https://1.2.3.4",
}, },
skipTLSVerify: false, skipTLSVerify: false,
}], }],
@ -149,8 +155,10 @@ describe("kubeconfig-sync.source tests", () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const c = (iter.first(rootSource.values())!)[0]; const c = (iter.first(rootSource.values())!)[0];
expect(c.kubeConfigPath).toBe("/bar"); runInAction(() => {
expect(c.contextName).toBe("context-name"); expect(c.kubeConfigPath.get()).toBe("/bar");
expect(c.contextName.get()).toBe("context-name");
});
}); });
it("should remove a cluster when it is removed from the contents", () => { it("should remove a cluster when it is removed from the contents", () => {
@ -158,7 +166,7 @@ describe("kubeconfig-sync.source tests", () => {
clusters: [{ clusters: [{
name: "cluster-name", name: "cluster-name",
cluster: { cluster: {
server: "1.2.3.4", server: "https://1.2.3.4",
}, },
skipTLSVerify: false, skipTLSVerify: false,
}], }],
@ -190,8 +198,8 @@ describe("kubeconfig-sync.source tests", () => {
const c = rootSource.values().next().value[0] as Cluster; const c = rootSource.values().next().value[0] as Cluster;
expect(c.kubeConfigPath).toBe("/bar"); expect(c.kubeConfigPath.get()).toBe("/bar");
expect(c.contextName).toBe("context-name"); expect(c.contextName.get()).toBe("context-name");
computeKubeconfigDiff("{}", rootSource, filePath); computeKubeconfigDiff("{}", rootSource, filePath);
@ -203,7 +211,7 @@ describe("kubeconfig-sync.source tests", () => {
clusters: [{ clusters: [{
name: "cluster-name", name: "cluster-name",
cluster: { cluster: {
server: "1.2.3.4", server: "https://1.2.3.4",
}, },
skipTLSVerify: false, skipTLSVerify: false,
}], }],
@ -243,15 +251,17 @@ describe("kubeconfig-sync.source tests", () => {
{ {
const c = rootSource.values().next().value[0] as Cluster; const c = rootSource.values().next().value[0] as Cluster;
expect(c.kubeConfigPath).toBe("/bar"); runInAction(() => {
expect(["context-name", "context-name-2"].includes(c.contextName)).toBe(true); expect(c.kubeConfigPath.get()).toBe("/bar");
expect(["context-name", "context-name-2"].includes(c.contextName.get())).toBe(true);
});
} }
const newContents = JSON.stringify({ const newContents = JSON.stringify({
clusters: [{ clusters: [{
name: "cluster-name", name: "cluster-name",
cluster: { cluster: {
server: "1.2.3.4", server: "https://1.2.3.4",
}, },
skipTLSVerify: false, skipTLSVerify: false,
}], }],
@ -283,8 +293,8 @@ describe("kubeconfig-sync.source tests", () => {
{ {
const c = rootSource.values().next().value[0] as Cluster; const c = rootSource.values().next().value[0] as Cluster;
expect(c.kubeConfigPath).toBe("/bar"); expect(c.kubeConfigPath.get()).toBe("/bar");
expect(c.contextName).toBe("context-name"); expect(c.contextName.get()).toBe("context-name");
} }
}); });
}); });
@ -444,7 +454,7 @@ const foobarConfig = JSON.stringify({
clusters: [{ clusters: [{
name: "cluster-name", name: "cluster-name",
cluster: { cluster: {
server: "1.2.3.4", server: "https://1.2.3.4",
}, },
skipTLSVerify: false, skipTLSVerify: false,
}], }],

View File

@ -10,13 +10,13 @@ import { homedir } from "os";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import type { CatalogEntity } from "../../../common/catalog"; import type { CatalogEntity } from "../../../common/catalog";
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import { Cluster } from "../../../common/cluster/cluster";
import { loadConfigFromString } from "../../../common/kube-helpers"; import { loadConfigFromString } from "../../../common/kube-helpers";
import clustersThatAreBeingDeletedInjectable from "../../cluster/are-being-deleted.injectable"; import clustersThatAreBeingDeletedInjectable from "../../cluster/are-being-deleted.injectable";
import { catalogEntityFromCluster } from "../../cluster/manager"; import { catalogEntityFromCluster } from "../../cluster/manager";
import createClusterInjectable from "../../create-cluster/create-cluster.injectable";
import configToModelsInjectable from "./config-to-models.injectable"; import configToModelsInjectable from "./config-to-models.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable"; import kubeconfigSyncLoggerInjectable from "./logger.injectable";
import clusterConnectionInjectable from "../../cluster/cluster-connection.injectable";
export type ComputeKubeconfigDiff = (contents: string, source: ObservableMap<string, [Cluster, CatalogEntity]>, filePath: string) => void; export type ComputeKubeconfigDiff = (contents: string, source: ObservableMap<string, [Cluster, CatalogEntity]>, filePath: string) => void;
@ -24,7 +24,6 @@ const computeKubeconfigDiffInjectable = getInjectable({
id: "compute-kubeconfig-diff", id: "compute-kubeconfig-diff",
instantiate: (di): ComputeKubeconfigDiff => { instantiate: (di): ComputeKubeconfigDiff => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
const createCluster = di.inject(createClusterInjectable);
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
const configToModels = di.inject(configToModelsInjectable); const configToModels = di.inject(configToModelsInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable); const logger = di.inject(kubeconfigSyncLoggerInjectable);
@ -51,7 +50,9 @@ const computeKubeconfigDiffInjectable = getInjectable({
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting // remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
clustersThatAreBeingDeleted.delete(value[0].id); clustersThatAreBeingDeleted.delete(value[0].id);
value[0].disconnect(); const clusterConnection = di.inject(clusterConnectionInjectable, value[0]);
clusterConnection.disconnect();
source.delete(contextName); source.delete(contextName);
logger.debug(`Removed old cluster from sync`, { filePath, contextName }); logger.debug(`Removed old cluster from sync`, { filePath, contextName });
continue; continue;
@ -71,9 +72,9 @@ const computeKubeconfigDiffInjectable = getInjectable({
// add new clusters to the source // add new clusters to the source
try { try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex"); const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = getClusterById(clusterId) ?? createCluster({ ...model, id: clusterId }, configData); const cluster = getClusterById(clusterId) ?? new Cluster({ ...model, id: clusterId }, configData);
if (!cluster.apiUrl) { if (!cluster.apiUrl.get()) {
throw new Error("Cluster constructor failed, see above error"); throw new Error("Cluster constructor failed, see above error");
} }

View File

@ -13,18 +13,18 @@ import requestClusterVersionInjectable from "./request-cluster-version.injectabl
const isGKE = (version: string) => version.includes("gke"); const isGKE = (version: string) => version.includes("gke");
const isEKS = (version: string) => version.includes("eks"); const isEKS = (version: string) => version.includes("eks");
const isIKS = (version: string) => version.includes("IKS"); const isIKS = (version: string) => version.includes("IKS");
const isAKS = (cluster: Cluster) => cluster.apiUrl.includes("azmk8s.io"); const isAKS = (cluster: Cluster) => cluster.apiUrl.get().includes("azmk8s.io");
const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-"); const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-");
const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.endsWith("k8s.ondigitalocean.com"); const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.get().endsWith("k8s.ondigitalocean.com");
const isMinikube = (cluster: Cluster) => cluster.contextName.startsWith("minikube"); const isMinikube = (cluster: Cluster) => cluster.contextName.get().startsWith("minikube");
const isMicrok8s = (cluster: Cluster) => cluster.contextName.startsWith("microk8s"); const isMicrok8s = (cluster: Cluster) => cluster.contextName.get().startsWith("microk8s");
const isKind = (cluster: Cluster) => cluster.contextName.startsWith("kubernetes-admin@kind-"); const isKind = (cluster: Cluster) => cluster.contextName.get().startsWith("kubernetes-admin@kind-");
const isDockerDesktop = (cluster: Cluster) => cluster.contextName === "docker-desktop"; const isDockerDesktop = (cluster: Cluster) => cluster.contextName.get() === "docker-desktop";
const isTke = (version: string) => version.includes("-tke."); const isTke = (version: string) => version.includes("-tke.");
const isCustom = (version: string) => version.includes("+"); const isCustom = (version: string) => version.includes("+");
const isVMWare = (version: string) => version.includes("+vmware"); const isVMWare = (version: string) => version.includes("+vmware");
const isRke = (version: string) => version.includes("-rancher"); const isRke = (version: string) => version.includes("-rancher");
const isRancherDesktop = (cluster: Cluster) => cluster.contextName === "rancher-desktop"; const isRancherDesktop = (cluster: Cluster) => cluster.contextName.get() === "rancher-desktop";
const isK3s = (version: string) => version.includes("+k3s"); const isK3s = (version: string) => version.includes("+k3s");
const isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s"); const isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s");
const isAlibaba = (version: string) => version.includes("-aliyun"); const isAlibaba = (version: string) => version.includes("-aliyun");

View File

@ -28,7 +28,7 @@ const clusterIdDetectorFactoryInjectable = getInjectable({
try { try {
id = await getDefaultNamespaceId(cluster); id = await getDefaultNamespaceId(cluster);
} catch(_) { } catch(_) {
id = cluster.apiUrl; id = cluster.apiUrl.get();
} }
const value = createHash("sha256").update(id).digest("hex"); const value = createHash("sha256").update(id).digest("hex");

View File

@ -7,8 +7,7 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje
import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; 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 directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { ClusterMetadataKey } from "../../common/cluster-types"; import { ClusterMetadataKey } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import clusterDistributionDetectorInjectable from "./cluster-distribution-detector.injectable"; import clusterDistributionDetectorInjectable from "./cluster-distribution-detector.injectable";
import clusterIdDetectorFactoryInjectable from "./cluster-id-detector.injectable"; import clusterIdDetectorFactoryInjectable from "./cluster-id-detector.injectable";
@ -64,9 +63,7 @@ describe("detect-cluster-metadata", () => {
detectClusterMetadata = di.inject(detectClusterMetadataInjectable); detectClusterMetadata = di.inject(detectClusterMetadataInjectable);
const createCluster = di.inject(createClusterInjectionToken); cluster = new Cluster({
cluster = createCluster({
id: "some-id", id: "some-id",
contextName: "some-context", contextName: "some-context",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",

View File

@ -11,7 +11,7 @@ export interface ClusterDetectionResult {
accuracy: number; accuracy: number;
} }
export interface FalibleOnlyClusterMetadataDetector { export interface FallibleOnlyClusterMetadataDetector {
readonly key: string; readonly key: string;
detect(cluster: Cluster): Promise<ClusterDetectionResult>; detect(cluster: Cluster): Promise<ClusterDetectionResult>;
} }

View File

@ -0,0 +1,21 @@
/**
* 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 type { Cluster } from "../../common/cluster/cluster";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
const kubeAuthProxyUrlInjectable = getInjectable({
id: "kube-auth-proxy-url",
instantiate: (di, cluster) => {
const lensProxyPort = di.inject(lensProxyPortInjectable);
return `https://127.0.0.1:${lensProxyPort.get()}/${cluster.id}`;
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default kubeAuthProxyUrlInjectable;

View File

@ -0,0 +1,29 @@
/**
* 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 type { KubeAuthUpdate } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import loggerInjectable from "../../common/logger.injectable";
export type BroadcastConnectionUpdate = (update: KubeAuthUpdate) => void;
const broadcastConnectionUpdateInjectable = getInjectable({
id: "broadcast-connection-update",
instantiate: (di, cluster): BroadcastConnectionUpdate => {
const broadcastMessage = di.inject(broadcastMessageInjectable);
const logger = di.inject(loggerInjectable);
return (update) => {
logger.debug(`[CLUSTER]: broadcasting connection update`, { ...update, meta: cluster.getMeta() });
broadcastMessage(`cluster:${cluster.id}:connection-update`, update);
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default broadcastConnectionUpdateInjectable;

View File

@ -0,0 +1,423 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { type KubeConfig, HttpError } from "@kubernetes/client-node";
import { reaction, comparer, runInAction } from "mobx";
import { ClusterStatus } from "../../common/cluster-types";
import type { CreateAuthorizationReview } from "../../common/cluster/authorization-review.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import type { CreateListNamespaces } from "../../common/cluster/list-namespaces.injectable";
import type { RequestNamespaceListPermissionsFor, RequestNamespaceListPermissions } from "../../common/cluster/request-namespace-list-permissions.injectable";
import type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable";
import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster";
import type { Logger } from "../../common/logger";
import type { KubeApiResource } from "../../common/rbac";
import { formatKubeApiResource } from "../../common/rbac";
import { disposer, isDefined, isRequestError } from "../../common/utils";
import { withConcurrencyLimit } from "../../common/utils/with-concurrency-limit";
import type { ClusterPrometheusHandler } from "./prometheus-handler/prometheus-handler";
import type { BroadcastConnectionUpdate } from "./broadcast-connection-update.injectable";
import type { KubeAuthProxyServer } from "./kube-auth-proxy-server.injectable";
import type { LoadProxyKubeconfig } from "./load-proxy-kubeconfig.injectable";
import type { RemoveProxyKubeconfig } from "./remove-proxy-kubeconfig.injectable";
import type { RequestApiResources } from "./request-api-resources.injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable";
import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.injectable";
import loggerInjectable from "../../common/logger.injectable";
import prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable";
import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable";
import requestApiResourcesInjectable from "./request-api-resources.injectable";
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
import type { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable";
import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token";
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
import { replaceObservableObject } from "../../common/utils/replace-observable-object";
interface Dependencies {
readonly logger: Logger;
readonly prometheusHandler: ClusterPrometheusHandler;
readonly kubeAuthProxyServer: KubeAuthProxyServer;
readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector;
createAuthorizationReview: CreateAuthorizationReview;
requestApiResources: RequestApiResources;
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor;
createListNamespaces: CreateListNamespaces;
detectClusterMetadata: DetectClusterMetadata;
broadcastMessage: BroadcastMessage;
broadcastConnectionUpdate: BroadcastConnectionUpdate;
loadProxyKubeconfig: LoadProxyKubeconfig;
removeProxyKubeconfig: RemoveProxyKubeconfig;
}
export type { ClusterConnection };
class ClusterConnection {
protected readonly eventsDisposer = disposer();
protected activated = false;
constructor(
private readonly dependencies: Dependencies,
private readonly cluster: Cluster,
) {}
private bindEvents() {
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.cluster.getMeta());
const refreshTimer = setInterval(() => {
if (!this.cluster.disconnected.get()) {
this.refresh();
}
}, 30_000); // every 30s
const refreshMetadataTimer = setInterval(() => {
if (this.cluster.available.get()) {
this.refreshAccessibilityAndMetadata();
}
}, 900000); // every 15 minutes
this.eventsDisposer.push(
reaction(
() => this.cluster.prometheusPreferences.get(),
preferences => this.dependencies.prometheusHandler.setupPrometheus(preferences),
{ equals: comparer.structural },
),
() => clearInterval(refreshTimer),
() => clearInterval(refreshMetadataTimer),
reaction(() => this.cluster.preferences.defaultNamespace, () => this.recreateProxyKubeconfig()),
);
}
protected async recreateProxyKubeconfig() {
this.dependencies.logger.info("[CLUSTER]: Recreating proxy kubeconfig");
try {
await this.dependencies.removeProxyKubeconfig();
await this.dependencies.loadProxyKubeconfig();
} catch (error) {
this.dependencies.logger.error(`[CLUSTER]: failed to recreate proxy kubeconfig`, error);
}
}
/**
* @param force force activation
*/
async activate(force = false) {
if (this.activated && !force) {
return;
}
this.dependencies.logger.info(`[CLUSTER]: activate`, this.cluster.getMeta());
if (!this.eventsDisposer.length) {
this.bindEvents();
}
if (this.cluster.disconnected || !this.cluster.accessible.get()) {
try {
this.dependencies.broadcastConnectionUpdate({
level: "info",
message: "Starting connection ...",
});
await this.reconnect();
} catch (error) {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: `Failed to start connection: ${error}`,
});
return;
}
}
try {
this.dependencies.broadcastConnectionUpdate({
level: "info",
message: "Refreshing connection status ...",
});
await this.refreshConnectionStatus();
} catch (error) {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: `Failed to connection status: ${error}`,
});
return;
}
if (this.cluster.accessible.get()) {
try {
this.dependencies.broadcastConnectionUpdate({
level: "info",
message: "Refreshing cluster accessibility ...",
});
await this.refreshAccessibility();
} catch (error) {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: `Failed to refresh accessibility: ${error}`,
});
return;
}
this.dependencies.broadcastConnectionUpdate({
level: "info",
message: "Connected, waiting for view to load ...",
});
}
this.activated = true;
}
async reconnect() {
this.dependencies.logger.info(`[CLUSTER]: reconnect`, this.cluster.getMeta());
await this.dependencies.kubeAuthProxyServer?.restart();
runInAction(() => {
this.cluster.disconnected.set(false);
});
}
disconnect() {
if (this.cluster.disconnected) {
return this.dependencies.logger.debug("[CLUSTER]: already disconnected", { id: this.cluster.id });
}
runInAction(() => {
this.dependencies.logger.info(`[CLUSTER]: disconnecting`, { id: this.cluster.id });
this.eventsDisposer();
this.dependencies.kubeAuthProxyServer?.stop();
this.cluster.disconnected.set(true);
this.cluster.online.set(false);
this.cluster.accessible.set(false);
this.cluster.ready.set(false);
this.activated = false;
this.cluster.allowedNamespaces.clear();
this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.cluster.id });
});
}
async refresh() {
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.cluster.getMeta());
await this.refreshConnectionStatus();
}
async refreshAccessibilityAndMetadata() {
await this.refreshAccessibility();
await this.refreshMetadata();
}
async refreshMetadata() {
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.cluster.getMeta());
const metadata = await this.dependencies.detectClusterMetadata(this.cluster);
runInAction(() => {
replaceObservableObject(this.cluster.metadata, metadata);
});
}
private async refreshAccessibility(): Promise<void> {
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta());
const proxyConfig = await this.dependencies.loadProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig);
const isAdmin = await canI({
namespace: "kube-system",
resource: "*",
verb: "create",
});
const isGlobalWatchEnabled = await canI({
verb: "watch",
resource: "*",
});
const allowedNamespaces = await this.requestAllowedNamespaces(proxyConfig);
const knownResources = await (async () => {
const result = await this.dependencies.requestApiResources(this.cluster);
if (result.callWasSuccessful) {
return result.response;
}
if (this.cluster.knownResources.length > 0) {
this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources, sticking with previous list`);
return this.cluster.knownResources;
}
this.dependencies.logger.warn(`[CLUSTER]: failed to list KUBE resources for the first time, blocking connection to cluster...`);
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Failed to list kube API resources, please reconnect...",
});
return [];
})();
const resourcesToShow = await this.getResourcesToShow(allowedNamespaces, knownResources, requestNamespaceListPermissions);
runInAction(() => {
this.cluster.isAdmin.set(isAdmin);
this.cluster.isGlobalWatchEnabled.set(isGlobalWatchEnabled);
this.cluster.allowedNamespaces.replace(allowedNamespaces);
this.cluster.knownResources.replace(knownResources);
this.cluster.resourcesToShow.replace(resourcesToShow);
this.cluster.ready.set(this.cluster.knownResources.length > 0);
});
this.dependencies.logger.debug(`[CLUSTER]: refreshed accessibility data`, this.cluster.getState());
}
async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus();
runInAction(() => {
this.cluster.online.set(connectionStatus > ClusterStatus.Offline);
this.cluster.accessible.set(connectionStatus == ClusterStatus.AccessGranted);
});
}
protected async getConnectionStatus(): Promise<ClusterStatus> {
try {
const versionData = await this.dependencies.clusterVersionDetector.detect(this.cluster);
runInAction(() => {
this.cluster.metadata.version = versionData.value;
});
return ClusterStatus.AccessGranted;
} catch (error) {
this.dependencies.logger.error(`[CLUSTER]: Failed to connect to "${this.cluster.contextName.get()}": ${error}`);
if (isRequestError(error)) {
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Invalid credentials",
});
return ClusterStatus.AccessDenied;
}
const message = String(error.error || error.message) || String(error);
this.dependencies.broadcastConnectionUpdate({
level: "error",
message,
});
return ClusterStatus.Offline;
}
if (error.failed === true) {
if (error.timedOut === true) {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Connection timed out",
});
return ClusterStatus.Offline;
}
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Failed to fetch credentials",
});
return ClusterStatus.AccessDenied;
}
const message = String(error.error || error.message) || String(error);
this.dependencies.broadcastConnectionUpdate({
level: "error",
message,
});
} else if (error instanceof Error || typeof error === "string") {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: `${error}`,
});
} else {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Unknown error has occurred",
});
}
return ClusterStatus.Offline;
}
}
protected async requestAllowedNamespaces(proxyConfig: KubeConfig) {
if (this.cluster.accessibleNamespaces.length) {
return this.cluster.accessibleNamespaces;
}
try {
const listNamespaces = this.dependencies.createListNamespaces(proxyConfig);
return await listNamespaces();
} catch (error) {
const ctx = proxyConfig.getContextObject(this.cluster.contextName.get());
const namespaceList = [ctx?.namespace].filter(isDefined);
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
const { response } = error as HttpError & { response: Response };
this.dependencies.logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.cluster.id, error: response.body });
this.dependencies.broadcastMessage(clusterListNamespaceForbiddenChannel, this.cluster.id);
}
return namespaceList;
}
}
protected async getResourcesToShow(allowedNamespaces: string[], knownResources: KubeApiResource[], req: RequestNamespaceListPermissions) {
if (!allowedNamespaces.length) {
return [];
}
const requestNamespaceListPermissions = withConcurrencyLimit(5)(req);
const namespaceListPermissions = allowedNamespaces.map(requestNamespaceListPermissions);
const canListResources = await Promise.all(namespaceListPermissions);
return knownResources
.filter((resource) => canListResources.some(fn => fn(resource)))
.map(formatKubeApiResource);
}
}
const clusterConnectionInjectable = getInjectable({
id: "cluster-connection",
instantiate: (di, cluster) => new ClusterConnection(
{
clusterVersionDetector: di.inject(clusterVersionDetectorInjectable),
kubeAuthProxyServer: di.inject(kubeAuthProxyServerInjectable, cluster),
logger: di.inject(loggerInjectable),
prometheusHandler: di.inject(prometheusHandlerInjectable, cluster),
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
broadcastMessage: di.inject(broadcastMessageInjectable),
createAuthorizationReview: di.inject(createAuthorizationReviewInjectable),
createListNamespaces: di.inject(createListNamespacesInjectable),
detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster),
requestApiResources: di.inject(requestApiResourcesInjectable),
requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable),
},
cluster,
),
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default clusterConnectionInjectable;

View File

@ -0,0 +1,114 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ServerOptions } from "http-proxy";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
export interface KubeAuthProxyServer {
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;
ensureAuthProxyUrl(): Promise<string>;
restart(): Promise<void>;
ensureRunning(): Promise<void>;
stop(): void;
}
const fourHoursInMs = 4 * 60 * 60 * 1000;
const thirtySecondsInMs = 30 * 1000;
const kubeAuthProxyServerInjectable = getInjectable({
id: "kube-auth-proxy-server",
instantiate: (di, cluster): KubeAuthProxyServer => {
const clusterUrl = new URL(cluster.apiUrl.get());
const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
const certificate = di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname);
let kubeAuthProxy: KubeAuthProxy | undefined = undefined;
let apiTarget: ServerOptions | undefined = undefined;
const ensureServerHelper = async (): Promise<KubeAuthProxy> => {
if (!kubeAuthProxy) {
const proxyEnv = Object.assign({}, process.env);
if (cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = cluster.preferences.httpsProxy;
}
kubeAuthProxy = createKubeAuthProxy(cluster, proxyEnv);
}
await kubeAuthProxy.run();
return kubeAuthProxy;
};
const newApiTarget = async (timeout: number): Promise<ServerOptions> => {
const kubeAuthProxy = await ensureServerHelper();
const headers: Record<string, string> = {};
if (clusterUrl.hostname) {
headers.Host = clusterUrl.hostname;
// fix current IPv6 inconsistency in url.Parse() and httpProxy.
// with url.Parse the IPv6 Hostname has no Square brackets but httpProxy needs the Square brackets to work.
if (headers.Host.includes(":")) {
headers.Host = `[${headers.Host}]`;
}
}
return {
target: {
protocol: "https:",
host: "127.0.0.1",
port: kubeAuthProxy.port,
path: kubeAuthProxy.apiPrefix,
ca: certificate.cert,
},
changeOrigin: true,
timeout,
secure: true,
headers,
};
};
const stopServer = () => {
kubeAuthProxy?.exit();
kubeAuthProxy = undefined;
apiTarget = undefined;
};
return {
getApiTarget: async (isLongRunningRequest = false) => {
if (isLongRunningRequest) {
return newApiTarget(fourHoursInMs);
}
return apiTarget ??= await newApiTarget(thirtySecondsInMs);
},
ensureAuthProxyUrl: async () => {
const kubeAuthProxy = await ensureServerHelper();
return `https://127.0.0.1:${kubeAuthProxy.port}${kubeAuthProxy.apiPrefix}`;
},
ensureRunning: async () => {
await ensureServerHelper();
},
restart: async () => {
stopServer();
await ensureServerHelper();
},
stop: stopServer,
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default kubeAuthProxyServerInjectable;

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster";
import loadConfigFromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable";
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
export type LoadProxyKubeconfig = () => Promise<KubeConfig>;
const loadProxyKubeconfigInjectable = getInjectable({
id: "load-proxy-kubeconfig",
instantiate: (di, cluster) => {
const loadConfigFromFile = di.inject(loadConfigFromFileInjectable);
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
return async () => {
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
const { config } = await loadConfigFromFile(proxyKubeconfigPath);
return config;
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default loadProxyKubeconfigInjectable;

View File

@ -7,6 +7,7 @@ import clusterStoreInjectable from "../../common/cluster-store/cluster-store.inj
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable"; import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable"; import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable";
import clusterConnectionInjectable from "./cluster-connection.injectable";
import { ClusterManager } from "./manager"; import { ClusterManager } from "./manager";
import updateEntityMetadataInjectable from "./update-entity-metadata.injectable"; import updateEntityMetadataInjectable from "./update-entity-metadata.injectable";
import updateEntitySpecInjectable from "./update-entity-spec.injectable"; import updateEntitySpecInjectable from "./update-entity-spec.injectable";
@ -23,6 +24,7 @@ const clusterManagerInjectable = getInjectable({
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
updateEntityMetadata: di.inject(updateEntityMetadataInjectable), updateEntityMetadata: di.inject(updateEntityMetadataInjectable),
updateEntitySpec: di.inject(updateEntitySpecInjectable), updateEntitySpec: di.inject(updateEntitySpecInjectable),
getClusterConnection: (cluster) => di.inject(clusterConnectionInjectable, cluster),
}), }),
}); });

View File

@ -17,6 +17,7 @@ import type { CatalogEntityRegistry } from "../catalog";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable"; import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable";
import type { UpdateEntitySpec } from "./update-entity-spec.injectable"; import type { UpdateEntitySpec } from "./update-entity-spec.injectable";
import type { ClusterConnection } from "./cluster-connection.injectable";
const logPrefix = "[CLUSTER-MANAGER]:"; const logPrefix = "[CLUSTER-MANAGER]:";
@ -28,8 +29,9 @@ interface Dependencies {
readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>; readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
readonly visibleCluster: IObservableValue<ClusterId | null>; readonly visibleCluster: IObservableValue<ClusterId | null>;
readonly logger: Logger; readonly logger: Logger;
readonly updateEntityMetadata: UpdateEntityMetadata; updateEntityMetadata: UpdateEntityMetadata;
readonly updateEntitySpec: UpdateEntitySpec; updateEntitySpec: UpdateEntitySpec;
getClusterConnection: (cluster: Cluster) => ClusterConnection;
} }
export class ClusterManager { export class ClusterManager {
@ -119,13 +121,13 @@ export class ClusterManager {
return LensKubernetesClusterStatus.DISCONNECTED; return LensKubernetesClusterStatus.DISCONNECTED;
} }
if (cluster.accessible) { if (cluster.accessible.get()) {
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTED, reason="cluster is accessible"`); this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTED, reason="cluster is accessible"`);
return LensKubernetesClusterStatus.CONNECTED; return LensKubernetesClusterStatus.CONNECTED;
} }
if (!cluster.disconnected) { if (!cluster.disconnected.get()) {
this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTING, reason="cluster is not disconnected"`); this.dependencies.logger.silly(`${logPrefix} setting entity ${entity.getName()} to CONNECTING, reason="cluster is not disconnected"`);
return LensKubernetesClusterStatus.CONNECTING; return LensKubernetesClusterStatus.CONNECTING;
@ -174,8 +176,8 @@ export class ClusterManager {
} }
} }
} else { } else {
cluster.kubeConfigPath = entity.spec.kubeconfigPath; cluster.kubeConfigPath.set(entity.spec.kubeconfigPath);
cluster.contextName = entity.spec.kubeconfigContext; cluster.contextName.set(entity.spec.kubeconfigContext);
if (entity.spec.accessibleNamespaces) { if (entity.spec.accessibleNamespaces) {
cluster.accessibleNamespaces.replace(entity.spec.accessibleNamespaces); cluster.accessibleNamespaces.replace(entity.spec.accessibleNamespaces);
@ -202,30 +204,43 @@ export class ClusterManager {
} }
} }
protected onNetworkOffline = () => { protected onNetworkOffline = async () => {
this.dependencies.logger.info(`${logPrefix} network is offline`); this.dependencies.logger.info(`${logPrefix} network is offline`);
this.dependencies.store.clustersList.forEach((cluster) => {
if (!cluster.disconnected) { await Promise.allSettled(
cluster.online = false; this.dependencies.store.clustersList
cluster.accessible = false; .filter(cluster => !cluster.disconnected.get())
cluster.refreshConnectionStatus().catch((e) => e); .map(async (cluster) => {
} cluster.online.set(false);
}); cluster.accessible.set(false);
await this.dependencies
.getClusterConnection(cluster)
.refreshConnectionStatus();
}),
);
}; };
protected onNetworkOnline = () => { protected onNetworkOnline = async () => {
this.dependencies.logger.info(`${logPrefix} network is online`); this.dependencies.logger.info(`${logPrefix} network is online`);
this.dependencies.store.clustersList.forEach((cluster) => {
if (!cluster.disconnected) { await Promise.allSettled(
cluster.refreshConnectionStatus().catch((e) => e); this.dependencies.store.clustersList
} .filter(cluster => !cluster.disconnected.get())
}); .map((cluster) => (
this.dependencies
.getClusterConnection(cluster)
.refreshConnectionStatus()
)),
);
}; };
stop() { stop() {
this.dependencies.store.clusters.forEach((cluster: Cluster) => { for (const cluster of this.dependencies.store.clustersList) {
cluster.disconnect(); this.dependencies
}); .getClusterConnection(cluster)
.disconnect();
}
} }
} }
@ -233,26 +248,26 @@ export function catalogEntityFromCluster(cluster: Cluster) {
return new KubernetesCluster({ return new KubernetesCluster({
metadata: { metadata: {
uid: cluster.id, uid: cluster.id,
name: cluster.name, name: cluster.name.get(),
source: "local", source: "local",
labels: { labels: {
...cluster.labels, ...cluster.labels,
}, },
distro: cluster.distribution, distro: cluster.distribution.get(),
kubeVersion: cluster.version, kubeVersion: cluster.version.get(),
}, },
spec: { spec: {
kubeconfigPath: cluster.kubeConfigPath, kubeconfigPath: cluster.kubeConfigPath.get(),
kubeconfigContext: cluster.contextName, kubeconfigContext: cluster.contextName.get(),
icon: {}, icon: {},
}, },
status: { status: {
phase: cluster.disconnected phase: cluster.disconnected.get()
? LensKubernetesClusterStatus.DISCONNECTED ? LensKubernetesClusterStatus.DISCONNECTED
: LensKubernetesClusterStatus.CONNECTED, : LensKubernetesClusterStatus.CONNECTED,
reason: "", reason: "",
message: "", message: "",
active: !cluster.disconnected, active: !cluster.disconnected.get(),
}, },
}); });
} }

View File

@ -0,0 +1,30 @@
/**
* 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 type { Cluster } from "../../../common/cluster/cluster";
import { createClusterPrometheusHandler } from "./prometheus-handler";
import getPrometheusProviderByKindInjectable from "../../prometheus/get-by-kind.injectable";
import prometheusProvidersInjectable from "../../prometheus/providers.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import loadProxyKubeconfigInjectable from "../load-proxy-kubeconfig.injectable";
const prometheusHandlerInjectable = getInjectable({
id: "prometheus-handler",
instantiate: (di, cluster) => createClusterPrometheusHandler(
{
getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable),
prometheusProviders: di.inject(prometheusProvidersInjectable),
logger: di.inject(loggerInjectable),
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
},
cluster,
),
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default prometheusHandlerInjectable;

View File

@ -0,0 +1,129 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { PrometheusProvider, PrometheusService } from "../../prometheus/provider";
import type { ClusterPrometheusPreferences } from "../../../common/cluster-types";
import type { Cluster } from "../../../common/cluster/cluster";
import { CoreV1Api } from "@kubernetes/client-node";
import type { GetPrometheusProviderByKind } from "../../prometheus/get-by-kind.injectable";
import type { IComputedValue } from "mobx";
import type { Logger } from "../../../common/logger";
import type { LoadProxyKubeconfig } from "../load-proxy-kubeconfig.injectable";
export interface PrometheusDetails {
prometheusPath: string;
provider: PrometheusProvider;
}
interface PrometheusServicePreferences {
namespace: string;
service: string;
port: number;
prefix: string;
}
interface Dependencies {
readonly prometheusProviders: IComputedValue<PrometheusProvider[]>;
readonly logger: Logger;
getPrometheusProviderByKind: GetPrometheusProviderByKind;
loadProxyKubeconfig: LoadProxyKubeconfig;
}
export interface ClusterPrometheusHandler {
setupPrometheus(preferences?: ClusterPrometheusPreferences): void;
getPrometheusDetails(): Promise<PrometheusDetails>;
}
const ensurePrometheusPath = ({ service, namespace, port }: PrometheusService) => `${namespace}/services/${service}:${port}`;
export const createClusterPrometheusHandler = (...args: [Dependencies, Cluster]): ClusterPrometheusHandler => {
const [deps, cluster] = args;
const {
getPrometheusProviderByKind,
loadProxyKubeconfig,
logger,
prometheusProviders,
} = deps;
let prometheusProvider: string | undefined = undefined;
let prometheus: PrometheusServicePreferences | undefined = undefined;
const setupPrometheus: ClusterPrometheusHandler["setupPrometheus"] = (preferences = {}) => {
prometheusProvider = preferences.prometheusProvider?.type;
prometheus = preferences.prometheus;
};
const ensurePrometheusProvider = (service: PrometheusService) => {
if (!prometheusProvider) {
logger.info(`[CONTEXT-HANDLER]: using ${service.kind} as prometheus provider for clusterId=${cluster.id}`);
prometheusProvider = service.kind;
}
return getPrometheusProviderByKind(prometheusProvider);
};
const listPotentialProviders = () => {
if (prometheusProvider) {
const provider = getPrometheusProviderByKind(prometheusProvider);
if (provider) {
return [provider];
}
}
return prometheusProviders.get();
};
const getPrometheusService = async (): Promise<PrometheusService> => {
setupPrometheus(cluster.preferences);
if (prometheus && prometheusProvider) {
return {
kind: prometheusProvider,
namespace: prometheus.namespace,
service: prometheus.service,
port: prometheus.port,
};
}
const providers = listPotentialProviders();
const proxyConfig = await loadProxyKubeconfig();
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
const potentialServices = await Promise.allSettled(
providers.map(provider => provider.getPrometheusService(apiClient)),
);
const errors = [];
for (const res of potentialServices) {
switch (res.status) {
case "rejected":
errors.push(res.reason);
break;
case "fulfilled":
if (res.value) {
return res.value;
}
}
}
throw new Error("No Prometheus service found", { cause: errors });
};
const getPrometheusDetails: ClusterPrometheusHandler["getPrometheusDetails"] = async () => {
const service = await getPrometheusService();
const prometheusPath = ensurePrometheusPath(service);
const provider = ensurePrometheusProvider(service);
return { prometheusPath, provider };
};
setupPrometheus(cluster.preferences);
return {
setupPrometheus,
getPrometheusDetails,
};
};

View File

@ -0,0 +1,23 @@
/**
* 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 type { Cluster } from "../../common/cluster/cluster";
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
export type RemoveProxyKubeconfig = () => Promise<void>;
const removeProxyKubeconfigInjectable = getInjectable({
id: "remove-proxy-kubeconfig",
instantiate: (di, cluster): RemoveProxyKubeconfig => {
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
return () => proxyKubeconfigManager.clear();
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default removeProxyKubeconfigInjectable;

View File

@ -12,6 +12,7 @@ import { withConcurrencyLimit } from "../../common/utils/with-concurrency-limit"
import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable"; import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable";
import type { AsyncResult } from "../../common/utils/async-result"; import type { AsyncResult } from "../../common/utils/async-result";
import { backoffCaller } from "../../common/utils/backoff-caller"; import { backoffCaller } from "../../common/utils/backoff-caller";
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
export type RequestApiResources = (cluster: Cluster) => Promise<AsyncResult<KubeApiResource[], Error>>; export type RequestApiResources = (cluster: Cluster) => Promise<AsyncResult<KubeApiResource[], Error>>;
@ -29,6 +30,7 @@ const requestApiResourcesInjectable = getInjectable({
return async (...args) => { return async (...args) => {
const [cluster] = args; const [cluster] = args;
const broadcastConnectionUpdate = di.inject(broadcastConnectionUpdateInjectable, cluster);
const requestKubeApiResources = withConcurrencyLimit(5)(requestKubeApiResourcesFor(cluster)); const requestKubeApiResources = withConcurrencyLimit(5)(requestKubeApiResourcesFor(cluster));
const groupLists: KubeResourceListGroup[] = []; const groupLists: KubeResourceListGroup[] = [];
@ -36,7 +38,10 @@ const requestApiResourcesInjectable = getInjectable({
for (const apiVersionRequester of apiVersionRequesters) { for (const apiVersionRequester of apiVersionRequesters) {
const result = await backoffCaller(() => apiVersionRequester(cluster), { const result = await backoffCaller(() => apiVersionRequester(cluster), {
onIntermediateError: (error, attempt) => { onIntermediateError: (error, attempt) => {
cluster.broadcastConnectUpdate(`Failed to list kube API resource kinds, attempt ${attempt}: ${error}`, "warning"); broadcastConnectionUpdate({
message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`,
level: "warning",
});
logger.warn(`[LIST-API-RESOURCES]: failed to list kube api resources: ${error}`, { attempt, clusterId: cluster.id }); logger.warn(`[LIST-API-RESOURCES]: failed to list kube api resources: ${error}`, { attempt, clusterId: cluster.id });
}, },
}); });
@ -56,7 +61,10 @@ const requestApiResourcesInjectable = getInjectable({
for (const result of results) { for (const result of results) {
if (!result.callWasSuccessful) { if (!result.callWasSuccessful) {
cluster.broadcastConnectUpdate(`Kube APIs under "${result.listGroup.path}" may not be displayed`, "warning"); broadcastConnectionUpdate({
message: `Kube APIs under "${result.listGroup.path}" may not be displayed`,
level: "warning",
});
continue; continue;
} }

View File

@ -17,6 +17,10 @@ interface Pre500WorkspaceStoreModel {
}[]; }[];
} }
interface Pre500ClusterModel extends ClusterModel {
workspace?: string;
}
const v500Beta10ClusterStoreMigrationInjectable = getInjectable({ const v500Beta10ClusterStoreMigrationInjectable = getInjectable({
id: "v5.0.0-beta.10-cluster-store-migration", id: "v5.0.0-beta.10-cluster-store-migration",
instantiate: (di) => { instantiate: (di) => {
@ -35,7 +39,7 @@ const v500Beta10ClusterStoreMigrationInjectable = getInjectable({
workspaces.set(id, name); workspaces.set(id, name);
} }
const clusters = (store.get("clusters") ?? []) as ClusterModel[]; const clusters = (store.get("clusters") ?? []) as Pre500ClusterModel[];
for (const cluster of clusters) { for (const cluster of clusters) {
if (cluster.workspace) { if (cluster.workspace) {

View File

@ -8,6 +8,15 @@ import { moveSync, removeSync } from "fs-extra";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { isDefined } from "../../../common/utils"; import { isDefined } from "../../../common/utils";
import joinPathsInjectable from "../../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
interface Pre500ClusterModel extends ClusterModel {
workspace?: string;
workspaces?: string[];
}
function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences { function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences {
if (left.prometheus && left.prometheusProvider) { if (left.prometheus && left.prometheusProvider) {
@ -27,8 +36,7 @@ function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: C
return {}; return {};
} }
function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences { const mergePreferences = (left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences => ({
return {
terminalCWD: left.terminalCWD || right.terminalCWD || undefined, terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
clusterName: left.clusterName || right.clusterName || undefined, clusterName: left.clusterName || right.clusterName || undefined,
iconOrder: left.iconOrder || right.iconOrder || undefined, iconOrder: left.iconOrder || right.iconOrder || undefined,
@ -36,15 +44,7 @@ function mergePreferences(left: ClusterPreferences, right: ClusterPreferences):
httpsProxy: left.httpsProxy || right.httpsProxy || undefined, httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []), hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
...mergePrometheusPreferences(left, right), ...mergePrometheusPreferences(left, right),
}; });
}
function mergeLabels(left: Record<string, string>, right: Record<string, string>): Record<string, string> {
return {
...right,
...left,
};
}
function mergeSet(...iterables: Iterable<string | undefined>[]): string[] { function mergeSet(...iterables: Iterable<string | undefined>[]): string[] {
const res = new Set<string>(); const res = new Set<string>();
@ -60,24 +60,17 @@ function mergeSet(...iterables: Iterable<string | undefined>[]): string[] {
return [...res]; return [...res];
} }
function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">): ClusterModel { const mergeClusterModel = (prev: Pre500ClusterModel, right: Omit<Pre500ClusterModel, "id">): Pre500ClusterModel => ({
return {
id: prev.id, id: prev.id,
kubeConfigPath: prev.kubeConfigPath, kubeConfigPath: prev.kubeConfigPath,
contextName: prev.contextName, contextName: prev.contextName,
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}), preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
metadata: prev.metadata, metadata: prev.metadata,
labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}), labels: { ...(right.labels ?? {}), ...(prev.labels ?? {}) },
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []), accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
workspace: prev.workspace || right.workspace, workspace: prev.workspace || right.workspace,
workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []), workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []),
}; });
}
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { clusterStoreMigrationInjectionToken } from "../../../common/cluster-store/migration-token";
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
const v500Beta13ClusterStoreMigrationInjectable = getInjectable({ const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
id: "v5.0.0-beta.13-cluster-store-migration", id: "v5.0.0-beta.13-cluster-store-migration",
@ -104,8 +97,8 @@ const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
version: "5.0.0-beta.13", version: "5.0.0-beta.13",
run(store) { run(store) {
const folder = joinPaths(userDataPath, "lens-local-storage"); const folder = joinPaths(userDataPath, "lens-local-storage");
const oldClusters = (store.get("clusters") ?? []) as ClusterModel[]; const oldClusters = (store.get("clusters") ?? []) as Pre500ClusterModel[];
const clusters = new Map<string, ClusterModel>(); const clusters = new Map<string, Pre500ClusterModel>();
for (const { id: oldId, ...cluster } of oldClusters) { for (const { id: oldId, ...cluster } of oldClusters) {
const newId = generateNewIdFor(cluster); const newId = generateNewIdFor(cluster);

View File

@ -3,6 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { toJS } from "mobx";
import type { KubernetesCluster } from "../../common/catalog-entities"; import type { KubernetesCluster } from "../../common/catalog-entities";
import { ClusterMetadataKey } from "../../common/cluster-types"; import { ClusterMetadataKey } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
@ -17,10 +18,10 @@ const updateEntityMetadataInjectable = getInjectable({
return (entity, cluster) => { return (entity, cluster) => {
entity.metadata.labels = { entity.metadata.labels = {
...entity.metadata.labels, ...entity.metadata.labels,
...cluster.labels, ...toJS(cluster.labels),
}; };
entity.metadata.distro = cluster.distribution; entity.metadata.distro = cluster.distribution.get();
entity.metadata.kubeVersion = cluster.version; entity.metadata.kubeVersion = cluster.version.get();
enumKeys(ClusterMetadataKey).forEach((key) => { enumKeys(ClusterMetadataKey).forEach((key) => {
const metadataKey = ClusterMetadataKey[key]; const metadataKey = ClusterMetadataKey[key];

View File

@ -7,8 +7,8 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { KubernetesCluster } from "../../common/catalog-entities"; import { KubernetesCluster } from "../../common/catalog-entities";
import { ClusterMetadataKey } from "../../common/cluster-types"; import { ClusterMetadataKey } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import { replaceObservableObject } from "../../common/utils/replace-observable-object";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable"; import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable";
import updateEntityMetadataInjectable from "./update-entity-metadata.injectable"; import updateEntityMetadataInjectable from "./update-entity-metadata.injectable";
@ -27,11 +27,10 @@ describe("update-entity-metadata", () => {
get: () => ({} as AppPaths), get: () => ({} as AppPaths),
set: () => {}, set: () => {},
})); }));
const createCluster = di.inject(createClusterInjectionToken);
updateEntityMetadata = di.inject(updateEntityMetadataInjectable); updateEntityMetadata = di.inject(updateEntityMetadataInjectable);
cluster = createCluster({ cluster = new Cluster({
id: "some-id", id: "some-id",
contextName: "some-context", contextName: "some-context",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
@ -50,10 +49,6 @@ describe("update-entity-metadata", () => {
}, },
}; };
cluster.metadata = {
...cluster.metadata,
};
entity = new KubernetesCluster({ entity = new KubernetesCluster({
metadata: { metadata: {
uid: "some-uid", uid: "some-uid",
@ -125,9 +120,9 @@ describe("update-entity-metadata", () => {
}); });
it("given cluster has labels, updates entity metadata with labels", () => { it("given cluster has labels, updates entity metadata with labels", () => {
cluster.labels = { replaceObservableObject(cluster.labels, {
"some-label": "some-value", "some-label": "some-value",
}; });
entity.metadata.labels = { entity.metadata.labels = {
"some-other-label": "some-other-value", "some-other-label": "some-other-value",
}; };
@ -139,9 +134,9 @@ describe("update-entity-metadata", () => {
}); });
it("given cluster has labels, overwrites entity metadata with cluster labels", () => { it("given cluster has labels, overwrites entity metadata with cluster labels", () => {
cluster.labels = { replaceObservableObject(cluster.labels, {
"some-label": "some-cluster-value", "some-label": "some-cluster-value",
}; });
entity.metadata.labels = { entity.metadata.labels = {
"some-label": "some-entity-value", "some-label": "some-entity-value",
}; };

View File

@ -6,8 +6,7 @@ import type { AppPaths } from "../../common/app-paths/app-path-injection-token";
import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable"; import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { KubernetesCluster } from "../../common/catalog-entities"; import { KubernetesCluster } from "../../common/catalog-entities";
import type { Cluster } from "../../common/cluster/cluster"; import { Cluster } from "../../common/cluster/cluster";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { UpdateEntitySpec } from "./update-entity-spec.injectable"; import type { UpdateEntitySpec } from "./update-entity-spec.injectable";
import updateEntitySpecInjectable from "./update-entity-spec.injectable"; import updateEntitySpecInjectable from "./update-entity-spec.injectable";
@ -25,11 +24,10 @@ describe("update-entity-spec", () => {
get: () => ({} as AppPaths), get: () => ({} as AppPaths),
set: () => {}, set: () => {},
})); }));
const createCluster = di.inject(createClusterInjectionToken);
updateEntitySpec = di.inject(updateEntitySpecInjectable); updateEntitySpec = di.inject(updateEntitySpecInjectable);
cluster = createCluster({ cluster = new Cluster({
id: "some-id", id: "some-id",
contextName: "some-context", contextName: "some-context",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",

View File

@ -1,219 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { PrometheusProvider, PrometheusService } from "../prometheus/provider";
import type { ClusterPrometheusPreferences } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster";
import type httpProxy from "http-proxy";
import type { UrlWithStringQuery } from "url";
import url from "url";
import { CoreV1Api } from "@kubernetes/client-node";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { GetPrometheusProviderByKind } from "../prometheus/get-by-kind.injectable";
import type { IComputedValue } from "mobx";
import type { Logger } from "../../common/logger";
export interface PrometheusDetails {
prometheusPath: string;
provider: PrometheusProvider;
}
interface PrometheusServicePreferences {
namespace: string;
service: string;
port: number;
prefix: string;
}
export interface ContextHandlerDependencies {
createKubeAuthProxy: CreateKubeAuthProxy;
getPrometheusProviderByKind: GetPrometheusProviderByKind;
readonly authProxyCa: string;
readonly prometheusProviders: IComputedValue<PrometheusProvider[]>;
readonly logger: Logger;
}
export interface ClusterContextHandler {
readonly clusterUrl: UrlWithStringQuery;
setupPrometheus(preferences?: ClusterPrometheusPreferences): void;
getPrometheusDetails(): Promise<PrometheusDetails>;
resolveAuthProxyUrl(): Promise<string>;
resolveAuthProxyCa(): string;
getApiTarget(isLongRunningRequest?: boolean): Promise<httpProxy.ServerOptions>;
restartServer(): Promise<void>;
ensureServer(): Promise<void>;
stopServer(): void;
}
export class ContextHandler implements ClusterContextHandler {
public readonly clusterUrl: UrlWithStringQuery;
protected kubeAuthProxy?: KubeAuthProxy;
protected apiTarget?: httpProxy.ServerOptions;
protected prometheusProvider?: string;
protected prometheus?: PrometheusServicePreferences;
constructor(private readonly dependencies: ContextHandlerDependencies, protected readonly cluster: Cluster) {
this.clusterUrl = url.parse(cluster.apiUrl);
this.setupPrometheus(cluster.preferences);
}
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
this.prometheusProvider = preferences.prometheusProvider?.type;
this.prometheus = preferences.prometheus;
}
public async getPrometheusDetails(): Promise<PrometheusDetails> {
const service = await this.getPrometheusService();
const prometheusPath = this.ensurePrometheusPath(service);
const provider = this.ensurePrometheusProvider(service);
return { prometheusPath, provider };
}
protected ensurePrometheusPath({ service, namespace, port }: PrometheusService): string {
return `${namespace}/services/${service}:${port}`;
}
protected ensurePrometheusProvider(service: PrometheusService): PrometheusProvider {
if (!this.prometheusProvider) {
this.dependencies.logger.info(`[CONTEXT-HANDLER]: using ${service.kind} as prometheus provider for clusterId=${this.cluster.id}`);
this.prometheusProvider = service.kind;
}
return this.dependencies.getPrometheusProviderByKind(this.prometheusProvider);
}
protected listPotentialProviders(): PrometheusProvider[] {
const provider = this.prometheusProvider && this.dependencies.getPrometheusProviderByKind(this.prometheusProvider);
if (provider) {
return [provider];
}
return this.dependencies.prometheusProviders.get();
}
protected async getPrometheusService(): Promise<PrometheusService> {
this.setupPrometheus(this.cluster.preferences);
if (this.prometheus && this.prometheusProvider) {
return {
kind: this.prometheusProvider,
namespace: this.prometheus.namespace,
service: this.prometheus.service,
port: this.prometheus.port,
};
}
const providers = this.listPotentialProviders();
const proxyConfig = await this.cluster.getProxyKubeconfig();
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
const potentialServices = await Promise.allSettled(
providers.map(provider => provider.getPrometheusService(apiClient)),
);
const errors = [];
for (const res of potentialServices) {
switch (res.status) {
case "rejected":
errors.push(res.reason);
break;
case "fulfilled":
if (res.value) {
return res.value;
}
}
}
throw new Error("No Prometheus service found", { cause: errors });
}
async resolveAuthProxyUrl(): Promise<string> {
const kubeAuthProxy = await this.ensureServerHelper();
return `https://127.0.0.1:${kubeAuthProxy.port}${kubeAuthProxy.apiPrefix}`;
}
resolveAuthProxyCa() {
return this.dependencies.authProxyCa;
}
async getApiTarget(isLongRunningRequest = false): Promise<httpProxy.ServerOptions> {
const timeout = isLongRunningRequest ? 4 * 60 * 60_000 : 30_000; // 4 hours for long running request, 30 seconds for the rest
if (isLongRunningRequest) {
return this.newApiTarget(timeout);
}
return this.apiTarget ??= await this.newApiTarget(timeout);
}
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
const kubeAuthProxy = await this.ensureServerHelper();
const headers: Record<string, string> = {};
if (this.clusterUrl.hostname) {
headers.Host = this.clusterUrl.hostname;
// fix current IPv6 inconsistency in url.Parse() and httpProxy.
// with url.Parse the IPv6 Hostname has no Square brackets but httpProxy needs the Square brackets to work.
if (headers.Host.includes(":")) {
headers.Host = `[${headers.Host}]`;
}
}
return {
target: {
protocol: "https:",
host: "127.0.0.1",
port: kubeAuthProxy.port,
path: kubeAuthProxy.apiPrefix,
ca: this.resolveAuthProxyCa(),
},
changeOrigin: true,
timeout,
secure: true,
headers,
};
}
protected async ensureServerHelper(): Promise<KubeAuthProxy> {
if (!this.kubeAuthProxy) {
const proxyEnv = Object.assign({}, process.env);
if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
}
this.kubeAuthProxy = this.dependencies.createKubeAuthProxy(this.cluster, proxyEnv);
await this.kubeAuthProxy.run();
return this.kubeAuthProxy;
}
await this.kubeAuthProxy.whenReady;
return this.kubeAuthProxy;
}
async ensureServer(): Promise<void> {
await this.ensureServerHelper();
}
async restartServer(): Promise<void> {
this.stopServer();
await this.ensureServerHelper();
}
stopServer() {
this.prometheus = undefined;
this.prometheusProvider = undefined;
this.kubeAuthProxy?.exit();
this.kubeAuthProxy = undefined;
this.apiTarget = undefined;
}
}

View File

@ -1,38 +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 { Cluster } from "../../common/cluster/cluster";
import type { ClusterContextHandler, ContextHandlerDependencies } from "./context-handler";
import { ContextHandler } from "./context-handler";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable";
import URLParse from "url-parse";
import getPrometheusProviderByKindInjectable from "../prometheus/get-by-kind.injectable";
import prometheusProvidersInjectable from "../prometheus/providers.injectable";
import loggerInjectable from "../../common/logger.injectable";
const createContextHandlerInjectable = getInjectable({
id: "create-context-handler",
instantiate: (di) => {
const dependencies: Omit<ContextHandlerDependencies, "authProxyCa"> = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
getPrometheusProviderByKind: di.inject(getPrometheusProviderByKindInjectable),
prometheusProviders: di.inject(prometheusProvidersInjectable),
logger: di.inject(loggerInjectable),
};
return (cluster: Cluster): ClusterContextHandler => {
const clusterUrl = new URLParse(cluster.apiUrl);
return new ContextHandler({
...dependencies,
authProxyCa: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname).cert,
}, cluster);
};
},
});
export default createContextHandlerInjectable;

View File

@ -1,49 +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 createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createListApiResourcesInjectable from "../cluster/request-api-resources.injectable";
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";
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
const createClusterInjectable = getInjectable({
id: "create-cluster",
instantiate: (di) => {
const dependencies: ClusterDependencies = {
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
logger: di.inject(loggerInjectable),
clusterVersionDetector: di.inject(clusterVersionDetectorInjectable),
createKubeconfigManager: di.inject(createKubeconfigManagerInjectable),
createKubectl: di.inject(createKubectlInjectable),
createContextHandler: di.inject(createContextHandlerInjectable),
createAuthorizationReview: di.inject(authorizationReviewInjectable),
requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable),
requestApiResources: di.inject(createListApiResourcesInjectable),
createListNamespaces: di.inject(listNamespacesInjectable),
broadcastMessage: di.inject(broadcastMessageInjectable),
loadConfigfromFile: di.inject(loadConfigfromFileInjectable),
detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
};
return (model, configData) => new Cluster(dependencies, model, configData);
},
injectionToken: createClusterInjectionToken,
});
export default createClusterInjectable;

View File

@ -11,6 +11,7 @@ import applicationMenuItemCompositeInjectable from "../../../../features/applica
import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable";
import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable";
import pushCatalogToRendererInjectable from "../../../catalog-sync-to-renderer/push-catalog-to-renderer.injectable"; import pushCatalogToRendererInjectable from "../../../catalog-sync-to-renderer/push-catalog-to-renderer.injectable";
import clusterConnectionInjectable from "../../../cluster/cluster-connection.injectable";
const setupIpcMainHandlersInjectable = getInjectable({ const setupIpcMainHandlersInjectable = getInjectable({
id: "setup-ipc-main-handlers", id: "setup-ipc-main-handlers",
@ -34,6 +35,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
clusterStore, clusterStore,
emitAppEvent, emitAppEvent,
getClusterById, getClusterById,
getClusterConnection: (cluster) => di.inject(clusterConnectionInjectable, cluster),
}); });
}, },
}; };

View File

@ -18,12 +18,15 @@ import { getApplicationMenuTemplate } from "../../../../features/application-men
import type { MenuItemRoot } from "../../../../features/application-menu/main/application-menu-item-composite.injectable"; import type { MenuItemRoot } from "../../../../features/application-menu/main/application-menu-item-composite.injectable";
import type { EmitAppEvent } from "../../../../common/app-event-bus/emit-event.injectable"; import type { EmitAppEvent } from "../../../../common/app-event-bus/emit-event.injectable";
import type { GetClusterById } from "../../../../common/cluster-store/get-by-id.injectable"; import type { GetClusterById } from "../../../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { ClusterConnection } from "../../../cluster/cluster-connection.injectable";
interface Dependencies { interface Dependencies {
applicationMenuItemComposite: IComputedValue<Composite<ApplicationMenuItemTypes | MenuItemRoot>>; applicationMenuItemComposite: IComputedValue<Composite<ApplicationMenuItemTypes | MenuItemRoot>>;
clusterStore: ClusterStore; clusterStore: ClusterStore;
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
getClusterById: GetClusterById; getClusterById: GetClusterById;
pushCatalogToRenderer: () => void; pushCatalogToRenderer: () => void;
getClusterConnection: (cluster: Cluster) => ClusterConnection;
} }
export const setupIpcMainHandlers = ({ export const setupIpcMainHandlers = ({
@ -32,10 +35,18 @@ export const setupIpcMainHandlers = ({
emitAppEvent, emitAppEvent,
getClusterById, getClusterById,
pushCatalogToRenderer, pushCatalogToRenderer,
getClusterConnection,
}: Dependencies) => { }: Dependencies) => {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { ipcMainHandle(clusterActivateHandler, async (event, clusterId: ClusterId, force = false) => {
return getClusterById(clusterId) const cluster = getClusterById(clusterId);
?.activate(force);
if (!cluster) {
return;
}
const clusterConnection = getClusterConnection(cluster);
await clusterConnection.activate(force);
}); });
ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => { ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
@ -51,10 +62,14 @@ export const setupIpcMainHandlers = ({
emitAppEvent({ name: "cluster", action: "stop" }); emitAppEvent({ name: "cluster", action: "stop" });
const cluster = getClusterById(clusterId); const cluster = getClusterById(clusterId);
if (cluster) { if (!cluster) {
cluster.disconnect(); return;
clusterFrameMap.delete(cluster.id);
} }
const clusterConnection = getClusterConnection(cluster);
clusterConnection.disconnect();
clusterFrameMap.delete(cluster.id);
}); });
ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action)); ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action));

View File

@ -5,6 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import type { DeleteHelmReleaseData } from "../delete-helm-release.injectable"; import type { DeleteHelmReleaseData } from "../delete-helm-release.injectable";
import deleteHelmReleaseInjectable from "../delete-helm-release.injectable"; import deleteHelmReleaseInjectable from "../delete-helm-release.injectable";
@ -16,11 +17,12 @@ const deleteClusterHelmReleaseInjectable = getInjectable({
const deleteHelmRelease = di.inject(deleteHelmReleaseInjectable); const deleteHelmRelease = di.inject(deleteHelmReleaseInjectable);
return async (cluster: Cluster, data: DeleteHelmReleaseData) => { return async (cluster: Cluster, data: DeleteHelmReleaseData) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
logger.debug(`[CLUSTER]: Delete helm release`, data); logger.debug(`[CLUSTER]: Delete helm release`, data);
return deleteHelmRelease(proxyKubeconfig, data); return deleteHelmRelease(proxyKubeconfigPath, data);
}; };
}, },
}); });

View File

@ -5,6 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import type { GetHelmReleaseHistoryData } from "../get-helm-release-history.injectable"; import type { GetHelmReleaseHistoryData } from "../get-helm-release-history.injectable";
import getHelmReleaseHistoryInjectable from "../get-helm-release-history.injectable"; import getHelmReleaseHistoryInjectable from "../get-helm-release-history.injectable";
@ -16,11 +17,12 @@ const getClusterHelmReleaseHistoryInjectable = getInjectable({
const getHelmReleaseHistory = di.inject(getHelmReleaseHistoryInjectable); const getHelmReleaseHistory = di.inject(getHelmReleaseHistoryInjectable);
return async (cluster: Cluster, data: GetHelmReleaseHistoryData) => { return async (cluster: Cluster, data: GetHelmReleaseHistoryData) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
logger.debug(`[CLUSTER]: Fetch release history for clusterId=${cluster.id}`, data); logger.debug(`[CLUSTER]: Fetch release history for clusterId=${cluster.id}`, data);
return getHelmReleaseHistory(proxyKubeconfig, data); return getHelmReleaseHistory(proxyKubeconfigPath, data);
}; };
}, },
}); });

View File

@ -7,6 +7,7 @@ import loggerInjectable from "../../../common/logger.injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import type { GetHelmReleaseValuesData } from "../get-helm-release-values.injectable"; import type { GetHelmReleaseValuesData } from "../get-helm-release-values.injectable";
import getHelmReleaseValuesInjectable from "../get-helm-release-values.injectable"; import getHelmReleaseValuesInjectable from "../get-helm-release-values.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
const getClusterHelmReleaseValuesInjectable = getInjectable({ const getClusterHelmReleaseValuesInjectable = getInjectable({
id: "get-cluster-helm-release-values", id: "get-cluster-helm-release-values",
@ -16,11 +17,12 @@ const getClusterHelmReleaseValuesInjectable = getInjectable({
const getHelmReleaseValues = di.inject(getHelmReleaseValuesInjectable); const getHelmReleaseValues = di.inject(getHelmReleaseValuesInjectable);
return async (cluster: Cluster, data: GetHelmReleaseValuesData) => { return async (cluster: Cluster, data: GetHelmReleaseValuesData) => {
const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
logger.debug(`[CLUSTER]: getting helm release values`, data); logger.debug(`[CLUSTER]: getting helm release values`, data);
return getHelmReleaseValues(pathToKubeconfig, data); return getHelmReleaseValues(proxyKubeconfigPath, data);
}; };
}, },
}); });

View File

@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import { isObject, json } from "../../../common/utils"; import { isObject, json } from "../../../common/utils";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import execHelmInjectable from "../exec-helm/exec-helm.injectable"; import execHelmInjectable from "../exec-helm/exec-helm.injectable";
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources/get-helm-release-resources.injectable"; import getHelmReleaseResourcesInjectable from "./get-helm-release-resources/get-helm-release-resources.injectable";
@ -18,7 +19,8 @@ const getHelmReleaseInjectable = getInjectable({
const getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable); const getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string) => { return async (cluster: Cluster, releaseName: string, namespace: string) => {
const kubeconfigPath = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
logger.debug("Fetch release"); logger.debug("Fetch release");
@ -28,7 +30,7 @@ const getHelmReleaseInjectable = getInjectable({
"--namespace", "--namespace",
namespace, namespace,
"--kubeconfig", "--kubeconfig",
kubeconfigPath, proxyKubeconfigPath,
"--output", "--output",
"json", "json",
]); ]);
@ -48,7 +50,7 @@ const getHelmReleaseInjectable = getInjectable({
const resourcesResult = await getHelmReleaseResources( const resourcesResult = await getHelmReleaseResources(
releaseName, releaseName,
namespace, namespace,
kubeconfigPath, proxyKubeconfigPath,
); );
if (!resourcesResult.callWasSuccessful) { if (!resourcesResult.callWasSuccessful) {

View File

@ -5,6 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { JsonObject } from "type-fest"; import type { JsonObject } from "type-fest";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import installHelmChartInjectable from "../install-helm-chart.injectable"; import installHelmChartInjectable from "../install-helm-chart.injectable";
export interface InstallChartArgs { export interface InstallChartArgs {
@ -22,11 +23,12 @@ const installClusterHelmChartInjectable = getInjectable({
const installHelmChart = di.inject(installHelmChartInjectable); const installHelmChart = di.inject(installHelmChartInjectable);
return async (cluster: Cluster, data: InstallChartArgs) => { return async (cluster: Cluster, data: InstallChartArgs) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
return installHelmChart({ return installHelmChart({
...data, ...data,
kubeconfigPath: proxyKubeconfig, kubeconfigPath: proxyKubeconfigPath,
}); });
}; };
}, },

View File

@ -5,6 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import listHelmReleasesInjectable from "../list-helm-releases.injectable"; import listHelmReleasesInjectable from "../list-helm-releases.injectable";
const listClusterHelmReleasesInjectable = getInjectable({ const listClusterHelmReleasesInjectable = getInjectable({
@ -15,11 +16,12 @@ const listClusterHelmReleasesInjectable = getInjectable({
const listHelmReleases = di.inject(listHelmReleasesInjectable); const listHelmReleases = di.inject(listHelmReleasesInjectable);
return async (cluster: Cluster, namespace?: string) => { return async (cluster: Cluster, namespace?: string) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
logger.debug(`[CLUSTER]: listing helm releases for clusterId=${cluster.id}`, { namespace }); logger.debug(`[CLUSTER]: listing helm releases for clusterId=${cluster.id}`, { namespace });
return listHelmReleases(proxyKubeconfig, namespace); return listHelmReleases(proxyKubeconfigPath, namespace);
}; };
}, },
}); });

View File

@ -5,6 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import type { RollbackHelmReleaseData } from "../rollback-helm-release.injectable"; import type { RollbackHelmReleaseData } from "../rollback-helm-release.injectable";
import rollbackHelmReleaseInjectable from "../rollback-helm-release.injectable"; import rollbackHelmReleaseInjectable from "../rollback-helm-release.injectable";
@ -16,11 +17,12 @@ const rollbackClusterHelmReleaseInjectable = getInjectable({
const rollbackHelmRelease = di.inject(rollbackHelmReleaseInjectable); const rollbackHelmRelease = di.inject(rollbackHelmReleaseInjectable);
return async (cluster: Cluster, data: RollbackHelmReleaseData) => { return async (cluster: Cluster, data: RollbackHelmReleaseData) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
logger.debug(`[CLUSTER]: rolling back helm release for clusterId=${cluster.id}`, data); logger.debug(`[CLUSTER]: rolling back helm release for clusterId=${cluster.id}`, data);
await rollbackHelmRelease(proxyKubeconfig, data); await rollbackHelmRelease(proxyKubeconfigPath, data);
}; };
}, },
}); });

View File

@ -10,6 +10,7 @@ import getHelmReleaseInjectable from "./get-helm-release.injectable";
import writeFileInjectable from "../../../common/fs/write-file.injectable"; import writeFileInjectable from "../../../common/fs/write-file.injectable";
import removePathInjectable from "../../../common/fs/remove.injectable"; import removePathInjectable from "../../../common/fs/remove.injectable";
import execHelmInjectable from "../exec-helm/exec-helm.injectable"; import execHelmInjectable from "../exec-helm/exec-helm.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
export interface UpdateChartArgs { export interface UpdateChartArgs {
chart: string; chart: string;
@ -28,7 +29,8 @@ const updateHelmReleaseInjectable = getInjectable({
const execHelm = di.inject(execHelmInjectable); const execHelm = di.inject(execHelmInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => { return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
const valuesFilePath = tempy.file({ name: "values.yaml" }); const valuesFilePath = tempy.file({ name: "values.yaml" });
logger.debug(`[HELM]: upgrading "${releaseName}" in "${namespace}" to ${data.version}`); logger.debug(`[HELM]: upgrading "${releaseName}" in "${namespace}" to ${data.version}`);
@ -43,7 +45,7 @@ const updateHelmReleaseInjectable = getInjectable({
"--version", data.version, "--version", data.version,
"--values", valuesFilePath, "--values", valuesFilePath,
"--namespace", namespace, "--namespace", namespace,
"--kubeconfig", proxyKubeconfig, "--kubeconfig", proxyKubeconfigPath,
]); ]);
if (result.callWasSuccessful === false) { if (result.callWasSuccessful === false) {

View File

@ -4,7 +4,6 @@
*/ */
import * as uuid from "uuid"; import * as uuid from "uuid";
import type { ClusterStoreModel } from "../../../common/cluster-store/cluster-store";
import type { Hotbar, HotbarItem } from "../../../common/hotbars/types"; import type { Hotbar, HotbarItem } from "../../../common/hotbars/types";
import { defaultHotbarCells, getEmptyHotbar } from "../../../common/hotbars/types"; import { defaultHotbarCells, getEmptyHotbar } from "../../../common/hotbars/types";
import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { getLegacyGlobalDiForExtensionApi } from "../../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
@ -17,6 +16,7 @@ import { hotbarStoreMigrationInjectionToken } from "../../../common/hotbars/migr
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for"; import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
import type { ClusterModel } from "../../../common/cluster-types";
interface Pre500WorkspaceStoreModel { interface Pre500WorkspaceStoreModel {
workspaces: { workspaces: {
@ -31,6 +31,15 @@ interface PartialHotbar {
items: (null | HotbarItem)[]; items: (null | HotbarItem)[];
} }
interface Pre500ClusterModel extends ClusterModel {
workspace?: string;
workspaces?: string[];
}
interface Pre500ClusterStoreModel {
clusters?: Pre500ClusterModel[];
}
const v500Beta10HotbarStoreMigrationInjectable = getInjectable({ const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
id: "v5.0.0-beta.10-hotbar-store-migration", id: "v5.0.0-beta.10-hotbar-store-migration",
instantiate: (di) => { instantiate: (di) => {
@ -59,7 +68,7 @@ const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
try { try {
const workspaceStoreData: Pre500WorkspaceStoreModel = readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json")); const workspaceStoreData: Pre500WorkspaceStoreModel = readJsonSync(joinPaths(userDataPath, "lens-workspace-store.json"));
const { clusters = [] }: ClusterStoreModel = readJsonSync(joinPaths(userDataPath, "lens-cluster-store.json")); const { clusters = [] }: Pre500ClusterStoreModel = readJsonSync(joinPaths(userDataPath, "lens-cluster-store.json"));
const workspaceHotbars = new Map<string, PartialHotbar>(); // mapping from WorkspaceId to HotBar const workspaceHotbars = new Map<string, PartialHotbar>(); // mapping from WorkspaceId to HotBar
for (const { id, name } of workspaceStoreData.workspaces) { for (const { id, name } of workspaceStoreData.workspaces) {

View File

@ -13,14 +13,15 @@ import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-
import lensK8sProxyPathInjectable from "./lens-k8s-proxy-path.injectable"; import lensK8sProxyPathInjectable from "./lens-k8s-proxy-path.injectable";
import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectable"; import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectable";
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy;
const createKubeAuthProxyInjectable = getInjectable({ const createKubeAuthProxyInjectable = getInjectable({
id: "create-kube-auth-proxy", id: "create-kube-auth-proxy",
instantiate: (di): CreateKubeAuthProxy => { instantiate: (di): CreateKubeAuthProxy => {
const dependencies: Omit<KubeAuthProxyDependencies, "proxyCert"> = { const dependencies: Omit<KubeAuthProxyDependencies, "proxyCert" | "broadcastConnectionUpdate"> = {
proxyBinPath: di.inject(lensK8sProxyPathInjectable), proxyBinPath: di.inject(lensK8sProxyPathInjectable),
spawn: di.inject(spawnInjectable), spawn: di.inject(spawnInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
@ -29,13 +30,14 @@ const createKubeAuthProxyInjectable = getInjectable({
dirname: di.inject(getDirnameOfPathInjectable), dirname: di.inject(getDirnameOfPathInjectable),
}; };
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { return (cluster, env) => {
const clusterUrl = new URL(cluster.apiUrl); const clusterUrl = new URL(cluster.apiUrl.get());
return new KubeAuthProxy({ return new KubeAuthProxy({
...dependencies, ...dependencies,
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname), proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
}, cluster, environmentVariables); broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
}, cluster, env);
}; };
}, },
}); });

View File

@ -7,7 +7,7 @@ import type { ChildProcess } from "child_process";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type { GetPortFromStream } from "../utils/get-port-from-stream.injectable"; import type { GetPortFromStream } from "../utils/get-port-from-stream.injectable";
import { makeObservable, observable, when } from "mobx"; import { observable, when } from "mobx";
import type { SelfSignedCert } from "selfsigned"; import type { SelfSignedCert } from "selfsigned";
import assert from "assert"; import assert from "assert";
import { TypedRegEx } from "typed-regex"; import { TypedRegEx } from "typed-regex";
@ -15,6 +15,7 @@ import type { Spawn } from "../child-process/spawn.injectable";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import type { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; import type { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable";
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
import type { BroadcastConnectionUpdate } from "../cluster/broadcast-connection-update.injectable";
const startingServeMatcher = "starting to serve on (?<address>.+)"; const startingServeMatcher = "starting to serve on (?<address>.+)";
const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), { const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
@ -29,6 +30,7 @@ export interface KubeAuthProxyDependencies {
waitUntilPortIsUsed: WaitUntilPortIsUsed; waitUntilPortIsUsed: WaitUntilPortIsUsed;
getPortFromStream: GetPortFromStream; getPortFromStream: GetPortFromStream;
dirname: GetDirnameOfPath; dirname: GetDirnameOfPath;
broadcastConnectionUpdate: BroadcastConnectionUpdate;
} }
export class KubeAuthProxy { export class KubeAuthProxy {
@ -44,19 +46,17 @@ export class KubeAuthProxy {
protected _port?: number; protected _port?: number;
protected proxyProcess?: ChildProcess; protected proxyProcess?: ChildProcess;
@observable protected ready = false; protected readonly ready = observable.box(false);
constructor(private readonly dependencies: KubeAuthProxyDependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) { constructor(
makeObservable(this); private readonly dependencies: KubeAuthProxyDependencies,
} protected readonly cluster: Cluster,
protected readonly env: NodeJS.ProcessEnv,
get whenReady() { ) {}
return when(() => this.ready);
}
public async run(): Promise<void> { public async run(): Promise<void> {
if (this.proxyProcess) { if (this.proxyProcess) {
return this.whenReady; return when(() => this.ready.get());
} }
const proxyBin = this.dependencies.proxyBinPath; const proxyBin = this.dependencies.proxyBinPath;
@ -65,26 +65,42 @@ export class KubeAuthProxy {
this.proxyProcess = this.dependencies.spawn(proxyBin, [], { this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
env: { env: {
...this.env, ...this.env,
KUBECONFIG: this.cluster.kubeConfigPath, KUBECONFIG: this.cluster.kubeConfigPath.get(),
KUBECONFIG_CONTEXT: this.cluster.contextName, KUBECONFIG_CONTEXT: this.cluster.contextName.get(),
API_PREFIX: this.apiPrefix, API_PREFIX: this.apiPrefix,
PROXY_KEY: cert.private, PROXY_KEY: cert.private,
PROXY_CERT: cert.cert, PROXY_CERT: cert.cert,
}, },
cwd: this.dependencies.dirname(this.cluster.kubeConfigPath), cwd: this.dependencies.dirname(this.cluster.kubeConfigPath.get()),
}); });
this.proxyProcess.on("error", (error) => { this.proxyProcess.on("error", (error) => {
this.cluster.broadcastConnectUpdate(error.message, "error"); this.dependencies.broadcastConnectionUpdate({
level: "error",
message: error.message,
});
this.exit(); this.exit();
}); });
this.proxyProcess.on("exit", (code) => { this.proxyProcess.on("exit", (code) => {
this.cluster.broadcastConnectUpdate(`proxy exited with code: ${code}`, code ? "error" : "info"); if (code) {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: `proxy exited with code: ${code}`,
});
} else {
this.dependencies.broadcastConnectionUpdate({
level: "info",
message: "proxy exited successfully",
});
}
this.exit(); this.exit();
}); });
this.proxyProcess.on("disconnect", () => { this.proxyProcess.on("disconnect", () => {
this.cluster.broadcastConnectUpdate("Proxy disconnected communications", "error"); this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Proxy disconnected communications",
});
this.exit(); this.exit();
}); });
@ -96,28 +112,40 @@ export class KubeAuthProxy {
return; return;
} }
this.cluster.broadcastConnectUpdate(data.toString(), "error"); this.dependencies.broadcastConnectionUpdate({
level: "error",
message: data.toString(),
});
}); });
this.proxyProcess.stdout.on("data", (data: Buffer) => { this.proxyProcess.stdout.on("data", (data: Buffer) => {
if (typeof this._port === "number") { if (typeof this._port === "number") {
this.cluster.broadcastConnectUpdate(data.toString()); this.dependencies.broadcastConnectionUpdate({
level: "info",
message: data.toString(),
});
} }
}); });
this._port = await this.dependencies.getPortFromStream(this.proxyProcess.stdout, { this._port = await this.dependencies.getPortFromStream(this.proxyProcess.stdout, {
lineRegex: startingServeRegex, lineRegex: startingServeRegex,
onFind: () => this.cluster.broadcastConnectUpdate("Authentication proxy started"), onFind: () => this.dependencies.broadcastConnectionUpdate({
level: "info",
message: "Authentication proxy started",
}),
}); });
this.dependencies.logger.info(`[KUBE-AUTH-PROXY]: found port=${this._port}`); this.dependencies.logger.info(`[KUBE-AUTH-PROXY]: found port=${this._port}`);
try { try {
await this.dependencies.waitUntilPortIsUsed(this.port, 500, 10000); await this.dependencies.waitUntilPortIsUsed(this.port, 500, 10000);
this.ready = true; this.ready.set(true);
} catch (error) { } catch (error) {
this.dependencies.logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error); this.dependencies.logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error);
this.cluster.broadcastConnectUpdate("Proxy port failed to be used within timelimit, restarting...", "error"); this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Proxy port failed to be used within time limit, restarting...",
});
this.exit(); this.exit();
return this.run(); return this.run();
@ -125,7 +153,7 @@ export class KubeAuthProxy {
} }
public exit() { public exit() {
this.ready = false; this.ready.set(false);
if (this.proxyProcess) { if (this.proxyProcess) {
this.dependencies.logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()); this.dependencies.logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());

View File

@ -2,44 +2,43 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import type { KubeconfigManagerDependencies } from "./kubeconfig-manager";
import { KubeconfigManager } from "./kubeconfig-manager"; import { KubeconfigManager } from "./kubeconfig-manager";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable";
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import writeFileInjectable from "../../common/fs/write-file.injectable"; import writeFileInjectable from "../../common/fs/write-file.injectable";
import removePathInjectable from "../../common/fs/remove.injectable"; import removePathInjectable from "../../common/fs/remove.injectable";
import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable";
import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable";
import kubeAuthProxyUrlInjectable from "../cluster/auth-proxy-url.injectable";
import loadKubeconfigInjectable from "../../common/cluster/load-kubeconfig.injectable";
export interface KubeConfigManagerInstantiationParameter { const kubeconfigManagerInjectable = getInjectable({
cluster: Cluster; id: "kubeconfig-manager",
}
export type CreateKubeconfigManager = (cluster: Cluster) => KubeconfigManager; instantiate: (di, cluster) => new KubeconfigManager(
{
const createKubeconfigManagerInjectable = getInjectable({
id: "create-kubeconfig-manager",
instantiate: (di): CreateKubeconfigManager => {
const dependencies: KubeconfigManagerDependencies = {
directoryForTemp: di.inject(directoryForTempInjectable), directoryForTemp: di.inject(directoryForTempInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
lensProxyPort: di.inject(lensProxyPortInjectable),
joinPaths: di.inject(joinPathsInjectable), joinPaths: di.inject(joinPathsInjectable),
getDirnameOfPath: di.inject(getDirnameOfPathInjectable), getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
removePath: di.inject(removePathInjectable), removePath: di.inject(removePathInjectable),
pathExists: di.inject(pathExistsInjectable), pathExists: di.inject(pathExistsInjectable),
writeFile: di.inject(writeFileInjectable), writeFile: di.inject(writeFileInjectable),
certificate: di.inject(lensProxyCertificateInjectable).get(), certificate: di.inject(lensProxyCertificateInjectable).get(),
}; loadKubeconfig: di.inject(loadKubeconfigInjectable, cluster),
kubeAuthProxyServer: di.inject(kubeAuthProxyServerInjectable, cluster),
return (cluster) => new KubeconfigManager(dependencies, cluster); kubeAuthProxyUrl: di.inject(kubeAuthProxyUrlInjectable, cluster),
}, },
cluster,
),
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
}); });
export default createKubeconfigManagerInjectable; export default kubeconfigManagerInjectable;

View File

@ -4,8 +4,6 @@
*/ */
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import type { Cluster } from "../../common/cluster/cluster";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import { dumpConfigYaml } from "../../common/kube-helpers"; import { dumpConfigYaml } from "../../common/kube-helpers";
import { isErrnoException } from "../../common/utils"; import { isErrnoException } from "../../common/utils";
import type { PartialDeep } from "type-fest"; import type { PartialDeep } from "type-fest";
@ -16,17 +14,22 @@ import type { PathExists } from "../../common/fs/path-exists.injectable";
import type { RemovePath } from "../../common/fs/remove.injectable"; import type { RemovePath } from "../../common/fs/remove.injectable";
import type { WriteFile } from "../../common/fs/write-file.injectable"; import type { WriteFile } from "../../common/fs/write-file.injectable";
import type { SelfSignedCert } from "selfsigned"; import type { SelfSignedCert } from "selfsigned";
import type { Cluster } from "../../common/cluster/cluster";
import type { LoadKubeconfig } from "../../common/cluster/load-kubeconfig.injectable";
import type { KubeAuthProxyServer } from "../cluster/kube-auth-proxy-server.injectable";
export interface KubeconfigManagerDependencies { interface KubeconfigManagerDependencies {
readonly directoryForTemp: string; readonly directoryForTemp: string;
readonly logger: Logger; readonly logger: Logger;
readonly lensProxyPort: { get: () => number }; readonly certificate: SelfSignedCert;
readonly kubeAuthProxyServer: KubeAuthProxyServer;
readonly kubeAuthProxyUrl: string;
joinPaths: JoinPaths; joinPaths: JoinPaths;
getDirnameOfPath: GetDirnameOfPath; getDirnameOfPath: GetDirnameOfPath;
pathExists: PathExists; pathExists: PathExists;
removePath: RemovePath; removePath: RemovePath;
writeFile: WriteFile; writeFile: WriteFile;
certificate: SelfSignedCert; loadKubeconfig: LoadKubeconfig;
} }
export class KubeconfigManager { export class KubeconfigManager {
@ -38,17 +41,16 @@ export class KubeconfigManager {
*/ */
protected tempFilePath: string | null = null; protected tempFilePath: string | null = null;
protected readonly contextHandler: ClusterContextHandler; constructor(
private readonly dependencies: KubeconfigManagerDependencies,
constructor(private readonly dependencies: KubeconfigManagerDependencies, protected cluster: Cluster) { private readonly cluster: Cluster,
this.contextHandler = cluster.contextHandler; ) {}
}
/** /**
* *
* @returns The path to the temporary kubeconfig * @returns The path to the temporary kubeconfig
*/ */
async getPath(): Promise<string> { async ensurePath(): Promise<string> {
if (this.tempFilePath === null || !(await this.dependencies.pathExists(this.tempFilePath))) { if (this.tempFilePath === null || !(await this.dependencies.pathExists(this.tempFilePath))) {
return await this.ensureFile(); return await this.ensureFile();
} }
@ -79,7 +81,7 @@ export class KubeconfigManager {
protected async ensureFile() { protected async ensureFile() {
try { try {
await this.contextHandler.ensureServer(); await this.dependencies.kubeAuthProxyServer.ensureRunning();
return this.tempFilePath = await this.createProxyKubeconfig(); return this.tempFilePath = await this.createProxyKubeconfig();
} catch (error) { } catch (error) {
@ -87,31 +89,26 @@ export class KubeconfigManager {
} }
} }
get resolveProxyUrl() {
return `https://127.0.0.1:${this.dependencies.lensProxyPort.get()}/${this.cluster.id}`;
}
/** /**
* Creates new "temporary" kubeconfig that point to the kubectl-proxy. * Creates new "temporary" kubeconfig that point to the kubectl-proxy.
* This way any user of the config does not need to know anything about the auth etc. details. * This way any user of the config does not need to know anything about the auth etc. details.
*/ */
protected async createProxyKubeconfig(): Promise<string> { protected async createProxyKubeconfig(): Promise<string> {
const { cluster } = this; const { id, preferences: { defaultNamespace }} = this.cluster;
const { contextName, id } = cluster; const contextName = this.cluster.contextName.get();
const tempFile = this.dependencies.joinPaths( const tempFile = this.dependencies.joinPaths(
this.dependencies.directoryForTemp, this.dependencies.directoryForTemp,
`kubeconfig-${id}`, `kubeconfig-${id}`,
); );
const kubeConfig = await cluster.getKubeconfig(); const kubeConfig = await this.dependencies.loadKubeconfig();
const { certificate } = this.dependencies;
const proxyConfig: PartialDeep<KubeConfig> = { const proxyConfig: PartialDeep<KubeConfig> = {
currentContext: contextName, currentContext: contextName,
clusters: [ clusters: [
{ {
name: contextName, name: contextName,
server: this.resolveProxyUrl, server: this.dependencies.kubeAuthProxyUrl,
skipTLSVerify: false, skipTLSVerify: false,
caData: Buffer.from(certificate.cert).toString("base64"), caData: Buffer.from(this.dependencies.certificate.cert).toString("base64"),
}, },
], ],
users: [ users: [
@ -122,7 +119,7 @@ export class KubeconfigManager {
user: "proxy", user: "proxy",
name: contextName, name: contextName,
cluster: contextName, cluster: contextName,
namespace: cluster.defaultNamespace || kubeConfig.getContextObject(contextName)?.namespace, namespace: defaultNamespace || kubeConfig.getContextObject(contextName)?.namespace,
}, },
], ],
}; };

View File

@ -5,7 +5,7 @@
import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import { kubectlApplyAllChannel } from "../../common/kube-helpers/channels"; import { kubectlApplyAllChannel } from "../../common/kube-helpers/channels";
import createResourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; import resourceApplierInjectable from "../resource-applier/create-resource-applier.injectable";
import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens";
const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjectable({ const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjectable({
@ -13,7 +13,6 @@ const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjecta
handler: (di) => { handler: (di) => {
const getClusterById = di.inject(getClusterByIdInjectable); const getClusterById = di.inject(getClusterByIdInjectable);
const emitAppEvent = di.inject(emitAppEventInjectable); const emitAppEvent = di.inject(emitAppEventInjectable);
const createResourceApplier = di.inject(createResourceApplierInjectable);
return async ({ return async ({
clusterId, clusterId,
@ -30,7 +29,9 @@ const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjecta
}; };
} }
return createResourceApplier(cluster).kubectlApplyAll(resources, extraArgs); const resourceApplier = di.inject(resourceApplierInjectable, cluster);
return resourceApplier.kubectlApplyAll(resources, extraArgs);
}; };
}, },
}); });

View File

@ -19,10 +19,12 @@ import joinPathsInjectable from "../../common/path/join-paths.injectable";
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
export type CreateKubectl = (version: string) => Kubectl;
const createKubectlInjectable = getInjectable({ const createKubectlInjectable = getInjectable({
id: "create-kubectl", id: "create-kubectl",
instantiate: (di) => { instantiate: (di): CreateKubectl => {
const dependencies: KubectlDependencies = { const dependencies: KubectlDependencies = {
userStore: di.inject(userStoreInjectable), userStore: di.inject(userStoreInjectable),
directoryForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable), directoryForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable),
@ -39,7 +41,7 @@ const createKubectlInjectable = getInjectable({
getBasenameOfPath: di.inject(getBasenameOfPathInjectable), getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
}; };
return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion); return (version) => new Kubectl(dependencies, version);
}, },
}); });

View File

@ -5,7 +5,7 @@
import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable";
import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
import { kubectlDeleteAllChannel } from "../../common/kube-helpers/channels"; import { kubectlDeleteAllChannel } from "../../common/kube-helpers/channels";
import createResourceApplierInjectable from "../resource-applier/create-resource-applier.injectable"; import resourceApplierInjectable from "../resource-applier/create-resource-applier.injectable";
import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "../utils/channel/channel-listeners/listener-tokens";
const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInjectable({ const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInjectable({
@ -13,7 +13,6 @@ const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInject
handler: (di) => { handler: (di) => {
const emitAppEvent = di.inject(emitAppEventInjectable); const emitAppEvent = di.inject(emitAppEventInjectable);
const getClusterById = di.inject(getClusterByIdInjectable); const getClusterById = di.inject(getClusterByIdInjectable);
const createResourceApplier = di.inject(createResourceApplierInjectable);
return async ({ return async ({
clusterId, clusterId,
@ -31,7 +30,9 @@ const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInject
}; };
} }
return createResourceApplier(cluster).kubectlDeleteAll(resources, extraArgs); const resourceApplier = di.inject(resourceApplierInjectable, cluster);
return resourceApplier.kubectlDeleteAll(resources, extraArgs);
}; };
}, },
}); });

View File

@ -4,7 +4,6 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { LensProxy } from "./lens-proxy"; import { LensProxy } from "./lens-proxy";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import routerInjectable from "../router/router.injectable"; import routerInjectable from "../router/router.injectable";
import httpProxy from "http-proxy"; import httpProxy from "http-proxy";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable"; import shellApiRequestInjectable from "./proxy-functions/shell-api-request.injectable";
@ -14,6 +13,8 @@ import emitAppEventInjectable from "../../common/app-event-bus/emit-event.inject
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable"; import lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.injectable";
import getClusterForRequestInjectable from "./get-cluster-for-request.injectable"; import getClusterForRequestInjectable from "./get-cluster-for-request.injectable";
import kubeAuthProxyServerInjectable from "../cluster/kube-auth-proxy-server.injectable";
import kubeApiUpgradeRequestInjectable from "./proxy-functions/kube-api-upgrade-request.injectable";
const lensProxyInjectable = getInjectable({ const lensProxyInjectable = getInjectable({
id: "lens-proxy", id: "lens-proxy",
@ -21,7 +22,7 @@ const lensProxyInjectable = getInjectable({
instantiate: (di) => new LensProxy({ instantiate: (di) => new LensProxy({
router: di.inject(routerInjectable), router: di.inject(routerInjectable),
proxy: httpProxy.createProxy(), proxy: httpProxy.createProxy(),
kubeApiUpgradeRequest, kubeApiUpgradeRequest: di.inject(kubeApiUpgradeRequestInjectable),
shellApiRequest: di.inject(shellApiRequestInjectable), shellApiRequest: di.inject(shellApiRequestInjectable),
getClusterForRequest: di.inject(getClusterForRequestInjectable), getClusterForRequest: di.inject(getClusterForRequestInjectable),
lensProxyPort: di.inject(lensProxyPortInjectable), lensProxyPort: di.inject(lensProxyPortInjectable),
@ -29,6 +30,7 @@ const lensProxyInjectable = getInjectable({
emitAppEvent: di.inject(emitAppEventInjectable), emitAppEvent: di.inject(emitAppEventInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
certificate: di.inject(lensProxyCertificateInjectable).get(), certificate: di.inject(lensProxyCertificateInjectable).get(),
getKubeAuthProxyServer: (cluster) => di.inject(kubeAuthProxyServerInjectable, cluster),
}), }),
}); });

View File

@ -9,7 +9,6 @@ import type http from "http";
import type httpProxy from "http-proxy"; import type httpProxy from "http-proxy";
import { apiPrefix, apiKubePrefix } from "../../common/vars"; import { apiPrefix, apiKubePrefix } from "../../common/vars";
import type { Router } from "../router/router"; import type { Router } from "../router/router";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type { ProxyApiRequestArgs } from "./proxy-functions"; import type { ProxyApiRequestArgs } from "./proxy-functions";
import { getBoolean } from "../utils/parse-query"; import { getBoolean } from "../utils/parse-query";
@ -18,6 +17,7 @@ import type { SetRequired } from "type-fest";
import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable"; import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import type { SelfSignedCert } from "selfsigned"; import type { SelfSignedCert } from "selfsigned";
import type { KubeAuthProxyServer } from "../cluster/kube-auth-proxy-server.injectable";
export type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined; export type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">; export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
@ -28,6 +28,7 @@ interface Dependencies {
shellApiRequest: LensProxyApiRequest; shellApiRequest: LensProxyApiRequest;
kubeApiUpgradeRequest: LensProxyApiRequest; kubeApiUpgradeRequest: LensProxyApiRequest;
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
getKubeAuthProxyServer: (cluster: Cluster) => KubeAuthProxyServer;
readonly router: Router; readonly router: Router;
readonly proxy: httpProxy; readonly proxy: httpProxy;
readonly lensProxyPort: { set: (portNumber: number) => void }; readonly lensProxyPort: { set: (portNumber: number) => void };
@ -220,15 +221,6 @@ export class LensProxy {
return proxy; return proxy;
} }
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ClusterContextHandler): Promise<httpProxy.ServerOptions | void> {
if (req.url?.startsWith(apiKubePrefix)) {
delete req.headers.authorization;
req.url = req.url.replace(apiKubePrefix, "");
return contextHandler.getApiTarget(isLongRunningRequest(req.url));
}
}
protected getRequestId(req: http.IncomingMessage): string { protected getRequestId(req: http.IncomingMessage): string {
assert(req.headers.host); assert(req.headers.host);
@ -238,8 +230,12 @@ export class LensProxy {
protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) { protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) {
const cluster = this.dependencies.getClusterForRequest(req); const cluster = this.dependencies.getClusterForRequest(req);
if (cluster) { if (cluster && req.url.startsWith(apiKubePrefix)) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); delete req.headers.authorization;
req.url = req.url.replace(apiKubePrefix, "");
const kubeAuthProxyServer = this.dependencies.getKubeAuthProxyServer(cluster);
const proxyTarget = await kubeAuthProxyServer.getApiTarget(isLongRunningRequest(req.url));
if (proxyTarget) { if (proxyTarget) {
return this.dependencies.proxy.web(req, res, proxyTarget); return this.dependencies.proxy.web(req, res, proxyTarget);

View File

@ -2,5 +2,4 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
export * from "./kube-api-upgrade-request";
export * from "./types"; export * from "./types";

View File

@ -0,0 +1,78 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { chunk } from "lodash";
import type { ConnectionOptions } from "tls";
import { connect } from "tls";
import url, { URL } from "url";
import { apiKubePrefix } from "../../../common/vars";
import { getInjectable } from "@ogre-tools/injectable";
import type { LensProxyApiRequest } from "../lens-proxy";
import kubeAuthProxyServerInjectable from "../../cluster/kube-auth-proxy-server.injectable";
import kubeAuthProxyCertificateInjectable from "../../kube-auth-proxy/kube-auth-proxy-certificate.injectable";
const skipRawHeaders = new Set(["Host", "Authorization"]);
const kubeApiUpgradeRequestInjectable = getInjectable({
id: "kube-api-upgrade-request",
instantiate: (di): LensProxyApiRequest => async ({ req, socket, head, cluster }) => {
const clusterUrl = new URL(cluster.apiUrl.get());
const kubeAuthProxyServer = di.inject(kubeAuthProxyServerInjectable, cluster);
const kubeAuthProxyCertificate = di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname);
const proxyUrl = await kubeAuthProxyServer.ensureAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const apiUrl = url.parse(cluster.apiUrl.get());
const pUrl = url.parse(proxyUrl);
const connectOpts: ConnectionOptions = {
port: pUrl.port ? parseInt(pUrl.port) : undefined,
host: pUrl.hostname ?? undefined,
ca: kubeAuthProxyCertificate.cert,
};
const proxySocket = connect(connectOpts, () => {
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
for (const [key, value] of chunk(req.rawHeaders, 2)) {
if (skipRawHeaders.has(key)) {
continue;
}
proxySocket.write(`${key}: ${value}\r\n`);
}
proxySocket.write("\r\n");
proxySocket.write(head);
});
proxySocket.setKeepAlive(true);
socket.setKeepAlive(true);
proxySocket.setTimeout(0);
socket.setTimeout(0);
proxySocket.on("data", function (chunk) {
socket.write(chunk);
});
proxySocket.on("end", function () {
socket.end();
});
proxySocket.on("error", function () {
socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`);
socket.end();
});
socket.on("data", function (chunk) {
proxySocket.write(chunk);
});
socket.on("end", function () {
proxySocket.end();
});
socket.on("error", function () {
proxySocket.end();
});
},
});
export default kubeApiUpgradeRequestInjectable;

View File

@ -1,66 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { chunk } from "lodash";
import type { ConnectionOptions } from "tls";
import { connect } from "tls";
import url from "url";
import { apiKubePrefix } from "../../../common/vars";
import type { ProxyApiRequestArgs } from "./types";
const skipRawHeaders = new Set(["Host", "Authorization"]);
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const proxyCa = cluster.contextHandler.resolveAuthProxyCa();
const apiUrl = url.parse(cluster.apiUrl);
const pUrl = url.parse(proxyUrl);
const connectOpts: ConnectionOptions = {
port: pUrl.port ? parseInt(pUrl.port) : undefined,
host: pUrl.hostname ?? undefined,
ca: proxyCa,
};
const proxySocket = connect(connectOpts, () => {
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
for (const [key, value] of chunk(req.rawHeaders, 2)) {
if (skipRawHeaders.has(key)) {
continue;
}
proxySocket.write(`${key}: ${value}\r\n`);
}
proxySocket.write("\r\n");
proxySocket.write(head);
});
proxySocket.setKeepAlive(true);
socket.setKeepAlive(true);
proxySocket.setTimeout(0);
socket.setTimeout(0);
proxySocket.on("data", function (chunk) {
socket.write(chunk);
});
proxySocket.on("end", function () {
socket.end();
});
proxySocket.on("error", function () {
socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`);
socket.end();
});
socket.on("data", function (chunk) {
proxySocket.write(chunk);
});
socket.on("end", function () {
proxySocket.end();
});
socket.on("error", function () {
proxySocket.end();
});
}

View File

@ -2,33 +2,36 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../common/app-event-bus/emit-event.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import removePathInjectable from "../../common/fs/remove.injectable"; import removePathInjectable from "../../common/fs/remove.injectable";
import execFileInjectable from "../../common/fs/exec-file.injectable"; import execFileInjectable from "../../common/fs/exec-file.injectable";
import writeFileInjectable from "../../common/fs/write-file.injectable"; import writeFileInjectable from "../../common/fs/write-file.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable";
import type { ResourceApplierDependencies } from "./resource-applier";
import { ResourceApplier } from "./resource-applier"; import { ResourceApplier } from "./resource-applier";
import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
import type { Cluster } from "../../common/cluster/cluster";
export type CreateResourceApplier = (cluster: Cluster) => ResourceApplier; const resourceApplierInjectable = getInjectable({
id: "resource-applier",
const createResourceApplierInjectable = getInjectable({ instantiate: (di, cluster) => new ResourceApplier(
id: "create-resource-applier", {
instantiate: (di): CreateResourceApplier => {
const deps: ResourceApplierDependencies = {
deleteFile: di.inject(removePathInjectable), deleteFile: di.inject(removePathInjectable),
emitAppEvent: di.inject(emitAppEventInjectable), emitAppEvent: di.inject(emitAppEventInjectable),
execFile: di.inject(execFileInjectable), execFile: di.inject(execFileInjectable),
joinPaths: di.inject(joinPathsInjectable), joinPaths: di.inject(joinPathsInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
writeFile: di.inject(writeFileInjectable), writeFile: di.inject(writeFileInjectable),
}; createKubectl: di.inject(createKubectlInjectable),
proxyKubeconfigManager: di.inject(kubeconfigManagerInjectable, cluster),
return (cluster) => new ResourceApplier(deps, cluster);
}, },
cluster,
),
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
}); });
export default createResourceApplierInjectable; export default resourceApplierInjectable;

View File

@ -15,6 +15,8 @@ import type { RemovePath } from "../../common/fs/remove.injectable";
import type { ExecFile } from "../../common/fs/exec-file.injectable"; import type { ExecFile } from "../../common/fs/exec-file.injectable";
import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { JoinPaths } from "../../common/path/join-paths.injectable";
import type { AsyncResult } from "../../common/utils/async-result"; import type { AsyncResult } from "../../common/utils/async-result";
import type { CreateKubectl } from "../kubectl/create-kubectl.injectable";
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
export interface ResourceApplierDependencies { export interface ResourceApplierDependencies {
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
@ -22,11 +24,24 @@ export interface ResourceApplierDependencies {
deleteFile: RemovePath; deleteFile: RemovePath;
execFile: ExecFile; execFile: ExecFile;
joinPaths: JoinPaths; joinPaths: JoinPaths;
createKubectl: CreateKubectl;
readonly proxyKubeconfigManager: KubeconfigManager;
readonly logger: Logger; readonly logger: Logger;
} }
export class ResourceApplier { export class ResourceApplier {
constructor(protected readonly dependencies: ResourceApplierDependencies, protected readonly cluster: Cluster) {} constructor(
protected readonly dependencies: ResourceApplierDependencies,
protected readonly cluster: Cluster,
) {}
private async getKubectlPath() {
const kubectl = this.dependencies.createKubectl(this.cluster.version.get());
await kubectl.ensureKubectl();
return kubectl.getPath();
}
/** /**
* Patch a kube resource's manifest, throwing any error that occurs. * Patch a kube resource's manifest, throwing any error that occurs.
@ -38,9 +53,8 @@ export class ResourceApplier {
async patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string> { async patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string> {
this.dependencies.emitAppEvent({ name: "resource", action: "patch" }); this.dependencies.emitAppEvent({ name: "resource", action: "patch" });
const kubectl = await this.cluster.ensureKubectl(); const kubectlPath = await this.getKubectlPath();
const kubectlPath = await kubectl.getPath(); const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const args = [ const args = [
"--kubeconfig", proxyKubeconfigPath, "--kubeconfig", proxyKubeconfigPath,
"patch", "patch",
@ -74,9 +88,8 @@ export class ResourceApplier {
} }
protected async kubectlApply(content: string): Promise<AsyncResult<string, string>> { protected async kubectlApply(content: string): Promise<AsyncResult<string, string>> {
const kubectl = await this.cluster.ensureKubectl(); const kubectlPath = await this.getKubectlPath();
const kubectlPath = await kubectl.getPath(); const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const fileName = tempy.file({ name: "resource.yaml" }); const fileName = tempy.file({ name: "resource.yaml" });
const args = [ const args = [
"apply", "apply",
@ -121,9 +134,8 @@ export class ResourceApplier {
} }
protected async kubectlCmdAll(subCmd: string, resources: string[], parentArgs: string[] = []): Promise<AsyncResult<string, string>> { protected async kubectlCmdAll(subCmd: string, resources: string[], parentArgs: string[] = []): Promise<AsyncResult<string, string>> {
const kubectl = await this.cluster.ensureKubectl(); const kubectlPath = await this.getKubectlPath();
const kubectlPath = await kubectl.getPath(); const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const tmpDir = tempy.directory(); const tmpDir = tempy.directory();
await Promise.all(resources.map((resource, index) => this.dependencies.writeFile( await Promise.all(resources.map((resource, index) => this.dependencies.writeFile(

View File

@ -10,15 +10,18 @@ import type { V1Secret } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import { clusterRoute } from "../../router/route"; import { clusterRoute } from "../../router/route";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import loadProxyKubeconfigInjectable from "../../cluster/load-proxy-kubeconfig.injectable";
const getServiceAccountRouteInjectable = getRouteInjectable({ const getServiceAccountRouteInjectable = getRouteInjectable({
id: "get-service-account-route", id: "get-service-account-route",
instantiate: () => clusterRoute({ instantiate: (di) => clusterRoute({
method: "get", method: "get",
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`, path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
})(async ({ params, cluster }) => { })(async ({ params, cluster }) => {
const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); const loadProxyKubeconfig = di.inject(loadProxyKubeconfigInjectable, cluster);
const proxyKubeconfig = await loadProxyKubeconfig();
const client = proxyKubeconfig.makeApiClient(CoreV1Api);
const secretList = await client.listNamespacedSecret(params.namespace); const secretList = await client.listNamespacedSecret(params.namespace);
const secret = secretList.body.items.find(secret => { const secret = secretList.body.items.find(secret => {
@ -64,9 +67,9 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
"kind": "Config", "kind": "Config",
"clusters": [ "clusters": [
{ {
"name": cluster.contextName, "name": cluster.contextName.get(),
"cluster": { "cluster": {
"server": cluster.apiUrl, "server": cluster.apiUrl.get(),
"certificate-authority-data": caCrt, "certificate-authority-data": caCrt,
}, },
}, },
@ -81,14 +84,14 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
], ],
"contexts": [ "contexts": [
{ {
"name": cluster.contextName, "name": cluster.contextName.get(),
"context": { "context": {
"user": username, "user": username,
"cluster": cluster.contextName, "cluster": cluster.contextName.get(),
"namespace": secret.metadata.namespace, "namespace": secret.metadata.namespace,
}, },
}, },
], ],
"current-context": cluster.contextName, "current-context": cluster.contextName.get(),
}); });
} }

View File

@ -14,6 +14,8 @@ import { isRequestError, object } from "../../../common/utils";
import type { GetMetrics } from "../../get-metrics.injectable"; import type { GetMetrics } from "../../get-metrics.injectable";
import getMetricsInjectable from "../../get-metrics.injectable"; import getMetricsInjectable from "../../get-metrics.injectable";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import prometheusHandlerInjectable from "../../cluster/prometheus-handler/prometheus-handler.injectable";
import { runInAction } from "mobx";
// This is used for backoff retry tracking. // This is used for backoff retry tracking.
const ATTEMPTS = [false, false, false, false, true]; const ATTEMPTS = [false, false, false, false, true];
@ -66,9 +68,10 @@ const addMetricsRouteInjectable = getRouteInjectable({
})(async ({ cluster, payload, query }) => { })(async ({ cluster, payload, query }) => {
const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries()); const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries());
const prometheusMetadata: ClusterPrometheusMetadata = {}; const prometheusMetadata: ClusterPrometheusMetadata = {};
const prometheusHandler = di.inject(prometheusHandlerInjectable, cluster);
try { try {
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails(); const { prometheusPath, provider } = await prometheusHandler.getPrometheusDetails();
prometheusMetadata.provider = provider?.kind; prometheusMetadata.provider = provider?.kind;
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type; prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
@ -115,7 +118,9 @@ const addMetricsRouteInjectable = getRouteInjectable({
return { response: {}}; return { response: {}};
} finally { } finally {
runInAction(() => {
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
});
} }
}); });
}, },

View File

@ -8,6 +8,7 @@ import { PortForward } from "./functionality/port-forward";
import createPortForwardInjectable from "./functionality/create-port-forward.injectable"; import createPortForwardInjectable from "./functionality/create-port-forward.injectable";
import { clusterRoute } from "../../router/route"; import { clusterRoute } from "../../router/route";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
const startPortForwardRouteInjectable = getRouteInjectable({ const startPortForwardRouteInjectable = getRouteInjectable({
id: "start-current-port-forward-route", id: "start-current-port-forward-route",
@ -24,6 +25,8 @@ const startPortForwardRouteInjectable = getRouteInjectable({
const port = Number(query.get("port")); const port = Number(query.get("port"));
const forwardPort = Number(query.get("forwardPort")); const forwardPort = Number(query.get("forwardPort"));
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
try { try {
let portForward = PortForward.getPortforward({ let portForward = PortForward.getPortforward({
clusterId: cluster.id, clusterId: cluster.id,
@ -42,8 +45,9 @@ const startPortForwardRouteInjectable = getRouteInjectable({
const thePort = 0 < forwardPort && forwardPort < 65536 const thePort = 0 < forwardPort && forwardPort < 65536
? forwardPort ? forwardPort
: 0; : 0;
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
portForward = createPortForward(await cluster.getProxyKubeconfigPath(), { portForward = createPortForward(proxyKubeconfigPath, {
clusterId: cluster.id, clusterId: cluster.id,
kind: resourceType, kind: resourceType,
namespace, namespace,

View File

@ -6,22 +6,22 @@ import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { payloadValidatedClusterRoute } from "../../router/route"; import { payloadValidatedClusterRoute } from "../../router/route";
import Joi from "joi"; import Joi from "joi";
import createResourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable"; import resourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable";
const createResourceRouteInjectable = getRouteInjectable({ const createResourceRouteInjectable = getRouteInjectable({
id: "create-resource-route", id: "create-resource-route",
instantiate: (di) => { instantiate: (di) => payloadValidatedClusterRoute({
const createResourceApplier = di.inject(createResourceApplierInjectable);
return payloadValidatedClusterRoute({
method: "post", method: "post",
path: `${apiPrefix}/stack`, path: `${apiPrefix}/stack`,
payloadValidator: Joi.string(), payloadValidator: Joi.string(),
})(async ({ cluster, payload }) => ({ })(async ({ cluster, payload }) => {
response: await createResourceApplier(cluster).create(payload), const resourceApplier = di.inject(resourceApplierInjectable, cluster);
}));
}, return ({
response: await resourceApplier.create(payload),
});
}),
}); });
export default createResourceRouteInjectable; export default createResourceRouteInjectable;

View File

@ -7,7 +7,7 @@ import { apiPrefix } from "../../../common/vars";
import { payloadValidatedClusterRoute } from "../../router/route"; import { payloadValidatedClusterRoute } from "../../router/route";
import Joi from "joi"; import Joi from "joi";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
import createResourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable"; import resourceApplierInjectable from "../../resource-applier/create-resource-applier.injectable";
interface PatchResourcePayload { interface PatchResourcePayload {
name: string; name: string;
@ -40,22 +40,22 @@ const patchResourcePayloadValidator = Joi.object<PatchResourcePayload, true, Pat
const patchResourceRouteInjectable = getRouteInjectable({ const patchResourceRouteInjectable = getRouteInjectable({
id: "patch-resource-route", id: "patch-resource-route",
instantiate: (di) => { instantiate: (di) => payloadValidatedClusterRoute({
const createResourceApplier = di.inject(createResourceApplierInjectable);
return payloadValidatedClusterRoute({
method: "patch", method: "patch",
path: `${apiPrefix}/stack`, path: `${apiPrefix}/stack`,
payloadValidator: patchResourcePayloadValidator, payloadValidator: patchResourcePayloadValidator,
})(async ({ cluster, payload }) => ({ })(async ({ cluster, payload }) => {
response: await createResourceApplier(cluster).patch( const resourceApplier = di.inject(resourceApplierInjectable, cluster);
return ({
response: await resourceApplier.patch(
payload.name, payload.name,
payload.kind, payload.kind,
payload.patch, payload.patch,
payload.ns, payload.ns,
), ),
})); });
}, }),
}); });
export default patchResourceRouteInjectable; export default patchResourceRouteInjectable;

View File

@ -52,16 +52,16 @@ export class LocalShellSession extends ShellSession {
protected async getShellArgs(shell: string): Promise<string[]> { protected async getShellArgs(shell: string): Promise<string[]> {
const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath(); const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries
? await this.kubectlBinDirP ? this.dependencies.directoryContainingKubectl
: this.dependencies.getDirnameOfPath(pathFromPreferences); : this.dependencies.getDirnameOfPath(pathFromPreferences);
switch(this.dependencies.getBasenameOfPath(shell)) { switch(this.dependencies.getBasenameOfPath(shell)) {
case "powershell.exe": case "powershell.exe":
return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.directoryForBinaries};$Env:PATH"}`]; return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.directoryForBinaries};$Env:PATH"}`];
case "bash": case "bash":
return ["--init-file", this.dependencies.joinPaths(await this.kubectlBinDirP, ".bash_set_path")]; return ["--init-file", this.dependencies.joinPaths(this.dependencies.directoryContainingKubectl, ".bash_set_path")];
case "fish": case "fish":
return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`]; return ["--login", "--init-command", `export PATH="${kubectlPathDir}:${this.dependencies.directoryForBinaries}:$PATH"; export KUBECONFIG="${await this.dependencies.proxyKubeconfigPath}"`];
case "zsh": case "zsh":
return ["--login"]; return ["--login"];
default: default:

View File

@ -24,6 +24,7 @@ import appNameInjectable from "../../../common/vars/app-name.injectable";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
import statInjectable from "../../../common/fs/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
export interface OpenLocalShellSessionArgs { export interface OpenLocalShellSessionArgs {
websocket: WebSocket; websocket: WebSocket;
@ -38,7 +39,7 @@ const openLocalShellSessionInjectable = getInjectable({
instantiate: (di): OpenLocalShellSession => { instantiate: (di): OpenLocalShellSession => {
const createKubectl = di.inject(createKubectlInjectable); const createKubectl = di.inject(createKubectlInjectable);
const dependencies: LocalShellSessionDependencies = { const dependencies: Omit<LocalShellSessionDependencies, "proxyKubeconfigPath" | "directoryContainingKubectl"> = {
directoryForBinaries: di.inject(directoryForBinariesInjectable), directoryForBinaries: di.inject(directoryForBinariesInjectable),
isMac: di.inject(isMacInjectable), isMac: di.inject(isMacInjectable),
isWindows: di.inject(isWindowsInjectable), isWindows: di.inject(isWindowsInjectable),
@ -57,9 +58,17 @@ const openLocalShellSessionInjectable = getInjectable({
stat: di.inject(statInjectable), stat: di.inject(statInjectable),
}; };
return (args) => { return async (args) => {
const kubectl = createKubectl(args.cluster.version); const kubectl = createKubectl(args.cluster.version.get());
const session = new LocalShellSession(dependencies, { kubectl, ...args }); const kubeconfigManager = di.inject(kubeconfigManagerInjectable, args.cluster);
const proxyKubeconfigPath = await kubeconfigManager.ensurePath();
const directoryContainingKubectl = await kubectl.binDir();
const session = new LocalShellSession({
...dependencies,
proxyKubeconfigPath,
directoryContainingKubectl,
}, { kubectl, ...args });
return session.open(); return session.open();
}; };

View File

@ -5,8 +5,9 @@
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import type WebSocket from "ws"; import type WebSocket from "ws";
import directoryForTempInjectable from "../../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import type { Cluster } from "../../../common/cluster/cluster"; import { Cluster } from "../../../common/cluster/cluster";
import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable"; import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable";
import pathExistsInjectable from "../../../common/fs/path-exists.injectable"; import pathExistsInjectable from "../../../common/fs/path-exists.injectable";
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable"; import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
@ -14,8 +15,11 @@ import statInjectable from "../../../common/fs/stat.injectable";
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable"; import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
import platformInjectable from "../../../common/vars/platform.injectable"; import platformInjectable from "../../../common/vars/platform.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { KubeconfigManager } from "../../kubeconfig-manager/kubeconfig-manager";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import createKubectlInjectable from "../../kubectl/create-kubectl.injectable"; import createKubectlInjectable from "../../kubectl/create-kubectl.injectable";
import type { Kubectl } from "../../kubectl/kubectl"; import type { Kubectl } from "../../kubectl/kubectl";
import lensProxyPortInjectable from "../../lens-proxy/lens-proxy-port.injectable";
import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
import type { OpenShellSession } from "../create-shell-session.injectable"; import type { OpenShellSession } from "../create-shell-session.injectable";
import type { SpawnPty } from "../spawn-pty.injectable"; import type { SpawnPty } from "../spawn-pty.injectable";
@ -29,6 +33,7 @@ describe("technical unit tests for local shell sessions", () => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "/some-directory-for-tmp");
di.override(buildVersionInjectable, () => ({ di.override(buildVersionInjectable, () => ({
get: () => "1.1.1", get: () => "1.1.1",
})); }));
@ -37,6 +42,7 @@ describe("technical unit tests for local shell sessions", () => {
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); }); di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); }); di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
di.override(statInjectable, () => () => { throw new Error("tried call stat without override"); }); di.override(statInjectable, () => () => { throw new Error("tried call stat without override"); });
di.inject(lensProxyPortInjectable).set(1111);
}); });
describe("when on windows", () => { describe("when on windows", () => {
@ -54,6 +60,10 @@ describe("technical unit tests for local shell sessions", () => {
getBundledPath: () => "/some-bundled-kubectl-path", getBundledPath: () => "/some-bundled-kubectl-path",
}) as Partial<Kubectl> as Kubectl); }) as Partial<Kubectl> as Kubectl);
di.override(kubeconfigManagerInjectable, () => ({
ensurePath: async () => "/some-proxy-kubeconfig-file",
} as Partial<KubeconfigManager> as KubeconfigManager));
openLocalShellSession = di.inject(openLocalShellSessionInjectable); openLocalShellSession = di.inject(openLocalShellSessionInjectable);
}); });
@ -89,11 +99,16 @@ describe("technical unit tests for local shell sessions", () => {
once: jest.fn(() => websocket), once: jest.fn(() => websocket),
} as Partial<WebSocket> as WebSocket; } as Partial<WebSocket> as WebSocket;
const cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-kube-config-path",
}, {
clusterServerUrl: "https://localhost:9999",
});
await openLocalShellSession({ await openLocalShellSession({
cluster: { cluster,
getProxyKubeconfigPath: async () => "/some-proxy-kubeconfig",
preferences: {},
} as Partial<Cluster> as Cluster,
tabId: "my-tab-id", tabId: "my-tab-id",
websocket, websocket,
}); });

View File

@ -13,6 +13,8 @@ import { NodeApi } from "../../../common/k8s-api/endpoints";
import { TerminalChannels } from "../../../common/terminal/channels"; import { TerminalChannels } from "../../../common/terminal/channels";
import type { CreateKubeJsonApiForCluster } from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable"; import type { CreateKubeJsonApiForCluster } from "../../../common/k8s-api/create-kube-json-api-for-cluster.injectable";
import type { CreateKubeApi } from "../../../common/k8s-api/create-kube-api.injectable"; import type { CreateKubeApi } from "../../../common/k8s-api/create-kube-api.injectable";
import { initialNodeShellImage } from "../../../common/cluster-types";
import type { LoadProxyKubeconfig } from "../../cluster/load-proxy-kubeconfig.injectable";
export interface NodeShellSessionArgs extends ShellSessionArgs { export interface NodeShellSessionArgs extends ShellSessionArgs {
nodeName: string; nodeName: string;
@ -21,6 +23,7 @@ export interface NodeShellSessionArgs extends ShellSessionArgs {
export interface NodeShellSessionDependencies extends ShellSessionDependencies { export interface NodeShellSessionDependencies extends ShellSessionDependencies {
createKubeJsonApiForCluster: CreateKubeJsonApiForCluster; createKubeJsonApiForCluster: CreateKubeJsonApiForCluster;
createKubeApi: CreateKubeApi; createKubeApi: CreateKubeApi;
loadProxyKubeconfig: LoadProxyKubeconfig;
} }
export class NodeShellSession extends ShellSession { export class NodeShellSession extends ShellSession {
@ -36,9 +39,8 @@ export class NodeShellSession extends ShellSession {
} }
public async open() { public async open() {
const kc = await this.cluster.getProxyKubeconfig(); const proxyKubeconfig = await this.dependencies.loadProxyKubeconfig();
const coreApi = kc.makeApiClient(CoreV1Api); const coreApi = proxyKubeconfig.makeApiClient(CoreV1Api);
const shell = await this.kubectl.getPath();
const cleanup = once(() => { const cleanup = once(() => {
coreApi coreApi
@ -50,7 +52,7 @@ export class NodeShellSession extends ShellSession {
try { try {
await this.createNodeShellPod(coreApi); await this.createNodeShellPod(coreApi);
await this.waitForRunningPod(kc); await this.waitForRunningPod(proxyKubeconfig);
} catch (error) { } catch (error) {
cleanup(); cleanup();
@ -92,13 +94,18 @@ export class NodeShellSession extends ShellSession {
break; break;
} }
await this.openShellProcess(shell, args, env); await this.openShellProcess(this.dependencies.directoryContainingKubectl, args, env);
} }
protected createNodeShellPod(coreApi: CoreV1Api) { protected createNodeShellPod(coreApi: CoreV1Api) {
const imagePullSecrets = this.cluster.imagePullSecret const {
imagePullSecret,
nodeShellImage,
} = this.cluster.preferences;
const imagePullSecrets = imagePullSecret
? [{ ? [{
name: this.cluster.imagePullSecret, name: imagePullSecret,
}] }]
: undefined; : undefined;
@ -121,7 +128,7 @@ export class NodeShellSession extends ShellSession {
priorityClassName: "system-node-critical", priorityClassName: "system-node-critical",
containers: [{ containers: [{
name: "shell", name: "shell",
image: this.cluster.nodeShellImage, image: nodeShellImage || initialNodeShellImage,
securityContext: { securityContext: {
privileged: true, privileged: true,
}, },

View File

@ -20,6 +20,8 @@ import buildVersionInjectable from "../../vars/build-version/build-version.injec
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
import statInjectable from "../../../common/fs/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable"; import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable";
import loadProxyKubeconfigInjectable from "../../cluster/load-proxy-kubeconfig.injectable";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
export interface NodeShellSessionArgs { export interface NodeShellSessionArgs {
websocket: WebSocket; websocket: WebSocket;
@ -34,7 +36,7 @@ const openNodeShellSessionInjectable = getInjectable({
id: "open-node-shell-session", id: "open-node-shell-session",
instantiate: (di): OpenNodeShellSession => { instantiate: (di): OpenNodeShellSession => {
const createKubectl = di.inject(createKubectlInjectable); const createKubectl = di.inject(createKubectlInjectable);
const dependencies: NodeShellSessionDependencies = { const dependencies: Omit<NodeShellSessionDependencies, "proxyKubeconfigPath" | "loadProxyKubeconfig" | "directoryContainingKubectl"> = {
isMac: di.inject(isMacInjectable), isMac: di.inject(isMacInjectable),
isWindows: di.inject(isWindowsInjectable), isWindows: di.inject(isWindowsInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
@ -50,8 +52,18 @@ const openNodeShellSessionInjectable = getInjectable({
}; };
return async (args) => { return async (args) => {
const kubectl = createKubectl(args.cluster.version); const kubectl = createKubectl(args.cluster.version.get());
const session = new NodeShellSession(dependencies, { kubectl, ...args }); const kubeconfigManager = di.inject(kubeconfigManagerInjectable, args.cluster);
const loadProxyKubeconfig = di.inject(loadProxyKubeconfigInjectable, args.cluster);
const proxyKubeconfigPath = await kubeconfigManager.ensurePath();
const directoryContainingKubectl = await kubectl.binDir();
const session = new NodeShellSession({
...dependencies,
loadProxyKubeconfig,
proxyKubeconfigPath,
directoryContainingKubectl,
}, { kubectl, ...args });
return session.open(); return session.open();
}; };

View File

@ -111,6 +111,8 @@ export interface ShellSessionDependencies {
readonly userShellSetting: IComputedValue<string>; readonly userShellSetting: IComputedValue<string>;
readonly appName: string; readonly appName: string;
readonly buildVersion: InitializableState<string>; readonly buildVersion: InitializableState<string>;
readonly proxyKubeconfigPath: string;
readonly directoryContainingKubectl: string;
computeShellEnvironment: ComputeShellEnvironment; computeShellEnvironment: ComputeShellEnvironment;
spawnPty: SpawnPty; spawnPty: SpawnPty;
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
@ -147,8 +149,6 @@ export abstract class ShellSession {
} }
protected running = false; protected running = false;
protected readonly kubectlBinDirP: Promise<string>;
protected readonly kubeconfigPathP: Promise<string>;
protected readonly terminalId: string; protected readonly terminalId: string;
protected readonly kubectl: Kubectl; protected readonly kubectl: Kubectl;
protected readonly websocket: WebSocket; protected readonly websocket: WebSocket;
@ -179,8 +179,6 @@ export abstract class ShellSession {
this.kubectl = kubectl; this.kubectl = kubectl;
this.websocket = websocket; this.websocket = websocket;
this.cluster = cluster; this.cluster = cluster;
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
this.kubectlBinDirP = this.kubectl.binDir();
this.terminalId = `${cluster.id}:${terminalId}`; this.terminalId = `${cluster.id}:${terminalId}`;
} }
@ -297,7 +295,7 @@ export abstract class ShellSession {
code !== WebSocketCloseEvent.AbnormalClosure code !== WebSocketCloseEvent.AbnormalClosure
&& code !== WebSocketCloseEvent.GoingAway && code !== WebSocketCloseEvent.GoingAway
) )
|| this.cluster.disconnected || this.cluster.disconnected.get()
); );
if (stopShellSession) { if (stopShellSession) {
@ -350,7 +348,7 @@ export abstract class ShellSession {
})(); })();
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(rawEnv))); const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(rawEnv)));
const pathStr = [await this.kubectlBinDirP, ...this.getPathEntries(), env.PATH].join(path.delimiter); const pathStr = [this.dependencies.directoryContainingKubectl, ...this.getPathEntries(), env.PATH].join(path.delimiter);
delete env.DEBUG; // don't pass DEBUG into shells delete env.DEBUG; // don't pass DEBUG into shells
@ -373,12 +371,12 @@ export abstract class ShellSession {
if (path.basename(env.PTYSHELL) === "zsh") { if (path.basename(env.PTYSHELL) === "zsh") {
env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME; env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME;
env.ZDOTDIR = await this.kubectlBinDirP; env.ZDOTDIR = this.dependencies.directoryContainingKubectl;
env.DISABLE_AUTO_UPDATE = "true"; env.DISABLE_AUTO_UPDATE = "true";
} }
env.PTYPID = process.pid.toString(); env.PTYPID = process.pid.toString();
env.KUBECONFIG = await this.kubeconfigPathP; env.KUBECONFIG = this.dependencies.proxyKubeconfigPath;
env.TERM_PROGRAM = this.dependencies.appName; env.TERM_PROGRAM = this.dependencies.appName;
env.TERM_PROGRAM_VERSION = this.dependencies.buildVersion.get(); env.TERM_PROGRAM_VERSION = this.dependencies.buildVersion.get();

View File

@ -3,20 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import type { Cluster } from "../../../../common/cluster/cluster";
import catalogEntityRegistryInjectable from "./registry.injectable"; import catalogEntityRegistryInjectable from "./registry.injectable";
export type GetActiveClusterEntity = () => Cluster | undefined; const activeEntityInternalClusterInjectable = getInjectable({
id: "active-entity-internal-cluster",
const getActiveClusterEntityInjectable = getInjectable({ instantiate: (di) => {
id: "get-active-cluster-entity",
instantiate: (di): GetActiveClusterEntity => {
const store = di.inject(clusterStoreInjectable); const store = di.inject(clusterStoreInjectable);
const entityRegistry = di.inject(catalogEntityRegistryInjectable); const entityRegistry = di.inject(catalogEntityRegistryInjectable);
return () => store.getById(entityRegistry.activeEntity?.getId()); return computed(() => store.getById(entityRegistry.activeEntity?.getId()));
}, },
}); });
export default getActiveClusterEntityInjectable; export default activeEntityInternalClusterInjectable;

View File

@ -5,14 +5,22 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx"; import { computed } from "mobx";
import type { ClusterMetricsResourceType } from "../../../../common/cluster-types"; 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({ const enabledMetricsInjectable = getInjectable({
id: "enabled-metrics", id: "enabled-metrics",
instantiate: (di, kind) => { 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({ lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, kind: ClusterMetricsResourceType) => kind, getInstanceKey: (di, kind: ClusterMetricsResourceType) => kind,

View File

@ -5,8 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable"; import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable"; import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import readFileInjectable from "../../../common/fs/read-file.injectable"; import loadKubeconfigInjectable from "../../../common/cluster/load-kubeconfig.injectable";
import { loadConfigFromString } from "../../../common/kube-helpers";
import loggerInjectable from "../../../common/logger.injectable"; import loggerInjectable from "../../../common/logger.injectable";
import openDeleteClusterDialogInjectable from "../../components/delete-cluster-dialog/open.injectable"; import openDeleteClusterDialogInjectable from "../../components/delete-cluster-dialog/open.injectable";
import { beforeFrameStartsSecondInjectionToken } from "../tokens"; import { beforeFrameStartsSecondInjectionToken } from "../tokens";
@ -18,7 +17,6 @@ const setupKubernetesClusterContextMenuOpenInjectable = getInjectable({
run: () => { run: () => {
const catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable); const catalogCategoryRegistry = di.inject(catalogCategoryRegistryInjectable);
const openDeleteClusterDialog = di.inject(openDeleteClusterDialogInjectable); const openDeleteClusterDialog = di.inject(openDeleteClusterDialogInjectable);
const readFile = di.inject(readFileInjectable);
const getClusterById = di.inject(getClusterByIdInjectable); const getClusterById = di.inject(getClusterByIdInjectable);
const logger = di.inject(loggerInjectable); 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 }); 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) { if (result.error) {
logger.error("[KUBERNETES-CLUSTER]: failed to parse kubeconfig file", result.error); logger.error("[KUBERNETES-CLUSTER]: failed to parse kubeconfig file", result.error);

View File

@ -55,7 +55,7 @@ const clusterFrameContextForNamespacedResourcesInjectable = getInjectable({
&& cluster.accessibleNamespaces.length === 0 && cluster.accessibleNamespaces.length === 0
&& allNamespaces.get().every(ns => namespaces.includes(ns)) && allNamespaces.get().every(ns => namespaces.includes(ns))
), ),
isGlobalWatchEnabled: () => cluster.isGlobalWatchEnabled, isGlobalWatchEnabled: () => cluster.isGlobalWatchEnabled.get(),
get allNamespaces() { get allNamespaces() {
return allNamespaces.get(); return allNamespaces.get();
}, },

View File

@ -15,7 +15,7 @@ const shouldShowResourceInjectable = getInjectable({
const cluster = di.inject(hostedClusterInjectable); const cluster = di.inject(hostedClusterInjectable);
return cluster return cluster
? computed(() => cluster.shouldShowResource(resource)) ? computed(() => cluster.resourcesToShow.has(formatKubeApiResource(resource)))
: computed(() => false); : computed(() => false);
}, },
injectionToken: shouldShowResourceInjectionToken, injectionToken: shouldShowResourceInjectionToken,

View File

@ -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;

View File

@ -6,6 +6,7 @@
import styles from "./cluster-overview.module.scss"; import styles from "./cluster-overview.module.scss";
import React from "react"; import React from "react";
import type { IComputedValue } from "mobx";
import { reaction } from "mobx"; import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import type { NodeStore } from "../+nodes/store"; import type { NodeStore } from "../+nodes/store";
@ -22,32 +23,30 @@ import type { EventStore } from "../+events/store";
import { withInjectables } from "@ogre-tools/injectable-react"; import { withInjectables } from "@ogre-tools/injectable-react";
import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable";
import type { SubscribeStores } from "../../kube-watch-api/kube-watch-api"; 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 subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import podStoreInjectable from "../+workloads-pods/store.injectable"; import podStoreInjectable from "../+workloads-pods/store.injectable";
import eventStoreInjectable from "../+events/store.injectable"; import eventStoreInjectable from "../+events/store.injectable";
import nodeStoreInjectable from "../+nodes/store.injectable"; import nodeStoreInjectable from "../+nodes/store.injectable";
import enabledMetricsInjectable from "../../api/catalog/entity/metrics-enabled.injectable";
interface Dependencies { interface Dependencies {
subscribeStores: SubscribeStores; subscribeStores: SubscribeStores;
clusterOverviewStore: ClusterOverviewStore; clusterOverviewStore: ClusterOverviewStore;
hostedCluster: Cluster;
podStore: PodStore; podStore: PodStore;
eventStore: EventStore; eventStore: EventStore;
nodeStore: NodeStore; nodeStore: NodeStore;
clusterMetricsAreVisible: IComputedValue<boolean>;
} }
@observer @observer
class NonInjectedClusterOverview extends React.Component<Dependencies> { class NonInjectedClusterOverview extends React.Component<Dependencies> {
private metricPoller = interval(60, () => this.loadMetrics()); private readonly metricPoller = interval(60, async () => {
try {
loadMetrics() { await this.props.clusterOverviewStore.loadMetrics();
if (this.props.hostedCluster.available) { } catch {
this.props.clusterOverviewStore.loadMetrics(); // ignore
}
} }
});
componentDidMount() { componentDidMount() {
this.metricPoller.start(true); this.metricPoller.start(true);
@ -97,14 +96,13 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
} }
render() { render() {
const { eventStore, nodeStore, hostedCluster } = this.props; const { eventStore, nodeStore, clusterMetricsAreVisible } = this.props;
const isLoaded = nodeStore.isLoaded && eventStore.isLoaded; const isLoaded = nodeStore.isLoaded && eventStore.isLoaded;
const isMetricHidden = hostedCluster.isMetricHidden(ClusterMetricsResourceType.Cluster);
return ( return (
<TabLayout scrollable> <TabLayout scrollable>
<div className={styles.ClusterOverview} data-testid="cluster-overview-page"> <div className={styles.ClusterOverview} data-testid="cluster-overview-page">
{this.renderClusterOverview(isLoaded, isMetricHidden)} {this.renderClusterOverview(isLoaded, clusterMetricsAreVisible.get())}
</div> </div>
</TabLayout> </TabLayout>
); );
@ -112,18 +110,12 @@ class NonInjectedClusterOverview extends React.Component<Dependencies> {
} }
export const ClusterOverview = withInjectables<Dependencies>(NonInjectedClusterOverview, { export const ClusterOverview = withInjectables<Dependencies>(NonInjectedClusterOverview, {
getProps: (di) => { getProps: (di) => ({
const hostedCluster = di.inject(hostedClusterInjectable);
assert(hostedCluster, "Only allowed to renderer ClusterOverview within cluster frame");
return {
subscribeStores: di.inject(subscribeStoresInjectable), subscribeStores: di.inject(subscribeStoresInjectable),
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
hostedCluster, clusterMetricsAreVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Cluster),
podStore: di.inject(podStoreInjectable), podStore: di.inject(podStoreInjectable),
eventStore: di.inject(eventStoreInjectable), eventStore: di.inject(eventStoreInjectable),
nodeStore: di.inject(nodeStoreInjectable), nodeStore: di.inject(nodeStoreInjectable),
}; }),
},
}); });

View File

@ -11,8 +11,8 @@ import { renderFor } from "../../test-utils/renderFor";
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable"; 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 directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import hostedClusterInjectable from "../../../cluster-frame-context/hosted-cluster.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 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", () => ({ jest.mock("../../kube-object-meta/kube-object-meta", () => ({
KubeObjectMeta: () => null, KubeObjectMeta: () => null,
@ -27,9 +27,7 @@ describe("SecretDetails tests", () => {
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true); di.override(storesAndApisCanBeCreatedInjectable, () => true);
const createCluster = di.inject(createClusterInjectable); di.override(hostedClusterInjectable, () => new Cluster({
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",

View File

@ -11,12 +11,12 @@ import { fireEvent } from "@testing-library/react";
import React from "react"; import React from "react";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; 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 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 type { Fetch } from "../../../common/fetch/fetch.injectable";
import fetchInjectable from "../../../common/fetch/fetch.injectable"; import fetchInjectable from "../../../common/fetch/fetch.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints"; import { Namespace } from "../../../common/k8s-api/endpoints";
import { createMockResponseFromString } from "../../../test-utils/mock-responses"; import { createMockResponseFromString } from "../../../test-utils/mock-responses";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import createClusterInjectable from "../../cluster/create-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
@ -58,9 +58,7 @@ describe("<NamespaceSelectFilter />", () => {
fetchMock = asyncFn(); fetchMock = asyncFn();
di.override(fetchInjectable, () => fetchMock); di.override(fetchInjectable, () => fetchMock);
const createCluster = di.inject(createClusterInjectable); di.override(hostedClusterInjectable, () => new Cluster({
di.override(hostedClusterInjectable, () => createCluster({
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",

Some files were not shown because too many files have changed in this diff Show More