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:
1 parent
e6d6d1d8f7
commit
ad9bafe2a5
133 files changed
+2544
-1961
No files matched your search
@@ -2,35 +2,29 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import { Kubectl } from "../kubectl/kubectl";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
|
||||
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||
import { parse } from "url";
|
||||
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.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 normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
|
||||
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
|
||||
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
|
||||
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable";
|
||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable";
|
||||
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
|
||||
import type { ClusterConnection } from "../cluster/cluster-connection.injectable";
|
||||
import clusterConnectionInjectable from "../cluster/cluster-connection.injectable";
|
||||
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
|
||||
|
||||
describe("create clusters", () => {
|
||||
let cluster: Cluster;
|
||||
let createCluster: CreateCluster;
|
||||
let clusterConnection: ClusterConnection;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
const di = getDiForUnitTesting();
|
||||
const clusterServerUrl = "https://192.168.64.3:8443";
|
||||
|
||||
@@ -39,65 +33,51 @@ describe("create clusters", () => {
|
||||
di.override(kubectlBinaryNameInjectable, () => "kubectl");
|
||||
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
||||
di.override(broadcastMessageInjectable, () => async () => {});
|
||||
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||
di.override(broadcastConnectionUpdateInjectable, () => async () => {});
|
||||
di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||
di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true);
|
||||
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
||||
restartServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
clusterUrl: parse(cluster.apiUrl),
|
||||
getApiTarget: jest.fn(),
|
||||
di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||
di.override(prometheusHandlerInjectable, () => ({
|
||||
getPrometheusDetails: jest.fn(),
|
||||
resolveAuthProxyCa: jest.fn(),
|
||||
resolveAuthProxyUrl: 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));
|
||||
|
||||
cluster = createCluster({
|
||||
cluster = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
}, {
|
||||
clusterServerUrl,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cluster.disconnect();
|
||||
clusterConnection = di.inject(clusterConnectionInjectable, cluster);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
expect(() => cluster.reconnect()).not.toThrowError();
|
||||
expect(() => clusterConnection.reconnect()).not.toThrowError();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
jest.spyOn(cluster, "reconnect");
|
||||
jest.spyOn(cluster, "refreshConnectionStatus");
|
||||
jest.spyOn(clusterConnection, "reconnect").mockImplementation(async () => {});
|
||||
jest.spyOn(clusterConnection, "refreshConnectionStatus").mockImplementation(async () => {});
|
||||
|
||||
await cluster.activate();
|
||||
await clusterConnection.activate();
|
||||
|
||||
expect(cluster.reconnect).toBeCalled();
|
||||
expect(cluster.refreshConnectionStatus).toBeCalled();
|
||||
|
||||
cluster.disconnect();
|
||||
jest.resetAllMocks();
|
||||
expect(clusterConnection.reconnect).toBeCalled();
|
||||
expect(clusterConnection.refreshConnectionStatus).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -3,16 +3,20 @@
|
||||
* 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 createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
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 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 {
|
||||
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", () => {
|
||||
let createContextHandler: (cluster: Cluster) => ClusterContextHandler;
|
||||
let di: DiContainer;
|
||||
let cluster: Cluster;
|
||||
|
||||
beforeEach(() => {
|
||||
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", () => {
|
||||
@@ -76,7 +88,7 @@ describe("ContextHandler", () => {
|
||||
}
|
||||
});
|
||||
|
||||
expect(() => createContextHandler(clusterStub).getPrometheusDetails()).rejects.toThrowError();
|
||||
expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError();
|
||||
});
|
||||
|
||||
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}`);
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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 { ChildProcess } from "child_process";
|
||||
import { Kubectl } from "../kubectl/kubectl";
|
||||
@@ -14,8 +14,6 @@ import type { Readable } from "stream";
|
||||
import { EventEmitter } from "stream";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
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 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";
|
||||
@@ -31,7 +29,6 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab
|
||||
const clusterServerUrl = "https://192.168.64.3:8443";
|
||||
|
||||
describe("kube auth proxy tests", () => {
|
||||
let createCluster: CreateCluster;
|
||||
let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
||||
let spawnMock: jest.Mock;
|
||||
let waitUntilPortIsUsedMock: jest.Mock;
|
||||
@@ -86,12 +83,11 @@ describe("kube auth proxy tests", () => {
|
||||
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
||||
|
||||
createCluster = di.inject(createClusterInjectionToken);
|
||||
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
|
||||
});
|
||||
|
||||
it("calling exit multiple times shouldn't throw", async () => {
|
||||
const cluster = createCluster({
|
||||
const cluster = new Cluster({
|
||||
id: "foobar",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
contextName: "minikube",
|
||||
@@ -114,8 +110,11 @@ describe("kube auth proxy tests", () => {
|
||||
beforeEach(async () => {
|
||||
mockedCP = mockDeep<ChildProcess>();
|
||||
listeners = new EventEmitter();
|
||||
const stderr = mockedCP.stderr = mock<Readable>();
|
||||
const stdout = mockedCP.stdout = mock<Readable>();
|
||||
const stderr = 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, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
||||
@@ -124,32 +123,32 @@ describe("kube auth proxy tests", () => {
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
return stderr;
|
||||
});
|
||||
mockedCP.stderr.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
|
||||
stderr.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
|
||||
listeners.removeAllListeners(event ?? `stderr/${String(event)}`);
|
||||
|
||||
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);
|
||||
|
||||
if (event === "data") {
|
||||
@@ -158,22 +157,22 @@ describe("kube auth proxy tests", () => {
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
return stdout;
|
||||
});
|
||||
mockedCP.stdout.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
|
||||
stdout.removeAllListeners.mockImplementation((event?: string | symbol): Readable => {
|
||||
listeners.removeAllListeners(event ?? `stdout/${String(event)}`);
|
||||
|
||||
return stdout;
|
||||
@@ -185,7 +184,7 @@ describe("kube auth proxy tests", () => {
|
||||
});
|
||||
waitUntilPortIsUsedMock.mockReturnValueOnce(Promise.resolve());
|
||||
|
||||
const cluster = createCluster({
|
||||
const cluster = new Cluster({
|
||||
id: "foobar",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
contextName: "minikube",
|
||||
@@ -194,37 +193,38 @@ describe("kube auth proxy tests", () => {
|
||||
});
|
||||
|
||||
proxy = createKubeAuthProxy(cluster, {});
|
||||
await proxy.run();
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast errors", async () => {
|
||||
await proxy.run();
|
||||
it("should call spawn and broadcast errors", () => {
|
||||
listeners.emit("error", { message: "foobarbat" });
|
||||
|
||||
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "foobarbat", level: "error" });
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast exit", async () => {
|
||||
await proxy.run();
|
||||
listeners.emit("exit", 0);
|
||||
it("should call spawn and broadcast exit as error when exitCode != 0", () => {
|
||||
listeners.emit("exit", 1);
|
||||
|
||||
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 () => {
|
||||
await proxy.run();
|
||||
it("should call spawn and broadcast exit as info when exitCode == 0", () => {
|
||||
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");
|
||||
|
||||
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "an error", level: "error" });
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast stdout serving info", async () => {
|
||||
await proxy.run();
|
||||
|
||||
it("should call spawn and broadcast stdout serving info", () => {
|
||||
expect(broadcastMessageMock).toBeCalledWith("cluster:foobar:connection-update", { message: "Authentication proxy started", level: "info" });
|
||||
});
|
||||
|
||||
it("should call spawn and broadcast stdout other info", async () => {
|
||||
await proxy.run();
|
||||
it("should call spawn and broadcast stdout other info", () => {
|
||||
listeners.emit("stdout/data", "some 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.
|
||||
*/
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.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 { parse } from "url";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import type { Logger } from "../../common/logger";
|
||||
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 readJsonSyncInjectable from "../../common/fs/read-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";
|
||||
|
||||
describe("kubeconfig manager tests", () => {
|
||||
let clusterFake: Cluster;
|
||||
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
let di: DiContainer;
|
||||
let loggerMock: jest.Mocked<Logger>;
|
||||
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(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
|
||||
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
||||
di.inject(lensProxyPortInjectable).set(9191);
|
||||
|
||||
readFileMock = asyncFn();
|
||||
di.override(readFileInjectable, () => readFileMock);
|
||||
@@ -78,23 +77,15 @@ describe("kubeconfig manager tests", () => {
|
||||
|
||||
ensureServerMock = asyncFn();
|
||||
|
||||
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
||||
restartServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
clusterUrl: parse(cluster.apiUrl),
|
||||
di.override(kubeAuthProxyServerInjectable, () => ({
|
||||
restart: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
getApiTarget: jest.fn(),
|
||||
getPrometheusDetails: jest.fn(),
|
||||
resolveAuthProxyCa: jest.fn(),
|
||||
resolveAuthProxyUrl: jest.fn(),
|
||||
setupPrometheus: jest.fn(),
|
||||
ensureServer: ensureServerMock,
|
||||
ensureRunning: ensureServerMock,
|
||||
ensureAuthProxyUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
||||
|
||||
clusterFake = createCluster({
|
||||
clusterFake = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "/minikube-config.yml",
|
||||
@@ -102,9 +93,7 @@ describe("kubeconfig manager tests", () => {
|
||||
clusterServerUrl,
|
||||
});
|
||||
|
||||
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("https://127.0.0.1:9191/foo");
|
||||
|
||||
kubeConfManager = createKubeconfigManager(clusterFake);
|
||||
kubeConfManager = di.inject(kubeconfigManagerInjectable, clusterFake);
|
||||
});
|
||||
|
||||
describe("when calling clear", () => {
|
||||
@@ -123,7 +112,7 @@ describe("kubeconfig manager tests", () => {
|
||||
let getPathPromise: Promise<string>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getPathPromise = kubeConfManager.getPath();
|
||||
getPathPromise = kubeConfManager.ensurePath();
|
||||
});
|
||||
|
||||
it("should not call pathExists()", () => {
|
||||
@@ -174,7 +163,7 @@ describe("kubeconfig manager tests", () => {
|
||||
beforeEach(async () => {
|
||||
await writeFileMock.resolveSpecific(
|
||||
[
|
||||
"/some-directory-for-temp/kubeconfig-foo",
|
||||
"/some-directory-for-temp/kubeconfig-foo",
|
||||
"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n",
|
||||
],
|
||||
);
|
||||
@@ -234,7 +223,7 @@ describe("kubeconfig manager tests", () => {
|
||||
let getPathPromise: Promise<string>;
|
||||
|
||||
beforeEach(async () => {
|
||||
getPathPromise = kubeConfManager.getPath();
|
||||
getPathPromise = kubeConfManager.ensurePath();
|
||||
});
|
||||
|
||||
it("should call pathExists", () => {
|
||||
@@ -303,7 +292,7 @@ describe("kubeconfig manager tests", () => {
|
||||
beforeEach(async () => {
|
||||
await writeFileMock.resolveSpecific(
|
||||
[
|
||||
"/some-directory-for-temp/kubeconfig-foo",
|
||||
"/some-directory-for-temp/kubeconfig-foo",
|
||||
"apiVersion: v1\nkind: Config\npreferences: {}\ncurrent-context: minikube\nclusters:\n - name: minikube\n cluster:\n certificate-authority-data: PGNhLWRhdGE+\n server: https://127.0.0.1:9191/foo\n insecure-skip-tls-verify: false\ncontexts:\n - name: minikube\n context:\n cluster: minikube\n user: proxy\nusers:\n - name: proxy\n user:\n username: lens\n password: fake\n",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { observable, ObservableMap } from "mobx";
|
||||
import { observable, ObservableMap, runInAction } from "mobx";
|
||||
import type { CatalogEntity } from "../../../common/catalog";
|
||||
import { loadFromOptions } from "../../../common/kube-helpers";
|
||||
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 readJsonSyncInjectable from "../../../common/fs/read-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", () => {
|
||||
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(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();
|
||||
di.override(getClusterByIdInjectable, () => id => clusters.get(id));
|
||||
|
||||
@@ -79,7 +85,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
const config = loadFromOptions({
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
users: [{
|
||||
@@ -117,7 +123,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
@@ -149,8 +155,10 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const c = (iter.first(rootSource.values())!)[0];
|
||||
|
||||
expect(c.kubeConfigPath).toBe("/bar");
|
||||
expect(c.contextName).toBe("context-name");
|
||||
runInAction(() => {
|
||||
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", () => {
|
||||
@@ -158,7 +166,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
@@ -190,8 +198,8 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
|
||||
const c = rootSource.values().next().value[0] as Cluster;
|
||||
|
||||
expect(c.kubeConfigPath).toBe("/bar");
|
||||
expect(c.contextName).toBe("context-name");
|
||||
expect(c.kubeConfigPath.get()).toBe("/bar");
|
||||
expect(c.contextName.get()).toBe("context-name");
|
||||
|
||||
computeKubeconfigDiff("{}", rootSource, filePath);
|
||||
|
||||
@@ -203,7 +211,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
@@ -243,15 +251,17 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
{
|
||||
const c = rootSource.values().next().value[0] as Cluster;
|
||||
|
||||
expect(c.kubeConfigPath).toBe("/bar");
|
||||
expect(["context-name", "context-name-2"].includes(c.contextName)).toBe(true);
|
||||
runInAction(() => {
|
||||
expect(c.kubeConfigPath.get()).toBe("/bar");
|
||||
expect(["context-name", "context-name-2"].includes(c.contextName.get())).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
const newContents = JSON.stringify({
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
skipTLSVerify: false,
|
||||
}],
|
||||
@@ -283,8 +293,8 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
{
|
||||
const c = rootSource.values().next().value[0] as Cluster;
|
||||
|
||||
expect(c.kubeConfigPath).toBe("/bar");
|
||||
expect(c.contextName).toBe("context-name");
|
||||
expect(c.kubeConfigPath.get()).toBe("/bar");
|
||||
expect(c.contextName.get()).toBe("context-name");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -444,7 +454,7 @@ const foobarConfig = JSON.stringify({
|
||||
clusters: [{
|
||||
name: "cluster-name",
|
||||
cluster: {
|
||||
server: "1.2.3.4",
|
||||
server: "https://1.2.3.4",
|
||||
},
|
||||
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 type { CatalogEntity } from "../../../common/catalog";
|
||||
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 clustersThatAreBeingDeletedInjectable from "../../cluster/are-being-deleted.injectable";
|
||||
import { catalogEntityFromCluster } from "../../cluster/manager";
|
||||
import createClusterInjectable from "../../create-cluster/create-cluster.injectable";
|
||||
import configToModelsInjectable from "./config-to-models.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;
|
||||
|
||||
@@ -24,7 +24,6 @@ const computeKubeconfigDiffInjectable = getInjectable({
|
||||
id: "compute-kubeconfig-diff",
|
||||
instantiate: (di): ComputeKubeconfigDiff => {
|
||||
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
|
||||
const createCluster = di.inject(createClusterInjectable);
|
||||
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
|
||||
const configToModels = di.inject(configToModelsInjectable);
|
||||
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
|
||||
clustersThatAreBeingDeleted.delete(value[0].id);
|
||||
|
||||
value[0].disconnect();
|
||||
const clusterConnection = di.inject(clusterConnectionInjectable, value[0]);
|
||||
|
||||
clusterConnection.disconnect();
|
||||
source.delete(contextName);
|
||||
logger.debug(`Removed old cluster from sync`, { filePath, contextName });
|
||||
continue;
|
||||
@@ -71,9 +72,9 @@ const computeKubeconfigDiffInjectable = getInjectable({
|
||||
// add new clusters to the source
|
||||
try {
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -13,18 +13,18 @@ import requestClusterVersionInjectable from "./request-cluster-version.injectabl
|
||||
const isGKE = (version: string) => version.includes("gke");
|
||||
const isEKS = (version: string) => version.includes("eks");
|
||||
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 isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
|
||||
const isMinikube = (cluster: Cluster) => cluster.contextName.startsWith("minikube");
|
||||
const isMicrok8s = (cluster: Cluster) => cluster.contextName.startsWith("microk8s");
|
||||
const isKind = (cluster: Cluster) => cluster.contextName.startsWith("kubernetes-admin@kind-");
|
||||
const isDockerDesktop = (cluster: Cluster) => cluster.contextName === "docker-desktop";
|
||||
const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.get().endsWith("k8s.ondigitalocean.com");
|
||||
const isMinikube = (cluster: Cluster) => cluster.contextName.get().startsWith("minikube");
|
||||
const isMicrok8s = (cluster: Cluster) => cluster.contextName.get().startsWith("microk8s");
|
||||
const isKind = (cluster: Cluster) => cluster.contextName.get().startsWith("kubernetes-admin@kind-");
|
||||
const isDockerDesktop = (cluster: Cluster) => cluster.contextName.get() === "docker-desktop";
|
||||
const isTke = (version: string) => version.includes("-tke.");
|
||||
const isCustom = (version: string) => version.includes("+");
|
||||
const isVMWare = (version: string) => version.includes("+vmware");
|
||||
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 isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s");
|
||||
const isAlibaba = (version: string) => version.includes("-aliyun");
|
||||
|
||||
@@ -28,7 +28,7 @@ const clusterIdDetectorFactoryInjectable = getInjectable({
|
||||
try {
|
||||
id = await getDefaultNamespaceId(cluster);
|
||||
} catch(_) {
|
||||
id = cluster.apiUrl;
|
||||
id = cluster.apiUrl.get();
|
||||
}
|
||||
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 directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import clusterDistributionDetectorInjectable from "./cluster-distribution-detector.injectable";
|
||||
import clusterIdDetectorFactoryInjectable from "./cluster-id-detector.injectable";
|
||||
@@ -64,9 +63,7 @@ describe("detect-cluster-metadata", () => {
|
||||
|
||||
detectClusterMetadata = di.inject(detectClusterMetadataInjectable);
|
||||
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
cluster = createCluster({
|
||||
cluster = new Cluster({
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface ClusterDetectionResult {
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
export interface FalibleOnlyClusterMetadataDetector {
|
||||
export interface FallibleOnlyClusterMetadataDetector {
|
||||
readonly key: string;
|
||||
detect(cluster: Cluster): Promise<ClusterDetectionResult>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
|
||||
import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable";
|
||||
import clusterConnectionInjectable from "./cluster-connection.injectable";
|
||||
import { ClusterManager } from "./manager";
|
||||
import updateEntityMetadataInjectable from "./update-entity-metadata.injectable";
|
||||
import updateEntitySpecInjectable from "./update-entity-spec.injectable";
|
||||
@@ -23,6 +24,7 @@ const clusterManagerInjectable = getInjectable({
|
||||
logger: di.inject(loggerInjectable),
|
||||
updateEntityMetadata: di.inject(updateEntityMetadataInjectable),
|
||||
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 { UpdateEntityMetadata } from "./update-entity-metadata.injectable";
|
||||
import type { UpdateEntitySpec } from "./update-entity-spec.injectable";
|
||||
import type { ClusterConnection } from "./cluster-connection.injectable";
|
||||
|
||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
|
||||
@@ -28,8 +29,9 @@ interface Dependencies {
|
||||
readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
|
||||
readonly visibleCluster: IObservableValue<ClusterId | null>;
|
||||
readonly logger: Logger;
|
||||
readonly updateEntityMetadata: UpdateEntityMetadata;
|
||||
readonly updateEntitySpec: UpdateEntitySpec;
|
||||
updateEntityMetadata: UpdateEntityMetadata;
|
||||
updateEntitySpec: UpdateEntitySpec;
|
||||
getClusterConnection: (cluster: Cluster) => ClusterConnection;
|
||||
}
|
||||
|
||||
export class ClusterManager {
|
||||
@@ -119,13 +121,13 @@ export class ClusterManager {
|
||||
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"`);
|
||||
|
||||
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"`);
|
||||
|
||||
return LensKubernetesClusterStatus.CONNECTING;
|
||||
@@ -174,8 +176,8 @@ export class ClusterManager {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||
cluster.contextName = entity.spec.kubeconfigContext;
|
||||
cluster.kubeConfigPath.set(entity.spec.kubeconfigPath);
|
||||
cluster.contextName.set(entity.spec.kubeconfigContext);
|
||||
|
||||
if (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.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.online = false;
|
||||
cluster.accessible = false;
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(
|
||||
this.dependencies.store.clustersList
|
||||
.filter(cluster => !cluster.disconnected.get())
|
||||
.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.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(
|
||||
this.dependencies.store.clustersList
|
||||
.filter(cluster => !cluster.disconnected.get())
|
||||
.map((cluster) => (
|
||||
this.dependencies
|
||||
.getClusterConnection(cluster)
|
||||
.refreshConnectionStatus()
|
||||
)),
|
||||
);
|
||||
};
|
||||
|
||||
stop() {
|
||||
this.dependencies.store.clusters.forEach((cluster: Cluster) => {
|
||||
cluster.disconnect();
|
||||
});
|
||||
for (const cluster of this.dependencies.store.clustersList) {
|
||||
this.dependencies
|
||||
.getClusterConnection(cluster)
|
||||
.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,26 +248,26 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
||||
return new KubernetesCluster({
|
||||
metadata: {
|
||||
uid: cluster.id,
|
||||
name: cluster.name,
|
||||
name: cluster.name.get(),
|
||||
source: "local",
|
||||
labels: {
|
||||
...cluster.labels,
|
||||
},
|
||||
distro: cluster.distribution,
|
||||
kubeVersion: cluster.version,
|
||||
distro: cluster.distribution.get(),
|
||||
kubeVersion: cluster.version.get(),
|
||||
},
|
||||
spec: {
|
||||
kubeconfigPath: cluster.kubeConfigPath,
|
||||
kubeconfigContext: cluster.contextName,
|
||||
kubeconfigPath: cluster.kubeConfigPath.get(),
|
||||
kubeconfigContext: cluster.contextName.get(),
|
||||
icon: {},
|
||||
},
|
||||
status: {
|
||||
phase: cluster.disconnected
|
||||
phase: cluster.disconnected.get()
|
||||
? LensKubernetesClusterStatus.DISCONNECTED
|
||||
: LensKubernetesClusterStatus.CONNECTED,
|
||||
reason: "",
|
||||
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 type { AsyncResult } from "../../common/utils/async-result";
|
||||
import { backoffCaller } from "../../common/utils/backoff-caller";
|
||||
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
|
||||
|
||||
export type RequestApiResources = (cluster: Cluster) => Promise<AsyncResult<KubeApiResource[], Error>>;
|
||||
|
||||
@@ -29,6 +30,7 @@ const requestApiResourcesInjectable = getInjectable({
|
||||
|
||||
return async (...args) => {
|
||||
const [cluster] = args;
|
||||
const broadcastConnectionUpdate = di.inject(broadcastConnectionUpdateInjectable, cluster);
|
||||
const requestKubeApiResources = withConcurrencyLimit(5)(requestKubeApiResourcesFor(cluster));
|
||||
|
||||
const groupLists: KubeResourceListGroup[] = [];
|
||||
@@ -36,7 +38,10 @@ const requestApiResourcesInjectable = getInjectable({
|
||||
for (const apiVersionRequester of apiVersionRequesters) {
|
||||
const result = await backoffCaller(() => apiVersionRequester(cluster), {
|
||||
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 });
|
||||
},
|
||||
});
|
||||
@@ -56,7 +61,10 @@ const requestApiResourcesInjectable = getInjectable({
|
||||
|
||||
for (const result of results) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ interface Pre500WorkspaceStoreModel {
|
||||
}[];
|
||||
}
|
||||
|
||||
interface Pre500ClusterModel extends ClusterModel {
|
||||
workspace?: string;
|
||||
}
|
||||
|
||||
const v500Beta10ClusterStoreMigrationInjectable = getInjectable({
|
||||
id: "v5.0.0-beta.10-cluster-store-migration",
|
||||
instantiate: (di) => {
|
||||
@@ -35,7 +39,7 @@ const v500Beta10ClusterStoreMigrationInjectable = getInjectable({
|
||||
workspaces.set(id, name);
|
||||
}
|
||||
|
||||
const clusters = (store.get("clusters") ?? []) as ClusterModel[];
|
||||
const clusters = (store.get("clusters") ?? []) as Pre500ClusterModel[];
|
||||
|
||||
for (const cluster of clusters) {
|
||||
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 { isDefined } from "../../../common/utils";
|
||||
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 {
|
||||
if (left.prometheus && left.prometheusProvider) {
|
||||
@@ -27,24 +36,15 @@ function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: C
|
||||
return {};
|
||||
}
|
||||
|
||||
function mergePreferences(left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences {
|
||||
return {
|
||||
terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
|
||||
clusterName: left.clusterName || right.clusterName || undefined,
|
||||
iconOrder: left.iconOrder || right.iconOrder || undefined,
|
||||
icon: left.icon || right.icon || undefined,
|
||||
httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
|
||||
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
|
||||
...mergePrometheusPreferences(left, right),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeLabels(left: Record<string, string>, right: Record<string, string>): Record<string, string> {
|
||||
return {
|
||||
...right,
|
||||
...left,
|
||||
};
|
||||
}
|
||||
const mergePreferences = (left: ClusterPreferences, right: ClusterPreferences): ClusterPreferences => ({
|
||||
terminalCWD: left.terminalCWD || right.terminalCWD || undefined,
|
||||
clusterName: left.clusterName || right.clusterName || undefined,
|
||||
iconOrder: left.iconOrder || right.iconOrder || undefined,
|
||||
icon: left.icon || right.icon || undefined,
|
||||
httpsProxy: left.httpsProxy || right.httpsProxy || undefined,
|
||||
hiddenMetrics: mergeSet(left.hiddenMetrics ?? [], right.hiddenMetrics ?? []),
|
||||
...mergePrometheusPreferences(left, right),
|
||||
});
|
||||
|
||||
function mergeSet(...iterables: Iterable<string | undefined>[]): string[] {
|
||||
const res = new Set<string>();
|
||||
@@ -60,24 +60,17 @@ function mergeSet(...iterables: Iterable<string | undefined>[]): string[] {
|
||||
return [...res];
|
||||
}
|
||||
|
||||
function mergeClusterModel(prev: ClusterModel, right: Omit<ClusterModel, "id">): ClusterModel {
|
||||
return {
|
||||
id: prev.id,
|
||||
kubeConfigPath: prev.kubeConfigPath,
|
||||
contextName: prev.contextName,
|
||||
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
|
||||
metadata: prev.metadata,
|
||||
labels: mergeLabels(prev.labels ?? {}, right.labels ?? {}),
|
||||
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
|
||||
workspace: prev.workspace || right.workspace,
|
||||
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 mergeClusterModel = (prev: Pre500ClusterModel, right: Omit<Pre500ClusterModel, "id">): Pre500ClusterModel => ({
|
||||
id: prev.id,
|
||||
kubeConfigPath: prev.kubeConfigPath,
|
||||
contextName: prev.contextName,
|
||||
preferences: mergePreferences(prev.preferences ?? {}, right.preferences ?? {}),
|
||||
metadata: prev.metadata,
|
||||
labels: { ...(right.labels ?? {}), ...(prev.labels ?? {}) },
|
||||
accessibleNamespaces: mergeSet(prev.accessibleNamespaces ?? [], right.accessibleNamespaces ?? []),
|
||||
workspace: prev.workspace || right.workspace,
|
||||
workspaces: mergeSet([prev.workspace, right.workspace], prev.workspaces ?? [], right.workspaces ?? []),
|
||||
});
|
||||
|
||||
const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
|
||||
id: "v5.0.0-beta.13-cluster-store-migration",
|
||||
@@ -104,8 +97,8 @@ const v500Beta13ClusterStoreMigrationInjectable = getInjectable({
|
||||
version: "5.0.0-beta.13",
|
||||
run(store) {
|
||||
const folder = joinPaths(userDataPath, "lens-local-storage");
|
||||
const oldClusters = (store.get("clusters") ?? []) as ClusterModel[];
|
||||
const clusters = new Map<string, ClusterModel>();
|
||||
const oldClusters = (store.get("clusters") ?? []) as Pre500ClusterModel[];
|
||||
const clusters = new Map<string, Pre500ClusterModel>();
|
||||
|
||||
for (const { id: oldId, ...cluster } of oldClusters) {
|
||||
const newId = generateNewIdFor(cluster);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { toJS } from "mobx";
|
||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
@@ -17,10 +18,10 @@ const updateEntityMetadataInjectable = getInjectable({
|
||||
return (entity, cluster) => {
|
||||
entity.metadata.labels = {
|
||||
...entity.metadata.labels,
|
||||
...cluster.labels,
|
||||
...toJS(cluster.labels),
|
||||
};
|
||||
entity.metadata.distro = cluster.distribution;
|
||||
entity.metadata.kubeVersion = cluster.version;
|
||||
entity.metadata.distro = cluster.distribution.get();
|
||||
entity.metadata.kubeVersion = cluster.version.get();
|
||||
|
||||
enumKeys(ClusterMetadataKey).forEach((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 { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { ClusterMetadataKey } from "../../common/cluster-types";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import { replaceObservableObject } from "../../common/utils/replace-observable-object";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { UpdateEntityMetadata } from "./update-entity-metadata.injectable";
|
||||
import updateEntityMetadataInjectable from "./update-entity-metadata.injectable";
|
||||
@@ -27,11 +27,10 @@ describe("update-entity-metadata", () => {
|
||||
get: () => ({} as AppPaths),
|
||||
set: () => {},
|
||||
}));
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
updateEntityMetadata = di.inject(updateEntityMetadataInjectable);
|
||||
|
||||
cluster = createCluster({
|
||||
cluster = new Cluster({
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
@@ -50,10 +49,6 @@ describe("update-entity-metadata", () => {
|
||||
},
|
||||
};
|
||||
|
||||
cluster.metadata = {
|
||||
...cluster.metadata,
|
||||
};
|
||||
|
||||
entity = new KubernetesCluster({
|
||||
metadata: {
|
||||
uid: "some-uid",
|
||||
@@ -125,9 +120,9 @@ describe("update-entity-metadata", () => {
|
||||
});
|
||||
|
||||
it("given cluster has labels, updates entity metadata with labels", () => {
|
||||
cluster.labels = {
|
||||
replaceObservableObject(cluster.labels, {
|
||||
"some-label": "some-value",
|
||||
};
|
||||
});
|
||||
entity.metadata.labels = {
|
||||
"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", () => {
|
||||
cluster.labels = {
|
||||
replaceObservableObject(cluster.labels, {
|
||||
"some-label": "some-cluster-value",
|
||||
};
|
||||
});
|
||||
entity.metadata.labels = {
|
||||
"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 directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
import { Cluster } from "../../common/cluster/cluster";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { UpdateEntitySpec } from "./update-entity-spec.injectable";
|
||||
import updateEntitySpecInjectable from "./update-entity-spec.injectable";
|
||||
@@ -25,11 +24,10 @@ describe("update-entity-spec", () => {
|
||||
get: () => ({} as AppPaths),
|
||||
set: () => {},
|
||||
}));
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
updateEntitySpec = di.inject(updateEntitySpecInjectable);
|
||||
|
||||
cluster = createCluster({
|
||||
cluster = new Cluster({
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
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;
|
||||
+2
@@ -11,6 +11,7 @@ import applicationMenuItemCompositeInjectable from "../../../../features/applica
|
||||
import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable";
|
||||
import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable";
|
||||
import pushCatalogToRendererInjectable from "../../../catalog-sync-to-renderer/push-catalog-to-renderer.injectable";
|
||||
import clusterConnectionInjectable from "../../../cluster/cluster-connection.injectable";
|
||||
|
||||
const setupIpcMainHandlersInjectable = getInjectable({
|
||||
id: "setup-ipc-main-handlers",
|
||||
@@ -34,6 +35,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
|
||||
clusterStore,
|
||||
emitAppEvent,
|
||||
getClusterById,
|
||||
getClusterConnection: (cluster) => di.inject(clusterConnectionInjectable, cluster),
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+21
-6
@@ -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 { EmitAppEvent } from "../../../../common/app-event-bus/emit-event.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 {
|
||||
applicationMenuItemComposite: IComputedValue<Composite<ApplicationMenuItemTypes | MenuItemRoot>>;
|
||||
clusterStore: ClusterStore;
|
||||
emitAppEvent: EmitAppEvent;
|
||||
getClusterById: GetClusterById;
|
||||
pushCatalogToRenderer: () => void;
|
||||
getClusterConnection: (cluster: Cluster) => ClusterConnection;
|
||||
}
|
||||
|
||||
export const setupIpcMainHandlers = ({
|
||||
@@ -32,10 +35,18 @@ export const setupIpcMainHandlers = ({
|
||||
emitAppEvent,
|
||||
getClusterById,
|
||||
pushCatalogToRenderer,
|
||||
getClusterConnection,
|
||||
}: Dependencies) => {
|
||||
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||
return getClusterById(clusterId)
|
||||
?.activate(force);
|
||||
ipcMainHandle(clusterActivateHandler, async (event, clusterId: ClusterId, force = false) => {
|
||||
const cluster = getClusterById(clusterId);
|
||||
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterConnection = getClusterConnection(cluster);
|
||||
|
||||
await clusterConnection.activate(force);
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
|
||||
@@ -51,10 +62,14 @@ export const setupIpcMainHandlers = ({
|
||||
emitAppEvent({ name: "cluster", action: "stop" });
|
||||
const cluster = getClusterById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
cluster.disconnect();
|
||||
clusterFrameMap.delete(cluster.id);
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterConnection = getClusterConnection(cluster);
|
||||
|
||||
clusterConnection.disconnect();
|
||||
clusterFrameMap.delete(cluster.id);
|
||||
});
|
||||
|
||||
ipcMainHandle(windowActionHandleChannel, (event, action) => handleWindowAction(action));
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
import type { DeleteHelmReleaseData } from "../delete-helm-release.injectable";
|
||||
import deleteHelmReleaseInjectable from "../delete-helm-release.injectable";
|
||||
|
||||
@@ -16,11 +17,12 @@ const deleteClusterHelmReleaseInjectable = getInjectable({
|
||||
const deleteHelmRelease = di.inject(deleteHelmReleaseInjectable);
|
||||
|
||||
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);
|
||||
|
||||
return deleteHelmRelease(proxyKubeconfig, data);
|
||||
return deleteHelmRelease(proxyKubeconfigPath, data);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
import type { GetHelmReleaseHistoryData } 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);
|
||||
|
||||
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);
|
||||
|
||||
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 { GetHelmReleaseValuesData } 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({
|
||||
id: "get-cluster-helm-release-values",
|
||||
@@ -16,11 +17,12 @@ const getClusterHelmReleaseValuesInjectable = getInjectable({
|
||||
const getHelmReleaseValues = di.inject(getHelmReleaseValuesInjectable);
|
||||
|
||||
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);
|
||||
|
||||
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 loggerInjectable from "../../../common/logger.injectable";
|
||||
import { isObject, json } from "../../../common/utils";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
import execHelmInjectable from "../exec-helm/exec-helm.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);
|
||||
|
||||
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");
|
||||
|
||||
@@ -28,7 +30,7 @@ const getHelmReleaseInjectable = getInjectable({
|
||||
"--namespace",
|
||||
namespace,
|
||||
"--kubeconfig",
|
||||
kubeconfigPath,
|
||||
proxyKubeconfigPath,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
@@ -48,7 +50,7 @@ const getHelmReleaseInjectable = getInjectable({
|
||||
const resourcesResult = await getHelmReleaseResources(
|
||||
releaseName,
|
||||
namespace,
|
||||
kubeconfigPath,
|
||||
proxyKubeconfigPath,
|
||||
);
|
||||
|
||||
if (!resourcesResult.callWasSuccessful) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { JsonObject } from "type-fest";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
import installHelmChartInjectable from "../install-helm-chart.injectable";
|
||||
|
||||
export interface InstallChartArgs {
|
||||
@@ -22,11 +23,12 @@ const installClusterHelmChartInjectable = getInjectable({
|
||||
const installHelmChart = di.inject(installHelmChartInjectable);
|
||||
|
||||
return async (cluster: Cluster, data: InstallChartArgs) => {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
|
||||
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
|
||||
|
||||
return installHelmChart({
|
||||
...data,
|
||||
kubeconfigPath: proxyKubeconfig,
|
||||
kubeconfigPath: proxyKubeconfigPath,
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
import listHelmReleasesInjectable from "../list-helm-releases.injectable";
|
||||
|
||||
const listClusterHelmReleasesInjectable = getInjectable({
|
||||
@@ -15,11 +16,12 @@ const listClusterHelmReleasesInjectable = getInjectable({
|
||||
const listHelmReleases = di.inject(listHelmReleasesInjectable);
|
||||
|
||||
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 });
|
||||
|
||||
return listHelmReleases(proxyKubeconfig, namespace);
|
||||
return listHelmReleases(proxyKubeconfigPath, namespace);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../../common/cluster/cluster";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
import type { RollbackHelmReleaseData } from "../rollback-helm-release.injectable";
|
||||
import rollbackHelmReleaseInjectable from "../rollback-helm-release.injectable";
|
||||
|
||||
@@ -16,11 +17,12 @@ const rollbackClusterHelmReleaseInjectable = getInjectable({
|
||||
const rollbackHelmRelease = di.inject(rollbackHelmReleaseInjectable);
|
||||
|
||||
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);
|
||||
|
||||
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 removePathInjectable from "../../../common/fs/remove.injectable";
|
||||
import execHelmInjectable from "../exec-helm/exec-helm.injectable";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
|
||||
export interface UpdateChartArgs {
|
||||
chart: string;
|
||||
@@ -28,7 +29,8 @@ const updateHelmReleaseInjectable = getInjectable({
|
||||
const execHelm = di.inject(execHelmInjectable);
|
||||
|
||||
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" });
|
||||
|
||||
logger.debug(`[HELM]: upgrading "${releaseName}" in "${namespace}" to ${data.version}`);
|
||||
@@ -43,7 +45,7 @@ const updateHelmReleaseInjectable = getInjectable({
|
||||
"--version", data.version,
|
||||
"--values", valuesFilePath,
|
||||
"--namespace", namespace,
|
||||
"--kubeconfig", proxyKubeconfig,
|
||||
"--kubeconfig", proxyKubeconfigPath,
|
||||
]);
|
||||
|
||||
if (result.callWasSuccessful === false) {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import * as uuid from "uuid";
|
||||
import type { ClusterStoreModel } from "../../../common/cluster-store/cluster-store";
|
||||
import type { Hotbar, HotbarItem } 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";
|
||||
@@ -17,6 +16,7 @@ import { hotbarStoreMigrationInjectionToken } from "../../../common/hotbars/migr
|
||||
import readJsonSyncInjectable from "../../../common/fs/read-json-sync.injectable";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import { generateNewIdFor } from "../../../common/utils/generate-new-id-for";
|
||||
import type { ClusterModel } from "../../../common/cluster-types";
|
||||
|
||||
interface Pre500WorkspaceStoreModel {
|
||||
workspaces: {
|
||||
@@ -31,6 +31,15 @@ interface PartialHotbar {
|
||||
items: (null | HotbarItem)[];
|
||||
}
|
||||
|
||||
interface Pre500ClusterModel extends ClusterModel {
|
||||
workspace?: string;
|
||||
workspaces?: string[];
|
||||
}
|
||||
|
||||
interface Pre500ClusterStoreModel {
|
||||
clusters?: Pre500ClusterModel[];
|
||||
}
|
||||
|
||||
const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
|
||||
id: "v5.0.0-beta.10-hotbar-store-migration",
|
||||
instantiate: (di) => {
|
||||
@@ -59,7 +68,7 @@ const v500Beta10HotbarStoreMigrationInjectable = getInjectable({
|
||||
|
||||
try {
|
||||
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
|
||||
|
||||
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 getPortFromStreamInjectable from "../utils/get-port-from-stream.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({
|
||||
id: "create-kube-auth-proxy",
|
||||
|
||||
instantiate: (di): CreateKubeAuthProxy => {
|
||||
const dependencies: Omit<KubeAuthProxyDependencies, "proxyCert"> = {
|
||||
const dependencies: Omit<KubeAuthProxyDependencies, "proxyCert" | "broadcastConnectionUpdate"> = {
|
||||
proxyBinPath: di.inject(lensK8sProxyPathInjectable),
|
||||
spawn: di.inject(spawnInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
@@ -29,13 +30,14 @@ const createKubeAuthProxyInjectable = getInjectable({
|
||||
dirname: di.inject(getDirnameOfPathInjectable),
|
||||
};
|
||||
|
||||
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
|
||||
const clusterUrl = new URL(cluster.apiUrl);
|
||||
return (cluster, env) => {
|
||||
const clusterUrl = new URL(cluster.apiUrl.get());
|
||||
|
||||
return new KubeAuthProxy({
|
||||
...dependencies,
|
||||
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 type { Cluster } from "../../common/cluster/cluster";
|
||||
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 assert from "assert";
|
||||
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 { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.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 startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
|
||||
@@ -29,6 +30,7 @@ export interface KubeAuthProxyDependencies {
|
||||
waitUntilPortIsUsed: WaitUntilPortIsUsed;
|
||||
getPortFromStream: GetPortFromStream;
|
||||
dirname: GetDirnameOfPath;
|
||||
broadcastConnectionUpdate: BroadcastConnectionUpdate;
|
||||
}
|
||||
|
||||
export class KubeAuthProxy {
|
||||
@@ -44,19 +46,17 @@ export class KubeAuthProxy {
|
||||
|
||||
protected _port?: number;
|
||||
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) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get whenReady() {
|
||||
return when(() => this.ready);
|
||||
}
|
||||
constructor(
|
||||
private readonly dependencies: KubeAuthProxyDependencies,
|
||||
protected readonly cluster: Cluster,
|
||||
protected readonly env: NodeJS.ProcessEnv,
|
||||
) {}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.proxyProcess) {
|
||||
return this.whenReady;
|
||||
return when(() => this.ready.get());
|
||||
}
|
||||
|
||||
const proxyBin = this.dependencies.proxyBinPath;
|
||||
@@ -65,26 +65,42 @@ export class KubeAuthProxy {
|
||||
this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
|
||||
env: {
|
||||
...this.env,
|
||||
KUBECONFIG: this.cluster.kubeConfigPath,
|
||||
KUBECONFIG_CONTEXT: this.cluster.contextName,
|
||||
KUBECONFIG: this.cluster.kubeConfigPath.get(),
|
||||
KUBECONFIG_CONTEXT: this.cluster.contextName.get(),
|
||||
API_PREFIX: this.apiPrefix,
|
||||
PROXY_KEY: cert.private,
|
||||
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.cluster.broadcastConnectUpdate(error.message, "error");
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: error.message,
|
||||
});
|
||||
this.exit();
|
||||
});
|
||||
|
||||
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.proxyProcess.on("disconnect", () => {
|
||||
this.cluster.broadcastConnectUpdate("Proxy disconnected communications", "error");
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: "Proxy disconnected communications",
|
||||
});
|
||||
this.exit();
|
||||
});
|
||||
|
||||
@@ -96,28 +112,40 @@ export class KubeAuthProxy {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cluster.broadcastConnectUpdate(data.toString(), "error");
|
||||
this.dependencies.broadcastConnectionUpdate({
|
||||
level: "error",
|
||||
message: data.toString(),
|
||||
});
|
||||
});
|
||||
|
||||
this.proxyProcess.stdout.on("data", (data: Buffer) => {
|
||||
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, {
|
||||
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}`);
|
||||
|
||||
try {
|
||||
await this.dependencies.waitUntilPortIsUsed(this.port, 500, 10000);
|
||||
this.ready = true;
|
||||
this.ready.set(true);
|
||||
} catch (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();
|
||||
|
||||
return this.run();
|
||||
@@ -125,7 +153,7 @@ export class KubeAuthProxy {
|
||||
}
|
||||
|
||||
public exit() {
|
||||
this.ready = false;
|
||||
this.ready.set(false);
|
||||
|
||||
if (this.proxyProcess) {
|
||||
this.dependencies.logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
|
||||
|
||||
+18
-19
@@ -2,44 +2,43 @@
|
||||
* 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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
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 loggerInjectable from "../../common/logger.injectable";
|
||||
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||
import writeFileInjectable from "../../common/fs/write-file.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove.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 {
|
||||
cluster: Cluster;
|
||||
}
|
||||
const kubeconfigManagerInjectable = getInjectable({
|
||||
id: "kubeconfig-manager",
|
||||
|
||||
export type CreateKubeconfigManager = (cluster: Cluster) => KubeconfigManager;
|
||||
|
||||
const createKubeconfigManagerInjectable = getInjectable({
|
||||
id: "create-kubeconfig-manager",
|
||||
|
||||
instantiate: (di): CreateKubeconfigManager => {
|
||||
const dependencies: KubeconfigManagerDependencies = {
|
||||
instantiate: (di, cluster) => new KubeconfigManager(
|
||||
{
|
||||
directoryForTemp: di.inject(directoryForTempInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
lensProxyPort: di.inject(lensProxyPortInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||
removePath: di.inject(removePathInjectable),
|
||||
pathExists: di.inject(pathExistsInjectable),
|
||||
writeFile: di.inject(writeFileInjectable),
|
||||
certificate: di.inject(lensProxyCertificateInjectable).get(),
|
||||
};
|
||||
|
||||
return (cluster) => new KubeconfigManager(dependencies, cluster);
|
||||
},
|
||||
loadKubeconfig: di.inject(loadKubeconfigInjectable, cluster),
|
||||
kubeAuthProxyServer: di.inject(kubeAuthProxyServerInjectable, 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 { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||
import { dumpConfigYaml } from "../../common/kube-helpers";
|
||||
import { isErrnoException } from "../../common/utils";
|
||||
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 { WriteFile } from "../../common/fs/write-file.injectable";
|
||||
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 logger: Logger;
|
||||
readonly lensProxyPort: { get: () => number };
|
||||
readonly certificate: SelfSignedCert;
|
||||
readonly kubeAuthProxyServer: KubeAuthProxyServer;
|
||||
readonly kubeAuthProxyUrl: string;
|
||||
joinPaths: JoinPaths;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
pathExists: PathExists;
|
||||
removePath: RemovePath;
|
||||
writeFile: WriteFile;
|
||||
certificate: SelfSignedCert;
|
||||
loadKubeconfig: LoadKubeconfig;
|
||||
}
|
||||
|
||||
export class KubeconfigManager {
|
||||
@@ -38,17 +41,16 @@ export class KubeconfigManager {
|
||||
*/
|
||||
protected tempFilePath: string | null = null;
|
||||
|
||||
protected readonly contextHandler: ClusterContextHandler;
|
||||
|
||||
constructor(private readonly dependencies: KubeconfigManagerDependencies, protected cluster: Cluster) {
|
||||
this.contextHandler = cluster.contextHandler;
|
||||
}
|
||||
constructor(
|
||||
private readonly dependencies: KubeconfigManagerDependencies,
|
||||
private readonly cluster: Cluster,
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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))) {
|
||||
return await this.ensureFile();
|
||||
}
|
||||
@@ -79,7 +81,7 @@ export class KubeconfigManager {
|
||||
|
||||
protected async ensureFile() {
|
||||
try {
|
||||
await this.contextHandler.ensureServer();
|
||||
await this.dependencies.kubeAuthProxyServer.ensureRunning();
|
||||
|
||||
return this.tempFilePath = await this.createProxyKubeconfig();
|
||||
} 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.
|
||||
* This way any user of the config does not need to know anything about the auth etc. details.
|
||||
*/
|
||||
protected async createProxyKubeconfig(): Promise<string> {
|
||||
const { cluster } = this;
|
||||
const { contextName, id } = cluster;
|
||||
const { id, preferences: { defaultNamespace }} = this.cluster;
|
||||
const contextName = this.cluster.contextName.get();
|
||||
const tempFile = this.dependencies.joinPaths(
|
||||
this.dependencies.directoryForTemp,
|
||||
`kubeconfig-${id}`,
|
||||
);
|
||||
const kubeConfig = await cluster.getKubeconfig();
|
||||
const { certificate } = this.dependencies;
|
||||
const kubeConfig = await this.dependencies.loadKubeconfig();
|
||||
const proxyConfig: PartialDeep<KubeConfig> = {
|
||||
currentContext: contextName,
|
||||
clusters: [
|
||||
{
|
||||
name: contextName,
|
||||
server: this.resolveProxyUrl,
|
||||
server: this.dependencies.kubeAuthProxyUrl,
|
||||
skipTLSVerify: false,
|
||||
caData: Buffer.from(certificate.cert).toString("base64"),
|
||||
caData: Buffer.from(this.dependencies.certificate.cert).toString("base64"),
|
||||
},
|
||||
],
|
||||
users: [
|
||||
@@ -122,7 +119,7 @@ export class KubeconfigManager {
|
||||
user: "proxy",
|
||||
name: 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 getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
|
||||
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";
|
||||
|
||||
const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjectable({
|
||||
@@ -13,7 +13,6 @@ const kubectlApplyAllChannelHandlerInjectable = getRequestChannelListenerInjecta
|
||||
handler: (di) => {
|
||||
const getClusterById = di.inject(getClusterByIdInjectable);
|
||||
const emitAppEvent = di.inject(emitAppEventInjectable);
|
||||
const createResourceApplier = di.inject(createResourceApplierInjectable);
|
||||
|
||||
return async ({
|
||||
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 loggerInjectable from "../../common/logger.injectable";
|
||||
|
||||
export type CreateKubectl = (version: string) => Kubectl;
|
||||
|
||||
const createKubectlInjectable = getInjectable({
|
||||
id: "create-kubectl",
|
||||
|
||||
instantiate: (di) => {
|
||||
instantiate: (di): CreateKubectl => {
|
||||
const dependencies: KubectlDependencies = {
|
||||
userStore: di.inject(userStoreInjectable),
|
||||
directoryForKubectlBinaries: di.inject(directoryForKubectlBinariesInjectable),
|
||||
@@ -39,7 +41,7 @@ const createKubectlInjectable = getInjectable({
|
||||
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 getClusterByIdInjectable from "../../common/cluster-store/get-by-id.injectable";
|
||||
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";
|
||||
|
||||
const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInjectable({
|
||||
@@ -13,7 +13,6 @@ const kubectlDeleteAllChannelHandlerInjectable = getRequestChannelListenerInject
|
||||
handler: (di) => {
|
||||
const emitAppEvent = di.inject(emitAppEventInjectable);
|
||||
const getClusterById = di.inject(getClusterByIdInjectable);
|
||||
const createResourceApplier = di.inject(createResourceApplierInjectable);
|
||||
|
||||
return async ({
|
||||
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 { LensProxy } from "./lens-proxy";
|
||||
import { kubeApiUpgradeRequest } from "./proxy-functions";
|
||||
import routerInjectable from "../router/router.injectable";
|
||||
import httpProxy from "http-proxy";
|
||||
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 lensProxyCertificateInjectable from "../../common/certificate/lens-proxy-certificate.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({
|
||||
id: "lens-proxy",
|
||||
@@ -21,7 +22,7 @@ const lensProxyInjectable = getInjectable({
|
||||
instantiate: (di) => new LensProxy({
|
||||
router: di.inject(routerInjectable),
|
||||
proxy: httpProxy.createProxy(),
|
||||
kubeApiUpgradeRequest,
|
||||
kubeApiUpgradeRequest: di.inject(kubeApiUpgradeRequestInjectable),
|
||||
shellApiRequest: di.inject(shellApiRequestInjectable),
|
||||
getClusterForRequest: di.inject(getClusterForRequestInjectable),
|
||||
lensProxyPort: di.inject(lensProxyPortInjectable),
|
||||
@@ -29,6 +30,7 @@ const lensProxyInjectable = getInjectable({
|
||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
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 { apiPrefix, apiKubePrefix } from "../../common/vars";
|
||||
import type { Router } from "../router/router";
|
||||
import type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
||||
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 { Logger } from "../../common/logger";
|
||||
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 ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
|
||||
@@ -28,6 +28,7 @@ interface Dependencies {
|
||||
shellApiRequest: LensProxyApiRequest;
|
||||
kubeApiUpgradeRequest: LensProxyApiRequest;
|
||||
emitAppEvent: EmitAppEvent;
|
||||
getKubeAuthProxyServer: (cluster: Cluster) => KubeAuthProxyServer;
|
||||
readonly router: Router;
|
||||
readonly proxy: httpProxy;
|
||||
readonly lensProxyPort: { set: (portNumber: number) => void };
|
||||
@@ -220,15 +221,6 @@ export class LensProxy {
|
||||
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 {
|
||||
assert(req.headers.host);
|
||||
|
||||
@@ -238,8 +230,12 @@ export class LensProxy {
|
||||
protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) {
|
||||
const cluster = this.dependencies.getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||
if (cluster && req.url.startsWith(apiKubePrefix)) {
|
||||
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) {
|
||||
return this.dependencies.proxy.web(req, res, proxyTarget);
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
export * from "./kube-api-upgrade-request";
|
||||
export * from "./types";
|
||||
+78
@@ -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.
|
||||
* 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 type { Cluster } from "../../common/cluster/cluster";
|
||||
import removePathInjectable from "../../common/fs/remove.injectable";
|
||||
import execFileInjectable from "../../common/fs/exec-file.injectable";
|
||||
import writeFileInjectable from "../../common/fs/write-file.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import type { ResourceApplierDependencies } 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 createResourceApplierInjectable = getInjectable({
|
||||
id: "create-resource-applier",
|
||||
instantiate: (di): CreateResourceApplier => {
|
||||
const deps: ResourceApplierDependencies = {
|
||||
const resourceApplierInjectable = getInjectable({
|
||||
id: "resource-applier",
|
||||
instantiate: (di, cluster) => new ResourceApplier(
|
||||
{
|
||||
deleteFile: di.inject(removePathInjectable),
|
||||
emitAppEvent: di.inject(emitAppEventInjectable),
|
||||
execFile: di.inject(execFileInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
writeFile: di.inject(writeFileInjectable),
|
||||
};
|
||||
|
||||
return (cluster) => new ResourceApplier(deps, cluster);
|
||||
},
|
||||
createKubectl: di.inject(createKubectlInjectable),
|
||||
proxyKubeconfigManager: di.inject(kubeconfigManagerInjectable, 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 { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
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 {
|
||||
emitAppEvent: EmitAppEvent;
|
||||
@@ -22,11 +24,24 @@ export interface ResourceApplierDependencies {
|
||||
deleteFile: RemovePath;
|
||||
execFile: ExecFile;
|
||||
joinPaths: JoinPaths;
|
||||
createKubectl: CreateKubectl;
|
||||
readonly proxyKubeconfigManager: KubeconfigManager;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -38,9 +53,8 @@ export class ResourceApplier {
|
||||
async patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string> {
|
||||
this.dependencies.emitAppEvent({ name: "resource", action: "patch" });
|
||||
|
||||
const kubectl = await this.cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
const kubectlPath = await this.getKubectlPath();
|
||||
const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath();
|
||||
const args = [
|
||||
"--kubeconfig", proxyKubeconfigPath,
|
||||
"patch",
|
||||
@@ -74,9 +88,8 @@ export class ResourceApplier {
|
||||
}
|
||||
|
||||
protected async kubectlApply(content: string): Promise<AsyncResult<string, string>> {
|
||||
const kubectl = await this.cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
const kubectlPath = await this.getKubectlPath();
|
||||
const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath();
|
||||
const fileName = tempy.file({ name: "resource.yaml" });
|
||||
const args = [
|
||||
"apply",
|
||||
@@ -121,9 +134,8 @@ export class ResourceApplier {
|
||||
}
|
||||
|
||||
protected async kubectlCmdAll(subCmd: string, resources: string[], parentArgs: string[] = []): Promise<AsyncResult<string, string>> {
|
||||
const kubectl = await this.cluster.ensureKubectl();
|
||||
const kubectlPath = await kubectl.getPath();
|
||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||
const kubectlPath = await this.getKubectlPath();
|
||||
const proxyKubeconfigPath = await this.dependencies.proxyKubeconfigManager.ensurePath();
|
||||
const tmpDir = tempy.directory();
|
||||
|
||||
await Promise.all(resources.map((resource, index) => this.dependencies.writeFile(
|
||||
|
||||
+10
-7
@@ -10,15 +10,18 @@ import type { V1Secret } from "@kubernetes/client-node";
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
import { dump } from "js-yaml";
|
||||
import loadProxyKubeconfigInjectable from "../../cluster/load-proxy-kubeconfig.injectable";
|
||||
|
||||
const getServiceAccountRouteInjectable = getRouteInjectable({
|
||||
id: "get-service-account-route",
|
||||
|
||||
instantiate: () => clusterRoute({
|
||||
instantiate: (di) => clusterRoute({
|
||||
method: "get",
|
||||
path: `${apiPrefix}/kubeconfig/service-account/{namespace}/{account}`,
|
||||
})(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 secret = secretList.body.items.find(secret => {
|
||||
@@ -64,9 +67,9 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
|
||||
"kind": "Config",
|
||||
"clusters": [
|
||||
{
|
||||
"name": cluster.contextName,
|
||||
"name": cluster.contextName.get(),
|
||||
"cluster": {
|
||||
"server": cluster.apiUrl,
|
||||
"server": cluster.apiUrl.get(),
|
||||
"certificate-authority-data": caCrt,
|
||||
},
|
||||
},
|
||||
@@ -81,14 +84,14 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster
|
||||
],
|
||||
"contexts": [
|
||||
{
|
||||
"name": cluster.contextName,
|
||||
"name": cluster.contextName.get(),
|
||||
"context": {
|
||||
"user": username,
|
||||
"cluster": cluster.contextName,
|
||||
"cluster": cluster.contextName.get(),
|
||||
"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 getMetricsInjectable from "../../get-metrics.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.
|
||||
const ATTEMPTS = [false, false, false, false, true];
|
||||
@@ -66,9 +68,10 @@ const addMetricsRouteInjectable = getRouteInjectable({
|
||||
})(async ({ cluster, payload, query }) => {
|
||||
const queryParams: Partial<Record<string, string>> = Object.fromEntries(query.entries());
|
||||
const prometheusMetadata: ClusterPrometheusMetadata = {};
|
||||
const prometheusHandler = di.inject(prometheusHandlerInjectable, cluster);
|
||||
|
||||
try {
|
||||
const { prometheusPath, provider } = await cluster.contextHandler.getPrometheusDetails();
|
||||
const { prometheusPath, provider } = await prometheusHandler.getPrometheusDetails();
|
||||
|
||||
prometheusMetadata.provider = provider?.kind;
|
||||
prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type;
|
||||
@@ -115,7 +118,9 @@ const addMetricsRouteInjectable = getRouteInjectable({
|
||||
|
||||
return { response: {}};
|
||||
} 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 { clusterRoute } from "../../router/route";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
|
||||
const startPortForwardRouteInjectable = getRouteInjectable({
|
||||
id: "start-current-port-forward-route",
|
||||
@@ -24,6 +25,8 @@ const startPortForwardRouteInjectable = getRouteInjectable({
|
||||
const port = Number(query.get("port"));
|
||||
const forwardPort = Number(query.get("forwardPort"));
|
||||
|
||||
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
|
||||
|
||||
try {
|
||||
let portForward = PortForward.getPortforward({
|
||||
clusterId: cluster.id,
|
||||
@@ -42,8 +45,9 @@ const startPortForwardRouteInjectable = getRouteInjectable({
|
||||
const thePort = 0 < forwardPort && forwardPort < 65536
|
||||
? forwardPort
|
||||
: 0;
|
||||
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
|
||||
|
||||
portForward = createPortForward(await cluster.getProxyKubeconfigPath(), {
|
||||
portForward = createPortForward(proxyKubeconfigPath, {
|
||||
clusterId: cluster.id,
|
||||
kind: resourceType,
|
||||
namespace,
|
||||
|
||||
+11
-11
@@ -6,22 +6,22 @@ import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import { payloadValidatedClusterRoute } from "../../router/route";
|
||||
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({
|
||||
id: "create-resource-route",
|
||||
|
||||
instantiate: (di) => {
|
||||
const createResourceApplier = di.inject(createResourceApplierInjectable);
|
||||
instantiate: (di) => payloadValidatedClusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/stack`,
|
||||
payloadValidator: Joi.string(),
|
||||
})(async ({ cluster, payload }) => {
|
||||
const resourceApplier = di.inject(resourceApplierInjectable, cluster);
|
||||
|
||||
return payloadValidatedClusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/stack`,
|
||||
payloadValidator: Joi.string(),
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await createResourceApplier(cluster).create(payload),
|
||||
}));
|
||||
},
|
||||
return ({
|
||||
response: await resourceApplier.create(payload),
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
export default createResourceRouteInjectable;
|
||||
@@ -7,7 +7,7 @@ import { apiPrefix } from "../../../common/vars";
|
||||
import { payloadValidatedClusterRoute } from "../../router/route";
|
||||
import Joi from "joi";
|
||||
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 {
|
||||
name: string;
|
||||
@@ -40,22 +40,22 @@ const patchResourcePayloadValidator = Joi.object<PatchResourcePayload, true, Pat
|
||||
const patchResourceRouteInjectable = getRouteInjectable({
|
||||
id: "patch-resource-route",
|
||||
|
||||
instantiate: (di) => {
|
||||
const createResourceApplier = di.inject(createResourceApplierInjectable);
|
||||
instantiate: (di) => payloadValidatedClusterRoute({
|
||||
method: "patch",
|
||||
path: `${apiPrefix}/stack`,
|
||||
payloadValidator: patchResourcePayloadValidator,
|
||||
})(async ({ cluster, payload }) => {
|
||||
const resourceApplier = di.inject(resourceApplierInjectable, cluster);
|
||||
|
||||
return payloadValidatedClusterRoute({
|
||||
method: "patch",
|
||||
path: `${apiPrefix}/stack`,
|
||||
payloadValidator: patchResourcePayloadValidator,
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await createResourceApplier(cluster).patch(
|
||||
return ({
|
||||
response: await resourceApplier.patch(
|
||||
payload.name,
|
||||
payload.kind,
|
||||
payload.patch,
|
||||
payload.ns,
|
||||
),
|
||||
}));
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
export default patchResourceRouteInjectable;
|
||||
@@ -52,16 +52,16 @@ export class LocalShellSession extends ShellSession {
|
||||
protected async getShellArgs(shell: string): Promise<string[]> {
|
||||
const pathFromPreferences = this.dependencies.userStore.kubectlBinariesPath || this.kubectl.getBundledPath();
|
||||
const kubectlPathDir = this.dependencies.userStore.downloadKubectlBinaries
|
||||
? await this.kubectlBinDirP
|
||||
? this.dependencies.directoryContainingKubectl
|
||||
: this.dependencies.getDirnameOfPath(pathFromPreferences);
|
||||
|
||||
switch(this.dependencies.getBasenameOfPath(shell)) {
|
||||
case "powershell.exe":
|
||||
return ["-NoExit", "-command", `& {$Env:PATH="${kubectlPathDir};${this.dependencies.directoryForBinaries};$Env:PATH"}`];
|
||||
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":
|
||||
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":
|
||||
return ["--login"];
|
||||
default:
|
||||
|
||||
@@ -24,6 +24,7 @@ import appNameInjectable from "../../../common/vars/app-name.injectable";
|
||||
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
|
||||
import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable";
|
||||
import statInjectable from "../../../common/fs/stat.injectable";
|
||||
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||
|
||||
export interface OpenLocalShellSessionArgs {
|
||||
websocket: WebSocket;
|
||||
@@ -38,7 +39,7 @@ const openLocalShellSessionInjectable = getInjectable({
|
||||
|
||||
instantiate: (di): OpenLocalShellSession => {
|
||||
const createKubectl = di.inject(createKubectlInjectable);
|
||||
const dependencies: LocalShellSessionDependencies = {
|
||||
const dependencies: Omit<LocalShellSessionDependencies, "proxyKubeconfigPath" | "directoryContainingKubectl"> = {
|
||||
directoryForBinaries: di.inject(directoryForBinariesInjectable),
|
||||
isMac: di.inject(isMacInjectable),
|
||||
isWindows: di.inject(isWindowsInjectable),
|
||||
@@ -57,9 +58,17 @@ const openLocalShellSessionInjectable = getInjectable({
|
||||
stat: di.inject(statInjectable),
|
||||
};
|
||||
|
||||
return (args) => {
|
||||
const kubectl = createKubectl(args.cluster.version);
|
||||
const session = new LocalShellSession(dependencies, { kubectl, ...args });
|
||||
return async (args) => {
|
||||
const kubectl = createKubectl(args.cluster.version.get());
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
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 type { Cluster } from "../../../common/cluster/cluster";
|
||||
import { Cluster } from "../../../common/cluster/cluster";
|
||||
import pathExistsSyncInjectable from "../../../common/fs/path-exists-sync.injectable";
|
||||
import pathExistsInjectable from "../../../common/fs/path-exists.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 platformInjectable from "../../../common/vars/platform.injectable";
|
||||
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 type { Kubectl } from "../../kubectl/kubectl";
|
||||
import lensProxyPortInjectable from "../../lens-proxy/lens-proxy-port.injectable";
|
||||
import buildVersionInjectable from "../../vars/build-version/build-version.injectable";
|
||||
import type { OpenShellSession } from "../create-shell-session.injectable";
|
||||
import type { SpawnPty } from "../spawn-pty.injectable";
|
||||
@@ -29,6 +33,7 @@ describe("technical unit tests for local shell sessions", () => {
|
||||
di = getDiForUnitTesting();
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||
di.override(directoryForTempInjectable, () => "/some-directory-for-tmp");
|
||||
di.override(buildVersionInjectable, () => ({
|
||||
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(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
|
||||
di.override(statInjectable, () => () => { throw new Error("tried call stat without override"); });
|
||||
di.inject(lensProxyPortInjectable).set(1111);
|
||||
});
|
||||
|
||||
describe("when on windows", () => {
|
||||
@@ -54,6 +60,10 @@ describe("technical unit tests for local shell sessions", () => {
|
||||
getBundledPath: () => "/some-bundled-kubectl-path",
|
||||
}) as Partial<Kubectl> as Kubectl);
|
||||
|
||||
di.override(kubeconfigManagerInjectable, () => ({
|
||||
ensurePath: async () => "/some-proxy-kubeconfig-file",
|
||||
} as Partial<KubeconfigManager> as KubeconfigManager));
|
||||
|
||||
openLocalShellSession = di.inject(openLocalShellSessionInjectable);
|
||||
});
|
||||
|
||||
@@ -89,11 +99,16 @@ describe("technical unit tests for local shell sessions", () => {
|
||||
once: jest.fn(() => 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({
|
||||
cluster: {
|
||||
getProxyKubeconfigPath: async () => "/some-proxy-kubeconfig",
|
||||
preferences: {},
|
||||
} as Partial<Cluster> as Cluster,
|
||||
cluster,
|
||||
tabId: "my-tab-id",
|
||||
websocket,
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ import { NodeApi } from "../../../common/k8s-api/endpoints";
|
||||
import { TerminalChannels } from "../../../common/terminal/channels";
|
||||
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 { initialNodeShellImage } from "../../../common/cluster-types";
|
||||
import type { LoadProxyKubeconfig } from "../../cluster/load-proxy-kubeconfig.injectable";
|
||||
|
||||
export interface NodeShellSessionArgs extends ShellSessionArgs {
|
||||
nodeName: string;
|
||||
@@ -21,6 +23,7 @@ export interface NodeShellSessionArgs extends ShellSessionArgs {
|
||||
export interface NodeShellSessionDependencies extends ShellSessionDependencies {
|
||||
createKubeJsonApiForCluster: CreateKubeJsonApiForCluster;
|
||||
createKubeApi: CreateKubeApi;
|
||||
loadProxyKubeconfig: LoadProxyKubeconfig;
|
||||
}
|
||||
|
||||
export class NodeShellSession extends ShellSession {
|
||||
@@ -36,9 +39,8 @@ export class NodeShellSession extends ShellSession {
|
||||
}
|
||||
|
||||
public async open() {
|
||||
const kc = await this.cluster.getProxyKubeconfig();
|
||||
const coreApi = kc.makeApiClient(CoreV1Api);
|
||||
const shell = await this.kubectl.getPath();
|
||||
const proxyKubeconfig = await this.dependencies.loadProxyKubeconfig();
|
||||
const coreApi = proxyKubeconfig.makeApiClient(CoreV1Api);
|
||||
|
||||
const cleanup = once(() => {
|
||||
coreApi
|
||||
@@ -50,7 +52,7 @@ export class NodeShellSession extends ShellSession {
|
||||
|
||||
try {
|
||||
await this.createNodeShellPod(coreApi);
|
||||
await this.waitForRunningPod(kc);
|
||||
await this.waitForRunningPod(proxyKubeconfig);
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
|
||||
@@ -92,13 +94,18 @@ export class NodeShellSession extends ShellSession {
|
||||
break;
|
||||
}
|
||||
|
||||
await this.openShellProcess(shell, args, env);
|
||||
await this.openShellProcess(this.dependencies.directoryContainingKubectl, args, env);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -121,7 +128,7 @@ export class NodeShellSession extends ShellSession {
|
||||
priorityClassName: "system-node-critical",
|
||||
containers: [{
|
||||
name: "shell",
|
||||
image: this.cluster.nodeShellImage,
|
||||
image: nodeShellImage || initialNodeShellImage,
|
||||
securityContext: {
|
||||
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 statInjectable from "../../../common/fs/stat.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 {
|
||||
websocket: WebSocket;
|
||||
@@ -34,7 +36,7 @@ const openNodeShellSessionInjectable = getInjectable({
|
||||
id: "open-node-shell-session",
|
||||
instantiate: (di): OpenNodeShellSession => {
|
||||
const createKubectl = di.inject(createKubectlInjectable);
|
||||
const dependencies: NodeShellSessionDependencies = {
|
||||
const dependencies: Omit<NodeShellSessionDependencies, "proxyKubeconfigPath" | "loadProxyKubeconfig" | "directoryContainingKubectl"> = {
|
||||
isMac: di.inject(isMacInjectable),
|
||||
isWindows: di.inject(isWindowsInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
@@ -50,8 +52,18 @@ const openNodeShellSessionInjectable = getInjectable({
|
||||
};
|
||||
|
||||
return async (args) => {
|
||||
const kubectl = createKubectl(args.cluster.version);
|
||||
const session = new NodeShellSession(dependencies, { kubectl, ...args });
|
||||
const kubectl = createKubectl(args.cluster.version.get());
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -111,6 +111,8 @@ export interface ShellSessionDependencies {
|
||||
readonly userShellSetting: IComputedValue<string>;
|
||||
readonly appName: string;
|
||||
readonly buildVersion: InitializableState<string>;
|
||||
readonly proxyKubeconfigPath: string;
|
||||
readonly directoryContainingKubectl: string;
|
||||
computeShellEnvironment: ComputeShellEnvironment;
|
||||
spawnPty: SpawnPty;
|
||||
emitAppEvent: EmitAppEvent;
|
||||
@@ -147,8 +149,6 @@ export abstract class ShellSession {
|
||||
}
|
||||
|
||||
protected running = false;
|
||||
protected readonly kubectlBinDirP: Promise<string>;
|
||||
protected readonly kubeconfigPathP: Promise<string>;
|
||||
protected readonly terminalId: string;
|
||||
protected readonly kubectl: Kubectl;
|
||||
protected readonly websocket: WebSocket;
|
||||
@@ -179,8 +179,6 @@ export abstract class ShellSession {
|
||||
this.kubectl = kubectl;
|
||||
this.websocket = websocket;
|
||||
this.cluster = cluster;
|
||||
this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath();
|
||||
this.kubectlBinDirP = this.kubectl.binDir();
|
||||
this.terminalId = `${cluster.id}:${terminalId}`;
|
||||
}
|
||||
|
||||
@@ -297,7 +295,7 @@ export abstract class ShellSession {
|
||||
code !== WebSocketCloseEvent.AbnormalClosure
|
||||
&& code !== WebSocketCloseEvent.GoingAway
|
||||
)
|
||||
|| this.cluster.disconnected
|
||||
|| this.cluster.disconnected.get()
|
||||
);
|
||||
|
||||
if (stopShellSession) {
|
||||
@@ -350,7 +348,7 @@ export abstract class ShellSession {
|
||||
})();
|
||||
|
||||
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
|
||||
|
||||
@@ -373,12 +371,12 @@ export abstract class ShellSession {
|
||||
|
||||
if (path.basename(env.PTYSHELL) === "zsh") {
|
||||
env.OLD_ZDOTDIR = env.ZDOTDIR || env.HOME;
|
||||
env.ZDOTDIR = await this.kubectlBinDirP;
|
||||
env.ZDOTDIR = this.dependencies.directoryContainingKubectl;
|
||||
env.DISABLE_AUTO_UPDATE = "true";
|
||||
}
|
||||
|
||||
env.PTYPID = process.pid.toString();
|
||||
env.KUBECONFIG = await this.kubeconfigPathP;
|
||||
env.KUBECONFIG = this.dependencies.proxyKubeconfigPath;
|
||||
env.TERM_PROGRAM = this.dependencies.appName;
|
||||
env.TERM_PROGRAM_VERSION = this.dependencies.buildVersion.get();
|
||||
|
||||
|
||||
Reference in new issue
Block a user