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:
parent
18d660ea77
commit
fe2fd4c1fd
2
package-lock.json
generated
2
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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}`);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -89,8 +89,6 @@ describe("kubeconfig manager tests", () => {
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "/minikube-config.yml",
|
||||
}, {
|
||||
clusterServerUrl,
|
||||
});
|
||||
|
||||
kubeConfManager = di.inject(kubeconfigManagerInjectable, clusterFake);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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([
|
||||
{
|
||||
validConfigs.push({
|
||||
kubeConfigPath: filePath,
|
||||
contextName: config.currentContext,
|
||||
},
|
||||
{
|
||||
clusterServerUrl: validationResult.cluster.server,
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -67,8 +67,6 @@ describe("detect-cluster-metadata", () => {
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
}, {
|
||||
clusterServerUrl: "foo",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -34,8 +34,6 @@ describe("update-entity-metadata", () => {
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
}, {
|
||||
clusterServerUrl: "foo",
|
||||
});
|
||||
|
||||
detectedMetadata = {
|
||||
|
||||
@ -31,8 +31,6 @@ describe("update-entity-spec", () => {
|
||||
id: "some-id",
|
||||
contextName: "some-context",
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
}, {
|
||||
clusterServerUrl: "foo",
|
||||
});
|
||||
|
||||
entity = new KubernetesCluster({
|
||||
|
||||
@ -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 (env) => {
|
||||
let port: number | undefined;
|
||||
let proxyProcess: ChildProcess | undefined;
|
||||
const ready = observable.box(false);
|
||||
const apiPrefix = `/${randomBytes(8).toString("hex")}`;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
return (cluster, env) => {
|
||||
const clusterUrl = new URL(cluster.apiUrl.get());
|
||||
const run = async (): Promise<void> => {
|
||||
if (proxyProcess) {
|
||||
return when(() => ready.get());
|
||||
}
|
||||
|
||||
return new KubeAuthProxyImpl({
|
||||
...dependencies,
|
||||
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
|
||||
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
|
||||
}, cluster, env);
|
||||
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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)) {
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}/>);
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -88,6 +62,29 @@ describe("Icon settings", () => {
|
||||
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} />,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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, () =>
|
||||
|
||||
@ -48,16 +48,11 @@ describe("<ClusterFrame />", () => {
|
||||
|
||||
testUsingFakeTime("2000-01-01 12:00:00am");
|
||||
|
||||
cluster = new Cluster(
|
||||
{
|
||||
cluster = new Cluster({
|
||||
contextName: "my-cluster",
|
||||
id: "123456",
|
||||
kubeConfigPath: "/irrelavent",
|
||||
},
|
||||
{
|
||||
clusterServerUrl: "https://localhost",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
di.override(hostedClusterInjectable, () => cluster);
|
||||
di.override(hostedClusterIdInjectable, () => cluster.id);
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user