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

Move Cluster.apiUrl to seperate injectable (#7354)

* Refactor out Cluster.apiUrl to direct reading

- Should help prevent using stale data

- Removes some uses of synchronous FS operations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Rename helper function to better communicate intent

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Improve prometheus handler tests to override less things

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-03-31 15:28:14 -04:00 committed by GitHub
parent 18d660ea77
commit fe2fd4c1fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 472 additions and 640 deletions

2
package-lock.json generated
View File

@ -38059,7 +38059,6 @@
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1",
"@k8slens/react-application": "^1.0.0-alpha.0",
"@kubernetes/client-node": "^0.18.1",
@ -38266,6 +38265,7 @@
"peerDependencies": {
"@k8slens/application": "^6.5.0-alpha.0",
"@k8slens/application-for-electron-main": "^6.5.0-alpha.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/legacy-extensions": "^1.0.0-alpha.0",
"@k8slens/messaging": "^1.0.0-alpha.1",
"@k8slens/messaging-for-main": "^1.0.0-alpha.1",

View File

@ -120,7 +120,6 @@
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1",
"@k8slens/react-application": "^1.0.0-alpha.0",
"@kubernetes/client-node": "^0.18.1",
@ -324,6 +323,7 @@
"peerDependencies": {
"@k8slens/application": "^6.5.0-alpha.0",
"@k8slens/application-for-electron-main": "^6.5.0-alpha.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/legacy-extensions": "^1.0.0-alpha.0",
"@k8slens/messaging": "^1.0.0-alpha.1",
"@k8slens/messaging-for-main": "^1.0.0-alpha.1",

View File

@ -183,7 +183,6 @@ export const initialNodeShellImage = "docker.io/alpine:3.13";
* The data representing a cluster's state, for passing between main and renderer
*/
export interface ClusterState {
apiUrl: string;
online: boolean;
disconnected: boolean;
accessible: boolean;

View File

@ -5,7 +5,7 @@
import { computed, observable, toJS, runInAction } from "mobx";
import type { KubeApiResource } from "../rbac";
import type { ClusterState, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, ClusterConfigData } from "../cluster-types";
import type { ClusterState, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel } from "../cluster-types";
import { ClusterMetadataKey, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
import type { IObservableValue } from "mobx";
import { replaceObservableObject } from "../utils/replace-observable-object";
@ -27,11 +27,6 @@ export class Cluster {
*/
readonly kubeConfigPath = observable.box() as IObservableValue<string>;
/**
* Kubernetes API server URL
*/
readonly apiUrl: IObservableValue<string>;
/**
* Describes if we can detect that cluster is online
*/
@ -122,7 +117,7 @@ export class Cluster {
*/
readonly prometheusPreferences = computed(() => pick(toJS(this.preferences), "prometheus", "prometheusProvider") as ClusterPrometheusPreferences);
constructor({ id, ...model }: ClusterModel, configData: ClusterConfigData) {
constructor({ id, ...model }: ClusterModel) {
const { error } = clusterModelIdChecker.validate({ id });
if (error) {
@ -131,7 +126,6 @@ export class Cluster {
this.id = id;
this.updateModel(model);
this.apiUrl = observable.box(configData.clusterServerUrl);
}
/**
@ -187,7 +181,6 @@ export class Cluster {
*/
getState(): ClusterState {
return {
apiUrl: this.apiUrl.get(),
online: this.online.get(),
ready: this.ready.get(),
disconnected: this.disconnected.get(),
@ -207,7 +200,6 @@ export class Cluster {
this.accessible.set(state.accessible);
this.allowedNamespaces.replace(state.allowedNamespaces);
this.resourcesToShow.replace(state.resourcesToShow);
this.apiUrl.set(state.apiUrl);
this.disconnected.set(state.disconnected);
this.isAdmin.set(state.isAdmin);
this.isGlobalWatchEnabled.set(state.isGlobalWatchEnabled);

View File

@ -51,8 +51,6 @@ describe("ApiManager", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
apiManager = di.inject(apiManagerInjectable);

View File

@ -43,8 +43,6 @@ describe("KubeApi", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
apiManager = di.inject(apiManagerInjectable);

View File

@ -62,8 +62,6 @@ describe("createKubeApiForRemoteCluster", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
fetchMock = asyncFn();
@ -168,8 +166,6 @@ describe("KubeApi", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
di.override(apiKubeInjectable, () => createKubeJsonApi({

View File

@ -130,6 +130,16 @@ export function loadConfigFromString(content: string): ConfigResult {
};
}
export function loadValidatedConfig(content: string, contextName: string): ValidateKubeConfigResult {
const { options, error } = loadToOptions(content);
if (error) {
return { error };
}
return validateKubeConfig(loadFromOptions(options), contextName);
}
export interface SplitConfigEntry {
config: KubeConfig;
validationResult: ValidateKubeConfigResult;

View File

@ -0,0 +1,28 @@
/**
* 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 "../cluster/cluster";
import readFileInjectable from "../fs/read-file.injectable";
import type { ValidateKubeConfigResult } from "../kube-helpers";
import { loadValidatedConfig } from "../kube-helpers";
import resolveTildeInjectable from "../path/resolve-tilde.injectable";
export type LoadValidatedClusterConfig = (cluster: Cluster) => Promise<ValidateKubeConfigResult>;
const loadValidatedClusterConfigInjectable = getInjectable({
id: "load-validated-cluster-config",
instantiate: (di): LoadValidatedClusterConfig => {
const readFile = di.inject(readFileInjectable);
const resolveTilde = di.inject(resolveTildeInjectable);
return async (cluster) => {
const data = await readFile(resolveTilde(cluster.kubeConfigPath.get()));
return loadValidatedConfig(data, cluster.contextName.get());
};
},
});
export default loadValidatedClusterConfigInjectable;

View File

@ -0,0 +1,48 @@
/**
* 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 { URL } from "url";
import type { Cluster } from "../../../../common/cluster/cluster";
import statInjectable from "../../../../common/fs/stat.injectable";
import loadValidatedClusterConfigInjectable from "../../../../common/kube-helpers/load-validated-config-from-file.injectable";
interface ClusterApiUrlState {
url: URL;
lastReadMtimeMs: number;
}
const clusterApiUrlInjectable = getInjectable({
id: "cluster-api-url",
instantiate: (di, cluster): () => Promise<URL> => {
const loadValidatedClusterConfig = di.inject(loadValidatedClusterConfigInjectable);
const stat = di.inject(statInjectable);
let state: ClusterApiUrlState | undefined;
return async () => {
const stats = await stat(cluster.kubeConfigPath.get());
if (!state || state.lastReadMtimeMs >= stats.mtimeMs) {
const result = await loadValidatedClusterConfig(cluster);
if (result.error) {
throw result.error;
}
state = {
url: new URL(result.cluster.server),
lastReadMtimeMs: stats.mtimeMs,
};
}
return state.url;
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default clusterApiUrlInjectable;

View File

@ -109,8 +109,6 @@ describe("Deleting a cluster", () => {
clusterName: "some-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
nonCurrentCluster = new Cluster({
id: "some-non-current-context-cluster",
@ -119,8 +117,6 @@ describe("Deleting a cluster", () => {
clusterName: "some-non-current-context-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
});
});
@ -197,8 +193,6 @@ describe("Deleting a cluster", () => {
clusterName: "some-cluster",
},
kubeConfigPath: joinPaths(directoryForKubeConfigs, "some-cluster.json"),
}, {
clusterServerUrl: singleClusterServerUrl,
});
});
@ -233,8 +227,6 @@ describe("Deleting a cluster", () => {
clusterName: "some-cluster",
},
kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: singleClusterServerUrl,
});
});

View File

@ -23,7 +23,6 @@ import type { WriteFileSync } from "../../../common/fs/write-file-sync.injectabl
import writeFileSyncInjectable from "../../../common/fs/write-file-sync.injectable";
import type { WriteBufferSync } from "../../../common/fs/write-buffer-sync.injectable";
import writeBufferSyncInjectable from "../../../common/fs/write-buffer-sync.injectable";
import { Cluster } from "../../../common/cluster/cluster";
import clustersPersistentStorageInjectable from "./common/storage.injectable";
import type { PersistentStorage } from "../../../common/persistent-storage/create.injectable";
import type { AddCluster } from "./common/add.injectable";
@ -32,6 +31,7 @@ import type { GetClusterById } from "./common/get-by-id.injectable";
import getClusterByIdInjectable from "./common/get-by-id.injectable";
import type { IComputedValue } from "mobx";
import clustersInjectable from "./common/clusters.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
// NOTE: this is intended to read the actual file system
const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png");
@ -102,7 +102,7 @@ describe("cluster storage technical tests", () => {
describe("with foo cluster added", () => {
beforeEach(() => {
const cluster = new Cluster({
addCluster({
id: "foo",
contextName: "foo",
preferences: {
@ -114,11 +114,7 @@ describe("cluster storage technical tests", () => {
getCustomKubeConfigFilePath("foo"),
kubeconfig,
),
}, {
clusterServerUrl,
});
addCluster(cluster);
});
it("adds new cluster to store", async () => {
@ -232,47 +228,6 @@ describe("cluster storage technical tests", () => {
});
});
describe("config with invalid cluster kubeconfig", () => {
beforeEach(() => {
writeFileSync("/invalid-kube-config", invalidKubeconfig);
writeFileSync("/valid-kube-config", kubeconfig);
writeJsonSync("/some-directory-for-user-data/lens-cluster-store.json", {
__internal__: {
migrations: {
version: "99.99.99",
},
},
clusters: [
{
id: "cluster1",
kubeConfigPath: "/invalid-kube-config",
contextName: "test",
preferences: { terminalCWD: "/foo" },
workspace: "foo",
},
{
id: "cluster2",
kubeConfigPath: "/valid-kube-config",
contextName: "foo",
preferences: { terminalCWD: "/foo" },
workspace: "default",
},
],
});
getCustomKubeConfigFilePath = di.inject(getCustomKubeConfigFilePathInjectable);
clustersPersistentStorage = di.inject(clustersPersistentStorageInjectable);
clustersPersistentStorage.loadAndStartSyncing();
});
it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = clusters.get();
expect(storedClusters.length).toBe(1);
});
});
describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => {
di.override(storeMigrationVersionInjectable, () => "3.6.0");
@ -315,32 +270,6 @@ describe("cluster storage technical tests", () => {
});
});
const invalidKubeconfig = JSON.stringify({
apiVersion: "v1",
clusters: [{
cluster: {
server: "https://localhost",
},
name: "test2",
}],
contexts: [{
context: {
cluster: "test",
user: "test",
},
name: "test",
}],
"current-context": "test",
kind: "Config",
preferences: {},
users: [{
user: {
token: "kubeconfig-user-q4lm4:xxxyyyy",
},
name: "test",
}],
});
const minimalValidKubeConfig = JSON.stringify({
apiVersion: "v1",
clusters: [

View File

@ -5,33 +5,23 @@
import { getInjectable } from "@ogre-tools/injectable";
import { action } from "mobx";
import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import type { ClusterModel } from "../../../../common/cluster-types";
import { Cluster } from "../../../../common/cluster/cluster";
import clustersStateInjectable from "./state.injectable";
import { setAndGet } from "@k8slens/utilities";
export type AddCluster = (clusterOrModel: ClusterModel | Cluster) => Cluster;
export type AddCluster = (clusterModel: ClusterModel) => Cluster;
const addClusterInjectable = getInjectable({
id: "add-cluster",
instantiate: (di): AddCluster => {
const clustersState = di.inject(clustersStateInjectable);
const emitAppEvent = di.inject(emitAppEventInjectable);
const readClusterConfigSync = di.inject(readClusterConfigSyncInjectable);
return action((clusterOrModel) => {
return action((clusterModel) => {
emitAppEvent({ name: "cluster", action: "add" });
const cluster = clusterOrModel instanceof Cluster
? clusterOrModel
: new Cluster(
clusterOrModel,
readClusterConfigSync(clusterOrModel),
);
clustersState.set(cluster.id, cluster);
return cluster;
return setAndGet(clustersState, clusterModel.id, new Cluster(clusterModel));
});
},
});

View File

@ -1,31 +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 { ClusterConfigData, ClusterModel } from "../../../../common/cluster-types";
import readFileSyncInjectable from "../../../../common/fs/read-file-sync.injectable";
import { loadConfigFromString, validateKubeConfig } from "../../../../common/kube-helpers";
export type ReadClusterConfigSync = (model: ClusterModel) => ClusterConfigData;
const readClusterConfigSyncInjectable = getInjectable({
id: "read-cluster-config-sync",
instantiate: (di): ReadClusterConfigSync => {
const readFileSync = di.inject(readFileSyncInjectable);
return ({ kubeConfigPath, contextName }) => {
const kubeConfigData = readFileSync(kubeConfigPath);
const { config } = loadConfigFromString(kubeConfigData);
const result = validateKubeConfig(config, contextName);
if (result.error) {
throw result.error;
}
return { clusterServerUrl: result.cluster.server };
};
},
});
export default readClusterConfigSyncInjectable;

View File

@ -6,7 +6,6 @@ import { iter } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import { comparer, action } from "mobx";
import { clusterStoreMigrationInjectionToken } from "./migration-token";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import type { ClusterId, ClusterModel } from "../../../../common/cluster-types";
import { Cluster } from "../../../../common/cluster/cluster";
import loggerInjectable from "../../../../common/logger.injectable";
@ -23,7 +22,6 @@ const clustersPersistentStorageInjectable = getInjectable({
id: "clusters-persistent-storage",
instantiate: (di) => {
const createPersistentStorage = di.inject(createPersistentStorageInjectable);
const readClusterConfigSync = di.inject(readClusterConfigSyncInjectable);
const clustersState = di.inject(clustersStateInjectable);
const logger = di.inject(loggerInjectable);
@ -39,7 +37,6 @@ const clustersPersistentStorageInjectable = getInjectable({
const currentClusters = new Map(clustersState);
const newClusters = new Map<ClusterId, Cluster>();
// update new clusters
for (const clusterModel of clusters) {
try {
let cluster = currentClusters.get(clusterModel.id);
@ -47,11 +44,9 @@ const clustersPersistentStorageInjectable = getInjectable({
if (cluster) {
cluster.updateModel(clusterModel);
} else {
cluster = new Cluster(
clusterModel,
readClusterConfigSync(clusterModel),
);
cluster = new Cluster(clusterModel);
}
newClusters.set(clusterModel.id, cluster);
} catch (error) {
logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${error}`);

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Cluster } from "../../common/cluster/cluster";
import type { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
@ -19,6 +19,8 @@ import createCanIInjectable from "../../common/cluster/create-can-i.injectable";
import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable";
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
import addClusterInjectable from "../../features/cluster/storage/common/add.injectable";
describe("create clusters", () => {
let cluster: Cluster;
@ -26,7 +28,7 @@ describe("create clusters", () => {
beforeEach(() => {
const di = getDiForUnitTesting();
const clusterServerUrl = "https://192.168.64.3:8443";
const writeJsonSync = di.inject(writeJsonSyncInjectable);
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
@ -42,27 +44,45 @@ describe("create clusters", () => {
setupPrometheus: jest.fn(),
}));
writeJsonSync("/minikube-config.yml", {
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
}],
"current-context": "minikube",
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
preferences: {},
});
di.override(kubeconfigManagerInjectable, () => ({
ensurePath: async () => "/some-proxy-kubeconfig-file",
} as Partial<KubeconfigManager> as KubeconfigManager));
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
cluster = new Cluster({
const addCluster = di.inject(addClusterInjectable);
cluster = addCluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
}, {
clusterServerUrl,
kubeConfigPath: "/minikube-config.yml",
});
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.get()).toBe("https://192.168.64.3:8443");
});
it("reconnect should not throw if contextHandler is missing", () => {
expect(() => clusterConnection.reconnect()).not.toThrowError();
});

View File

@ -3,8 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable";
import { Cluster } from "../../common/cluster/cluster";
import type { ChildProcess } from "child_process";
import { Kubectl } from "../kubectl/kubectl";
import type { DeepMockProxy } from "jest-mock-extended";
@ -12,7 +10,7 @@ import { mockDeep, mock } from "jest-mock-extended";
import type { Readable } from "stream";
import { EventEmitter } from "stream";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { CreateKubeAuthProxy, KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import spawnInjectable from "../child-process/spawn.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
@ -25,15 +23,17 @@ import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable"
import ensureDirInjectable from "../../common/fs/ensure-dir.injectable";
import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable";
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
const clusterServerUrl = "https://192.168.64.3:8443";
import type { Cluster } from "../../common/cluster/cluster";
import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable";
import addClusterInjectable from "../../features/cluster/storage/common/add.injectable";
describe("kube auth proxy tests", () => {
let createKubeAuthProxy: CreateKubeAuthProxy;
let spawnMock: jest.Mock;
let waitUntilPortIsUsedMock: jest.Mock;
let broadcastMessageMock: jest.Mock;
let getBasenameOfPath: GetBasenameOfPath;
let cluster: Cluster;
let kubeAuthProxy: KubeAuthProxy;
beforeEach(async () => {
const di = getDiForUnitTesting();
@ -51,7 +51,7 @@ describe("kube auth proxy tests", () => {
clusters: [{
name: "minikube",
cluster: {
server: clusterServerUrl,
server: "https://192.168.64.3:8443",
},
}],
"current-context": "minikube",
@ -83,29 +83,25 @@ describe("kube auth proxy tests", () => {
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin");
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
const addCluster = di.inject(addClusterInjectable);
cluster = addCluster({
id: "foobar",
kubeConfigPath: "/minikube-config.yml",
contextName: "minikube",
});
kubeAuthProxy = di.inject(createKubeAuthProxyInjectable, cluster)({});
});
it("calling exit multiple times shouldn't throw", async () => {
const cluster = new Cluster({
id: "foobar",
kubeConfigPath: "minikube-config.yml",
contextName: "minikube",
}, {
clusterServerUrl,
});
const kap = createKubeAuthProxy(cluster, {});
kap.exit();
kap.exit();
kap.exit();
kubeAuthProxy.exit();
kubeAuthProxy.exit();
kubeAuthProxy.exit();
});
describe("spawn tests", () => {
let mockedCP: DeepMockProxy<ChildProcess>;
let listeners: EventEmitter;
let proxy: KubeAuthProxy;
beforeEach(async () => {
mockedCP = mockDeep<ChildProcess>();
@ -184,16 +180,7 @@ describe("kube auth proxy tests", () => {
});
waitUntilPortIsUsedMock.mockReturnValueOnce(Promise.resolve());
const cluster = new Cluster({
id: "foobar",
kubeConfigPath: "minikube-config.yml",
contextName: "minikube",
}, {
clusterServerUrl,
});
proxy = createKubeAuthProxy(cluster, {});
await proxy.run();
await kubeAuthProxy.run();
});
it("should call spawn and broadcast errors", () => {

View File

@ -89,8 +89,6 @@ describe("kubeconfig manager tests", () => {
id: "foo",
contextName: "minikube",
kubeConfigPath: "/minikube-config.yml",
}, {
clusterServerUrl,
});
kubeConfManager = di.inject(kubeconfigManagerInjectable, clusterFake);

View File

@ -13,8 +13,8 @@ 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";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable";
enum ServiceResult {
Success,
@ -47,37 +47,46 @@ describe("PrometheusHandler", () => {
let di: DiContainer;
let cluster: Cluster;
beforeEach(() => {
beforeEach(async () => {
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(createKubeAuthProxyInjectable, () => () => ({
apiPrefix: "/some-api-prefix",
exit: () => {},
run: async () => {},
port: 9191,
}));
di.override(directoryForTempInjectable, () => "/some-temp-dir");
di.inject(lensProxyPortInjectable).set(12345);
const writeJsonFile = di.inject(writeJsonFileInjectable);
const kubeConfigPath = "/some/path-to-a-config";
const contextName = "some-context-name";
await writeJsonFile(kubeConfigPath, {
apiVersion: "v1",
kind: "Config",
clusters: [{
name: "some-cluster-name",
cluster: {
server: "https://localhost:8989",
},
}],
users: [{
name: "some-user-name",
}],
contexts: [{
name: contextName,
context: {
user: "some-user-name",
cluster: "some-cluster-name",
},
}],
});
cluster = new Cluster({
contextName: "some-context-name",
contextName,
id: "some-cluster-id",
kubeConfigPath: "/some/path",
}, {
clusterServerUrl: "http://localhost:81",
kubeConfigPath,
});
});

View File

@ -97,8 +97,8 @@ describe("kubeconfig-sync.source tests", () => {
const models = configToModels(config, "/bar");
expect(models.length).toBe(1);
expect(models[0][0].contextName).toBe("context-name");
expect(models[0][0].kubeConfigPath).toBe("/bar");
expect(models[0].contextName).toBe("context-name");
expect(models[0].kubeConfigPath).toBe("/bar");
});
});

View File

@ -38,15 +38,15 @@ const computeKubeconfigDiffInjectable = getInjectable({
}
const rawModels = configToModels(config, filePath);
const models = new Map(rawModels.map(([model, configData]) => [model.contextName, [model, configData] as const]));
const models = new Map(rawModels.map((model) => [model.contextName, model]));
logger.debug(`File now has ${models.size} entries`, { filePath });
for (const [contextName, value] of source) {
const data = models.get(contextName);
const model = models.get(contextName);
// remove and disconnect clusters that were removed from the config
if (!data) {
if (!model) {
// 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);
@ -63,21 +63,16 @@ const computeKubeconfigDiffInjectable = getInjectable({
// diff against that
// or update the model and mark it as not needed to be added
value[0].updateModel(data[0]);
value[0].updateModel(model);
models.delete(contextName);
logger.debug(`Updated old cluster from sync`, { filePath, contextName });
}
for (const [contextName, [model, configData]] of models) {
for (const [contextName, model] of models) {
// add new clusters to the source
try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = getClusterById(clusterId) ?? new Cluster({ ...model, id: clusterId }, configData);
if (!cluster.apiUrl.get()) {
throw new Error("Cluster constructor failed, see above error");
}
const cluster = getClusterById(clusterId) ?? new Cluster({ ...model, id: clusterId });
const entity = catalogEntityFromCluster(cluster);
if (!filePath.startsWith(directoryForKubeConfigs)) {

View File

@ -4,11 +4,11 @@
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterConfigData, UpdateClusterModel } from "../../../common/cluster-types";
import type { UpdateClusterModel } from "../../../common/cluster-types";
import { splitConfig } from "../../../common/kube-helpers";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
export type ConfigToModels = (rootConfig: KubeConfig, filePath: string) => [UpdateClusterModel, ClusterConfigData][];
export type ConfigToModels = (rootConfig: KubeConfig, filePath: string) => UpdateClusterModel[];
const configToModelsInjectable = getInjectable({
id: "config-to-models",
@ -22,15 +22,10 @@ const configToModelsInjectable = getInjectable({
if (validationResult.error) {
logger.debug(`context failed validation: ${validationResult.error}`, { context: config.currentContext, filePath });
} else {
validConfigs.push([
{
kubeConfigPath: filePath,
contextName: config.currentContext,
},
{
clusterServerUrl: validationResult.cluster.server,
},
]);
validConfigs.push({
kubeConfigPath: filePath,
contextName: config.currentContext,
});
}
}

View File

@ -9,22 +9,24 @@ import { getInjectable } from "@ogre-tools/injectable";
import k8SRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import requestClusterVersionInjectable from "./request-cluster-version.injectable";
import type { URL } from "url";
import clusterApiUrlInjectable from "../../features/cluster/connections/main/api-url.injectable";
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.get().includes("azmk8s.io");
const isAKS = (apiUrl: URL) => apiUrl.hostname.includes("azmk8s.io");
const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-");
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 isDigitalOcean = (apiUrl: URL) => apiUrl.hostname.endsWith("k8s.ondigitalocean.com");
const isMinikube = (contextName: string) => contextName.startsWith("minikube");
const isMicrok8s = (contextName: string) => contextName.startsWith("microk8s");
const isKind = (contextName: string) => contextName.startsWith("kubernetes-admin@kind-");
const isDockerDesktop = (contextName: string) => contextName === "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.get() === "rancher-desktop";
const isRancherDesktop = (contextName: string) => contextName === "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");
@ -49,12 +51,14 @@ const clusterDistributionDetectorInjectable = getInjectable({
key: ClusterMetadataKey.DISTRIBUTION,
detect: async (cluster) => {
const version = await requestClusterVersion(cluster);
const apiUrl = await di.inject(clusterApiUrlInjectable, cluster)();
const contextName = cluster.contextName.get();
if (isRke(version)) {
return { value: "rke", accuracy: 80 };
}
if (isRancherDesktop(cluster)) {
if (isRancherDesktop(contextName)) {
return { value: "rancher-desktop", accuracy: 80 };
}
@ -74,11 +78,11 @@ const clusterDistributionDetectorInjectable = getInjectable({
return { value: "iks", accuracy: 80 };
}
if (isAKS(cluster)) {
if (isAKS(apiUrl)) {
return { value: "aks", accuracy: 80 };
}
if (isDigitalOcean(cluster)) {
if (isDigitalOcean(apiUrl)) {
return { value: "digitalocean", accuracy: 90 };
}
@ -106,19 +110,19 @@ const clusterDistributionDetectorInjectable = getInjectable({
return { value: "tencent", accuracy: 90 };
}
if (isMinikube(cluster)) {
if (isMinikube(contextName)) {
return { value: "minikube", accuracy: 80 };
}
if (isMicrok8s(cluster)) {
if (isMicrok8s(contextName)) {
return { value: "microk8s", accuracy: 80 };
}
if (isKind(cluster)) {
if (isKind(contextName)) {
return { value: "kind", accuracy: 70 };
}
if (isDockerDesktop(cluster)) {
if (isDockerDesktop(contextName)) {
return { value: "docker-desktop", accuracy: 80 };
}

View File

@ -9,6 +9,7 @@ import { ClusterMetadataKey } from "../../common/cluster-types";
import { getInjectable } from "@ogre-tools/injectable";
import k8SRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import clusterApiUrlInjectable from "../../features/cluster/connections/main/api-url.injectable";
const clusterIdDetectorFactoryInjectable = getInjectable({
id: "cluster-id-detector-factory",
@ -28,7 +29,7 @@ const clusterIdDetectorFactoryInjectable = getInjectable({
try {
id = await getDefaultNamespaceId(cluster);
} catch(_) {
id = cluster.apiUrl.get();
id = (await di.inject(clusterApiUrlInjectable, cluster)()).toString();
}
const value = createHash("sha256").update(id).digest("hex");

View File

@ -67,8 +67,6 @@ describe("detect-cluster-metadata", () => {
id: "some-id",
contextName: "some-context",
kubeConfigPath: "minikube-config.yml",
}, {
clusterServerUrl: "foo",
});
});

View File

@ -9,6 +9,7 @@ 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/create-kube-auth-proxy.injectable";
import clusterApiUrlInjectable from "../../features/cluster/connections/main/api-url.injectable";
export interface KubeAuthProxyServer {
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;
@ -24,23 +25,23 @@ 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);
const clusterApiUrl = di.inject(clusterApiUrlInjectable, cluster);
const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable, cluster);
let kubeAuthProxy: KubeAuthProxy | undefined = undefined;
let apiTarget: ServerOptions | undefined = undefined;
const ensureServerHelper = async (): Promise<KubeAuthProxy> => {
if (!kubeAuthProxy) {
const proxyEnv = Object.assign({}, process.env);
const proxyEnv = {
...process.env,
};
if (cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = cluster.preferences.httpsProxy;
}
kubeAuthProxy = createKubeAuthProxy(cluster, proxyEnv);
kubeAuthProxy = createKubeAuthProxy(proxyEnv);
}
await kubeAuthProxy.run();
@ -49,31 +50,24 @@ const kubeAuthProxyServerInjectable = getInjectable({
};
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}]`;
}
}
const { hostname } = await clusterApiUrl();
const certificate = di.inject(kubeAuthProxyCertificateInjectable, hostname);
const { port, apiPrefix: path } = await ensureServerHelper();
return {
target: {
protocol: "https:",
host: "127.0.0.1",
port: kubeAuthProxy.port,
path: kubeAuthProxy.apiPrefix,
port,
path,
ca: certificate.cert,
},
changeOrigin: true,
timeout,
secure: true,
headers,
headers: {
Host: hostname,
},
};
};

View File

@ -7,7 +7,6 @@ import "../../common/ipc/cluster";
import type { IComputedValue, IObservableValue, ObservableSet } from "mobx";
import { action, makeObservable, observe, reaction, toJS } from "mobx";
import type { Cluster } from "../../common/cluster/cluster";
import { isErrnoException } from "@k8slens/utilities";
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../../common/ipc";
import { once } from "lodash";
@ -158,26 +157,12 @@ export class ClusterManager {
const cluster = this.dependencies.getClusterById(entity.getId());
if (!cluster) {
const model = {
this.dependencies.addCluster({
id: entity.getId(),
kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext,
accessibleNamespaces: entity.spec.accessibleNamespaces ?? [],
};
try {
/**
* Add the bare minimum of data to ClusterStore. And especially no
* preferences, as those might be configured by the entity's source
*/
this.dependencies.addCluster(model);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
this.dependencies.logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
} else {
this.dependencies.logger.error(`${logPrefix} failed to add cluster: ${error}`, model);
}
}
});
} else {
cluster.kubeConfigPath.set(entity.spec.kubeconfigPath);
cluster.contextName.set(entity.spec.kubeconfigContext);

View File

@ -34,8 +34,6 @@ describe("update-entity-metadata", () => {
id: "some-id",
contextName: "some-context",
kubeConfigPath: "minikube-config.yml",
}, {
clusterServerUrl: "foo",
});
detectedMetadata = {

View File

@ -31,8 +31,6 @@ describe("update-entity-spec", () => {
id: "some-id",
contextName: "some-context",
kubeConfigPath: "minikube-config.yml",
}, {
clusterServerUrl: "foo",
});
entity = new KubernetesCluster({

View File

@ -2,18 +2,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 type { KubeAuthProxyDependencies } from "./kube-auth-proxy";
import { KubeAuthProxyImpl } from "./kube-auth-proxy";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster";
import spawnInjectable from "../child-process/spawn.injectable";
import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.injectable";
import loggerInjectable from "../../common/logger.injectable";
import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-port-is-used.injectable";
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 randomBytesInjectable from "../../common/utils/random-bytes.injectable";
import type { ChildProcess } from "child_process";
import { observable, when } from "mobx";
import assert from "assert";
import clusterApiUrlInjectable from "../../features/cluster/connections/main/api-url.injectable";
import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.injectable";
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
import { TypedRegEx } from "typed-regex";
export interface KubeAuthProxy {
readonly apiPrefix: string;
@ -22,31 +26,159 @@ export interface KubeAuthProxy {
exit: () => void;
}
export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy;
export type CreateKubeAuthProxy = (env: NodeJS.ProcessEnv) => KubeAuthProxy;
const startingServeMatcher = "starting to serve on (?<address>.+)";
const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
rawMatcher: startingServeMatcher,
});
const createKubeAuthProxyInjectable = getInjectable({
id: "create-kube-auth-proxy",
instantiate: (di): CreateKubeAuthProxy => {
const dependencies: Omit<KubeAuthProxyDependencies, "proxyCert" | "broadcastConnectionUpdate"> = {
proxyBinPath: di.inject(lensK8sProxyPathInjectable),
spawn: di.inject(spawnInjectable),
logger: di.inject(loggerInjectable),
waitUntilPortIsUsed: di.inject(waitUntilPortIsUsedInjectable),
getPortFromStream: di.inject(getPortFromStreamInjectable),
dirname: di.inject(getDirnameOfPathInjectable),
};
instantiate: (di, cluster): CreateKubeAuthProxy => {
const lensK8sProxyPath = di.inject(lensK8sProxyPathInjectable);
const spawn = di.inject(spawnInjectable);
const logger = di.inject(loggerInjectable);
const waitUntilPortIsUsed = di.inject(waitUntilPortIsUsedInjectable);
const getPortFromStream = di.inject(getPortFromStreamInjectable);
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
const randomBytes = di.inject(randomBytesInjectable);
const clusterApiUrl = di.inject(clusterApiUrlInjectable, cluster);
const broadcastConnectionUpdate = di.inject(broadcastConnectionUpdateInjectable, cluster);
return (cluster, env) => {
const clusterUrl = new URL(cluster.apiUrl.get());
return (env) => {
let port: number | undefined;
let proxyProcess: ChildProcess | undefined;
const ready = observable.box(false);
const apiPrefix = `/${randomBytes(8).toString("hex")}`;
return new KubeAuthProxyImpl({
...dependencies,
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
}, cluster, env);
const exit = () => {
ready.set(false);
if (proxyProcess) {
logger.debug("[KUBE-AUTH]: stopping local proxy", cluster.getMeta());
proxyProcess.removeAllListeners();
proxyProcess.stderr?.removeAllListeners();
proxyProcess.stdout?.removeAllListeners();
proxyProcess.kill();
proxyProcess = undefined;
}
};
const run = async (): Promise<void> => {
if (proxyProcess) {
return when(() => ready.get());
}
const apiUrl = await clusterApiUrl();
const certificate = di.inject(kubeAuthProxyCertificateInjectable, apiUrl.hostname);
proxyProcess = spawn(lensK8sProxyPath, [], {
env: {
...env,
KUBECONFIG: cluster.kubeConfigPath.get(),
KUBECONFIG_CONTEXT: cluster.contextName.get(),
API_PREFIX: apiPrefix,
PROXY_KEY: certificate.private,
PROXY_CERT: certificate.cert,
},
cwd: getDirnameOfPath(cluster.kubeConfigPath.get()),
});
proxyProcess.on("error", (error) => {
broadcastConnectionUpdate({
level: "error",
message: error.message,
});
exit();
});
proxyProcess.on("exit", (code) => {
if (code) {
broadcastConnectionUpdate({
level: "error",
message: `proxy exited with code: ${code}`,
});
} else {
broadcastConnectionUpdate({
level: "info",
message: "proxy exited successfully",
});
}
exit();
});
proxyProcess.on("disconnect", () => {
broadcastConnectionUpdate({
level: "error",
message: "Proxy disconnected communications",
});
exit();
});
assert(proxyProcess.stderr);
assert(proxyProcess.stdout);
proxyProcess.stderr.on("data", (data: Buffer) => {
if (data.includes("http: TLS handshake error")) {
return;
}
broadcastConnectionUpdate({
level: "error",
message: data.toString(),
});
});
proxyProcess.stdout.on("data", (data: Buffer) => {
if (typeof port === "number") {
broadcastConnectionUpdate({
level: "info",
message: data.toString(),
});
}
});
port = await getPortFromStream(proxyProcess.stdout, {
lineRegex: startingServeRegex,
onFind: () => broadcastConnectionUpdate({
level: "info",
message: "Authentication proxy started",
}),
});
logger.info(`[KUBE-AUTH-PROXY]: found port=${port}`);
try {
await waitUntilPortIsUsed(port, 500, 10000);
ready.set(true);
} catch (error) {
logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error);
broadcastConnectionUpdate({
level: "error",
message: "Proxy port failed to be used within time limit, restarting...",
});
exit();
return run();
}
};
return {
apiPrefix,
exit,
run,
get port() {
assert(port, "port has not yet been initialized");
return port;
},
};
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, cluster: Cluster) => cluster.id,
}),
});
export default createKubeAuthProxyInjectable;

View File

@ -1,168 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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 { observable, when } from "mobx";
import type { SelfSignedCert } from "selfsigned";
import assert from "assert";
import { TypedRegEx } from "typed-regex";
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";
import type { KubeAuthProxy } from "./create-kube-auth-proxy.injectable";
const startingServeMatcher = "starting to serve on (?<address>.+)";
const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
rawMatcher: startingServeMatcher,
});
export interface KubeAuthProxyDependencies {
readonly proxyBinPath: string;
readonly proxyCert: SelfSignedCert;
readonly logger: Logger;
spawn: Spawn;
waitUntilPortIsUsed: WaitUntilPortIsUsed;
getPortFromStream: GetPortFromStream;
dirname: GetDirnameOfPath;
broadcastConnectionUpdate: BroadcastConnectionUpdate;
}
export class KubeAuthProxyImpl implements KubeAuthProxy {
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
public get port(): number {
const port = this._port;
assert(port, "port has not yet been initialized");
return port;
}
protected _port?: number;
protected proxyProcess?: ChildProcess;
protected readonly ready = observable.box(false);
constructor(
private readonly dependencies: KubeAuthProxyDependencies,
protected readonly cluster: Cluster,
protected readonly env: NodeJS.ProcessEnv,
) {}
public async run(): Promise<void> {
if (this.proxyProcess) {
return when(() => this.ready.get());
}
const proxyBin = this.dependencies.proxyBinPath;
const cert = this.dependencies.proxyCert;
this.proxyProcess = this.dependencies.spawn(proxyBin, [], {
env: {
...this.env,
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.get()),
});
this.proxyProcess.on("error", (error) => {
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: error.message,
});
this.exit();
});
this.proxyProcess.on("exit", (code) => {
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.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Proxy disconnected communications",
});
this.exit();
});
assert(this.proxyProcess.stderr);
assert(this.proxyProcess.stdout);
this.proxyProcess.stderr.on("data", (data: Buffer) => {
if (data.includes("http: TLS handshake error")) {
return;
}
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: data.toString(),
});
});
this.proxyProcess.stdout.on("data", (data: Buffer) => {
if (typeof this._port === "number") {
this.dependencies.broadcastConnectionUpdate({
level: "info",
message: data.toString(),
});
}
});
this._port = await this.dependencies.getPortFromStream(this.proxyProcess.stdout, {
lineRegex: startingServeRegex,
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.set(true);
} catch (error) {
this.dependencies.logger.warn("[KUBE-AUTH-PROXY]: waitUntilUsed failed", error);
this.dependencies.broadcastConnectionUpdate({
level: "error",
message: "Proxy port failed to be used within time limit, restarting...",
});
this.exit();
return this.run();
}
}
public exit() {
this.ready.set(false);
if (this.proxyProcess) {
this.dependencies.logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
this.proxyProcess.removeAllListeners();
this.proxyProcess.stderr?.removeAllListeners();
this.proxyProcess.stdout?.removeAllListeners();
this.proxyProcess.kill();
this.proxyProcess = undefined;
}
}
}

View File

@ -6,24 +6,24 @@
import { chunk } from "lodash";
import type { ConnectionOptions } from "tls";
import { connect } from "tls";
import url, { URL } from "url";
import 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";
import clusterApiUrlInjectable from "../../../features/cluster/connections/main/api-url.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 clusterApiUrl = await di.inject(clusterApiUrlInjectable, cluster)();
const kubeAuthProxyServer = di.inject(kubeAuthProxyServerInjectable, cluster);
const kubeAuthProxyCertificate = di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname);
const kubeAuthProxyCertificate = di.inject(kubeAuthProxyCertificateInjectable, clusterApiUrl.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,
@ -33,7 +33,7 @@ const kubeApiUpgradeRequestInjectable = getInjectable({
const proxySocket = connect(connectOpts, () => {
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
proxySocket.write(`Host: ${clusterApiUrl.host}\r\n`);
for (const [key, value] of chunk(req.rawHeaders, 2)) {
if (skipRawHeaders.has(key)) {

View File

@ -5,12 +5,11 @@
import { apiPrefix } from "../../../common/vars";
import { getRouteInjectable } from "../../router/router.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import type { V1Secret } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node";
import { clusterRoute } from "../../router/route";
import { dump } from "js-yaml";
import * as yaml from "js-yaml";
import loadProxyKubeconfigInjectable from "../../cluster/load-proxy-kubeconfig.injectable";
import clusterApiUrlInjectable from "../../../features/cluster/connections/main/api-url.injectable";
const getServiceAccountRouteInjectable = getRouteInjectable({
id: "get-service-account-route",
@ -25,73 +24,57 @@ const getServiceAccountRouteInjectable = getRouteInjectable({
const secretList = await client.listNamespacedSecret(params.namespace);
const secret = secretList.body.items.find(secret => {
const { annotations } = secret.metadata ?? {};
const { annotations = {}} = secret.metadata ?? {};
return annotations?.["kubernetes.io/service-account.name"] === params.account;
return annotations["kubernetes.io/service-account.name"] === params.account;
});
if (!secret) {
if (!secret || !secret.data || !secret.metadata) {
return {
error: "No secret found",
statusCode: 404,
};
}
const kubeconfig = generateKubeConfig(params.account, secret, cluster);
if (!kubeconfig) {
return {
error: "No secret found",
statusCode: 404,
};
}
const { token, "ca.crt": caCrt } = secret.data;
const apiUrl = (await di.inject(clusterApiUrlInjectable, cluster)()).toString();
const contextName = cluster.contextName.get();
return {
response: kubeconfig,
response: yaml.dump({
apiVersion: "v1",
kind: "Config",
clusters: [
{
name: contextName,
cluster: {
server: apiUrl,
"certificate-authority-data": caCrt,
},
},
],
users: [
{
name: params.account,
user: {
token: Buffer.from(token, "base64").toString("utf8"),
},
},
],
contexts: [
{
name: `${contextName}-${params.account}`,
context: {
user: params.account,
cluster: contextName,
namespace: secret.metadata.namespace,
},
},
],
"current-context": contextName,
}),
};
}),
});
export default getServiceAccountRouteInjectable;
function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster): string | undefined {
if (!secret.data || !secret.metadata) {
return undefined;
}
const { token, "ca.crt": caCrt } = secret.data;
const tokenData = Buffer.from(token, "base64");
return dump({
"apiVersion": "v1",
"kind": "Config",
"clusters": [
{
"name": cluster.contextName.get(),
"cluster": {
"server": cluster.apiUrl.get(),
"certificate-authority-data": caCrt,
},
},
],
"users": [
{
"name": username,
"user": {
"token": tokenData.toString("utf8"),
},
},
],
"contexts": [
{
"name": [cluster.contextName.get(), username].join("-"),
"context": {
"user": username,
"cluster": cluster.contextName.get(),
"namespace": secret.metadata.namespace,
},
},
],
"current-context": cluster.contextName.get(),
});
}

View File

@ -103,8 +103,6 @@ describe("technical unit tests for local shell sessions", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-kube-config-path",
}, {
clusterServerUrl: "https://localhost:9999",
});
await openLocalShellSession({

View File

@ -53,8 +53,6 @@ describe("<HpaDetails/>", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
render = renderFor(di);

View File

@ -31,8 +31,6 @@ describe("SecretDetails tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
const secret = new Secret({

View File

@ -62,8 +62,6 @@ describe("<NamespaceSelectFilter />", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
namespaceStore = di.inject(namespaceStoreInjectable);

View File

@ -115,8 +115,6 @@ describe("NamespaceStore", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
namespaceStore = di.inject(namespaceStoreInjectable);

View File

@ -40,8 +40,6 @@ describe("ClusterRoleBindingDialog tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
render = renderFor(di);

View File

@ -36,8 +36,6 @@ describe("RoleBindingDialog tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
render = renderFor(di);

View File

@ -131,8 +131,6 @@ describe("CronJob Store tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
cronJobStore = di.inject(cronJobStoreInjectable);

View File

@ -148,8 +148,6 @@ describe("DaemonSet Store tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
const podStore = di.inject(podStoreInjectable);

View File

@ -220,8 +220,6 @@ describe("Deployment Store tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
const podStore = di.inject(podStoreInjectable);

View File

@ -185,8 +185,6 @@ describe("Job Store tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
jobStore = di.inject(jobStoreInjectable);

View File

@ -131,8 +131,6 @@ describe("Pod Store tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
podStore = di.inject(podStoreInjectable);

View File

@ -148,8 +148,6 @@ describe("ReplicaSet Store tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
const podStore = di.inject(podStoreInjectable);

View File

@ -148,8 +148,6 @@ describe("StatefulSet Store tests", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
statefulSetStore = di.inject(statefulSetStoreInjectable);

View File

@ -55,8 +55,6 @@ describe("ClusterLocalTerminalSettings", () => {
terminalCWD: "/foobar",
defaultNamespace: "kube-system",
},
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
@ -76,8 +74,6 @@ describe("ClusterLocalTerminalSettings", () => {
preferences: {
terminalCWD: "/foobar",
},
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
@ -98,8 +94,6 @@ describe("ClusterLocalTerminalSettings", () => {
preferences: {
terminalCWD: "/foobar",
},
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
@ -129,8 +123,6 @@ describe("ClusterLocalTerminalSettings", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);
@ -161,8 +153,6 @@ describe("ClusterLocalTerminalSettings", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some/path",
}, {
clusterServerUrl: "https://localhost:12345",
});
const dom = render(<ClusterLocalTerminalSetting cluster={cluster}/>);

View File

@ -17,32 +17,6 @@ import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInje
import { runInAction } from "mobx";
import { getInjectable, type DiContainer } from "@ogre-tools/injectable";
const cluster = new Cluster({
contextName: "some-context",
id: "some-id",
kubeConfigPath: "/some/path/to/kubeconfig",
preferences: {
clusterName: "some-cluster-name",
},
}, {
clusterServerUrl: "https://localhost:9999",
});
const clusterEntity = new KubernetesCluster({
metadata: {
labels: {},
name: "some-kubernetes-cluster",
uid: "some-entity-id",
},
spec: {
kubeconfigContext: "some-context",
kubeconfigPath: "/some/path/to/kubeconfig",
},
status: {
phase: "connecting",
},
});
const newMenuItem = getInjectable({
id: "cluster-icon-settings-menu-test-item",
@ -67,7 +41,7 @@ function CustomSettingsComponent(props: ClusterIconSettingComponentProps) {
</span>
</div>
);
}
}
const newSettingsReactComponent = getInjectable({
id: "cluster-icon-settings-react-component",
@ -86,8 +60,31 @@ describe("Icon settings", () => {
beforeEach(() => {
di = getDiForUnitTesting();
const render = renderFor(di);
const cluster = new Cluster({
contextName: "some-context",
id: "some-id",
kubeConfigPath: "/some/path/to/kubeconfig",
preferences: {
clusterName: "some-cluster-name",
},
});
const clusterEntity = new KubernetesCluster({
metadata: {
labels: {},
name: "some-kubernetes-cluster",
uid: "some-entity-id",
},
spec: {
kubeconfigContext: "some-context",
kubeconfigPath: "/some/path/to/kubeconfig",
},
status: {
phase: "connecting",
},
});
rendered = render(
<ClusterIconSetting cluster={cluster} entity={clusterEntity} />,

View File

@ -38,8 +38,6 @@ describe("kube-object-list-layout", () => {
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
render = renderFor(di);

View File

@ -528,8 +528,6 @@ export const getApplicationBuilder = () => {
id: "some-cluster-id",
contextName: "some-context-name",
kubeConfigPath: "/some-path-to-kube-config",
}, {
clusterServerUrl: "https://localhost:12345",
});
windowDi.override(activeKubernetesClusterInjectable, () =>

View File

@ -48,16 +48,11 @@ describe("<ClusterFrame />", () => {
testUsingFakeTime("2000-01-01 12:00:00am");
cluster = new Cluster(
{
contextName: "my-cluster",
id: "123456",
kubeConfigPath: "/irrelavent",
},
{
clusterServerUrl: "https://localhost",
},
);
cluster = new Cluster({
contextName: "my-cluster",
id: "123456",
kubeConfigPath: "/irrelavent",
});
di.override(hostedClusterInjectable, () => cluster);
di.override(hostedClusterIdInjectable, () => cluster.id);

View File

@ -85,6 +85,15 @@ export async function getOrInsertWithAsync<K, V>(map: Map<K, V>, key: K, asyncBu
return map.get(key)!;
}
/**
* Insert `val` into `map` under `key` and then get the value back
*/
export function setAndGet<K, V>(map: Map<K, V>, key: K, val: V): V {
map.set(key, val);
return map.get(key)!;
}
/**
* Set the value associated with `key` iff there was not a previous value
* @param map The map to interact with