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:
parent
e6d6d1d8f7
commit
ad9bafe2a5
286
package-lock.json
generated
286
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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 () => {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,41 +18,33 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => {
|
|
||||||
return (proxyConfig) => {
|
|
||||||
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
|
|
||||||
|
|
||||||
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const { body } = await api.createSelfSubjectAccessReview({
|
|
||||||
apiVersion: "authorization.k8s.io/v1",
|
|
||||||
kind: "SelfSubjectAccessReview",
|
|
||||||
spec: { resourceAttributes },
|
|
||||||
});
|
|
||||||
|
|
||||||
return body.status?.allowed ?? false;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const authorizationReviewInjectable = getInjectable({
|
|
||||||
id: "authorization-review",
|
id: "authorization-review",
|
||||||
instantiate: (di) => {
|
instantiate: (di): CreateAuthorizationReview => {
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
return authorizationReview({ logger });
|
return (proxyConfig) => {
|
||||||
|
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
|
||||||
|
|
||||||
|
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const { body } = await api.createSelfSubjectAccessReview({
|
||||||
|
apiVersion: "authorization.k8s.io/v1",
|
||||||
|
kind: "SelfSubjectAccessReview",
|
||||||
|
spec: { resourceAttributes },
|
||||||
|
});
|
||||||
|
|
||||||
|
return body.status?.allowed ?? false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default authorizationReviewInjectable;
|
export default createAuthorizationReviewInjectable;
|
||||||
|
|||||||
@ -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 {
|
export class Cluster {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cluster
|
|
||||||
*
|
|
||||||
* @beta
|
|
||||||
*/
|
|
||||||
export class Cluster implements ClusterModel {
|
|
||||||
/** Unique id for a cluster */
|
|
||||||
public readonly id: ClusterId;
|
|
||||||
private kubeCtl: Kubectl | undefined;
|
|
||||||
/**
|
/**
|
||||||
* Context handler
|
* Unique id for a cluster
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
protected readonly _contextHandler: ClusterContextHandler | undefined;
|
readonly id: ClusterId;
|
||||||
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,454 +148,83 @@ export class Cluster implements ClusterModel {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.kubeConfigPath = model.kubeConfigPath;
|
|
||||||
this.contextName = model.contextName;
|
|
||||||
|
|
||||||
if (model.workspace) {
|
|
||||||
this.workspace = model.workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.workspaces) {
|
|
||||||
this.workspaces = model.workspaces;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.preferences) {
|
|
||||||
this.preferences = model.preferences;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.metadata) {
|
|
||||||
this.metadata = model.metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.accessibleNamespaces) {
|
|
||||||
this.accessibleNamespaces.replace(model.accessibleNamespaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.labels) {
|
|
||||||
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(() => {
|
runInAction(() => {
|
||||||
this.metadata = {
|
this.kubeConfigPath.set(model.kubeConfigPath);
|
||||||
...this.metadata,
|
this.contextName.set(model.contextName);
|
||||||
...newMetadata,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (model.preferences) {
|
||||||
* @internal
|
replaceObservableObject(this.preferences, model.preferences);
|
||||||
*/
|
|
||||||
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;
|
if (model.metadata) {
|
||||||
}
|
replaceObservableObject(this.metadata, model.metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.accessibleNamespaces) {
|
||||||
|
this.accessibleNamespaces.replace(model.accessibleNamespaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.labels) {
|
||||||
|
replaceObservableObject(this.labels, model.labels);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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.allowedNamespaces.replace(state.allowedNamespaces);
|
this.accessible.set(state.accessible);
|
||||||
this.allowedResources.replace(state.allowedResources);
|
this.allowedNamespaces.replace(state.allowedNamespaces);
|
||||||
this.apiUrl = state.apiUrl;
|
this.resourcesToShow.replace(state.resourcesToShow);
|
||||||
this.disconnected = state.disconnected;
|
this.apiUrl.set(state.apiUrl);
|
||||||
this.isAdmin = state.isAdmin;
|
this.disconnected.set(state.disconnected);
|
||||||
this.isGlobalWatchEnabled = state.isGlobalWatchEnabled;
|
this.isAdmin.set(state.isAdmin);
|
||||||
this.online = state.online;
|
this.isGlobalWatchEnabled.set(state.isGlobalWatchEnabled);
|
||||||
this.ready = state.ready;
|
this.online.set(state.online);
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
|
||||||
});
|
|
||||||
@ -9,21 +9,21 @@ 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 coreApi = config.makeApiClient(CoreV1Api);
|
|
||||||
|
|
||||||
return async () => {
|
const createListNamespacesInjectable = getInjectable({
|
||||||
const { body: { items }} = await coreApi.listNamespace();
|
id: "create-list-namespaces",
|
||||||
|
instantiate: (): CreateListNamespaces => (config) => {
|
||||||
|
const coreApi = config.makeApiClient(CoreV1Api);
|
||||||
|
|
||||||
return items
|
return async () => {
|
||||||
.map(ns => ns.metadata?.name)
|
const { body: { items }} = await coreApi.listNamespace();
|
||||||
.filter(isDefined);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const listNamespacesInjectable = getInjectable({
|
return items
|
||||||
id: "list-namespaces",
|
.map(ns => ns.metadata?.name)
|
||||||
instantiate: () => listNamespaces,
|
.filter(isDefined);
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default listNamespacesInjectable;
|
export default createListNamespacesInjectable;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -25,7 +25,7 @@ export interface BackoffCallerOptions<E> {
|
|||||||
maxAttempts?: number;
|
maxAttempts?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In miliseconds
|
* In milliseconds
|
||||||
* @default 1000
|
* @default 1000
|
||||||
*/
|
*/
|
||||||
initialTimeout?: number;
|
initialTimeout?: number;
|
||||||
|
|||||||
18
packages/core/src/common/utils/replace-observable-object.ts
Normal file
18
packages/core/src/common/utils/replace-observable-object.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -64,9 +64,11 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
|
|||||||
windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock);
|
windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.allowKubeResource({
|
builder.afterWindowStart(() => {
|
||||||
apiName: "namespaces",
|
builder.allowKubeResource({
|
||||||
group: "",
|
apiName: "namespaces",
|
||||||
|
group: "",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -35,9 +35,11 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
|
|||||||
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
|
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.allowKubeResource({
|
builder.afterWindowStart(() => {
|
||||||
apiName: "namespaces",
|
builder.allowKubeResource({
|
||||||
group: "",
|
apiName: "namespaces",
|
||||||
|
group: "",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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({
|
|
||||||
apiName: "pods",
|
builder.afterWindowStart(() => {
|
||||||
group: "",
|
builder.allowKubeResource({
|
||||||
|
apiName: "pods",
|
||||||
|
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", () => {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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" });
|
||||||
|
|||||||
@ -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", () => {
|
||||||
|
|||||||
213
packages/core/src/main/__test__/prometheus-handler.test.ts
Normal file
213
packages/core/src/main/__test__/prometheus-handler.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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,
|
||||||
}],
|
}],
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
21
packages/core/src/main/cluster/auth-proxy-url.injectable.ts
Normal file
21
packages/core/src/main/cluster/auth-proxy-url.injectable.ts
Normal 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;
|
||||||
@ -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;
|
||||||
423
packages/core/src/main/cluster/cluster-connection.injectable.ts
Normal file
423
packages/core/src/main/cluster/cluster-connection.injectable.ts
Normal 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;
|
||||||
|
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,24 +36,15 @@ 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,
|
icon: left.icon || right.icon || undefined,
|
||||||
icon: left.icon || right.icon || undefined,
|
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: { ...(right.labels ?? {}), ...(prev.labels ?? {}) },
|
||||||
labels: mergeLabels(prev.labels ?? {}, right.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);
|
||||||
|
|||||||
@ -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];
|
||||||
|
|||||||
@ -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",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
runInAction(() => {
|
||||||
|
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
method: "post",
|
||||||
|
path: `${apiPrefix}/stack`,
|
||||||
|
payloadValidator: Joi.string(),
|
||||||
|
})(async ({ cluster, payload }) => {
|
||||||
|
const resourceApplier = di.inject(resourceApplierInjectable, cluster);
|
||||||
|
|
||||||
return payloadValidatedClusterRoute({
|
return ({
|
||||||
method: "post",
|
response: await resourceApplier.create(payload),
|
||||||
path: `${apiPrefix}/stack`,
|
});
|
||||||
payloadValidator: Joi.string(),
|
}),
|
||||||
})(async ({ cluster, payload }) => ({
|
|
||||||
response: await createResourceApplier(cluster).create(payload),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default createResourceRouteInjectable;
|
export default createResourceRouteInjectable;
|
||||||
|
|||||||
@ -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);
|
method: "patch",
|
||||||
|
path: `${apiPrefix}/stack`,
|
||||||
|
payloadValidator: patchResourcePayloadValidator,
|
||||||
|
})(async ({ cluster, payload }) => {
|
||||||
|
const resourceApplier = di.inject(resourceApplierInjectable, cluster);
|
||||||
|
|
||||||
return payloadValidatedClusterRoute({
|
return ({
|
||||||
method: "patch",
|
response: await resourceApplier.patch(
|
||||||
path: `${apiPrefix}/stack`,
|
|
||||||
payloadValidator: patchResourcePayloadValidator,
|
|
||||||
})(async ({ cluster, payload }) => ({
|
|
||||||
response: await createResourceApplier(cluster).patch(
|
|
||||||
payload.name,
|
payload.name,
|
||||||
payload.kind,
|
payload.kind,
|
||||||
payload.patch,
|
payload.patch,
|
||||||
payload.ns,
|
payload.ns,
|
||||||
),
|
),
|
||||||
}));
|
});
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default patchResourceRouteInjectable;
|
export default patchResourceRouteInjectable;
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
|
||||||
import type { ClusterDependencies } from "../../common/cluster/cluster";
|
|
||||||
import { Cluster } from "../../common/cluster/cluster";
|
|
||||||
import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
|
||||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
|
||||||
import loggerInjectable from "../../common/logger.injectable";
|
|
||||||
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
|
|
||||||
import loadConfigfromFileInjectable from "../../common/kube-helpers/load-config-from-file.injectable";
|
|
||||||
|
|
||||||
const createClusterInjectable = getInjectable({
|
|
||||||
id: "create-cluster",
|
|
||||||
|
|
||||||
instantiate: (di) => {
|
|
||||||
const dependencies: ClusterDependencies = {
|
|
||||||
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
|
|
||||||
logger: di.inject(loggerInjectable),
|
|
||||||
broadcastMessage: di.inject(broadcastMessageInjectable),
|
|
||||||
loadConfigfromFile: di.inject(loadConfigfromFileInjectable),
|
|
||||||
|
|
||||||
// TODO: Dismantle wrong abstraction
|
|
||||||
// Note: "as never" to get around strictness in unnatural scenario
|
|
||||||
createKubeconfigManager: () => undefined as never,
|
|
||||||
createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");},
|
|
||||||
createContextHandler: () => undefined as never,
|
|
||||||
createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
|
||||||
requestNamespaceListPermissionsFor: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
|
||||||
createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
|
||||||
requestApiResources: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
|
||||||
detectClusterMetadata: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
|
||||||
clusterVersionDetector: {
|
|
||||||
detect: () => { throw new Error("Tried to access back-end feature in front-end."); },
|
|
||||||
key: "irrelavent",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (model, configData) => new Cluster(dependencies, model, configData);
|
|
||||||
},
|
|
||||||
|
|
||||||
injectionToken: createClusterInjectionToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default createClusterInjectable;
|
|
||||||
@ -6,6 +6,7 @@
|
|||||||
import styles from "./cluster-overview.module.scss";
|
import 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);
|
subscribeStores: di.inject(subscribeStoresInjectable),
|
||||||
|
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
||||||
assert(hostedCluster, "Only allowed to renderer ClusterOverview within cluster frame");
|
clusterMetricsAreVisible: di.inject(enabledMetricsInjectable, ClusterMetricsResourceType.Cluster),
|
||||||
|
podStore: di.inject(podStoreInjectable),
|
||||||
return {
|
eventStore: di.inject(eventStoreInjectable),
|
||||||
subscribeStores: di.inject(subscribeStoresInjectable),
|
nodeStore: di.inject(nodeStoreInjectable),
|
||||||
clusterOverviewStore: di.inject(clusterOverviewStoreInjectable),
|
}),
|
||||||
hostedCluster,
|
|
||||||
podStore: di.inject(podStoreInjectable),
|
|
||||||
eventStore: di.inject(eventStoreInjectable),
|
|
||||||
nodeStore: di.inject(nodeStoreInjectable),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import { renderFor } from "../../test-utils/renderFor";
|
|||||||
import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable";
|
import 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",
|
||||||
|
|||||||
@ -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
Loading…
Reference in New Issue
Block a user