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", "@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1", "@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0", "@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1",
"@k8slens/react-application": "^1.0.0-alpha.0", "@k8slens/react-application": "^1.0.0-alpha.0",
"@kubernetes/client-node": "^0.18.1", "@kubernetes/client-node": "^0.18.1",
@ -38266,6 +38265,7 @@
"peerDependencies": { "peerDependencies": {
"@k8slens/application": "^6.5.0-alpha.0", "@k8slens/application": "^6.5.0-alpha.0",
"@k8slens/application-for-electron-main": "^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/legacy-extensions": "^1.0.0-alpha.0",
"@k8slens/messaging": "^1.0.0-alpha.1", "@k8slens/messaging": "^1.0.0-alpha.1",
"@k8slens/messaging-for-main": "^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", "@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1", "@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0", "@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1",
"@k8slens/react-application": "^1.0.0-alpha.0", "@k8slens/react-application": "^1.0.0-alpha.0",
"@kubernetes/client-node": "^0.18.1", "@kubernetes/client-node": "^0.18.1",
@ -324,6 +323,7 @@
"peerDependencies": { "peerDependencies": {
"@k8slens/application": "^6.5.0-alpha.0", "@k8slens/application": "^6.5.0-alpha.0",
"@k8slens/application-for-electron-main": "^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/legacy-extensions": "^1.0.0-alpha.0",
"@k8slens/messaging": "^1.0.0-alpha.1", "@k8slens/messaging": "^1.0.0-alpha.1",
"@k8slens/messaging-for-main": "^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 * The data representing a cluster's state, for passing between main and renderer
*/ */
export interface ClusterState { export interface ClusterState {
apiUrl: string;
online: boolean; online: boolean;
disconnected: boolean; disconnected: boolean;
accessible: boolean; accessible: boolean;

View File

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

View File

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

View File

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

View File

@ -62,8 +62,6 @@ describe("createKubeApiForRemoteCluster", () => {
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
})); }));
fetchMock = asyncFn(); fetchMock = asyncFn();
@ -168,8 +166,6 @@ describe("KubeApi", () => {
contextName: "some-context-name", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig", kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
})); }));
di.override(apiKubeInjectable, () => createKubeJsonApi({ 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 { export interface SplitConfigEntry {
config: KubeConfig; config: KubeConfig;
validationResult: ValidateKubeConfigResult; 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", clusterName: "some-current-context-cluster",
}, },
kubeConfigPath: "./temp-kube-config", kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
}); });
nonCurrentCluster = new Cluster({ nonCurrentCluster = new Cluster({
id: "some-non-current-context-cluster", id: "some-non-current-context-cluster",
@ -119,8 +117,6 @@ describe("Deleting a cluster", () => {
clusterName: "some-non-current-context-cluster", clusterName: "some-non-current-context-cluster",
}, },
kubeConfigPath: "./temp-kube-config", kubeConfigPath: "./temp-kube-config",
}, {
clusterServerUrl: currentClusterServerUrl,
}); });
}); });
@ -197,8 +193,6 @@ describe("Deleting a cluster", () => {
clusterName: "some-cluster", clusterName: "some-cluster",
}, },
kubeConfigPath: joinPaths(directoryForKubeConfigs, "some-cluster.json"), kubeConfigPath: joinPaths(directoryForKubeConfigs, "some-cluster.json"),
}, {
clusterServerUrl: singleClusterServerUrl,
}); });
}); });
@ -233,8 +227,6 @@ describe("Deleting a cluster", () => {
clusterName: "some-cluster", clusterName: "some-cluster",
}, },
kubeConfigPath: "./temp-kube-config", 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 writeFileSyncInjectable from "../../../common/fs/write-file-sync.injectable";
import type { WriteBufferSync } from "../../../common/fs/write-buffer-sync.injectable"; import type { WriteBufferSync } from "../../../common/fs/write-buffer-sync.injectable";
import writeBufferSyncInjectable 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 clustersPersistentStorageInjectable from "./common/storage.injectable";
import type { PersistentStorage } from "../../../common/persistent-storage/create.injectable"; import type { PersistentStorage } from "../../../common/persistent-storage/create.injectable";
import type { AddCluster } from "./common/add.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 getClusterByIdInjectable from "./common/get-by-id.injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import clustersInjectable from "./common/clusters.injectable"; import clustersInjectable from "./common/clusters.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
// NOTE: this is intended to read the actual file system // NOTE: this is intended to read the actual file system
const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png"); const testDataIcon = readFileSync("test-data/cluster-store-migration-icon.png");
@ -102,7 +102,7 @@ describe("cluster storage technical tests", () => {
describe("with foo cluster added", () => { describe("with foo cluster added", () => {
beforeEach(() => { beforeEach(() => {
const cluster = new Cluster({ addCluster({
id: "foo", id: "foo",
contextName: "foo", contextName: "foo",
preferences: { preferences: {
@ -114,11 +114,7 @@ describe("cluster storage technical tests", () => {
getCustomKubeConfigFilePath("foo"), getCustomKubeConfigFilePath("foo"),
kubeconfig, kubeconfig,
), ),
}, {
clusterServerUrl,
}); });
addCluster(cluster);
}); });
it("adds new cluster to store", async () => { 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", () => { describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => { beforeEach(() => {
di.override(storeMigrationVersionInjectable, () => "3.6.0"); 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({ const minimalValidKubeConfig = JSON.stringify({
apiVersion: "v1", apiVersion: "v1",
clusters: [ clusters: [

View File

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

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 { getInjectable } from "@ogre-tools/injectable";
import { comparer, action } from "mobx"; import { comparer, action } from "mobx";
import { clusterStoreMigrationInjectionToken } from "./migration-token"; import { clusterStoreMigrationInjectionToken } from "./migration-token";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import type { ClusterId, ClusterModel } from "../../../../common/cluster-types"; import type { ClusterId, ClusterModel } from "../../../../common/cluster-types";
import { Cluster } from "../../../../common/cluster/cluster"; import { Cluster } from "../../../../common/cluster/cluster";
import loggerInjectable from "../../../../common/logger.injectable"; import loggerInjectable from "../../../../common/logger.injectable";
@ -23,7 +22,6 @@ const clustersPersistentStorageInjectable = getInjectable({
id: "clusters-persistent-storage", id: "clusters-persistent-storage",
instantiate: (di) => { instantiate: (di) => {
const createPersistentStorage = di.inject(createPersistentStorageInjectable); const createPersistentStorage = di.inject(createPersistentStorageInjectable);
const readClusterConfigSync = di.inject(readClusterConfigSyncInjectable);
const clustersState = di.inject(clustersStateInjectable); const clustersState = di.inject(clustersStateInjectable);
const logger = di.inject(loggerInjectable); const logger = di.inject(loggerInjectable);
@ -39,7 +37,6 @@ const clustersPersistentStorageInjectable = getInjectable({
const currentClusters = new Map(clustersState); const currentClusters = new Map(clustersState);
const newClusters = new Map<ClusterId, Cluster>(); const newClusters = new Map<ClusterId, Cluster>();
// update new clusters
for (const clusterModel of clusters) { for (const clusterModel of clusters) {
try { try {
let cluster = currentClusters.get(clusterModel.id); let cluster = currentClusters.get(clusterModel.id);
@ -47,11 +44,9 @@ const clustersPersistentStorageInjectable = getInjectable({
if (cluster) { if (cluster) {
cluster.updateModel(clusterModel); cluster.updateModel(clusterModel);
} else { } else {
cluster = new Cluster( cluster = new Cluster(clusterModel);
clusterModel,
readClusterConfigSync(clusterModel),
);
} }
newClusters.set(clusterModel.id, cluster); newClusters.set(clusterModel.id, cluster);
} catch (error) { } catch (error) {
logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${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. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl/kubectl"; import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
@ -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 createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable";
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.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", () => { describe("create clusters", () => {
let cluster: Cluster; let cluster: Cluster;
@ -26,7 +28,7 @@ describe("create clusters", () => {
beforeEach(() => { beforeEach(() => {
const di = getDiForUnitTesting(); 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(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "some-directory-for-temp"); di.override(directoryForTempInjectable, () => "some-directory-for-temp");
@ -42,27 +44,45 @@ describe("create clusters", () => {
setupPrometheus: jest.fn(), 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, () => ({ di.override(kubeconfigManagerInjectable, () => ({
ensurePath: async () => "/some-proxy-kubeconfig-file", ensurePath: async () => "/some-proxy-kubeconfig-file",
} as Partial<KubeconfigManager> as KubeconfigManager)); } as Partial<KubeconfigManager> as KubeconfigManager));
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
cluster = new Cluster({ const addCluster = di.inject(addClusterInjectable);
cluster = addCluster({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "/minikube-config.yml",
}, {
clusterServerUrl,
}); });
clusterConnection = di.inject(clusterConnectionInjectable, cluster); 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", () => { it("reconnect should not throw if contextHandler is missing", () => {
expect(() => clusterConnection.reconnect()).not.toThrowError(); expect(() => clusterConnection.reconnect()).not.toThrowError();
}); });

View File

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

View File

@ -89,8 +89,6 @@ describe("kubeconfig manager tests", () => {
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "/minikube-config.yml", kubeConfigPath: "/minikube-config.yml",
}, {
clusterServerUrl,
}); });
kubeConfManager = di.inject(kubeconfigManagerInjectable, clusterFake); 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 prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable"; import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
import loadProxyKubeconfigInjectable from "../cluster/load-proxy-kubeconfig.injectable"; import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import { KubeConfig } from "@kubernetes/client-node"; import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable";
enum ServiceResult { enum ServiceResult {
Success, Success,
@ -47,37 +47,46 @@ describe("PrometheusHandler", () => {
let di: DiContainer; let di: DiContainer;
let cluster: Cluster; let cluster: Cluster;
beforeEach(() => { beforeEach(async () => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
di.override(loadProxyKubeconfigInjectable, (di, cluster) => async () => { di.override(createKubeAuthProxyInjectable, () => () => ({
const res = new KubeConfig(); apiPrefix: "/some-api-prefix",
exit: () => {},
res.addCluster({ run: async () => {},
name: "some-cluster-name", port: 9191,
server: cluster.apiUrl.get(), }));
skipTLSVerify: false,
});
res.addContext({
cluster: "some-cluster-name",
name: "some-context-name",
user: "some-user-name",
});
res.addUser({
name: "some-user-name",
});
res.setCurrentContext("some-context-name");
return res;
});
di.override(directoryForTempInjectable, () => "/some-temp-dir"); di.override(directoryForTempInjectable, () => "/some-temp-dir");
di.inject(lensProxyPortInjectable).set(12345); 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({ cluster = new Cluster({
contextName: "some-context-name", contextName,
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some/path", kubeConfigPath,
}, {
clusterServerUrl: "http://localhost:81",
}); });
}); });

View File

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

View File

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

View File

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

View File

@ -9,22 +9,24 @@ import { getInjectable } from "@ogre-tools/injectable";
import k8SRequestInjectable from "../k8s-request.injectable"; import k8SRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import requestClusterVersionInjectable from "./request-cluster-version.injectable"; 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 isGKE = (version: string) => version.includes("gke");
const isEKS = (version: string) => version.includes("eks"); const isEKS = (version: string) => version.includes("eks");
const isIKS = (version: string) => version.includes("IKS"); const isIKS = (version: string) => version.includes("IKS");
const isAKS = (cluster: Cluster) => cluster.apiUrl.get().includes("azmk8s.io"); const isAKS = (apiUrl: URL) => apiUrl.hostname.includes("azmk8s.io");
const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-"); const isMirantis = (version: string) => version.includes("-mirantis-") || version.includes("-docker-");
const isDigitalOcean = (cluster: Cluster) => cluster.apiUrl.get().endsWith("k8s.ondigitalocean.com"); const isDigitalOcean = (apiUrl: URL) => apiUrl.hostname.endsWith("k8s.ondigitalocean.com");
const isMinikube = (cluster: Cluster) => cluster.contextName.get().startsWith("minikube"); const isMinikube = (contextName: string) => contextName.startsWith("minikube");
const isMicrok8s = (cluster: Cluster) => cluster.contextName.get().startsWith("microk8s"); const isMicrok8s = (contextName: string) => contextName.startsWith("microk8s");
const isKind = (cluster: Cluster) => cluster.contextName.get().startsWith("kubernetes-admin@kind-"); const isKind = (contextName: string) => contextName.startsWith("kubernetes-admin@kind-");
const isDockerDesktop = (cluster: Cluster) => cluster.contextName.get() === "docker-desktop"; const isDockerDesktop = (contextName: string) => contextName === "docker-desktop";
const isTke = (version: string) => version.includes("-tke."); const isTke = (version: string) => version.includes("-tke.");
const isCustom = (version: string) => version.includes("+"); const isCustom = (version: string) => version.includes("+");
const isVMWare = (version: string) => version.includes("+vmware"); const isVMWare = (version: string) => version.includes("+vmware");
const isRke = (version: string) => version.includes("-rancher"); const isRke = (version: string) => version.includes("-rancher");
const isRancherDesktop = (cluster: Cluster) => cluster.contextName.get() === "rancher-desktop"; const isRancherDesktop = (contextName: string) => contextName === "rancher-desktop";
const isK3s = (version: string) => version.includes("+k3s"); const isK3s = (version: string) => version.includes("+k3s");
const isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s"); const isK0s = (version: string) => version.includes("-k0s") || version.includes("+k0s");
const isAlibaba = (version: string) => version.includes("-aliyun"); const isAlibaba = (version: string) => version.includes("-aliyun");
@ -49,12 +51,14 @@ const clusterDistributionDetectorInjectable = getInjectable({
key: ClusterMetadataKey.DISTRIBUTION, key: ClusterMetadataKey.DISTRIBUTION,
detect: async (cluster) => { detect: async (cluster) => {
const version = await requestClusterVersion(cluster); const version = await requestClusterVersion(cluster);
const apiUrl = await di.inject(clusterApiUrlInjectable, cluster)();
const contextName = cluster.contextName.get();
if (isRke(version)) { if (isRke(version)) {
return { value: "rke", accuracy: 80 }; return { value: "rke", accuracy: 80 };
} }
if (isRancherDesktop(cluster)) { if (isRancherDesktop(contextName)) {
return { value: "rancher-desktop", accuracy: 80 }; return { value: "rancher-desktop", accuracy: 80 };
} }
@ -74,11 +78,11 @@ const clusterDistributionDetectorInjectable = getInjectable({
return { value: "iks", accuracy: 80 }; return { value: "iks", accuracy: 80 };
} }
if (isAKS(cluster)) { if (isAKS(apiUrl)) {
return { value: "aks", accuracy: 80 }; return { value: "aks", accuracy: 80 };
} }
if (isDigitalOcean(cluster)) { if (isDigitalOcean(apiUrl)) {
return { value: "digitalocean", accuracy: 90 }; return { value: "digitalocean", accuracy: 90 };
} }
@ -106,19 +110,19 @@ const clusterDistributionDetectorInjectable = getInjectable({
return { value: "tencent", accuracy: 90 }; return { value: "tencent", accuracy: 90 };
} }
if (isMinikube(cluster)) { if (isMinikube(contextName)) {
return { value: "minikube", accuracy: 80 }; return { value: "minikube", accuracy: 80 };
} }
if (isMicrok8s(cluster)) { if (isMicrok8s(contextName)) {
return { value: "microk8s", accuracy: 80 }; return { value: "microk8s", accuracy: 80 };
} }
if (isKind(cluster)) { if (isKind(contextName)) {
return { value: "kind", accuracy: 70 }; return { value: "kind", accuracy: 70 };
} }
if (isDockerDesktop(cluster)) { if (isDockerDesktop(contextName)) {
return { value: "docker-desktop", accuracy: 80 }; 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 { getInjectable } from "@ogre-tools/injectable";
import k8SRequestInjectable from "../k8s-request.injectable"; import k8SRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import clusterApiUrlInjectable from "../../features/cluster/connections/main/api-url.injectable";
const clusterIdDetectorFactoryInjectable = getInjectable({ const clusterIdDetectorFactoryInjectable = getInjectable({
id: "cluster-id-detector-factory", id: "cluster-id-detector-factory",
@ -28,7 +29,7 @@ const clusterIdDetectorFactoryInjectable = getInjectable({
try { try {
id = await getDefaultNamespaceId(cluster); id = await getDefaultNamespaceId(cluster);
} catch(_) { } catch(_) {
id = cluster.apiUrl.get(); id = (await di.inject(clusterApiUrlInjectable, cluster)()).toString();
} }
const value = createHash("sha256").update(id).digest("hex"); const value = createHash("sha256").update(id).digest("hex");

View File

@ -67,8 +67,6 @@ describe("detect-cluster-metadata", () => {
id: "some-id", id: "some-id",
contextName: "some-context", contextName: "some-context",
kubeConfigPath: "minikube-config.yml", 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 createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.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 type { KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import clusterApiUrlInjectable from "../../features/cluster/connections/main/api-url.injectable";
export interface KubeAuthProxyServer { export interface KubeAuthProxyServer {
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>; getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;
@ -24,23 +25,23 @@ const thirtySecondsInMs = 30 * 1000;
const kubeAuthProxyServerInjectable = getInjectable({ const kubeAuthProxyServerInjectable = getInjectable({
id: "kube-auth-proxy-server", id: "kube-auth-proxy-server",
instantiate: (di, cluster): KubeAuthProxyServer => { instantiate: (di, cluster): KubeAuthProxyServer => {
const clusterUrl = new URL(cluster.apiUrl.get()); const clusterApiUrl = di.inject(clusterApiUrlInjectable, cluster);
const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable, cluster);
const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
const certificate = di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname);
let kubeAuthProxy: KubeAuthProxy | undefined = undefined; let kubeAuthProxy: KubeAuthProxy | undefined = undefined;
let apiTarget: ServerOptions | undefined = undefined; let apiTarget: ServerOptions | undefined = undefined;
const ensureServerHelper = async (): Promise<KubeAuthProxy> => { const ensureServerHelper = async (): Promise<KubeAuthProxy> => {
if (!kubeAuthProxy) { if (!kubeAuthProxy) {
const proxyEnv = Object.assign({}, process.env); const proxyEnv = {
...process.env,
};
if (cluster.preferences.httpsProxy) { if (cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = cluster.preferences.httpsProxy; proxyEnv.HTTPS_PROXY = cluster.preferences.httpsProxy;
} }
kubeAuthProxy = createKubeAuthProxy(cluster, proxyEnv); kubeAuthProxy = createKubeAuthProxy(proxyEnv);
} }
await kubeAuthProxy.run(); await kubeAuthProxy.run();
@ -49,31 +50,24 @@ const kubeAuthProxyServerInjectable = getInjectable({
}; };
const newApiTarget = async (timeout: number): Promise<ServerOptions> => { const newApiTarget = async (timeout: number): Promise<ServerOptions> => {
const kubeAuthProxy = await ensureServerHelper(); const { hostname } = await clusterApiUrl();
const headers: Record<string, string> = {}; const certificate = di.inject(kubeAuthProxyCertificateInjectable, hostname);
const { port, apiPrefix: path } = await ensureServerHelper();
if (clusterUrl.hostname) {
headers.Host = clusterUrl.hostname;
// fix current IPv6 inconsistency in url.Parse() and httpProxy.
// with url.Parse the IPv6 Hostname has no Square brackets but httpProxy needs the Square brackets to work.
if (headers.Host.includes(":")) {
headers.Host = `[${headers.Host}]`;
}
}
return { return {
target: { target: {
protocol: "https:", protocol: "https:",
host: "127.0.0.1", host: "127.0.0.1",
port: kubeAuthProxy.port, port,
path: kubeAuthProxy.apiPrefix, path,
ca: certificate.cert, ca: certificate.cert,
}, },
changeOrigin: true, changeOrigin: true,
timeout, timeout,
secure: true, secure: true,
headers, headers: {
Host: hostname,
},
}; };
}; };

View File

@ -7,7 +7,6 @@ import "../../common/ipc/cluster";
import type { IComputedValue, IObservableValue, ObservableSet } from "mobx"; import type { IComputedValue, IObservableValue, ObservableSet } from "mobx";
import { action, makeObservable, observe, reaction, toJS } from "mobx"; import { action, makeObservable, observe, reaction, toJS } from "mobx";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import { isErrnoException } from "@k8slens/utilities";
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster"; import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../../common/ipc"; import { ipcMainOn } from "../../common/ipc";
import { once } from "lodash"; import { once } from "lodash";
@ -158,26 +157,12 @@ export class ClusterManager {
const cluster = this.dependencies.getClusterById(entity.getId()); const cluster = this.dependencies.getClusterById(entity.getId());
if (!cluster) { if (!cluster) {
const model = { this.dependencies.addCluster({
id: entity.getId(), id: entity.getId(),
kubeConfigPath: entity.spec.kubeconfigPath, kubeConfigPath: entity.spec.kubeconfigPath,
contextName: entity.spec.kubeconfigContext, contextName: entity.spec.kubeconfigContext,
accessibleNamespaces: entity.spec.accessibleNamespaces ?? [], 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 { } else {
cluster.kubeConfigPath.set(entity.spec.kubeconfigPath); cluster.kubeConfigPath.set(entity.spec.kubeconfigPath);
cluster.contextName.set(entity.spec.kubeconfigContext); cluster.contextName.set(entity.spec.kubeconfigContext);

View File

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

View File

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

View File

@ -2,18 +2,22 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { KubeAuthProxyDependencies } from "./kube-auth-proxy";
import { KubeAuthProxyImpl } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster"; import type { Cluster } from "../../common/cluster/cluster";
import spawnInjectable from "../child-process/spawn.injectable"; import spawnInjectable from "../child-process/spawn.injectable";
import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-port-is-used.injectable"; import waitUntilPortIsUsedInjectable from "./wait-until-port-is-used/wait-until-port-is-used.injectable";
import lensK8sProxyPathInjectable from "./lens-k8s-proxy-path.injectable"; import lensK8sProxyPathInjectable from "./lens-k8s-proxy-path.injectable";
import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectable"; import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectable";
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import 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 broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
import { TypedRegEx } from "typed-regex";
export interface KubeAuthProxy { export interface KubeAuthProxy {
readonly apiPrefix: string; readonly apiPrefix: string;
@ -22,31 +26,159 @@ export interface KubeAuthProxy {
exit: () => void; 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({ const createKubeAuthProxyInjectable = getInjectable({
id: "create-kube-auth-proxy", id: "create-kube-auth-proxy",
instantiate: (di): CreateKubeAuthProxy => { instantiate: (di, cluster): CreateKubeAuthProxy => {
const dependencies: Omit<KubeAuthProxyDependencies, "proxyCert" | "broadcastConnectionUpdate"> = { const lensK8sProxyPath = di.inject(lensK8sProxyPathInjectable);
proxyBinPath: di.inject(lensK8sProxyPathInjectable), const spawn = di.inject(spawnInjectable);
spawn: di.inject(spawnInjectable), const logger = di.inject(loggerInjectable);
logger: di.inject(loggerInjectable), const waitUntilPortIsUsed = di.inject(waitUntilPortIsUsedInjectable);
waitUntilPortIsUsed: di.inject(waitUntilPortIsUsedInjectable), const getPortFromStream = di.inject(getPortFromStreamInjectable);
getPortFromStream: di.inject(getPortFromStreamInjectable), const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
dirname: di.inject(getDirnameOfPathInjectable), const randomBytes = di.inject(randomBytesInjectable);
}; const clusterApiUrl = di.inject(clusterApiUrlInjectable, cluster);
const broadcastConnectionUpdate = di.inject(broadcastConnectionUpdateInjectable, cluster);
return (cluster, env) => { return (env) => {
const clusterUrl = new URL(cluster.apiUrl.get()); let port: number | undefined;
let proxyProcess: ChildProcess | undefined;
const ready = observable.box(false);
const apiPrefix = `/${randomBytes(8).toString("hex")}`;
return new KubeAuthProxyImpl({ const exit = () => {
...dependencies, ready.set(false);
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster), if (proxyProcess) {
}, cluster, env); 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; 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 { chunk } from "lodash";
import type { ConnectionOptions } from "tls"; import type { ConnectionOptions } from "tls";
import { connect } from "tls"; import { connect } from "tls";
import url, { URL } from "url"; import url from "url";
import { apiKubePrefix } from "../../../common/vars"; import { apiKubePrefix } from "../../../common/vars";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { LensProxyApiRequest } from "../lens-proxy"; import type { LensProxyApiRequest } from "../lens-proxy";
import kubeAuthProxyServerInjectable from "../../cluster/kube-auth-proxy-server.injectable"; import kubeAuthProxyServerInjectable from "../../cluster/kube-auth-proxy-server.injectable";
import kubeAuthProxyCertificateInjectable from "../../kube-auth-proxy/kube-auth-proxy-certificate.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 skipRawHeaders = new Set(["Host", "Authorization"]);
const kubeApiUpgradeRequestInjectable = getInjectable({ const kubeApiUpgradeRequestInjectable = getInjectable({
id: "kube-api-upgrade-request", id: "kube-api-upgrade-request",
instantiate: (di): LensProxyApiRequest => async ({ req, socket, head, cluster }) => { 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 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 proxyUrl = await kubeAuthProxyServer.ensureAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
const apiUrl = url.parse(cluster.apiUrl.get());
const pUrl = url.parse(proxyUrl); const pUrl = url.parse(proxyUrl);
const connectOpts: ConnectionOptions = { const connectOpts: ConnectionOptions = {
port: pUrl.port ? parseInt(pUrl.port) : undefined, port: pUrl.port ? parseInt(pUrl.port) : undefined,
@ -33,7 +33,7 @@ const kubeApiUpgradeRequestInjectable = getInjectable({
const proxySocket = connect(connectOpts, () => { const proxySocket = 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: ${clusterApiUrl.host}\r\n`);
for (const [key, value] of chunk(req.rawHeaders, 2)) { for (const [key, value] of chunk(req.rawHeaders, 2)) {
if (skipRawHeaders.has(key)) { if (skipRawHeaders.has(key)) {

View File

@ -5,12 +5,11 @@
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { getRouteInjectable } from "../../router/router.injectable"; 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 { CoreV1Api } from "@kubernetes/client-node";
import { clusterRoute } from "../../router/route"; 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 loadProxyKubeconfigInjectable from "../../cluster/load-proxy-kubeconfig.injectable";
import clusterApiUrlInjectable from "../../../features/cluster/connections/main/api-url.injectable";
const getServiceAccountRouteInjectable = getRouteInjectable({ const getServiceAccountRouteInjectable = getRouteInjectable({
id: "get-service-account-route", id: "get-service-account-route",
@ -25,73 +24,57 @@ const getServiceAccountRouteInjectable = getRouteInjectable({
const secretList = await client.listNamespacedSecret(params.namespace); const secretList = await client.listNamespacedSecret(params.namespace);
const secret = secretList.body.items.find(secret => { const secret = secretList.body.items.find(secret => {
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 { return {
error: "No secret found", error: "No secret found",
statusCode: 404, statusCode: 404,
}; };
} }
const kubeconfig = generateKubeConfig(params.account, secret, cluster); const { token, "ca.crt": caCrt } = secret.data;
const apiUrl = (await di.inject(clusterApiUrlInjectable, cluster)()).toString();
if (!kubeconfig) { const contextName = cluster.contextName.get();
return {
error: "No secret found",
statusCode: 404,
};
}
return { 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; 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", contextName: "some-context-name",
id: "some-cluster-id", id: "some-cluster-id",
kubeConfigPath: "/some-kube-config-path", kubeConfigPath: "/some-kube-config-path",
}, {
clusterServerUrl: "https://localhost:9999",
}); });
await openLocalShellSession({ await openLocalShellSession({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,32 +17,6 @@ import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInje
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import { getInjectable, type DiContainer } from "@ogre-tools/injectable"; 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({ const newMenuItem = getInjectable({
id: "cluster-icon-settings-menu-test-item", id: "cluster-icon-settings-menu-test-item",
@ -67,7 +41,7 @@ function CustomSettingsComponent(props: ClusterIconSettingComponentProps) {
</span> </span>
</div> </div>
); );
} }
const newSettingsReactComponent = getInjectable({ const newSettingsReactComponent = getInjectable({
id: "cluster-icon-settings-react-component", id: "cluster-icon-settings-react-component",
@ -86,8 +60,31 @@ describe("Icon settings", () => {
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
const render = renderFor(di); 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( rendered = render(
<ClusterIconSetting cluster={cluster} entity={clusterEntity} />, <ClusterIconSetting cluster={cluster} entity={clusterEntity} />,

View File

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

View File

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

View File

@ -48,16 +48,11 @@ describe("<ClusterFrame />", () => {
testUsingFakeTime("2000-01-01 12:00:00am"); testUsingFakeTime("2000-01-01 12:00:00am");
cluster = new Cluster( cluster = new Cluster({
{ contextName: "my-cluster",
contextName: "my-cluster", id: "123456",
id: "123456", kubeConfigPath: "/irrelavent",
kubeConfigPath: "/irrelavent", });
},
{
clusterServerUrl: "https://localhost",
},
);
di.override(hostedClusterInjectable, () => cluster); di.override(hostedClusterInjectable, () => cluster);
di.override(hostedClusterIdInjectable, () => cluster.id); 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)!; 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 * Set the value associated with `key` iff there was not a previous value
* @param map The map to interact with * @param map The map to interact with