mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Enable TLS on lens-k8s-proxy (#4941)
* wip: enable tls on lens-k8s-proxy Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * type -> interface Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * more dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * run di.runSetups() after app is ready Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * tls fixes & refactor Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * cleanup Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
2186b26d12
commit
0fa89ecbfa
@ -255,6 +255,7 @@
|
|||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.9",
|
"request-promise-native": "^1.0.9",
|
||||||
"rfc6902": "^4.0.2",
|
"rfc6902": "^4.0.2",
|
||||||
|
"selfsigned": "^2.0.0",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"shell-env": "^3.0.1",
|
"shell-env": "^3.0.1",
|
||||||
"spdy": "^4.0.2",
|
"spdy": "^4.0.2",
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import { createClusterInjectionToken } from "../cluster/create-cluster-injection
|
|||||||
|
|
||||||
import directoryForUserDataInjectable
|
import directoryForUserDataInjectable
|
||||||
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import kubeAuthProxyCaInjectable from "../../main/kube-auth-proxy/kube-auth-proxy-ca.injectable";
|
||||||
|
import createKubeAuthProxyCertFilesInjectable from "../../main/kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
@ -87,6 +89,8 @@ describe("cluster-store", () => {
|
|||||||
mainDi = dis.mainDi;
|
mainDi = dis.mainDi;
|
||||||
|
|
||||||
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
|
mainDi.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||||
|
mainDi.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||||
|
|
||||||
await dis.runSetups();
|
await dis.runSetups();
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "..
|
|||||||
import { HotbarStore } from "../hotbar-store";
|
import { HotbarStore } from "../hotbar-store";
|
||||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import writeFileInjectable from "../fs/write-file.injectable";
|
||||||
|
|
||||||
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
|
jest.mock("../../main/catalog/catalog-entity-registry", () => ({
|
||||||
catalogEntityRegistry: {
|
catalogEntityRegistry: {
|
||||||
@ -115,6 +116,7 @@ describe("HotbarStore", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
di.override(writeFileInjectable, () => () => undefined);
|
||||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import type { DiContainer } from "@ogre-tools/injectable";
|
|||||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
|
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
|
||||||
import { defaultTheme } from "../vars";
|
import { defaultTheme } from "../vars";
|
||||||
|
import writeFileInjectable from "../fs/write-file.injectable";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
@ -46,6 +47,7 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
mainDi = dis.mainDi;
|
mainDi = dis.mainDi;
|
||||||
|
|
||||||
|
mainDi.override(writeFileInjectable, () => () => undefined);
|
||||||
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||||
|
|
||||||
await dis.runSetups();
|
await dis.runSetups();
|
||||||
|
|||||||
25
src/common/fs/write-file.injectable.ts
Normal file
25
src/common/fs/write-file.injectable.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* 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 path from "path";
|
||||||
|
import fsInjectable from "./fs.injectable";
|
||||||
|
|
||||||
|
const writeFileInjectable = getInjectable({
|
||||||
|
id: "write-file",
|
||||||
|
|
||||||
|
instantiate: (di) => {
|
||||||
|
const { writeFile, ensureDir } = di.inject(fsInjectable);
|
||||||
|
|
||||||
|
return async (filePath: string, content: string | Buffer) => {
|
||||||
|
await ensureDir(path.dirname(filePath), { mode: 0o755 });
|
||||||
|
|
||||||
|
await writeFile(filePath, content, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default writeFileInjectable;
|
||||||
@ -41,6 +41,7 @@ import type { ClusterModel } from "../../common/cluster-types";
|
|||||||
import { createClusterInjectionToken } 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 authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||||
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||||
|
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
|
|
||||||
@ -81,6 +82,9 @@ describe("create clusters", () => {
|
|||||||
|
|
||||||
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||||
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||||
|
di.override(createContextHandlerInjectable, () => () => {
|
||||||
|
throw new Error("you should never come here");
|
||||||
|
});
|
||||||
|
|
||||||
createCluster = di.inject(createClusterInjectionToken);
|
createCluster = di.inject(createClusterInjectionToken);
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import mockFs from "mock-fs";
|
|||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
|
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
|
||||||
|
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
@ -80,6 +82,9 @@ describe("ContextHandler", () => {
|
|||||||
"tmp": {},
|
"tmp": {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
di.override(createKubeAuthProxyInjectable, () => ({} as any));
|
||||||
|
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
|
|
||||||
createContextHandler = di.inject(createContextHandlerInjectable);
|
createContextHandler = di.inject(createContextHandlerInjectable);
|
||||||
|
|||||||
@ -50,6 +50,9 @@ import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
|||||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import spawnInjectable from "../child-process/spawn.injectable";
|
||||||
|
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
|
||||||
|
import createKubeAuthProxyCertFilesInjectable from "../kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
@ -92,6 +95,10 @@ describe("kube auth proxy tests", () => {
|
|||||||
|
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
di.override(spawnInjectable, () => mockSpawn);
|
||||||
|
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||||
|
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||||
|
|
||||||
mockFs(mockMinikubeConfig);
|
mockFs(mockMinikubeConfig);
|
||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
@ -130,7 +137,7 @@ describe("kube auth proxy tests", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockedCP = mock<ChildProcess>();
|
mockedCP = mock<ChildProcess>();
|
||||||
listeners = new EventEmitter();
|
listeners = new EventEmitter();
|
||||||
|
|
||||||
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
|
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
|
||||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
||||||
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
||||||
|
|||||||
@ -40,15 +40,18 @@ import * as path from "path";
|
|||||||
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
|
||||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||||
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||||
|
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||||
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
|
|
||||||
describe("kubeconfig manager tests", () => {
|
describe("kubeconfig manager tests", () => {
|
||||||
let cluster: Cluster;
|
let clusterFake: Cluster;
|
||||||
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||||
|
let di: DiContainer;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
|
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
|
||||||
|
|
||||||
@ -78,17 +81,21 @@ describe("kubeconfig manager tests", () => {
|
|||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
|
|
||||||
|
di.override(createContextHandlerInjectable, () => () => {
|
||||||
|
throw new Error("you should never come here");
|
||||||
|
});
|
||||||
|
|
||||||
const createCluster = di.inject(createClusterInjectionToken);
|
const createCluster = di.inject(createClusterInjectionToken);
|
||||||
|
|
||||||
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
||||||
|
|
||||||
cluster = createCluster({
|
clusterFake = createCluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "minikube",
|
||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml",
|
||||||
});
|
});
|
||||||
|
|
||||||
cluster.contextHandler = {
|
clusterFake.contextHandler = {
|
||||||
ensureServer: () => Promise.resolve(),
|
ensureServer: () => Promise.resolve(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
@ -100,7 +107,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should create 'temp' kube config with proxy", async () => {
|
it("should create 'temp' kube config with proxy", async () => {
|
||||||
const kubeConfManager = createKubeconfigManager(cluster);
|
const kubeConfManager = createKubeconfigManager(clusterFake);
|
||||||
|
|
||||||
expect(logger.error).not.toBeCalled();
|
expect(logger.error).not.toBeCalled();
|
||||||
expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`);
|
expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`);
|
||||||
@ -115,7 +122,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
|
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
|
||||||
const kubeConfManager = createKubeconfigManager(cluster);
|
const kubeConfManager = createKubeconfigManager(clusterFake);
|
||||||
|
|
||||||
const configPath = await kubeConfManager.getPath();
|
const configPath = await kubeConfManager.getPath();
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
|||||||
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
||||||
import directoryForKubeConfigsInjectable
|
import directoryForKubeConfigsInjectable
|
||||||
from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||||
|
import kubeAuthProxyCaInjectable from "../../kube-auth-proxy/kube-auth-proxy-ca.injectable";
|
||||||
|
import createKubeAuthProxyCertFilesInjectable from "../../kube-auth-proxy/create-kube-auth-proxy-cert-files.injectable";
|
||||||
|
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
@ -40,6 +42,9 @@ describe("kubeconfig-sync.source tests", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
di.override(kubeAuthProxyCaInjectable, () => Promise.resolve(Buffer.from("ca")));
|
||||||
|
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||||
|
|
||||||
mockFs();
|
mockFs();
|
||||||
|
|
||||||
await di.runSetups();
|
await di.runSetups();
|
||||||
|
|||||||
17
src/main/child-process/spawn.injectable.ts
Normal file
17
src/main/child-process/spawn.injectable.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 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 { spawn } from "child_process";
|
||||||
|
|
||||||
|
const spawnInjectable = getInjectable({
|
||||||
|
id: "spawn",
|
||||||
|
|
||||||
|
instantiate: () => {
|
||||||
|
return spawn;
|
||||||
|
},
|
||||||
|
causesSideEffects: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default spawnInjectable;
|
||||||
@ -12,6 +12,7 @@ import url, { UrlWithStringQuery } from "url";
|
|||||||
import { CoreV1Api } from "@kubernetes/client-node";
|
import { CoreV1Api } from "@kubernetes/client-node";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
|
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
|
||||||
|
import type { CreateKubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||||
|
|
||||||
export interface PrometheusDetails {
|
export interface PrometheusDetails {
|
||||||
prometheusPath: string;
|
prometheusPath: string;
|
||||||
@ -26,7 +27,8 @@ interface PrometheusServicePreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
createKubeAuthProxy: CreateKubeAuthProxy;
|
||||||
|
authProxyCa: Promise<Buffer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContextHandler {
|
export class ContextHandler {
|
||||||
@ -118,7 +120,11 @@ export class ContextHandler {
|
|||||||
await this.ensureServer();
|
await this.ensureServer();
|
||||||
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||||
|
|
||||||
return `http://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`;
|
return `https://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveAuthProxyCa() {
|
||||||
|
return this.dependencies.authProxyCa;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApiTarget(isLongRunningRequest = false): Promise<httpProxy.ServerOptions> {
|
async getApiTarget(isLongRunningRequest = false): Promise<httpProxy.ServerOptions> {
|
||||||
@ -132,10 +138,23 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||||
|
await this.ensureServer();
|
||||||
|
|
||||||
|
const caFileContents = await this.resolveAuthProxyCa();
|
||||||
|
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||||
|
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
target: await this.resolveAuthProxyUrl(),
|
target: {
|
||||||
|
protocol: "https:",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: this.kubeAuthProxy.port,
|
||||||
|
path: apiPrefix,
|
||||||
|
ca: caFileContents.toString(),
|
||||||
|
},
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
timeout,
|
timeout,
|
||||||
|
secure: true,
|
||||||
headers: {
|
headers: {
|
||||||
"Host": this.clusterUrl.hostname,
|
"Host": this.clusterUrl.hostname,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,13 +6,17 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
import { ContextHandler } from "./context-handler";
|
import { ContextHandler } from "./context-handler";
|
||||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||||
|
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
|
||||||
|
|
||||||
const createContextHandlerInjectable = getInjectable({
|
const createContextHandlerInjectable = getInjectable({
|
||||||
id: "create-context-handler",
|
id: "create-context-handler",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
|
const authProxyCa = di.inject(kubeAuthProxyCaInjectable);
|
||||||
|
|
||||||
const dependencies = {
|
const dependencies = {
|
||||||
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
|
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
|
||||||
|
authProxyCa,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (cluster: Cluster) => new ContextHandler(dependencies, cluster);
|
return (cluster: Cluster) => new ContextHandler(dependencies, cluster);
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
|
|||||||
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
|
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
|
||||||
import readFileInjectable from "../common/fs/read-file.injectable";
|
import readFileInjectable from "../common/fs/read-file.injectable";
|
||||||
import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
||||||
|
import spawnInjectable from "./child-process/spawn.injectable";
|
||||||
|
|
||||||
export const getDiForUnitTesting = (
|
export const getDiForUnitTesting = (
|
||||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||||
@ -46,6 +47,13 @@ export const getDiForUnitTesting = (
|
|||||||
di.override(appNameInjectable, () => "some-electron-app-name");
|
di.override(appNameInjectable, () => "some-electron-app-name");
|
||||||
di.override(registerChannelInjectable, () => () => undefined);
|
di.override(registerChannelInjectable, () => () => undefined);
|
||||||
di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory");
|
di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory");
|
||||||
|
di.override(spawnInjectable, () => () => {
|
||||||
|
return {
|
||||||
|
stderr: { on: jest.fn(), removeAllListeners: jest.fn() },
|
||||||
|
stdout: { on: jest.fn(), removeAllListeners: jest.fn() },
|
||||||
|
on: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
});
|
||||||
|
|
||||||
di.override(writeJsonFileInjectable, () => () => {
|
di.override(writeJsonFileInjectable, () => () => {
|
||||||
throw new Error("Tried to write JSON file to file system without specifying explicit override.");
|
throw new Error("Tried to write JSON file to file system without specifying explicit override.");
|
||||||
|
|||||||
@ -39,7 +39,7 @@ import { WeblinkStore } from "../common/weblink-store";
|
|||||||
import { SentryInit } from "../common/sentry";
|
import { SentryInit } from "../common/sentry";
|
||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
import { initMenu } from "./menu/menu";
|
import { initMenu } from "./menu/menu";
|
||||||
import { kubeApiRequest } from "./proxy-functions";
|
import { kubeApiUpgradeRequest } from "./proxy-functions";
|
||||||
import { initTray } from "./tray/tray";
|
import { initTray } from "./tray/tray";
|
||||||
import { ShellSession } from "./shell-session/shell-session";
|
import { ShellSession } from "./shell-session/shell-session";
|
||||||
import { getDi } from "./getDi";
|
import { getDi } from "./getDi";
|
||||||
@ -62,9 +62,11 @@ const di = getDi();
|
|||||||
|
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
|
|
||||||
di.runSetups().then(() => {
|
app.on("ready", async () => {
|
||||||
injectSystemCAs();
|
await di.runSetups();
|
||||||
|
|
||||||
|
injectSystemCAs();
|
||||||
|
|
||||||
const onCloseCleanup = disposer();
|
const onCloseCleanup = disposer();
|
||||||
const onQuitCleanup = disposer();
|
const onQuitCleanup = disposer();
|
||||||
|
|
||||||
@ -124,173 +126,6 @@ di.runSetups().then(() => {
|
|||||||
WindowManager.getInstance(false)?.ensureMainWindow();
|
WindowManager.getInstance(false)?.ensureMainWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("ready", async () => {
|
|
||||||
const directoryForExes = di.inject(directoryForExesInjectable);
|
|
||||||
|
|
||||||
logger.info(`🚀 Starting ${productName} from "${directoryForExes}"`);
|
|
||||||
logger.info("🐚 Syncing shell environment");
|
|
||||||
await shellSync();
|
|
||||||
|
|
||||||
powerMonitor.on("shutdown", () => app.exit());
|
|
||||||
|
|
||||||
registerFileProtocol("static", __static);
|
|
||||||
|
|
||||||
PrometheusProviderRegistry.createInstance();
|
|
||||||
initializers.initPrometheusProviderRegistry();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The following sync MUST be done before HotbarStore creation, because that
|
|
||||||
* store has migrations that will remove items that previous migrations add
|
|
||||||
* if this is not present
|
|
||||||
*/
|
|
||||||
syncGeneralEntities();
|
|
||||||
|
|
||||||
logger.info("💾 Loading stores");
|
|
||||||
|
|
||||||
const userStore = di.inject(userStoreInjectable);
|
|
||||||
|
|
||||||
userStore.startMainReactions();
|
|
||||||
|
|
||||||
// ClusterStore depends on: UserStore
|
|
||||||
const clusterStore = di.inject(clusterStoreInjectable);
|
|
||||||
|
|
||||||
clusterStore.provideInitialFromMain();
|
|
||||||
|
|
||||||
// HotbarStore depends on: ClusterStore
|
|
||||||
HotbarStore.createInstance();
|
|
||||||
|
|
||||||
WeblinkStore.createInstance();
|
|
||||||
|
|
||||||
syncWeblinks();
|
|
||||||
|
|
||||||
HelmRepoManager.createInstance(); // create the instance
|
|
||||||
|
|
||||||
const router = di.inject(routerInjectable);
|
|
||||||
const shellApiRequest = di.inject(shellApiRequestInjectable);
|
|
||||||
|
|
||||||
const lensProxy = LensProxy.createInstance(router, httpProxy.createProxy(), {
|
|
||||||
getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req),
|
|
||||||
kubeApiRequest,
|
|
||||||
shellApiRequest,
|
|
||||||
});
|
|
||||||
|
|
||||||
ClusterManager.createInstance().init();
|
|
||||||
|
|
||||||
initializers.initClusterMetadataDetectors();
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info("🔌 Starting LensProxy");
|
|
||||||
await lensProxy.listen(); // lensProxy.port available
|
|
||||||
} catch (error) {
|
|
||||||
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
|
|
||||||
|
|
||||||
return app.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// test proxy connection
|
|
||||||
try {
|
|
||||||
logger.info("🔎 Testing LensProxy connection ...");
|
|
||||||
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
|
|
||||||
|
|
||||||
if (getAppVersion() !== versionFromProxy) {
|
|
||||||
logger.error("Proxy server responded with invalid response");
|
|
||||||
|
|
||||||
return app.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("⚡ LensProxy connection OK");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`🛑 LensProxy: failed connection test: ${error}`);
|
|
||||||
|
|
||||||
const hostsPath = isWindows
|
|
||||||
? "C:\\windows\\system32\\drivers\\etc\\hosts"
|
|
||||||
: "/etc/hosts";
|
|
||||||
const message = [
|
|
||||||
`Failed connection test: ${error}`,
|
|
||||||
"Check to make sure that no other versions of Lens are running",
|
|
||||||
`Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`,
|
|
||||||
"If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.",
|
|
||||||
];
|
|
||||||
|
|
||||||
dialog.showErrorBox("Lens Proxy Error", message.join("\n\n"));
|
|
||||||
|
|
||||||
return app.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
|
||||||
|
|
||||||
extensionLoader.init();
|
|
||||||
|
|
||||||
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
|
||||||
|
|
||||||
extensionDiscovery.init();
|
|
||||||
|
|
||||||
// Start the app without showing the main window when auto starting on login
|
|
||||||
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
|
||||||
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
|
|
||||||
|
|
||||||
logger.info("🖥️ Starting WindowManager");
|
|
||||||
const windowManager = WindowManager.createInstance();
|
|
||||||
const menuItems = di.inject(electronMenuItemsInjectable);
|
|
||||||
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
|
||||||
|
|
||||||
onQuitCleanup.push(
|
|
||||||
initMenu(windowManager, menuItems),
|
|
||||||
initTray(windowManager, trayMenuItems),
|
|
||||||
() => ShellSession.cleanup(),
|
|
||||||
);
|
|
||||||
|
|
||||||
installDeveloperTools();
|
|
||||||
|
|
||||||
if (!startHidden) {
|
|
||||||
windowManager.ensureMainWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
|
|
||||||
onCloseCleanup.push(startCatalogSyncToRenderer(catalogEntityRegistry));
|
|
||||||
|
|
||||||
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
|
|
||||||
|
|
||||||
await ensureDir(directoryForKubeConfigs);
|
|
||||||
|
|
||||||
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
|
|
||||||
|
|
||||||
kubeConfigSyncManager.startSync();
|
|
||||||
|
|
||||||
startUpdateChecking();
|
|
||||||
lensProtocolRouterMain.rendererLoaded = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("🧩 Initializing extensions");
|
|
||||||
|
|
||||||
// call after windowManager to see splash earlier
|
|
||||||
try {
|
|
||||||
const extensions = await extensionDiscovery.load();
|
|
||||||
|
|
||||||
// Start watching after bundled extensions are loaded
|
|
||||||
extensionDiscovery.watchExtensions();
|
|
||||||
|
|
||||||
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
|
||||||
extensionDiscovery.events
|
|
||||||
.on("add", (extension: InstalledExtension) => {
|
|
||||||
extensionLoader.addExtension(extension);
|
|
||||||
})
|
|
||||||
.on("remove", (lensExtensionId: LensExtensionId) => {
|
|
||||||
extensionLoader.removeExtension(lensExtensionId);
|
|
||||||
});
|
|
||||||
|
|
||||||
extensionLoader.initExtensions(extensions);
|
|
||||||
} catch (error) {
|
|
||||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
|
||||||
console.error(error);
|
|
||||||
console.trace();
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
appEventBus.emit({ name: "service", action: "start" });
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("activate", (event, hasVisibleWindows) => {
|
app.on("activate", (event, hasVisibleWindows) => {
|
||||||
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
||||||
|
|
||||||
@ -349,7 +184,172 @@ di.runSetups().then(() => {
|
|||||||
lensProtocolRouterMain.route(rawUrl);
|
lensProtocolRouterMain.route(rawUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("[APP-MAIN] waiting for 'ready' and other messages");
|
logger.debug("[APP-MAIN] waiting for 'ready' and other messages");
|
||||||
|
|
||||||
|
const directoryForExes = di.inject(directoryForExesInjectable);
|
||||||
|
|
||||||
|
logger.info(`🚀 Starting ${productName} from "${directoryForExes}"`);
|
||||||
|
logger.info("🐚 Syncing shell environment");
|
||||||
|
await shellSync();
|
||||||
|
|
||||||
|
powerMonitor.on("shutdown", () => app.exit());
|
||||||
|
|
||||||
|
registerFileProtocol("static", __static);
|
||||||
|
|
||||||
|
PrometheusProviderRegistry.createInstance();
|
||||||
|
initializers.initPrometheusProviderRegistry();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following sync MUST be done before HotbarStore creation, because that
|
||||||
|
* store has migrations that will remove items that previous migrations add
|
||||||
|
* if this is not present
|
||||||
|
*/
|
||||||
|
syncGeneralEntities();
|
||||||
|
|
||||||
|
logger.info("💾 Loading stores");
|
||||||
|
|
||||||
|
const userStore = di.inject(userStoreInjectable);
|
||||||
|
|
||||||
|
userStore.startMainReactions();
|
||||||
|
|
||||||
|
// ClusterStore depends on: UserStore
|
||||||
|
const clusterStore = di.inject(clusterStoreInjectable);
|
||||||
|
|
||||||
|
clusterStore.provideInitialFromMain();
|
||||||
|
|
||||||
|
// HotbarStore depends on: ClusterStore
|
||||||
|
HotbarStore.createInstance();
|
||||||
|
|
||||||
|
WeblinkStore.createInstance();
|
||||||
|
|
||||||
|
syncWeblinks();
|
||||||
|
|
||||||
|
HelmRepoManager.createInstance(); // create the instance
|
||||||
|
|
||||||
|
const router = di.inject(routerInjectable);
|
||||||
|
const shellApiRequest = di.inject(shellApiRequestInjectable);
|
||||||
|
|
||||||
|
const lensProxy = LensProxy.createInstance(router, httpProxy.createProxy(), {
|
||||||
|
getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req),
|
||||||
|
kubeApiUpgradeRequest,
|
||||||
|
shellApiRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
ClusterManager.createInstance().init();
|
||||||
|
|
||||||
|
initializers.initClusterMetadataDetectors();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info("🔌 Starting LensProxy");
|
||||||
|
await lensProxy.listen(); // lensProxy.port available
|
||||||
|
} catch (error) {
|
||||||
|
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`);
|
||||||
|
|
||||||
|
return app.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// test proxy connection
|
||||||
|
try {
|
||||||
|
logger.info("🔎 Testing LensProxy connection ...");
|
||||||
|
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
|
||||||
|
|
||||||
|
if (getAppVersion() !== versionFromProxy) {
|
||||||
|
logger.error("Proxy server responded with invalid response");
|
||||||
|
|
||||||
|
return app.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("⚡ LensProxy connection OK");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`🛑 LensProxy: failed connection test: ${error}`);
|
||||||
|
|
||||||
|
const hostsPath = isWindows
|
||||||
|
? "C:\\windows\\system32\\drivers\\etc\\hosts"
|
||||||
|
: "/etc/hosts";
|
||||||
|
const message = [
|
||||||
|
`Failed connection test: ${error}`,
|
||||||
|
"Check to make sure that no other versions of Lens are running",
|
||||||
|
`Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`,
|
||||||
|
"If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.",
|
||||||
|
];
|
||||||
|
|
||||||
|
dialog.showErrorBox("Lens Proxy Error", message.join("\n\n"));
|
||||||
|
|
||||||
|
return app.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
|
extensionLoader.init();
|
||||||
|
|
||||||
|
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||||
|
|
||||||
|
extensionDiscovery.init();
|
||||||
|
|
||||||
|
// Start the app without showing the main window when auto starting on login
|
||||||
|
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||||
|
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
|
||||||
|
|
||||||
|
logger.info("🖥️ Starting WindowManager");
|
||||||
|
const windowManager = WindowManager.createInstance();
|
||||||
|
const menuItems = di.inject(electronMenuItemsInjectable);
|
||||||
|
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||||
|
|
||||||
|
onQuitCleanup.push(
|
||||||
|
initMenu(windowManager, menuItems),
|
||||||
|
initTray(windowManager, trayMenuItems),
|
||||||
|
() => ShellSession.cleanup(),
|
||||||
|
);
|
||||||
|
|
||||||
|
installDeveloperTools();
|
||||||
|
|
||||||
|
if (!startHidden) {
|
||||||
|
windowManager.ensureMainWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
|
||||||
|
onCloseCleanup.push(startCatalogSyncToRenderer(catalogEntityRegistry));
|
||||||
|
|
||||||
|
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
|
||||||
|
|
||||||
|
await ensureDir(directoryForKubeConfigs);
|
||||||
|
|
||||||
|
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
|
||||||
|
|
||||||
|
kubeConfigSyncManager.startSync();
|
||||||
|
|
||||||
|
startUpdateChecking();
|
||||||
|
lensProtocolRouterMain.rendererLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("🧩 Initializing extensions");
|
||||||
|
|
||||||
|
// call after windowManager to see splash earlier
|
||||||
|
try {
|
||||||
|
const extensions = await extensionDiscovery.load();
|
||||||
|
|
||||||
|
// Start watching after bundled extensions are loaded
|
||||||
|
extensionDiscovery.watchExtensions();
|
||||||
|
|
||||||
|
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
||||||
|
extensionDiscovery.events
|
||||||
|
.on("add", (extension: InstalledExtension) => {
|
||||||
|
extensionLoader.addExtension(extension);
|
||||||
|
})
|
||||||
|
.on("remove", (lensExtensionId: LensExtensionId) => {
|
||||||
|
extensionLoader.removeExtension(lensExtensionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionLoader.initExtensions(extensions);
|
||||||
|
} catch (error) {
|
||||||
|
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||||
|
console.error(error);
|
||||||
|
console.trace();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
appEventBus.emit({ name: "service", action: "start" });
|
||||||
|
}, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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 * as selfsigned from "selfsigned";
|
||||||
|
import { createKubeAuthProxyCertFiles } from "./create-kube-auth-proxy-cert-files";
|
||||||
|
import writeFileInjectable from "../../common/fs/write-file.injectable";
|
||||||
|
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const createKubeAuthProxyCertFilesInjectable = getInjectable({
|
||||||
|
id: "create-kube-auth-proxy-cert-files",
|
||||||
|
|
||||||
|
instantiate: async (di) => {
|
||||||
|
const userData = di.inject(directoryForUserDataInjectable);
|
||||||
|
const certPath = path.join(userData, "kube-auth-proxy");
|
||||||
|
|
||||||
|
return createKubeAuthProxyCertFiles(certPath, {
|
||||||
|
generate: selfsigned.generate,
|
||||||
|
writeFile: di.inject(writeFileInjectable),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createKubeAuthProxyCertFilesInjectable;
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from "path";
|
||||||
|
import type * as selfsigned from "selfsigned";
|
||||||
|
|
||||||
|
type SelfSignedGenerate = typeof selfsigned.generate;
|
||||||
|
|
||||||
|
interface CreateKubeAuthProxyCertificateFilesDependencies {
|
||||||
|
generate: SelfSignedGenerate;
|
||||||
|
writeFile: (path: string, content: string | Buffer) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKubeAuthProxyCertificate(generate: SelfSignedGenerate): selfsigned.SelfSignedCert {
|
||||||
|
const opts = [
|
||||||
|
{ name: "commonName", value: "Lens Certificate Authority" },
|
||||||
|
{ name: "organizationName", value: "Lens" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return generate(opts, {
|
||||||
|
keySize: 2048,
|
||||||
|
algorithm: "sha256",
|
||||||
|
days: 365,
|
||||||
|
extensions: [
|
||||||
|
{ name: "basicConstraints", cA: true },
|
||||||
|
{ name: "subjectAltName", altNames: [
|
||||||
|
{ type: 2, value: "localhost" },
|
||||||
|
{ type: 7, ip: "127.0.0.1" },
|
||||||
|
] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createKubeAuthProxyCertFiles(dir: string, dependencies: CreateKubeAuthProxyCertificateFilesDependencies): Promise<string> {
|
||||||
|
const cert = getKubeAuthProxyCertificate(dependencies.generate);
|
||||||
|
|
||||||
|
await dependencies.writeFile(path.join(dir, "proxy.key"), cert.private);
|
||||||
|
await dependencies.writeFile(path.join(dir, "proxy.crt"), cert.cert);
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
@ -8,14 +8,20 @@ import type { Cluster } from "../../common/cluster/cluster";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { getBinaryName } from "../../common/vars";
|
import { getBinaryName } from "../../common/vars";
|
||||||
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
import directoryForBundledBinariesInjectable from "../../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
|
||||||
|
import spawnInjectable from "../child-process/spawn.injectable";
|
||||||
|
import createKubeAuthProxyCertFilesInjectable from "./create-kube-auth-proxy-cert-files.injectable";
|
||||||
|
|
||||||
|
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
||||||
|
|
||||||
const createKubeAuthProxyInjectable = getInjectable({
|
const createKubeAuthProxyInjectable = getInjectable({
|
||||||
id: "create-kube-auth-proxy",
|
id: "create-kube-auth-proxy",
|
||||||
|
|
||||||
instantiate: (di) => {
|
instantiate: (di): CreateKubeAuthProxy => {
|
||||||
const binaryName = getBinaryName("lens-k8s-proxy");
|
const binaryName = getBinaryName("lens-k8s-proxy");
|
||||||
const dependencies: KubeAuthProxyDependencies = {
|
const dependencies: KubeAuthProxyDependencies = {
|
||||||
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
|
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
|
||||||
|
proxyCertPath: di.inject(createKubeAuthProxyCertFilesInjectable),
|
||||||
|
spawn: di.inject(spawnInjectable),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => (
|
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => (
|
||||||
|
|||||||
22
src/main/kube-auth-proxy/kube-auth-proxy-ca.injectable.ts
Normal file
22
src/main/kube-auth-proxy/kube-auth-proxy-ca.injectable.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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 path from "path";
|
||||||
|
import readFileInjectable from "../../common/fs/read-file.injectable";
|
||||||
|
import createKubeAuthProxyCertFilesInjectable from "./create-kube-auth-proxy-cert-files.injectable";
|
||||||
|
|
||||||
|
const kubeAuthProxyCaInjectable = getInjectable({
|
||||||
|
id: "kube-auth-proxy-ca",
|
||||||
|
|
||||||
|
instantiate: async (di) => {
|
||||||
|
const certPath = await di.inject(createKubeAuthProxyCertFilesInjectable);
|
||||||
|
|
||||||
|
const readFile = di.inject(readFileInjectable);
|
||||||
|
|
||||||
|
return readFile(path.join(certPath, "proxy.crt"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default kubeAuthProxyCaInjectable;
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ChildProcess, spawn } from "child_process";
|
import type { ChildProcess, spawn } from "child_process";
|
||||||
import { waitUntilUsed } from "tcp-port-used";
|
import { waitUntilUsed } from "tcp-port-used";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
@ -15,6 +15,8 @@ const startingServeRegex = /starting to serve on (?<address>.+)/i;
|
|||||||
|
|
||||||
export interface KubeAuthProxyDependencies {
|
export interface KubeAuthProxyDependencies {
|
||||||
proxyBinPath: string;
|
proxyBinPath: string;
|
||||||
|
proxyCertPath: Promise<string>;
|
||||||
|
spawn: typeof spawn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubeAuthProxy {
|
export class KubeAuthProxy {
|
||||||
@ -42,13 +44,15 @@ export class KubeAuthProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const proxyBin = this.dependencies.proxyBinPath;
|
const proxyBin = this.dependencies.proxyBinPath;
|
||||||
|
const certPath = await this.dependencies.proxyCertPath;
|
||||||
|
|
||||||
this.proxyProcess = spawn(proxyBin, [], {
|
this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
|
||||||
env: {
|
env: {
|
||||||
...this.env,
|
...this.env,
|
||||||
KUBECONFIG: this.cluster.kubeConfigPath,
|
KUBECONFIG: this.cluster.kubeConfigPath,
|
||||||
KUBECONFIG_CONTEXT: this.cluster.contextName,
|
KUBECONFIG_CONTEXT: this.cluster.contextName,
|
||||||
API_PREFIX: this.apiPrefix,
|
API_PREFIX: this.apiPrefix,
|
||||||
|
CERT_PATH: certPath,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.proxyProcess.on("error", (error) => {
|
this.proxyProcess.on("error", (error) => {
|
||||||
@ -66,11 +70,15 @@ export class KubeAuthProxy {
|
|||||||
this.exit();
|
this.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.proxyProcess.stderr.on("data", (data) => {
|
this.proxyProcess.stderr.on("data", (data: Buffer) => {
|
||||||
|
if (data.includes("http: TLS handshake error")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.cluster.broadcastConnectUpdate(data.toString(), true);
|
this.cluster.broadcastConnectUpdate(data.toString(), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.proxyProcess.stdout.on("data", (data: any) => {
|
this.proxyProcess.stdout.on("data", (data: Buffer) => {
|
||||||
if (typeof this._port === "number") {
|
if (typeof this._port === "number") {
|
||||||
this.cluster.broadcastConnectUpdate(data.toString());
|
this.cluster.broadcastConnectUpdate(data.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
|
|||||||
export interface LensProxyFunctions {
|
export interface LensProxyFunctions {
|
||||||
getClusterForRequest: GetClusterForRequest;
|
getClusterForRequest: GetClusterForRequest;
|
||||||
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||||
kubeApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const watchParam = "watch";
|
const watchParam = "watch";
|
||||||
@ -61,7 +61,7 @@ export class LensProxy extends Singleton {
|
|||||||
|
|
||||||
public port: number;
|
public port: number;
|
||||||
|
|
||||||
constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiRequest, getClusterForRequest }: LensProxyFunctions) {
|
constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiUpgradeRequest, getClusterForRequest }: LensProxyFunctions) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.configureProxy(proxy);
|
this.configureProxy(proxy);
|
||||||
@ -88,7 +88,7 @@ export class LensProxy extends Singleton {
|
|||||||
return socket.destroy();
|
return socket.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
const reqHandler = isInternal ? shellApiRequest : kubeApiRequest;
|
const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest;
|
||||||
|
|
||||||
(async () => reqHandler({ req, socket, head, cluster }))()
|
(async () => reqHandler({ req, socket, head, cluster }))()
|
||||||
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
|
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
export * from "./kube-api-request";
|
export * from "./kube-api-upgrade-request";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|||||||
@ -4,21 +4,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { chunk } from "lodash";
|
import { chunk } from "lodash";
|
||||||
import net from "net";
|
import tls from "tls";
|
||||||
import url from "url";
|
import url from "url";
|
||||||
import { apiKubePrefix } from "../../common/vars";
|
import { apiKubePrefix } from "../../common/vars";
|
||||||
import type { ProxyApiRequestArgs } from "./types";
|
import type { ProxyApiRequestArgs } from "./types";
|
||||||
|
|
||||||
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
const skipRawHeaders = new Set(["Host", "Authorization"]);
|
||||||
|
|
||||||
export async function kubeApiRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
export async function kubeApiUpgradeRequest({ req, socket, head, cluster }: ProxyApiRequestArgs) {
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||||
|
const proxyCa = await cluster.contextHandler.resolveAuthProxyCa();
|
||||||
const apiUrl = url.parse(cluster.apiUrl);
|
const apiUrl = url.parse(cluster.apiUrl);
|
||||||
const pUrl = url.parse(proxyUrl);
|
const pUrl = url.parse(proxyUrl);
|
||||||
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
|
const connectOpts = {
|
||||||
const proxySocket = new net.Socket();
|
port: parseInt(pUrl.port),
|
||||||
|
host: pUrl.hostname,
|
||||||
|
ca: proxyCa,
|
||||||
|
};
|
||||||
|
|
||||||
proxySocket.connect(connectOpts, () => {
|
const proxySocket = tls.connect(connectOpts, () => {
|
||||||
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
||||||
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
||||||
|
|
||||||
@ -15,6 +15,7 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog";
|
|||||||
import type { ClusterModel } from "../../../../common/cluster-types";
|
import type { ClusterModel } from "../../../../common/cluster-types";
|
||||||
import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing";
|
import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing";
|
||||||
import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token";
|
import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token";
|
||||||
|
import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable";
|
||||||
|
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
@ -91,6 +92,8 @@ describe("<DeleteClusterDialog />", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });
|
const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
mainDi.override(createContextHandlerInjectable, () => () => undefined);
|
||||||
|
|
||||||
mockFs();
|
mockFs();
|
||||||
|
|
||||||
await runSetups();
|
await runSetups();
|
||||||
|
|||||||
26
types/selfsigned.d.ts
vendored
Normal file
26
types/selfsigned.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module "selfsigned" {
|
||||||
|
export interface SelfSignedCert {
|
||||||
|
private: string;
|
||||||
|
public: string;
|
||||||
|
cert: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateAttributes = Array<any>;
|
||||||
|
|
||||||
|
interface GenerateOptions {
|
||||||
|
keySize?: number;
|
||||||
|
days?: number;
|
||||||
|
algorithm?: "sha1" | "sha256";
|
||||||
|
extensions?: any;
|
||||||
|
pkcs7?: boolean;
|
||||||
|
clientCertificate?: boolean;
|
||||||
|
clientCertificateCN?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generate(GenerateAttributes, GenerateOptions): SelfSignedCert;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user