mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
TLS cert per cluster for lens-k8s-proxy (#5081)
Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
477814f1e0
commit
b3574e1a21
@ -44,7 +44,7 @@
|
||||
"postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version"
|
||||
},
|
||||
"config": {
|
||||
"k8sProxyVersion": "0.1.5",
|
||||
"k8sProxyVersion": "0.2.1",
|
||||
"bundledKubectlVersion": "1.23.3",
|
||||
"bundledHelmVersion": "3.7.2",
|
||||
"sentryDsn": ""
|
||||
|
||||
@ -24,8 +24,6 @@ import { createClusterInjectionToken } from "../cluster/create-cluster-injection
|
||||
|
||||
import directoryForUserDataInjectable
|
||||
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);
|
||||
|
||||
@ -89,8 +87,6 @@ describe("cluster-store", () => {
|
||||
mainDi = dis.mainDi;
|
||||
|
||||
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
mainDi.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||
mainDi.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||
|
||||
await dis.runSetups();
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import mockFs from "mock-fs";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
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", () => ({
|
||||
@ -83,7 +82,6 @@ describe("ContextHandler", () => {
|
||||
});
|
||||
|
||||
di.override(createKubeAuthProxyInjectable, () => ({} as any));
|
||||
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||
|
||||
await di.runSetups();
|
||||
|
||||
|
||||
@ -52,8 +52,6 @@ import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-p
|
||||
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
|
||||
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);
|
||||
|
||||
@ -97,8 +95,6 @@ describe("kube auth proxy tests", () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(spawnInjectable, () => mockSpawn);
|
||||
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||
di.override(kubeAuthProxyCaInjectable, () => ({} as any));
|
||||
|
||||
mockFs(mockMinikubeConfig);
|
||||
|
||||
|
||||
@ -14,11 +14,8 @@ import { ClusterManager } from "../../cluster-manager";
|
||||
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
|
||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
|
||||
import directoryForKubeConfigsInjectable
|
||||
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";
|
||||
|
||||
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
@ -42,9 +39,6 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
beforeEach(async () => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(kubeAuthProxyCaInjectable, () => Promise.resolve(Buffer.from("ca")));
|
||||
di.override(createKubeAuthProxyCertFilesInjectable, () => ({} as any));
|
||||
|
||||
mockFs();
|
||||
|
||||
await di.runSetups();
|
||||
@ -62,6 +56,7 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
ClusterManager.resetInstance();
|
||||
ClusterStore.resetInstance();
|
||||
});
|
||||
|
||||
describe("configsToModels", () => {
|
||||
|
||||
@ -28,7 +28,7 @@ interface PrometheusServicePreferences {
|
||||
|
||||
interface Dependencies {
|
||||
createKubeAuthProxy: CreateKubeAuthProxy;
|
||||
authProxyCa: Promise<Buffer>;
|
||||
authProxyCa: string;
|
||||
}
|
||||
|
||||
export class ContextHandler {
|
||||
@ -123,7 +123,7 @@ export class ContextHandler {
|
||||
return `https://127.0.0.1:${this.kubeAuthProxy.port}${this.kubeAuthProxy.apiPrefix}${path}`;
|
||||
}
|
||||
|
||||
async resolveAuthProxyCa() {
|
||||
resolveAuthProxyCa() {
|
||||
return this.dependencies.authProxyCa;
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ export class ContextHandler {
|
||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||
await this.ensureServer();
|
||||
|
||||
const caFileContents = await this.resolveAuthProxyCa();
|
||||
const ca = this.dependencies.authProxyCa;
|
||||
const clusterPath = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||
const apiPrefix = `${this.kubeAuthProxy.apiPrefix}${clusterPath}`;
|
||||
|
||||
@ -150,7 +150,7 @@ export class ContextHandler {
|
||||
host: "127.0.0.1",
|
||||
port: this.kubeAuthProxy.port,
|
||||
path: apiPrefix,
|
||||
ca: caFileContents.toString(),
|
||||
ca,
|
||||
},
|
||||
changeOrigin: true,
|
||||
timeout,
|
||||
|
||||
@ -3,23 +3,27 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import selfsigned from "selfsigned";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||
import kubeAuthProxyCaInjectable from "../kube-auth-proxy/kube-auth-proxy-ca.injectable";
|
||||
import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate";
|
||||
import URLParse from "url-parse";
|
||||
|
||||
const createContextHandlerInjectable = getInjectable({
|
||||
id: "create-context-handler",
|
||||
|
||||
instantiate: (di) => {
|
||||
const authProxyCa = di.inject(kubeAuthProxyCaInjectable);
|
||||
return (cluster: Cluster) => {
|
||||
const clusterUrl = new URLParse(cluster.apiUrl);
|
||||
|
||||
const dependencies = {
|
||||
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
|
||||
authProxyCa,
|
||||
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
|
||||
};
|
||||
|
||||
return (cluster: Cluster) => new ContextHandler(dependencies, cluster);
|
||||
return new ContextHandler(dependencies, cluster);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -1,26 +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 * 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;
|
||||
@ -1,43 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -6,10 +6,11 @@ import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { KubeAuthProxy, KubeAuthProxyDependencies } from "./kube-auth-proxy";
|
||||
import type { Cluster } from "../../common/cluster/cluster";
|
||||
import path from "path";
|
||||
import selfsigned from "selfsigned";
|
||||
import { getBinaryName } from "../../common/vars";
|
||||
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";
|
||||
import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
|
||||
|
||||
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
||||
|
||||
@ -18,15 +19,17 @@ const createKubeAuthProxyInjectable = getInjectable({
|
||||
|
||||
instantiate: (di): CreateKubeAuthProxy => {
|
||||
const binaryName = getBinaryName("lens-k8s-proxy");
|
||||
|
||||
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
|
||||
const clusterUrl = new URL(cluster.apiUrl);
|
||||
const dependencies: KubeAuthProxyDependencies = {
|
||||
proxyBinPath: path.join(di.inject(directoryForBundledBinariesInjectable), binaryName),
|
||||
proxyCertPath: di.inject(createKubeAuthProxyCertFilesInjectable),
|
||||
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
|
||||
spawn: di.inject(spawnInjectable),
|
||||
};
|
||||
|
||||
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => (
|
||||
new KubeAuthProxy(dependencies, cluster, environmentVariables)
|
||||
);
|
||||
return new KubeAuthProxy(dependencies, cluster, environmentVariables);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
39
src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts
Normal file
39
src/main/kube-auth-proxy/get-kube-auth-proxy-certificate.ts
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type * as selfsigned from "selfsigned";
|
||||
|
||||
type SelfSignedGenerate = typeof selfsigned.generate;
|
||||
|
||||
const certCache: Map<string, selfsigned.SelfSignedCert> = new Map();
|
||||
|
||||
export function getKubeAuthProxyCertificate(hostname: string, generate: SelfSignedGenerate, useCache = true): selfsigned.SelfSignedCert {
|
||||
if (useCache && certCache.has(hostname)) {
|
||||
return certCache.get(hostname);
|
||||
}
|
||||
|
||||
const opts = [
|
||||
{ name: "commonName", value: "Lens Certificate Authority" },
|
||||
{ name: "organizationName", value: "Lens" },
|
||||
];
|
||||
|
||||
const cert = generate(opts, {
|
||||
keySize: 2048,
|
||||
algorithm: "sha256",
|
||||
days: 365,
|
||||
extensions: [
|
||||
{ name: "basicConstraints", cA: true },
|
||||
{ name: "subjectAltName", altNames: [
|
||||
{ type: 2, value: hostname },
|
||||
{ type: 2, value: "localhost" },
|
||||
{ type: 7, ip: "127.0.0.1" },
|
||||
] },
|
||||
],
|
||||
});
|
||||
|
||||
certCache.set(hostname, cert);
|
||||
|
||||
return cert;
|
||||
}
|
||||
@ -1,22 +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 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;
|
||||
@ -10,12 +10,13 @@ import type { Cluster } from "../../common/cluster/cluster";
|
||||
import logger from "../logger";
|
||||
import { getPortFrom } from "../utils/get-port";
|
||||
import { makeObservable, observable, when } from "mobx";
|
||||
import type { SelfSignedCert } from "selfsigned";
|
||||
|
||||
const startingServeRegex = /starting to serve on (?<address>.+)/i;
|
||||
|
||||
export interface KubeAuthProxyDependencies {
|
||||
proxyBinPath: string;
|
||||
proxyCertPath: Promise<string>;
|
||||
proxyCert: SelfSignedCert;
|
||||
spawn: typeof spawn;
|
||||
}
|
||||
|
||||
@ -44,7 +45,7 @@ export class KubeAuthProxy {
|
||||
}
|
||||
|
||||
const proxyBin = this.dependencies.proxyBinPath;
|
||||
const certPath = await this.dependencies.proxyCertPath;
|
||||
const cert = this.dependencies.proxyCert;
|
||||
|
||||
this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
|
||||
env: {
|
||||
@ -52,7 +53,8 @@ export class KubeAuthProxy {
|
||||
KUBECONFIG: this.cluster.kubeConfigPath,
|
||||
KUBECONFIG_CONTEXT: this.cluster.contextName,
|
||||
API_PREFIX: this.apiPrefix,
|
||||
CERT_PATH: certPath,
|
||||
PROXY_KEY: cert.private,
|
||||
PROXY_CERT: cert.cert,
|
||||
},
|
||||
});
|
||||
this.proxyProcess.on("error", (error) => {
|
||||
|
||||
@ -57,8 +57,9 @@ const NonInjectedLogsDockTab = observer(({ className, tab, model, subscribeStore
|
||||
setTimeout(() => {
|
||||
const overlay = document.querySelector(".PodLogs .list span.active");
|
||||
|
||||
if (!overlay) return;
|
||||
if (typeof overlay?.scrollIntoViewIfNeeded === "function") {
|
||||
overlay.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
|
||||
@ -81,7 +81,9 @@ export class Tab extends React.PureComponent<TabProps> {
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
this.ref.current?.scrollIntoViewIfNeeded();
|
||||
if (typeof this.ref.current?.scrollIntoViewIfNeeded === "function") {
|
||||
this.ref.current.scrollIntoViewIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
|
||||
2
types/dom.d.ts
vendored
2
types/dom.d.ts
vendored
@ -6,7 +6,7 @@ export {};
|
||||
|
||||
declare global {
|
||||
interface Element {
|
||||
scrollIntoViewIfNeeded(opt_center?: boolean): void;
|
||||
scrollIntoViewIfNeeded?(opt_center?: boolean): void;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user