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

Cascade different refactorings to eliminate global shared state from app paths

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2021-12-28 15:47:15 +02:00
parent 908a40975e
commit 9d33fff906
No known key found for this signature in database
GPG Key ID: 5F465B5672372402
336 changed files with 9105 additions and 3307 deletions

View File

@ -12,6 +12,9 @@
<excludeFolder url="file://$MODULE_DIR$/extensions/pod-menu/node_modules" /> <excludeFolder url="file://$MODULE_DIR$/extensions/pod-menu/node_modules" />
<excludeFolder url="file://$MODULE_DIR$/static/build" /> <excludeFolder url="file://$MODULE_DIR$/static/build" />
<excludeFolder url="file://$MODULE_DIR$/src/extensions/npm/extensions/dist" /> <excludeFolder url="file://$MODULE_DIR$/src/extensions/npm/extensions/dist" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/binaries" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// This mock exists because library causes criminal side-effect on import
export const autoUpdater = {};

23
__mocks__/node-pty.ts Normal file
View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// This mock exists because library causes criminal side-effect on import
export default {};

View File

@ -196,8 +196,8 @@
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1", "@kubernetes/client-node": "^0.16.1",
"@ogre-tools/injectable": "2.0.0", "@ogre-tools/injectable": "3.0.0",
"@ogre-tools/injectable-react": "2.0.0", "@ogre-tools/injectable-react": "3.0.0",
"@sentry/electron": "^2.5.4", "@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0", "@sentry/integrations": "^6.15.0",
"abort-controller": "^3.0.0", "abort-controller": "^3.0.0",

View File

@ -20,29 +20,18 @@
*/ */
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { AppPaths } from "../app-paths";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store";
import { action, comparer, makeObservable, observable, toJS } from "mobx"; import { action, comparer, makeObservable, observable, toJS } from "mobx";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
AppPaths.init(); import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: { ipcMain: {
handle: jest.fn(),
on: jest.fn(), on: jest.fn(),
removeAllListeners: jest.fn(),
off: jest.fn(), off: jest.fn(),
send: jest.fn(),
}, },
})); }));
@ -105,10 +94,17 @@ describe("BaseStore", () => {
let store: TestStore; let store: TestStore;
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory");
await dis.runSetups();
store = undefined; store = undefined;
TestStore.resetInstance(); TestStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-user-data-directory": {
"test-store.json": JSON.stringify({}), "test-store.json": JSON.stringify({}),
}, },
}; };
@ -130,7 +126,7 @@ describe("BaseStore", () => {
a: "foo", b: "bar", c: "hello", a: "foo", b: "bar", c: "hello",
}); });
const data = JSON.parse(readFileSync("tmp/test-store.json").toString()); const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString());
expect(data).toEqual({ a: "foo", b: "bar", c: "hello" }); expect(data).toEqual({ a: "foo", b: "bar", c: "hello" });
}); });
@ -153,7 +149,7 @@ describe("BaseStore", () => {
expect(fileSpy).toHaveBeenCalledTimes(2); expect(fileSpy).toHaveBeenCalledTimes(2);
const data = JSON.parse(readFileSync("tmp/test-store.json").toString()); const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString());
expect(data).toEqual({ a: "a", b: "b", c: "" }); expect(data).toEqual({ a: "a", b: "b", c: "" });
}); });

View File

@ -24,17 +24,29 @@ import mockFs from "mock-fs";
import yaml from "js-yaml"; import yaml from "js-yaml";
import path from "path"; import path from "path";
import fse from "fs-extra"; import fse from "fs-extra";
import { Cluster } from "../../main/cluster"; import type { Cluster } from "../cluster/cluster";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store/cluster-store";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import type { ClusterId } from "../cluster-types"; import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
import { getCustomKubeConfigPath } from "../utils"; import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import { AppPaths } from "../app-paths"; import type { ClusterModel } from "../cluster-types";
import type {
DependencyInjectionContainer,
} from "@ogre-tools/injectable";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); const testDataIcon = fs.readFileSync(
"test-data/cluster-store-migration-icon.png",
);
const kubeconfig = ` const kubeconfig = `
apiVersion: v1 apiVersion: v1
clusters: clusters:
@ -59,25 +71,17 @@ users:
token: kubeconfig-user-q4lm4:xxxyyyy token: kubeconfig-user-q4lm4:xxxyyyy
`; `;
function embed(clusterId: ClusterId, contents: any): string { const embed = (directoryName: string, contents: any): string => {
const absPath = getCustomKubeConfigPath(clusterId); fse.ensureDirSync(path.dirname(directoryName));
fse.writeFileSync(directoryName, contents, {
encoding: "utf-8",
mode: 0o600,
});
fse.ensureDirSync(path.dirname(absPath)); return directoryName;
fse.writeFileSync(absPath, contents, { encoding: "utf-8", mode: 0o600 }); };
return absPath;
}
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: { ipcMain: {
handle: jest.fn(), handle: jest.fn(),
on: jest.fn(), on: jest.fn(),
@ -87,21 +91,46 @@ jest.mock("electron", () => ({
}, },
})); }));
AppPaths.init(); describe("cluster-store", () => {
let mainDi: DependencyInjectionContainer;
let clusterStore: ClusterStore;
let createCluster: (model: ClusterModel) => Cluster;
describe("empty config", () => {
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mockFs();
mainDi = dis.mainDi;
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await dis.runSetups();
createCluster = mainDi.inject(createClusterInjectionToken);
});
describe("empty config", () => {
let getCustomKubeConfigDirectory: (directoryName: string) => string;
beforeEach(async () => {
getCustomKubeConfigDirectory = mainDi.inject(
getCustomKubeConfigDirectoryInjectable,
);
// TODO: Remove these by removing Singleton base-class from BaseStore
ClusterStore.getInstance(false)?.unregisterIpcListener(); ClusterStore.getInstance(false)?.unregisterIpcListener();
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({}), "lens-cluster-store.json": JSON.stringify({}),
}, },
}; };
mockFs(mockOpts); mockFs(mockOpts);
ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -110,32 +139,37 @@ describe("empty config", () => {
describe("with foo cluster added", () => { describe("with foo cluster added", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.getInstance().addCluster( const cluster = createCluster({
new Cluster({
id: "foo", id: "foo",
contextName: "foo", contextName: "foo",
preferences: { preferences: {
terminalCWD: "/tmp", terminalCWD: "/some-directory-for-user-data",
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
clusterName: "minikube", clusterName: "minikube",
}, },
kubeConfigPath: embed("foo", kubeconfig), kubeConfigPath: embed(
}), getCustomKubeConfigDirectory("foo"),
); kubeconfig,
),
});
clusterStore.addCluster(cluster);
}); });
it("adds new cluster to store", async () => { it("adds new cluster to store", async () => {
const storedCluster = ClusterStore.getInstance().getById("foo"); const storedCluster = clusterStore.getById("foo");
expect(storedCluster.id).toBe("foo"); expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/tmp"); expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data");
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5"); expect(storedCluster.preferences.icon).toBe(
"data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
);
}); });
}); });
describe("with prod and dev clusters added", () => { describe("with prod and dev clusters added", () => {
beforeEach(() => { beforeEach(() => {
const store = ClusterStore.getInstance(); const store = clusterStore;
store.addCluster({ store.addCluster({
id: "prod", id: "prod",
@ -143,7 +177,10 @@ describe("empty config", () => {
preferences: { preferences: {
clusterName: "prod", clusterName: "prod",
}, },
kubeConfigPath: embed("prod", kubeconfig), kubeConfigPath: embed(
getCustomKubeConfigDirectory("prod"),
kubeconfig,
),
}); });
store.addCluster({ store.addCluster({
id: "dev", id: "dev",
@ -151,29 +188,33 @@ describe("empty config", () => {
preferences: { preferences: {
clusterName: "dev", clusterName: "dev",
}, },
kubeConfigPath: embed("dev", kubeconfig), kubeConfigPath: embed(
getCustomKubeConfigDirectory("dev"),
kubeconfig,
),
}); });
}); });
it("check if store can contain multiple clusters", () => { it("check if store can contain multiple clusters", () => {
expect(ClusterStore.getInstance().hasClusters()).toBeTruthy(); expect(clusterStore.hasClusters()).toBeTruthy();
expect(ClusterStore.getInstance().clusters.size).toBe(2); expect(clusterStore.clusters.size).toBe(2);
}); });
it("check if cluster's kubeconfig file saved", () => { it("check if cluster's kubeconfig file saved", () => {
const file = embed("boo", "kubeconfig"); const file = embed(getCustomKubeConfigDirectory("boo"), "kubeconfig");
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
}); });
}); });
}); });
describe("config with existing clusters", () => { describe("config with existing clusters", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"temp-kube-config": kubeconfig, "temp-kube-config": kubeconfig,
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {
@ -209,7 +250,7 @@ describe("config with existing clusters", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -217,14 +258,14 @@ describe("config with existing clusters", () => {
}); });
it("allows to retrieve a cluster", () => { it("allows to retrieve a cluster", () => {
const storedCluster = ClusterStore.getInstance().getById("cluster1"); const storedCluster = clusterStore.getById("cluster1");
expect(storedCluster.id).toBe("cluster1"); expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo"); expect(storedCluster.preferences.terminalCWD).toBe("/foo");
}); });
it("allows getting all of the clusters", async () => { it("allows getting all of the clusters", async () => {
const storedClusters = ClusterStore.getInstance().clustersList; const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(3); expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1"); expect(storedClusters[0].id).toBe("cluster1");
@ -233,9 +274,9 @@ describe("config with existing clusters", () => {
expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2"); expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2");
expect(storedClusters[2].id).toBe("cluster3"); expect(storedClusters[2].id).toBe("cluster3");
}); });
}); });
describe("config with invalid cluster kubeconfig", () => { describe("config with invalid cluster kubeconfig", () => {
beforeEach(() => { beforeEach(() => {
const invalidKubeconfig = ` const invalidKubeconfig = `
apiVersion: v1 apiVersion: v1
@ -258,10 +299,11 @@ users:
`; `;
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"invalid-kube-config": invalidKubeconfig, "invalid-kube-config": invalidKubeconfig,
"valid-kube-config": kubeconfig, "valid-kube-config": kubeconfig,
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {
@ -283,7 +325,6 @@ users:
preferences: { terminalCWD: "/foo" }, preferences: { terminalCWD: "/foo" },
workspace: "default", workspace: "default",
}, },
], ],
}), }),
}, },
@ -291,7 +332,7 @@ users:
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -299,44 +340,18 @@ users:
}); });
it("does not enable clusters with invalid kubeconfig", () => { it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = ClusterStore.getInstance().clustersList; const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(1); expect(storedClusters.length).toBe(1);
}); });
}); });
const minimalValidKubeConfig = JSON.stringify({ describe("pre 2.0 config with an existing cluster", () => {
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",
user: {
"client-certificate": "/Users/foo/.minikube/client.crt",
"client-key": "/Users/foo/.minikube/client.key",
},
}],
kind: "Config",
preferences: {},
});
describe("pre 2.0 config with an existing cluster", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {
@ -350,7 +365,7 @@ describe("pre 2.0 config with an existing cluster", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -358,17 +373,17 @@ describe("pre 2.0 config with an existing cluster", () => {
}); });
it("migrates to modern format with kubeconfig in a file", async () => { it("migrates to modern format with kubeconfig in a file", async () => {
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath; const config = clusterStore.clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`); expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`);
}); });
}); });
describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => { describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {
@ -378,40 +393,42 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
cluster1: { cluster1: {
kubeConfig: JSON.stringify({ kubeConfig: JSON.stringify({
apiVersion: "v1", apiVersion: "v1",
clusters: [{ clusters: [
{
cluster: { cluster: {
server: "https://10.211.55.6:8443", server: "https://10.211.55.6:8443",
}, },
name: "minikube", name: "minikube",
}], },
contexts: [{ ],
contexts: [
{
context: { context: {
cluster: "minikube", cluster: "minikube",
user: "minikube", user: "minikube",
name: "minikube", name: "minikube",
}, },
name: "minikube", name: "minikube",
}], },
],
"current-context": "minikube", "current-context": "minikube",
kind: "Config", kind: "Config",
preferences: {}, preferences: {},
users: [{ users: [
{
name: "minikube", name: "minikube",
user: { user: {
"client-certificate": "/Users/foo/.minikube/client.crt", "client-certificate": "/Users/foo/.minikube/client.crt",
"client-key": "/Users/foo/.minikube/client.key", "client-key": "/Users/foo/.minikube/client.key",
"auth-provider": { "auth-provider": {
config: { config: {
"access-token": [ "access-token": ["should be string"],
"should be string", expiry: ["should be string"],
},
},
},
},
], ],
expiry: [
"should be string",
],
},
},
},
}],
}), }),
}, },
}), }),
@ -420,7 +437,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -428,20 +445,24 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
}); });
it("replaces array format access token and expiry into string", async () => { it("replaces array format access token and expiry into string", async () => {
const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath; const file = clusterStore.clustersList[0].kubeConfigPath;
const config = fs.readFileSync(file, "utf8"); const config = fs.readFileSync(file, "utf8");
const kc = yaml.load(config) as Record<string, any>; const kc = yaml.load(config) as Record<string, any>;
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string"); expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe(
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string"); "should be string",
);
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe(
"should be string",
);
});
}); });
});
describe("pre 2.6.0 config with a cluster icon", () => { describe("pre 2.6.0 config with a cluster icon", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {
@ -452,17 +473,17 @@ describe("pre 2.6.0 config with a cluster icon", () => {
kubeConfig: minimalValidKubeConfig, kubeConfig: minimalValidKubeConfig,
icon: "icon_path", icon: "icon_path",
preferences: { preferences: {
terminalCWD: "/tmp", terminalCWD: "/some-directory-for-user-data",
}, },
}, },
}), }),
"icon_path": testDataIcon, icon_path: testDataIcon,
}, },
}; };
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -470,19 +491,21 @@ describe("pre 2.6.0 config with a cluster icon", () => {
}); });
it("moves the icon into preferences", async () => { it("moves the icon into preferences", async () => {
const storedClusterData = ClusterStore.getInstance().clustersList[0]; const storedClusterData = clusterStore.clustersList[0];
expect(storedClusterData.hasOwnProperty("icon")).toBe(false); expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true); expect(
storedClusterData.preferences.icon.startsWith("data:;base64,"),
).toBe(true);
});
}); });
});
describe("for a pre 2.7.0-beta.0 config without a workspace", () => { describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {
@ -492,7 +515,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
cluster1: { cluster1: {
kubeConfig: minimalValidKubeConfig, kubeConfig: minimalValidKubeConfig,
preferences: { preferences: {
terminalCWD: "/tmp", terminalCWD: "/some-directory-for-user-data",
}, },
}, },
}), }),
@ -501,19 +524,19 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}); });
}); });
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(() => {
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {
@ -531,13 +554,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
}, },
], ],
}), }),
"icon_path": testDataIcon, icon_path: testDataIcon,
}, },
}; };
mockFs(mockOpts); mockFs(mockOpts);
return ClusterStore.createInstance(); clusterStore = mainDi.inject(clusterStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -545,14 +568,47 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
}); });
it("migrates to modern format with kubeconfig in a file", async () => { it("migrates to modern format with kubeconfig in a file", async () => {
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath; const config = clusterStore.clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig); expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
}); });
it("migrates to modern format with icon not in file", async () => { it("migrates to modern format with icon not in file", async () => {
const { icon } = ClusterStore.getInstance().clustersList[0].preferences; const { icon } = clusterStore.clustersList[0].preferences;
expect(icon.startsWith("data:;base64,")).toBe(true); expect(icon.startsWith("data:;base64,")).toBe(true);
}); });
});
});
const minimalValidKubeConfig = JSON.stringify({
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",
user: {
"client-certificate": "/Users/foo/.minikube/client.crt",
"client-key": "/Users/foo/.minikube/client.key",
},
},
],
kind: "Config",
preferences: {},
}); });

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { appEventBus, AppEvent } from "../event-bus"; import { appEventBus, AppEvent } from "../app-event-bus/event-bus";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";

View File

@ -23,10 +23,11 @@ import { anyObject } from "jest-mock-extended";
import { merge } from "lodash"; import { merge } from "lodash";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { AppPaths } from "../app-paths";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store"; import { HotbarStore } from "../hotbar-store";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import directoryForUserDataInjectable
from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
jest.mock("../../main/catalog/catalog-entity-registry", () => ({ jest.mock("../../main/catalog/catalog-entity-registry", () => ({
catalogEntityRegistry: { catalogEntityRegistry: {
@ -109,37 +110,24 @@ const awsCluster = getMockCatalogEntity({
}, },
}); });
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
AppPaths.init();
describe("HotbarStore", () => { describe("HotbarStore", () => {
beforeEach(() => { beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await di.runSetups();
mockFs({ mockFs({
"tmp": { "some-directory-for-user-data": {
"lens-hotbar-store.json": JSON.stringify({}), "lens-hotbar-store.json": JSON.stringify({}),
}, },
}); });
ClusterStore.createInstance();
HotbarStore.createInstance(); HotbarStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
ClusterStore.resetInstance();
HotbarStore.resetInstance(); HotbarStore.resetInstance();
mockFs.restore(); mockFs.restore();
}); });
@ -339,7 +327,7 @@ describe("HotbarStore", () => {
beforeEach(() => { beforeEach(() => {
HotbarStore.resetInstance(); HotbarStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "some-directory-for-user-data": {
"lens-hotbar-store.json": JSON.stringify({ "lens-hotbar-store.json": JSON.stringify({
__internal__: { __internal__: {
migrations: { migrations: {

View File

@ -43,18 +43,39 @@ import { SemVer } from "semver";
import electron from "electron"; import electron from "electron";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import { ThemeStore } from "../../renderer/theme.store"; import { ThemeStore } from "../../renderer/theme.store";
import type { ClusterStoreModel } from "../cluster-store"; import type { ClusterStoreModel } from "../cluster-store/cluster-store";
import { AppPaths } from "../app-paths"; import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import userStoreInjectable from "../user-store/user-store.injectable";
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
AppPaths.init();
describe("user store tests", () => { describe("user store tests", () => {
let userStore: UserStore;
let mainDi: DependencyInjectionContainer;
beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mockFs();
mainDi = dis.mainDi;
mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
await dis.runSetups();
});
afterEach(() => {
mockFs.restore();
});
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEach(() => { beforeEach(() => {
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" }}); mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }});
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve()); userStore = mainDi.inject(userStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -63,46 +84,38 @@ describe("user store tests", () => {
}); });
it("allows setting and retrieving lastSeenAppVersion", () => { it("allows setting and retrieving lastSeenAppVersion", () => {
const us = UserStore.getInstance(); userStore.lastSeenAppVersion = "1.2.3";
expect(userStore.lastSeenAppVersion).toBe("1.2.3");
us.lastSeenAppVersion = "1.2.3";
expect(us.lastSeenAppVersion).toBe("1.2.3");
}); });
it("allows setting and getting preferences", () => { it("allows setting and getting preferences", () => {
const us = UserStore.getInstance(); userStore.httpsProxy = "abcd://defg";
us.httpsProxy = "abcd://defg"; expect(userStore.httpsProxy).toBe("abcd://defg");
expect(userStore.colorTheme).toBe(ThemeStore.defaultTheme);
expect(us.httpsProxy).toBe("abcd://defg"); userStore.colorTheme = "light";
expect(us.colorTheme).toBe(ThemeStore.defaultTheme); expect(userStore.colorTheme).toBe("light");
us.colorTheme = "light";
expect(us.colorTheme).toBe("light");
}); });
it("correctly resets theme to default value", async () => { it("correctly resets theme to default value", async () => {
const us = UserStore.getInstance(); userStore.colorTheme = "some other theme";
userStore.resetTheme();
us.colorTheme = "some other theme"; expect(userStore.colorTheme).toBe(ThemeStore.defaultTheme);
us.resetTheme();
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
}); });
it("correctly calculates if the last seen version is an old release", () => { it("correctly calculates if the last seen version is an old release", () => {
const us = UserStore.getInstance(); expect(userStore.isNewVersion).toBe(true);
expect(us.isNewVersion).toBe(true); userStore.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format();
expect(userStore.isNewVersion).toBe(false);
us.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format();
expect(us.isNewVersion).toBe(false);
}); });
}); });
describe("migrations", () => { describe("migrations", () => {
beforeEach(() => { beforeEach(() => {
mockFs({ mockFs({
"tmp": { "some-directory-for-user-data": {
"config.json": JSON.stringify({ "config.json": JSON.stringify({
user: { username: "foobar" }, user: { username: "foobar" },
preferences: { colorTheme: "light" }, preferences: { colorTheme: "light" },
@ -112,7 +125,7 @@ describe("user store tests", () => {
clusters: [ clusters: [
{ {
id: "foobar", id: "foobar",
kubeConfigPath: "tmp/extension_data/foo/bar", kubeConfigPath: "some-directory-for-user-data/extension_data/foo/bar",
}, },
{ {
id: "barfoo", id: "barfoo",
@ -129,7 +142,7 @@ describe("user store tests", () => {
}, },
}); });
UserStore.createInstance(); userStore = mainDi.inject(userStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -138,16 +151,12 @@ describe("user store tests", () => {
}); });
it("sets last seen app version to 0.0.0", () => { it("sets last seen app version to 0.0.0", () => {
const us = UserStore.getInstance(); expect(userStore.lastSeenAppVersion).toBe("0.0.0");
expect(us.lastSeenAppVersion).toBe("0.0.0");
}); });
it.only("skips clusters for adding to kube-sync with files under extension_data/", () => { it.only("skips clusters for adding to kube-sync with files under extension_data/", () => {
const us = UserStore.getInstance(); expect(userStore.syncKubeconfigEntries.has("some-directory-for-user-data/extension_data/foo/bar")).toBe(false);
expect(userStore.syncKubeconfigEntries.has("some/other/path")).toBe(true);
expect(us.syncKubeconfigEntries.has("tmp/extension_data/foo/bar")).toBe(false);
expect(us.syncKubeconfigEntries.has("some/other/path")).toBe(true);
}); });
}); });
}); });

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appEventBus } from "./event-bus";
const appEventBusInjectable = getInjectable({
instantiate: () => appEventBus,
lifecycle: lifecycleEnum.singleton,
});
export default appEventBusInjectable;

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { EventEmitter } from "./event-emitter"; import { EventEmitter } from "../event-emitter";
export type AppEvent = { export type AppEvent = {
name: string; name: string;

View File

@ -1,127 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { app, ipcMain, ipcRenderer } from "electron";
import { observable, when } from "mobx";
import path from "path";
import logger from "./logger";
import { fromEntries, toJS } from "./utils";
import { isWindows } from "./vars";
export type PathName = Parameters<typeof app["getPath"]>[0];
const pathNames: PathName[] = [
"home",
"appData",
"userData",
"cache",
"temp",
"exe",
"module",
"desktop",
"documents",
"downloads",
"music",
"pictures",
"videos",
"logs",
"crashDumps",
];
if (isWindows) {
pathNames.push("recent");
}
export class AppPaths {
private static paths = observable.box<Record<PathName, string> | undefined>();
private static readonly ipcChannel = "get-app-paths";
/**
* Initializes the local copy of the paths from electron.
*/
static async init(): Promise<void> {
logger.info(`[APP-PATHS]: initializing`);
if (AppPaths.paths.get()) {
return void logger.error("[APP-PATHS]: init called more than once");
}
if (ipcMain) {
AppPaths.initMain();
} else {
await AppPaths.initRenderer();
}
}
private static initMain(): void {
if (process.env.CICD) {
app.setPath("appData", process.env.CICD);
}
app.setPath("userData", path.join(app.getPath("appData"), app.getName()));
const getPath = (pathName: PathName) => {
try {
return app.getPath(pathName);
} catch {
logger.debug(`[APP-PATHS] No path found for ${pathName}`);
return "";
}
};
AppPaths.paths.set(fromEntries(pathNames.map(pathName => [pathName, getPath(pathName)] as const).filter(([, path]) => path)));
ipcMain.handle(AppPaths.ipcChannel, () => toJS(AppPaths.paths.get()));
}
private static async initRenderer(): Promise<void> {
const paths = await ipcRenderer.invoke(AppPaths.ipcChannel);
if (!paths || typeof paths !== "object") {
throw Object.assign(new Error("[APP-PATHS]: ipc handler returned unexpected data"), { data: paths });
}
AppPaths.paths.set(paths);
}
/**
* An alternative to `app.getPath()` for use in renderer and common.
* This function throws if called before initialization.
* @param name The name of the path field
*/
static get(name: PathName): string {
if (!AppPaths.paths.get()) {
throw new Error("AppPaths.init() has not been called");
}
return AppPaths.paths.get()[name];
}
/**
* An async version of `AppPaths.get()` which waits for `AppPaths.init()` to
* be called before returning
*/
static async getAsync(name: PathName): Promise<string> {
await when(() => Boolean(AppPaths.paths.get()));
return AppPaths.paths.get()[name];
}
}

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { PathName } from "./app-path-names";
import { createChannel } from "../ipc-channel/create-channel/create-channel";
export type AppPaths = Record<PathName, string>;
export const appPathsInjectionToken = getInjectionToken<AppPaths>();
export const appPathsIpcChannel = createChannel<AppPaths>("app-paths");

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { app as electronApp } from "electron";
export type PathName = Parameters<typeof electronApp["getPath"]>[0];
export const pathNames: PathName[] = [
"home",
"appData",
"userData",
"cache",
"temp",
"exe",
"module",
"desktop",
"documents",
"downloads",
"music",
"pictures",
"videos",
"logs",
"crashDumps",
"recent",
];

View File

@ -0,0 +1,159 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import { AppPaths, appPathsInjectionToken } from "./app-path-injection-token";
import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import type { PathName } from "./app-path-names";
import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable";
describe("app-paths", () => {
let mainDi: DependencyInjectionContainer;
let rendererDi: DependencyInjectionContainer;
let runSetups: () => Promise<void[]>;
beforeEach(() => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mainDi = dis.mainDi;
rendererDi = dis.rendererDi;
runSetups = dis.runSetups;
const defaultAppPathsStub: AppPaths = {
appData: "some-app-data",
cache: "some-cache",
crashDumps: "some-crash-dumps",
desktop: "some-desktop",
documents: "some-documents",
downloads: "some-downloads",
exe: "some-exe",
home: "some-home-path",
logs: "some-logs",
module: "some-module",
music: "some-music",
pictures: "some-pictures",
recent: "some-recent",
temp: "some-temp",
videos: "some-videos",
userData: "some-irrelevant",
};
mainDi.override(
getElectronAppPathInjectable,
() =>
(key: PathName): string | null =>
defaultAppPathsStub[key],
);
mainDi.override(
setElectronAppPathInjectable,
() =>
(key: PathName, path: string): void => {
defaultAppPathsStub[key] = path;
},
);
mainDi.override(appNameInjectable, () => "some-app-name");
});
describe("normally", () => {
beforeEach(async () => {
await runSetups();
});
it("given in renderer, when injecting app paths, returns application specific app paths", () => {
const actual = rendererDi.inject(appPathsInjectionToken);
expect(actual).toEqual({
appData: "some-app-data",
cache: "some-cache",
crashDumps: "some-crash-dumps",
desktop: "some-desktop",
documents: "some-documents",
downloads: "some-downloads",
exe: "some-exe",
home: "some-home-path",
logs: "some-logs",
module: "some-module",
music: "some-music",
pictures: "some-pictures",
recent: "some-recent",
temp: "some-temp",
videos: "some-videos",
userData: "some-app-data/some-app-name",
});
});
it("given in main, when injecting app paths, returns application specific app paths", () => {
const actual = mainDi.inject(appPathsInjectionToken);
expect(actual).toEqual({
appData: "some-app-data",
cache: "some-cache",
crashDumps: "some-crash-dumps",
desktop: "some-desktop",
documents: "some-documents",
downloads: "some-downloads",
exe: "some-exe",
home: "some-home-path",
logs: "some-logs",
module: "some-module",
music: "some-music",
pictures: "some-pictures",
recent: "some-recent",
temp: "some-temp",
videos: "some-videos",
userData: "some-app-data/some-app-name",
});
});
});
describe("when running integration tests", () => {
beforeEach(async () => {
mainDi.override(
directoryForIntegrationTestingInjectable,
() => "some-integration-testing-app-data",
);
await runSetups();
});
it("given in renderer, when injecting path for app data, has integration specific app data path", () => {
const { appData, userData } = rendererDi.inject(appPathsInjectionToken);
expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data",
userData: "some-integration-testing-app-data/some-app-name",
});
});
it("given in main, when injecting path for app data, has integration specific app data path", () => {
const { appData, userData } = rendererDi.inject(appPathsInjectionToken);
expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data",
userData: "some-integration-testing-app-data/some-app-name",
});
});
});
});

View File

@ -18,16 +18,15 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path"; import path from "path";
import * as uuid from "uuid"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import { AppPaths } from "../app-paths";
import type { ClusterId } from "../cluster-types";
export function storedKubeConfigFolder(): string { const directoryForBinariesInjectable = getInjectable({
return path.resolve(AppPaths.get("userData"), "kubeconfigs"); instantiate: (di) =>
} path.join(di.inject(directoryForUserDataInjectable), "binaries"),
export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string { lifecycle: lifecycleEnum.singleton,
return path.resolve(storedKubeConfigFolder(), clusterId); });
}
export default directoryForBinariesInjectable;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
const directoryForDownloadsInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).downloads,
lifecycle: lifecycleEnum.singleton,
});
export default directoryForDownloadsInjectable;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
const directoryForExesInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).exe,
lifecycle: lifecycleEnum.singleton,
});
export default directoryForExesInjectable;

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import path from "path";
const directoryForKubeConfigsInjectable = getInjectable({
instantiate: (di) =>
path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"),
lifecycle: lifecycleEnum.singleton,
});
export default directoryForKubeConfigsInjectable;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
const directoryForTempInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).temp,
lifecycle: lifecycleEnum.singleton,
});
export default directoryForTempInjectable;

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { appPathsInjectionToken } from "../app-path-injection-token";
const directoryForUserDataInjectable = getInjectable({
instantiate: (di) => di.inject(appPathsInjectionToken).userData,
lifecycle: lifecycleEnum.singleton,
});
export default directoryForUserDataInjectable;

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path";
import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable";
const getCustomKubeConfigDirectoryInjectable = getInjectable({
instantiate: (di) => (directoryName: string) => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
return path.resolve(
directoryForKubeConfigs,
directoryName,
);
},
lifecycle: lifecycleEnum.singleton,
});
export default getCustomKubeConfigDirectoryInjectable;

View File

@ -30,7 +30,9 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { isTestEnv } from "./vars"; import { isTestEnv } from "./vars";
import { kebabCase } from "lodash"; import { kebabCase } from "lodash";
import { AppPaths } from "./app-paths"; import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable
from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
export interface BaseStoreParams<T> extends ConfOptions<T> { export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: { syncOptions?: {
@ -102,7 +104,9 @@ export abstract class BaseStore<T> extends Singleton {
} }
protected cwd() { protected cwd() {
return AppPaths.get("userData"); const di = getLegacyGlobalDiForExtensionApi();
return di.inject(directoryForUserDataInjectable);
} }
protected saveToFile(model: T) { protected saveToFile(model: T) {

View File

@ -22,7 +22,7 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store/cluster-store";
import { broadcastMessage, requestMain } from "../ipc"; import { broadcastMessage, requestMain } from "../ipc";
import { CatalogCategory, CatalogCategorySpec } from "../catalog"; import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { app } from "electron"; import { app } from "electron";

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
const clusterStoreInjectable = getInjectable({
instantiate: (di) =>
ClusterStore.createInstance({
createCluster: di.inject(createClusterInjectionToken),
}),
lifecycle: lifecycleEnum.singleton,
});
export default clusterStoreInjectable;

View File

@ -19,16 +19,17 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { ipcMain, ipcRenderer, webFrame } from "electron"; import { ipcMain, ipcRenderer, webFrame } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; import { action, comparer, computed, makeObservable, observable, reaction } from "mobx";
import { BaseStore } from "./base-store"; import { BaseStore } from "../base-store";
import { Cluster } from "../main/cluster"; import { Cluster } from "../cluster/cluster";
import migrations from "../migrations/cluster-store"; import migrations from "../../migrations/cluster-store";
import logger from "../main/logger"; import logger from "../../main/logger";
import { appEventBus } from "./event-bus"; import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle, requestMain } from "./ipc"; import { ipcMainHandle, requestMain } from "../ipc";
import { disposer, toJS } from "./utils"; import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "./cluster-types"; import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
export interface ClusterStoreModel { export interface ClusterStoreModel {
clusters?: ClusterModel[]; clusters?: ClusterModel[];
@ -36,13 +37,17 @@ export interface ClusterStoreModel {
const initialStates = "cluster:states"; const initialStates = "cluster:states";
interface Dependencies {
createCluster: (model: ClusterModel) => Cluster
}
export class ClusterStore extends BaseStore<ClusterStoreModel> { export class ClusterStore extends BaseStore<ClusterStoreModel> {
readonly displayName = "ClusterStore"; readonly displayName = "ClusterStore";
clusters = observable.map<ClusterId, Cluster>(); clusters = observable.map<ClusterId, Cluster>();
protected disposer = disposer(); protected disposer = disposer();
constructor() { constructor(private dependencies: Dependencies) {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -123,7 +128,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
const cluster = clusterOrModel instanceof Cluster const cluster = clusterOrModel instanceof Cluster
? clusterOrModel ? clusterOrModel
: new Cluster(clusterOrModel); : this.dependencies.createCluster(clusterOrModel);
this.clusters.set(cluster.id, cluster); this.clusters.set(cluster.id, cluster);
@ -143,7 +148,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
if (cluster) { if (cluster) {
cluster.updateModel(clusterModel); cluster.updateModel(clusterModel);
} else { } else {
cluster = new Cluster(clusterModel); cluster = this.dependencies.createCluster(clusterModel);
} }
newClusters.set(clusterModel.id, cluster); newClusters.set(clusterModel.id, cluster);
} catch (error) { } catch (error) {

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { getHostedClusterId } from "../../utils";
import clusterStoreInjectable from "../cluster-store.injectable";
const hostedClusterInjectable = getInjectable({
instantiate: (di) => {
const hostedClusterId = getHostedClusterId();
return di.inject(clusterStoreInjectable).getById(hostedClusterId);
},
lifecycle: lifecycleEnum.singleton,
});
export default hostedClusterInjectable;

View File

@ -21,22 +21,29 @@
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../ipc";
import { ContextHandler } from "./context-handler"; import type { ContextHandler } from "../../main/context-handler/context-handler";
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { Kubectl } from "./kubectl"; import type { Kubectl } from "../../main/kubectl/kubectl";
import { KubeconfigManager } from "./kubeconfig-manager"; import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../common/kube-helpers"; import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers";
import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../common/rbac"; import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../rbac";
import logger from "./logger"; import logger from "../../main/logger";
import { VersionDetector } from "./cluster-detectors/version-detector"; import { VersionDetector } from "../../main/cluster-detectors/version-detector";
import { DetectorRegistry } from "./cluster-detectors/detector-registry"; import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
import { disposer, storedKubeConfigFolder, toJS } from "../common/utils"; import { disposer, toJS } from "../utils";
import type { Response } from "request"; import type { Response } from "request";
interface Dependencies {
directoryForKubeConfigs: string,
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager,
createContextHandler: (cluster: Cluster) => ContextHandler,
createKubectl: (clusterVersion: string) => Kubectl
}
/** /**
* Cluster * Cluster
* *
@ -221,7 +228,7 @@ export class Cluster implements ClusterModel, ClusterState {
return this.preferences.defaultNamespace; return this.preferences.defaultNamespace;
} }
constructor(model: ClusterModel) { constructor(private dependencies: Dependencies, model: ClusterModel) {
makeObservable(this); makeObservable(this);
this.id = model.id; this.id = model.id;
this.updateModel(model); this.updateModel(model);
@ -237,8 +244,8 @@ export class Cluster implements ClusterModel, ClusterState {
if (ipcMain) { if (ipcMain) {
// for the time being, until renderer gets its own cluster type // for the time being, until renderer gets its own cluster type
this.contextHandler = new ContextHandler(this); this.contextHandler = this.dependencies.createContextHandler(this);
this.proxyKubeconfigManager = new KubeconfigManager(this, this.contextHandler); this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this);
logger.debug(`[CLUSTER]: Cluster init success`, { logger.debug(`[CLUSTER]: Cluster init success`, {
id: this.id, id: this.id,
@ -362,7 +369,7 @@ export class Cluster implements ClusterModel, ClusterState {
* @internal * @internal
*/ */
async ensureKubectl() { async ensureKubectl() {
this.kubeCtl ??= new Kubectl(this.version); this.kubeCtl ??= this.dependencies.createKubectl(this.version);
await this.kubeCtl.ensureKubectl(); await this.kubeCtl.ensureKubectl();
@ -719,6 +726,6 @@ export class Cluster implements ClusterModel, ClusterState {
} }
isInLocalKubeconfig() { isInLocalKubeconfig() {
return this.kubeConfigPath.startsWith(storedKubeConfigFolder()); return this.kubeConfigPath.startsWith(this.dependencies.directoryForKubeConfigs);
} }
} }

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { ClusterModel } from "../cluster-types";
import type { Cluster } from "./cluster";
export const createClusterInjectionToken =
getInjectionToken<(model: ClusterModel) => Cluster>();

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
const directoryForLensLocalStorageInjectable = getInjectable({
instantiate: (di) =>
path.resolve(
di.inject(directoryForUserDataInjectable),
"lens-local-storage",
),
lifecycle: lifecycleEnum.singleton,
});
export default directoryForLensLocalStorageInjectable;

View File

@ -45,7 +45,7 @@ export class EventEmitter<D extends [...any[]]> {
this.listeners.length = 0; this.listeners.length = 0;
} }
emit(...data: D) { emit = (...data: D) => {
for (const [callback, { once }] of this.listeners) { for (const [callback, { once }] of this.listeners) {
if (once) { if (once) {
this.removeListener(callback); this.removeListener(callback);
@ -55,5 +55,5 @@ export class EventEmitter<D extends [...any[]]> {
break; break;
} }
} }
} };
} }

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import fse from "fs-extra";
const fsInjectable = getInjectable({
instantiate: () => fse,
causesSideEffects: true,
lifecycle: lifecycleEnum.singleton,
});
export default fsInjectable;

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { readJsonFile } from "./read-json-file";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import fsInjectable from "../fs.injectable";
const readJsonFileInjectable = getInjectable({
instantiate: (di) => readJsonFile({
fs: di.inject(fsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default readJsonFileInjectable;

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { JsonObject } from "type-fest";
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
interface Dependencies {
fs: {
readJson: (filePath: string) => Promise<JsonObject>;
};
}
export const readJsonFile =
({ fs }: Dependencies) =>
(filePath: string) =>
fs.readJson(filePath);

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { writeJsonFile } from "./write-json-file";
import fsInjectable from "../fs.injectable";
const writeJsonFileInjectable = getInjectable({
instantiate: (di) => writeJsonFile({ fs: di.inject(fsInjectable) }),
lifecycle: lifecycleEnum.singleton,
});
export default writeJsonFileInjectable;

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import path from "path";
import type { JsonObject } from "type-fest";
interface Dependencies {
fs: {
ensureDir: (
directoryName: string,
options: { mode: number }
) => Promise<void>;
writeJson: (
filePath: string,
contentObject: JsonObject,
options: { spaces: number }
) => Promise<void>;
};
}
export const writeJsonFile =
({ fs }: Dependencies) =>
async (filePath: string, contentObject: JsonObject) => {
const directoryName = path.dirname(filePath);
await fs.ensureDir(directoryName, { mode: 0o755 });
await fs.writeJson(filePath, contentObject, { spaces: 2 });
};

24
src/common/ipc-channel/channel.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export interface Channel<TInstance> {
name: string;
_template: TInstance;
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Channel } from "../channel";
export const createChannel = <TInstance>(name: string): Channel<TInstance> => ({
name,
_template: null,
});

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { Cluster } from "../../main/cluster"; import type { Cluster } from "../cluster/cluster";
export interface ClusterContext { export interface ClusterContext {
cluster?: Cluster; cluster?: Cluster;

View File

@ -23,7 +23,7 @@ import { JsonApi } from "./json-api";
import { KubeJsonApi } from "./kube-json-api"; import { KubeJsonApi } from "./kube-json-api";
import { apiKubePrefix, apiPrefix, isDebugging, isDevelopment } from "../../common/vars"; import { apiKubePrefix, apiPrefix, isDebugging, isDevelopment } from "../../common/vars";
import { isClusterPageContext } from "../utils/cluster-id-url-parsing"; import { isClusterPageContext } from "../utils/cluster-id-url-parsing";
import { appEventBus } from "../event-bus"; import { appEventBus } from "../app-event-bus/event-bus";
let apiBase: JsonApi; let apiBase: JsonApi;
let apiKube: KubeJsonApi; let apiKube: KubeJsonApi;

View File

@ -30,7 +30,7 @@ import { apiBase, apiKube } from "./index";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import { KubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object"; import { KubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object";
import byline from "byline"; import byline from "byline";
import type { IKubeWatchEvent } from "./kube-watch-api"; import type { IKubeWatchEvent } from "./kube-watch-event";
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api"; import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
import { noop } from "../utils"; import { noop } from "../utils";
import type { RequestInit } from "node-fetch"; import type { RequestInit } from "node-fetch";

View File

@ -24,7 +24,7 @@ import type { ClusterContext } from "./cluster-context";
import { action, computed, makeObservable, observable, reaction, when } from "mobx"; import { action, computed, makeObservable, observable, reaction, when } from "mobx";
import { autoBind, noop, rejectPromiseBy } from "../utils"; import { autoBind, noop, rejectPromiseBy } from "../utils";
import { KubeObject, KubeStatus } from "./kube-object"; import { KubeObject, KubeStatus } from "./kube-object";
import type { IKubeWatchEvent } from "./kube-watch-api"; import type { IKubeWatchEvent } from "./kube-watch-event";
import { ItemStore } from "../item.store"; import { ItemStore } from "../item.store";
import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api"; import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse"; import { parseKubeApi } from "./kube-api-parse";
@ -323,14 +323,14 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return this.api.create(params, data); return this.api.create(params, data);
} }
async create(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> { create = async (params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> => {
const newItem = await this.createItem(params, data); const newItem = await this.createItem(params, data);
const items = this.sortItems([...this.items, newItem]); const items = this.sortItems([...this.items, newItem]);
this.items.replace(items); this.items.replace(items);
return newItem; return newItem;
} };
private postUpdate(rawItem: KubeJsonApiData): T { private postUpdate(rawItem: KubeJsonApiData): T {
const newItem = new this.api.objectConstructor(rawItem); const newItem = new this.api.objectConstructor(rawItem);

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { KubeJsonApiData } from "./kube-json-api";
export interface IKubeWatchEvent<T extends KubeJsonApiData> {
type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR";
object?: T;
}

View File

@ -27,7 +27,7 @@ import logger from "../../main/logger";
import { app } from "electron"; import { app } from "electron";
import { requestMain } from "../ipc"; import { requestMain } from "../ipc";
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc"; import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store/cluster-store";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { productName } from "../vars"; import { productName } from "../vars";

View File

@ -18,19 +18,13 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { UserStore } from "./user-store";
import { createStorage } from "../../utils"; const userStoreInjectable = getInjectable({
instantiate: () => UserStore.createInstance(),
export interface SidebarStorageState { lifecycle: lifecycleEnum.singleton,
width: number;
expanded: {
[itemId: string]: boolean;
}
}
export const defaultSidebarWidth = 200;
export const sidebarStorage = createStorage<SidebarStorageState>("sidebar", {
width: defaultSidebarWidth,
expanded: {},
}); });
export default userStoreInjectable;

View File

@ -26,12 +26,10 @@ import { BaseStore } from "../base-store";
import migrations, { fileNameMigration } from "../../migrations/user-store"; import migrations, { fileNameMigration } from "../../migrations/user-store";
import { getAppVersion } from "../utils/app-version"; import { getAppVersion } from "../utils/app-version";
import { kubeConfigDefaultPath } from "../kube-helpers"; import { kubeConfigDefaultPath } from "../kube-helpers";
import { appEventBus } from "../event-bus"; import { appEventBus } from "../app-event-bus/event-bus";
import path from "path";
import { ObservableToggleSet, toJS } from "../../renderer/utils"; import { ObservableToggleSet, toJS } from "../../renderer/utils";
import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { AppPaths } from "../app-paths";
export interface UserStoreModel { export interface UserStoreModel {
lastSeenAppVersion: string; lastSeenAppVersion: string;
@ -233,11 +231,3 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
return toJS(model); return toJS(model);
} }
} }
/**
* Getting default directory to download kubectl binaries
* @returns string
*/
export function getDefaultKubectlDownloadPath(): string {
return path.join(AppPaths.get("userData"), "binaries");
}

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store/cluster-store";
import type { KubeResource } from "../rbac"; import type { KubeResource } from "../rbac";
import { getHostedClusterId } from "./cluster-id-url-parsing"; import { getHostedClusterId } from "./cluster-id-url-parsing";

View File

@ -43,7 +43,6 @@ export * from "./extended-map";
export * from "./formatDuration"; export * from "./formatDuration";
export * from "./getRandId"; export * from "./getRandId";
export * from "./hash-set"; export * from "./hash-set";
export * from "./local-kubeconfig";
export * from "./n-fircate"; export * from "./n-fircate";
export * from "./objects"; export * from "./objects";
export * from "./openExternal"; export * from "./openExternal";

View File

@ -22,12 +22,12 @@
import type { ExtensionLoader } from "../extension-loader"; import type { ExtensionLoader } from "../extension-loader";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import { AppPaths } from "../../common/app-paths";
import { runInAction } from "mobx"; import { runInAction } from "mobx";
import updateExtensionsStateInjectable import updateExtensionsStateInjectable
from "../extension-loader/update-extensions-state/update-extensions-state.injectable"; from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import mockFs from "mock-fs";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -122,21 +122,26 @@ jest.mock(
}, },
); );
// TODO: Remove explicit global initialization at unclear time window
AppPaths.init();
describe("ExtensionLoader", () => { describe("ExtensionLoader", () => {
let extensionLoader: ExtensionLoader; let extensionLoader: ExtensionLoader;
let updateExtensionStateMock: jest.Mock; let updateExtensionStateMock: jest.Mock;
beforeEach(() => { beforeEach(async () => {
const di = getDiForUnitTesting(); const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mockFs();
updateExtensionStateMock = jest.fn(); updateExtensionStateMock = jest.fn();
di.override(updateExtensionsStateInjectable, () => updateExtensionStateMock); dis.mainDi.override(updateExtensionsStateInjectable, () => updateExtensionStateMock);
extensionLoader = di.inject(extensionLoaderInjectable); await dis.runSetups();
extensionLoader = dis.mainDi.inject(extensionLoaderInjectable);
});
afterEach(() => {
mockFs.restore();
}); });
it("renderer updates extension after ipc broadcast", async done => { it("renderer updates extension after ipc broadcast", async done => {

View File

@ -19,5 +19,5 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export { appEventBus } from "../../common/event-bus"; export { appEventBus } from "../../common/app-event-bus/event-bus";
export type { AppEvent } from "../../common/event-bus"; export type { AppEvent } from "../../common/app-event-bus/event-bus";

View File

@ -26,12 +26,9 @@ import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compat
import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable"; import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable";
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable"; import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
import installExtensionInjectable import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
from "../extension-installer/install-extension/install-extension.injectable"; import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
import extensionPackageRootDirectoryInjectable import installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable";
from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
import installExtensionsInjectable
from "../extension-installer/install-extensions/install-extensions.injectable";
const extensionDiscoveryInjectable = getInjectable({ const extensionDiscoveryInjectable = getInjectable({
instantiate: (di) => instantiate: (di) =>
@ -51,7 +48,10 @@ const extensionDiscoveryInjectable = getInjectable({
installExtension: di.inject(installExtensionInjectable), installExtension: di.inject(installExtensionInjectable),
installExtensions: di.inject(installExtensionsInjectable), installExtensions: di.inject(installExtensionsInjectable),
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
extensionPackageRootDirectory: di.inject(
extensionPackageRootDirectoryInjectable,
),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -25,11 +25,10 @@ import path from "path";
import type { ExtensionDiscovery } from "./extension-discovery"; import type { ExtensionDiscovery } from "./extension-discovery";
import os from "os"; import os from "os";
import { Console } from "console"; import { Console } from "console";
import { AppPaths } from "../../common/app-paths";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import extensionDiscoveryInjectable from "./extension-discovery.injectable"; import extensionDiscoveryInjectable from "./extension-discovery.injectable";
import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
jest.setTimeout(60_000); jest.setTimeout(60_000);
@ -53,16 +52,22 @@ jest.mock("electron", () => ({
}, },
})); }));
AppPaths.init();
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
const mockedWatch = watch as jest.MockedFunction<typeof watch>; const mockedWatch = watch as jest.MockedFunction<typeof watch>;
describe("ExtensionDiscovery", () => { describe("ExtensionDiscovery", () => {
let extensionDiscovery: ExtensionDiscovery; let extensionDiscovery: ExtensionDiscovery;
beforeEach(() => { beforeEach(async () => {
const di = getDiForUnitTesting(); const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]:
JSON.stringify({
name: "my-extension",
}),
});
di.override(installExtensionInjectable, () => () => Promise.resolve()); di.override(installExtensionInjectable, () => () => Promise.resolve());
@ -71,23 +76,16 @@ describe("ExtensionDiscovery", () => {
() => "some-extension-packages-root", () => "some-extension-packages-root",
); );
extensionDiscovery = di.inject(extensionDiscoveryInjectable); await di.runSetups();
});
describe("with mockFs", () => { extensionDiscovery = di.inject(extensionDiscoveryInjectable);
beforeEach(() => {
mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]:
JSON.stringify({
name: "my-extension",
}),
});
}); });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
}); });
it("emits add for added extension", async (done) => { it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void; let addHandler: (filePath: string) => void;
@ -135,7 +133,6 @@ describe("ExtensionDiscovery", () => {
), ),
); );
}); });
});
it("doesn't emit add for added file under extension", async (done) => { it("doesn't emit add for added file under extension", async (done) => {
let addHandler: (filePath: string) => void; let addHandler: (filePath: string) => void;

View File

@ -20,9 +20,16 @@
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionInstaller } from "./extension-installer"; import { ExtensionInstaller } from "./extension-installer";
import extensionPackageRootDirectoryInjectable from "./extension-package-root-directory/extension-package-root-directory.injectable";
const extensionInstallerInjectable = getInjectable({ const extensionInstallerInjectable = getInjectable({
instantiate: () => new ExtensionInstaller(), instantiate: (di) =>
new ExtensionInstaller({
extensionPackageRootDirectory: di.inject(
extensionPackageRootDirectoryInjectable,
),
}),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -24,20 +24,21 @@ import child_process from "child_process";
import fs from "fs-extra"; import fs from "fs-extra";
import path from "path"; import path from "path";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { extensionPackagesRoot } from "../extension-loader";
import type { PackageJson } from "type-fest"; import type { PackageJson } from "type-fest";
const logModule = "[EXTENSION-INSTALLER]"; const logModule = "[EXTENSION-INSTALLER]";
interface Dependencies {
extensionPackageRootDirectory: string
}
/** /**
* Installs dependencies for extensions * Installs dependencies for extensions
*/ */
export class ExtensionInstaller { export class ExtensionInstaller {
private installLock = new AwaitLock(); private installLock = new AwaitLock();
get extensionPackagesRoot() { constructor(private dependencies: Dependencies) {}
return extensionPackagesRoot();
}
get npmPath() { get npmPath() {
return __non_webpack_require__.resolve("npm/bin/npm-cli"); return __non_webpack_require__.resolve("npm/bin/npm-cli");
@ -46,7 +47,7 @@ export class ExtensionInstaller {
/** /**
* Write package.json to the file system and execute npm install for it. * Write package.json to the file system and execute npm install for it.
*/ */
async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise<void> { installPackages = async (packageJsonPath: string, packagesJson: PackageJson): Promise<void> => {
// Mutual exclusion to install packages in sequence // Mutual exclusion to install packages in sequence
await this.installLock.acquireAsync(); await this.installLock.acquireAsync();
@ -56,34 +57,34 @@ export class ExtensionInstaller {
mode: 0o600, mode: 0o600,
}); });
logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`); logger.info(`${logModule} installing dependencies at ${this.dependencies.extensionPackageRootDirectory}`);
await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"]); await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"]);
logger.info(`${logModule} dependencies installed at ${extensionPackagesRoot()}`); logger.info(`${logModule} dependencies installed at ${this.dependencies.extensionPackageRootDirectory}`);
} finally { } finally {
this.installLock.release(); this.installLock.release();
} }
} };
/** /**
* Install single package using npm * Install single package using npm
*/ */
async installPackage(name: string): Promise<void> { installPackage = async (name: string): Promise<void> => {
// Mutual exclusion to install packages in sequence // Mutual exclusion to install packages in sequence
await this.installLock.acquireAsync(); await this.installLock.acquireAsync();
try { try {
logger.info(`${logModule} installing package from ${name} to ${extensionPackagesRoot()}`); logger.info(`${logModule} installing package from ${name} to ${this.dependencies.extensionPackageRootDirectory}`);
await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock", "--no-save", name]); await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock", "--no-save", name]);
logger.info(`${logModule} package ${name} installed to ${extensionPackagesRoot()}`); logger.info(`${logModule} package ${name} installed to ${this.dependencies.extensionPackageRootDirectory}`);
} finally { } finally {
this.installLock.release(); this.installLock.release();
} }
} };
private npm(args: string[]): Promise<void> { private npm(args: string[]): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = child_process.fork(this.npmPath, args, { const child = child_process.fork(this.npmPath, args, {
cwd: extensionPackagesRoot(), cwd: this.dependencies.extensionPackageRootDirectory,
silent: true, silent: true,
env: {}, env: {},
}); });

View File

@ -19,11 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionInstallerInjectable from "../extension-installer.injectable"; import directoryForUserDataInjectable
from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const extensionPackageRootDirectoryInjectable = getInjectable({ const extensionPackageRootDirectoryInjectable = getInjectable({
instantiate: (di) => instantiate: (di) => di.inject(directoryForUserDataInjectable),
di.inject(extensionInstallerInjectable).extensionPackagesRoot,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { createExtensionInstance } from "./create-extension-instance";
import fileSystemProvisionerStoreInjectable from "./file-system-provisioner-store/file-system-provisioner-store.injectable";
const createExtensionInstanceInjectable = getInjectable({
instantiate: (di) => createExtensionInstance({
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createExtensionInstanceInjectable;

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { LensExtensionConstructor } from "../../lens-extension";
import type { InstalledExtension } from "../../extension-discovery/extension-discovery";
import {
LensExtensionDependencies,
setLensExtensionDependencies,
} from "../../lens-extension-set-dependencies";
export const createExtensionInstance =
(dependencies: LensExtensionDependencies) =>
(ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => {
const instance = new ExtensionClass(extension);
instance[setLensExtensionDependencies](dependencies);
return instance;
};

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import path from "path";
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const directoryForExtensionDataInjectable = getInjectable({
instantiate: (di) =>
path.join(di.inject(directoryForUserDataInjectable), "extension_data"),
lifecycle: lifecycleEnum.singleton,
});
export default directoryForExtensionDataInjectable;

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { FileSystemProvisionerStore } from "./file-system-provisioner-store";
import directoryForExtensionDataInjectable from "./directory-for-extension-data/directory-for-extension-data.injectable";
const fileSystemProvisionerStoreInjectable = getInjectable({
instantiate: (di) =>
FileSystemProvisionerStore.createInstance({
directoryForExtensionData: di.inject(
directoryForExtensionDataInjectable,
),
}),
lifecycle: lifecycleEnum.singleton,
});
export default fileSystemProvisionerStoreInjectable;

View File

@ -24,24 +24,28 @@ import { SHA256 } from "crypto-js";
import fse from "fs-extra"; import fse from "fs-extra";
import { action, makeObservable, observable } from "mobx"; import { action, makeObservable, observable } from "mobx";
import path from "path"; import path from "path";
import { BaseStore } from "../common/base-store"; import { BaseStore } from "../../../../common/base-store";
import type { LensExtensionId } from "../extensions/lens-extension"; import type { LensExtensionId } from "../../../lens-extension";
import { toJS } from "../common/utils"; import { toJS } from "../../../../common/utils";
import { AppPaths } from "../common/app-paths";
interface FSProvisionModel { interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths extensions: Record<string, string>; // extension names to paths
} }
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> { interface Dependencies {
directoryForExtensionData: string
}
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
readonly displayName = "FilesystemProvisionerStore"; readonly displayName = "FilesystemProvisionerStore";
registeredExtensions = observable.map<LensExtensionId, string>(); registeredExtensions = observable.map<LensExtensionId, string>();
constructor() { constructor(private dependencies: Dependencies) {
super({ super({
configName: "lens-filesystem-provisioner-store", configName: "lens-filesystem-provisioner-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
}); });
makeObservable(this); makeObservable(this);
this.load(); this.load();
} }
@ -56,7 +60,8 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
if (!this.registeredExtensions.has(extensionName)) { if (!this.registeredExtensions.has(extensionName)) {
const salt = randomBytes(32).toString("hex"); const salt = randomBytes(32).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString(); const hashedName = SHA256(`${extensionName}/${salt}`).toString();
const dirPath = path.resolve(AppPaths.get("userData"), "extension_data", hashedName);
const dirPath = path.resolve(this.dependencies.directoryForExtensionData, hashedName);
this.registeredExtensions.set(extensionName, dirPath); this.registeredExtensions.set(extensionName, dirPath);
} }

View File

@ -21,11 +21,14 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader"; import { ExtensionLoader } from "./extension-loader";
import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable"; import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable";
import createExtensionInstanceInjectable
from "./create-extension-instance/create-extension-instance.injectable";
const extensionLoaderInjectable = getInjectable({ const extensionLoaderInjectable = getInjectable({
instantiate: (di) => instantiate: (di) =>
new ExtensionLoader({ new ExtensionLoader({
updateExtensionsState: di.inject(updateExtensionsStateInjectable), updateExtensionsState: di.inject(updateExtensionsStateInjectable),
createExtensionInstance: di.inject(createExtensionInstanceInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -24,7 +24,6 @@ import { EventEmitter } from "events";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path"; import path from "path";
import { AppPaths } from "../../common/app-paths";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc"; import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
import { Disposer, toJS } from "../../common/utils"; import { Disposer, toJS } from "../../common/utils";
import logger from "../../main/logger"; import logger from "../../main/logger";
@ -35,14 +34,16 @@ import type { LensRendererExtension } from "../lens-renderer-extension";
import * as registries from "../registries"; import * as registries from "../registries";
import type { LensExtensionState } from "../extensions-store/extensions-store"; import type { LensExtensionState } from "../extensions-store/extensions-store";
export function extensionPackagesRoot() {
return path.join(AppPaths.get("userData"));
}
const logModule = "[EXTENSIONS-LOADER]"; const logModule = "[EXTENSIONS-LOADER]";
interface Dependencies { interface Dependencies {
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void
createExtensionInstance: (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension,
}
export interface ExtensionLoading {
isBundled: boolean,
loaded: Promise<void>
} }
/** /**
@ -81,6 +82,7 @@ export class ExtensionLoader {
constructor(protected dependencies : Dependencies) { constructor(protected dependencies : Dependencies) {
makeObservable(this); makeObservable(this);
observe(this.instances, change => { observe(this.instances, change => {
switch (change.type) { switch (change.type) {
case "add": case "add":
@ -260,7 +262,7 @@ export class ExtensionLoader {
this.autoInitExtensions(() => Promise.resolve([])); this.autoInitExtensions(() => Promise.resolve([]));
} }
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer = () => {
logger.debug(`${logModule}: load on main renderer (cluster manager)`); logger.debug(`${logModule}: load on main renderer (cluster manager)`);
return this.autoInitExtensions(async (extension: LensRendererExtension) => { return this.autoInitExtensions(async (extension: LensRendererExtension) => {
@ -286,9 +288,9 @@ export class ExtensionLoader {
return removeItems; return removeItems;
}); });
} };
loadOnClusterRenderer(entity: KubernetesCluster) { loadOnClusterRenderer = (entity: KubernetesCluster) => {
logger.debug(`${logModule}: load on cluster renderer (dashboard)`); logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => { this.autoInitExtensions(async (extension: LensRendererExtension) => {
@ -316,12 +318,12 @@ export class ExtensionLoader {
return removeItems; return removeItems;
}); });
} };
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) { protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
const loadingExtensions: { isBundled: boolean, loaded: Promise<void> }[] = []; const loadingExtensions: ExtensionLoading[] = [];
reaction(() => this.toJSON(), installedExtensions => { reaction(() => this.toJSON(), async installedExtensions => {
for (const [extId, extension] of installedExtensions) { for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
@ -334,7 +336,10 @@ export class ExtensionLoader {
continue; continue;
} }
const instance = new LensExtensionClass(extension); const instance = this.dependencies.createExtensionInstance(
LensExtensionClass,
extension,
);
const loaded = instance.enable(register).catch((err) => { const loaded = instance.enable(register).catch((err) => {
logger.error(`${logModule}: failed to enable`, { ext: extension, err }); logger.error(`${logModule}: failed to enable`, { ext: extension, err });

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import directoryForUserDataInjectable
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const extensionPackagesRootInjectable = getInjectable({
instantiate: (di) => di.inject(directoryForUserDataInjectable),
lifecycle: lifecycleEnum.singleton,
});
export default extensionPackagesRootInjectable;

View File

@ -59,9 +59,9 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
} }
@action @action
mergeState(extensionsState: Record<LensExtensionId, LensExtensionState>) { mergeState = (extensionsState: Record<LensExtensionId, LensExtensionState>) => {
this.state.merge(extensionsState); this.state.merge(extensionsState);
} };
@action @action
protected fromStore({ extensions }: LensExtensionsStoreModel) { protected fromStore({ extensions }: LensExtensionsStoreModel) {

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { FileSystemProvisionerStore } from "./extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store";
// This symbol encapsulates setting of dependencies to only happen locally in Lens Core
// and not by e.g. authors of extensions
export const setLensExtensionDependencies = Symbol("set-lens-extension-dependencies");
export interface LensExtensionDependencies {
fileSystemProvisionerStore: FileSystemProvisionerStore
}

View File

@ -21,11 +21,14 @@
import type { InstalledExtension } from "./extension-discovery/extension-discovery"; import type { InstalledExtension } from "./extension-discovery/extension-discovery";
import { action, observable, makeObservable, computed } from "mobx"; import { action, observable, makeObservable, computed } from "mobx";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger"; import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries"; import type { ProtocolHandlerRegistration } from "./registries";
import type { PackageJson } from "type-fest"; import type { PackageJson } from "type-fest";
import { Disposer, disposer } from "../common/utils"; import { Disposer, disposer } from "../common/utils";
import {
LensExtensionDependencies,
setLensExtensionDependencies,
} from "./lens-extension-set-dependencies";
export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension; export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@ -75,6 +78,12 @@ export class LensExtension {
return this.manifest.description; return this.manifest.description;
} }
private dependencies: LensExtensionDependencies;
[setLensExtensionDependencies] = (dependencies: LensExtensionDependencies) => {
this.dependencies = dependencies;
};
/** /**
* getExtensionFileFolder returns the path to an already created folder. This * getExtensionFileFolder returns the path to an already created folder. This
* folder is for the sole use of this extension. * folder is for the sole use of this extension.
@ -83,7 +92,7 @@ export class LensExtension {
* folder name. * folder name.
*/ */
async getExtensionFileFolder(): Promise<string> { async getExtensionFileFolder(): Promise<string> {
return FilesystemProvisionerStore.getInstance().requestDirectory(this.id); return this.dependencies.fileSystemProvisionerStore.requestDirectory(this.id);
} }
@action @action

View File

@ -26,10 +26,10 @@ import React from "react";
import fse from "fs-extra"; import fse from "fs-extra";
import { Console } from "console"; import { Console } from "console";
import { stderr, stdout } from "process"; import { stderr, stdout } from "process";
import { TerminalStore } from "../../../renderer/components/dock/terminal.store";
import { ThemeStore } from "../../../renderer/theme.store"; import { ThemeStore } from "../../../renderer/theme.store";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { AppPaths } from "../../../common/app-paths"; import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
import mockFs from "mock-fs";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
@ -47,14 +47,18 @@ jest.mock("electron", () => ({
}, },
})); }));
AppPaths.init();
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
let ext: LensExtension = null; let ext: LensExtension = null;
describe("page registry tests", () => { describe("page registry tests", () => {
beforeEach(async () => { beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mockFs();
await dis.runSetups();
ext = new LensExtension({ ext = new LensExtension({
manifest: { manifest: {
name: "foo-bar", name: "foo-bar",
@ -69,7 +73,6 @@ describe("page registry tests", () => {
}); });
UserStore.createInstance(); UserStore.createInstance();
ThemeStore.createInstance(); ThemeStore.createInstance();
TerminalStore.createInstance();
ClusterPageRegistry.createInstance(); ClusterPageRegistry.createInstance();
GlobalPageRegistry.createInstance().add({ GlobalPageRegistry.createInstance().add({
id: "page-with-params", id: "page-with-params",
@ -105,10 +108,10 @@ describe("page registry tests", () => {
afterEach(() => { afterEach(() => {
GlobalPageRegistry.resetInstance(); GlobalPageRegistry.resetInstance();
ClusterPageRegistry.resetInstance(); ClusterPageRegistry.resetInstance();
TerminalStore.resetInstance();
ThemeStore.resetInstance(); ThemeStore.resetInstance();
UserStore.resetInstance(); UserStore.resetInstance();
fse.remove("tmp"); fse.remove("tmp");
mockFs.restore();
}); });
describe("getPageUrl", () => { describe("getPageUrl", () => {

View File

@ -71,5 +71,12 @@ export * from "../../renderer/components/+events/kube-event-details";
// specific exports // specific exports
export * from "../../renderer/components/status-brick"; export * from "../../renderer/components/status-brick";
export { terminalStore, createTerminalTab, TerminalStore } from "../../renderer/components/dock/terminal.store";
export { logTabStore } from "../../renderer/components/dock/log-tab.store"; // Mikko
// export { terminalStore, TerminalStore } from "../../renderer/components/dock/terminal-store/terminal.store";
//
// // Mikko
// export { createTerminalTab } from "../../renderer/components/dock/terminal-store/terminal.store";
//
// // Mikko
// export { logTabStore } from "../../renderer/components/dock/log-tab-store/log-tab.store";

View File

@ -86,7 +86,7 @@ export type { NetworkPolicyStore } from "../../renderer/components/+network-poli
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store"; export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store";
export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store"; export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store";
export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store"; export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store";
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store"; export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace-store/namespace.store";
export type { ServiceAccountsStore } from "../../renderer/components/+user-management/+service-accounts/store"; export type { ServiceAccountsStore } from "../../renderer/components/+user-management/+service-accounts/store";
export type { RolesStore } from "../../renderer/components/+user-management/+roles/store"; export type { RolesStore } from "../../renderer/components/+user-management/+roles/store";
export type { RoleBindingsStore } from "../../renderer/components/+user-management/+role-bindings/store"; export type { RoleBindingsStore } from "../../renderer/components/+user-management/+role-bindings/store";

View File

@ -45,25 +45,29 @@ jest.mock("winston", () => ({
})); }));
jest.mock("../../common/ipc"); jest.mock("../../common/ipc");
jest.mock("../context-handler");
jest.mock("request"); jest.mock("request");
jest.mock("request-promise-native"); jest.mock("request-promise-native");
import { Console } from "console"; import { Console } from "console";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { Cluster } from "../cluster"; import type { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl"; import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { ClusterModel } from "../../common/cluster-types";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
describe("create clusters", () => { describe("create clusters", () => {
beforeEach(() => { let cluster: Cluster;
let createCluster: (model: ClusterModel) => Cluster;
beforeEach(async () => {
jest.clearAllMocks(); jest.clearAllMocks();
});
let c: Cluster; const di = getDiForUnitTesting({ doGeneralOverrides: true });
beforeEach(() => {
const mockOpts = { const mockOpts = {
"minikube-config.yml": JSON.stringify({ "minikube-config.yml": JSON.stringify({
apiVersion: "v1", apiVersion: "v1",
@ -89,8 +93,14 @@ describe("create clusters", () => {
}; };
mockFs(mockOpts); mockFs(mockOpts);
await di.runSetups();
createCluster = di.inject(createClusterInjectionToken);
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true)); jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
c = new Cluster({
cluster = createCluster({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
@ -102,48 +112,39 @@ describe("create clusters", () => {
}); });
it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => { it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => {
expect(c.apiUrl).toBe("https://192.168.64.3:8443"); expect(cluster.apiUrl).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(() => c.reconnect()).not.toThrowError(); expect(() => cluster.reconnect()).not.toThrowError();
}); });
it("disconnect should not throw if contextHandler is missing", () => { it("disconnect should not throw if contextHandler is missing", () => {
expect(() => c.disconnect()).not.toThrowError(); expect(() => cluster.disconnect()).not.toThrowError();
}); });
it("activating cluster should try to connect to cluster and do a refresh", async () => { it("activating cluster should try to connect to cluster and do a refresh", async () => {
const cluster = createCluster({
const c = new class extends Cluster {
// only way to mock protected methods, without these we leak promises
protected bindEvents() {
return;
}
async ensureKubectl() {
return Promise.resolve(null);
}
}({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
}); });
c.contextHandler = { cluster.contextHandler = {
ensureServer: jest.fn(), ensureServer: jest.fn(),
stopServer: jest.fn(), stopServer: jest.fn(),
} as any; } as any;
jest.spyOn(c, "reconnect"); jest.spyOn(cluster, "reconnect");
jest.spyOn(c, "canI"); jest.spyOn(cluster, "canI");
jest.spyOn(c, "refreshConnectionStatus"); jest.spyOn(cluster, "refreshConnectionStatus");
await c.activate(); await cluster.activate();
expect(c.reconnect).toBeCalled(); expect(cluster.reconnect).toBeCalled();
expect(c.refreshConnectionStatus).toBeCalled(); expect(cluster.refreshConnectionStatus).toBeCalled();
c.disconnect(); cluster.disconnect();
jest.resetAllMocks(); jest.resetAllMocks();
}); });
}); });

View File

@ -20,10 +20,12 @@
*/ */
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import { ContextHandler } from "../context-handler"; import type { ContextHandler } from "../context-handler/context-handler";
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus"; import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { AppPaths } from "../../common/app-paths"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { Cluster } from "../../common/cluster/cluster";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
@ -77,25 +79,28 @@ class TestProvider extends PrometheusProvider {
} }
} }
function getHandler() { const clusterStub = {
return new ContextHandler(({
getProxyKubeconfig: (): any => ({ getProxyKubeconfig: (): any => ({
makeApiClient: (): any => undefined, makeApiClient: (): any => undefined,
}), }),
apiUrl: "http://localhost:81", apiUrl: "http://localhost:81",
}) as any); } as Cluster;
}
AppPaths.init();
describe("ContextHandler", () => { describe("ContextHandler", () => {
beforeEach(() => { let createContextHandler: (cluster: Cluster) => ContextHandler;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({ mockFs({
"tmp": {}, "tmp": {},
}); });
await di.runSetups();
createContextHandler = di.inject(createContextHandlerInjectable);
PrometheusProviderRegistry.createInstance(); PrometheusProviderRegistry.createInstance();
UserStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -124,7 +129,12 @@ describe("ContextHandler", () => {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
} }
expect(() => (getHandler() as any).getPrometheusService()).rejects.toBeDefined(); expect(() => {
// TODO: Unit test shouldn't access protected or private methods
const contextHandler = createContextHandler(clusterStub) as any;
return contextHandler.getPrometheusService();
}).rejects.toBeDefined();
}); });
it.each([ it.each([
@ -150,7 +160,10 @@ describe("ContextHandler", () => {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
} }
const service = await (getHandler() as any).getPrometheusService(); // TODO: Unit test shouldn't access protected or private methods
const contextHandler = createContextHandler(clusterStub) as any;
const service = await contextHandler.getPrometheusService();
expect(service.id === `id_${failures}`); expect(service.id === `id_${failures}`);
}); });
@ -178,7 +191,10 @@ describe("ContextHandler", () => {
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
} }
const service = await (getHandler() as any).getPrometheusService(); // TODO: Unit test shouldn't access protected or private methods
const contextHandler = createContextHandler(clusterStub) as any;
const service = await contextHandler.getPrometheusService();
expect(service.id === "id_0"); expect(service.id === "id_0");
}); });
@ -212,7 +228,10 @@ describe("ContextHandler", () => {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
} }
const service = await (getHandler() as any).getPrometheusService(); // TODO: Unit test shouldn't access protected or private methods
const contextHandler = createContextHandler(clusterStub) as any;
const service = await contextHandler.getPrometheusService();
expect(service.id === "id_0"); expect(service.id === "id_0");
}); });
@ -225,7 +244,10 @@ describe("ContextHandler", () => {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
const service = await (getHandler() as any).getPrometheusService(); // TODO: Unit test shouldn't access protected or private methods
const contextHandler = createContextHandler(clusterStub) as any;
const service = await contextHandler.getPrometheusService();
expect(service.id).not.toBe("id_2"); expect(service.id).not.toBe("id_2");
}); });

View File

@ -19,6 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { ClusterModel } from "../../common/cluster-types";
jest.mock("winston", () => ({ jest.mock("winston", () => ({
format: { format: {
colorize: jest.fn(), colorize: jest.fn(),
@ -48,11 +50,11 @@ jest.mock("../../common/ipc");
jest.mock("child_process"); jest.mock("child_process");
jest.mock("tcp-port-used"); jest.mock("tcp-port-used");
import { Cluster } from "../cluster"; import type { Cluster } from "../../common/cluster/cluster";
import { KubeAuthProxy } from "../kube-auth-proxy"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import { broadcastMessage } from "../../common/ipc"; import { broadcastMessage } from "../../common/ipc";
import { ChildProcess, spawn } from "child_process"; import { ChildProcess, spawn } from "child_process";
import { bundledKubectlPath, Kubectl } from "../kubectl"; import { bundledKubectlPath, Kubectl } from "../kubectl/kubectl";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { EventEmitter, Readable } from "stream"; import { EventEmitter, Readable } from "stream";
@ -60,7 +62,9 @@ import { UserStore } from "../../common/user-store";
import { Console } from "console"; import { Console } from "console";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { AppPaths } from "../../common/app-paths"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -68,25 +72,11 @@ const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcas
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>; const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>; const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilUsed>;
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
AppPaths.init();
describe("kube auth proxy tests", () => { describe("kube auth proxy tests", () => {
beforeEach(() => { let createCluster: (model: ClusterModel) => Cluster;
let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
beforeEach(async () => {
jest.clearAllMocks(); jest.clearAllMocks();
const mockMinikubeConfig = { const mockMinikubeConfig = {
@ -115,7 +105,16 @@ describe("kube auth proxy tests", () => {
"tmp": {}, "tmp": {},
}; };
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs(mockMinikubeConfig); mockFs(mockMinikubeConfig);
await di.runSetups();
createCluster = di.inject(createClusterInjectionToken);
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
UserStore.createInstance(); UserStore.createInstance();
}); });
@ -125,7 +124,13 @@ describe("kube auth proxy tests", () => {
}); });
it("calling exit multiple times shouldn't throw", async () => { it("calling exit multiple times shouldn't throw", async () => {
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube" }), {}); const cluster = createCluster({
id: "foobar",
kubeConfigPath: "minikube-config.yml",
contextName: "minikube",
});
const kap = createKubeAuthProxy(cluster, {});
kap.exit(); kap.exit();
kap.exit(); kap.exit();
@ -211,9 +216,13 @@ describe("kube auth proxy tests", () => {
}); });
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()); mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube" }); const cluster = createCluster({
id: "foobar",
kubeConfigPath: "minikube-config.yml",
contextName: "minikube",
});
proxy = new KubeAuthProxy(cluster, {}); proxy = createKubeAuthProxy(cluster, {});
}); });
it("should call spawn and broadcast errors", async () => { it("should call spawn and broadcast errors", async () => {

View File

@ -18,6 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getDiForUnitTesting } from "../getDiForUnitTesting";
const logger = { const logger = {
silly: jest.fn(), silly: jest.fn(),
@ -46,41 +47,28 @@ jest.mock("winston", () => ({
}, },
})); }));
import { KubeconfigManager } from "../kubeconfig-manager"; import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { Cluster } from "../cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type { ContextHandler } from "../context-handler";
import fse from "fs-extra"; import fse from "fs-extra";
import { loadYaml } from "@kubernetes/client-node"; import { loadYaml } from "@kubernetes/client-node";
import { Console } from "console"; import { Console } from "console";
import * as path from "path"; import * as path from "path";
import { AppPaths } from "../../common/app-paths"; import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
jest.mock("electron", () => ({ import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
AppPaths.init();
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => { describe("kubeconfig manager tests", () => {
let cluster: Cluster; let cluster: Cluster;
let contextHandler: ContextHandler; let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
beforeEach(() => {
const mockOpts = { const mockOpts = {
"minikube-config.yml": JSON.stringify({ "minikube-config.yml": JSON.stringify({
apiVersion: "v1", apiVersion: "v1",
@ -107,14 +95,22 @@ describe("kubeconfig manager tests", () => {
mockFs(mockOpts); mockFs(mockOpts);
cluster = new Cluster({ await di.runSetups();
const createCluster = di.inject(createClusterInjectionToken);
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
cluster = createCluster({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml", kubeConfigPath: "minikube-config.yml",
}); });
contextHandler = {
cluster.contextHandler = {
ensureServer: () => Promise.resolve(), ensureServer: () => Promise.resolve(),
} as any; } as any;
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo"); jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
}); });
@ -123,10 +119,10 @@ describe("kubeconfig manager tests", () => {
}); });
it("should create 'temp' kube config with proxy", async () => { it("should create 'temp' kube config with proxy", async () => {
const kubeConfManager = new KubeconfigManager(cluster, contextHandler); const kubeConfManager = createKubeconfigManager(cluster);
expect(logger.error).not.toBeCalled(); expect(logger.error).not.toBeCalled();
expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`); expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`);
// this causes an intermittent "ENXIO: no such device or address, read" error // this causes an intermittent "ENXIO: no such device or address, read" error
// const file = await fse.readFile(await kubeConfManager.getPath()); // const file = await fse.readFile(await kubeConfManager.getPath());
const file = fse.readFileSync(await kubeConfManager.getPath()); const file = fse.readFileSync(await kubeConfManager.getPath());
@ -138,7 +134,8 @@ describe("kubeconfig manager tests", () => {
}); });
it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => { it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => {
const kubeConfManager = new KubeconfigManager(cluster, contextHandler); const kubeConfManager = createKubeconfigManager(cluster);
const configPath = await kubeConfManager.getPath(); const configPath = await kubeConfManager.getPath();
expect(await fse.pathExists(configPath)).toBe(true); expect(await fse.pathExists(configPath)).toBe(true);

View File

@ -19,7 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { AppPaths } from "../../common/app-paths";
import { Router } from "../router"; import { Router } from "../router";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
@ -38,8 +37,6 @@ jest.mock("electron", () => ({
}, },
})); }));
AppPaths.init();
describe("Router", () => { describe("Router", () => {
it("blocks path traversal attacks", async () => { it("blocks path traversal attacks", async () => {
const response: any = { const response: any = {

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable";
const appNameInjectable = getInjectable({
instantiate: (di) => di.inject(electronAppInjectable).name,
lifecycle: lifecycleEnum.singleton,
});
export default appNameInjectable;

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import {
DependencyInjectionContainer,
getInjectable,
lifecycleEnum,
} from "@ogre-tools/injectable";
import {
appPathsInjectionToken,
appPathsIpcChannel,
} from "../../common/app-paths/app-path-injection-token";
import registerChannelInjectable from "./register-channel/register-channel.injectable";
import { getAppPaths } from "./get-app-paths";
import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable";
import path from "path";
import appNameInjectable from "./app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable";
const appPathsInjectable = getInjectable({
setup: (di) => {
const directoryForIntegrationTesting = di.inject(
directoryForIntegrationTestingInjectable,
);
if (directoryForIntegrationTesting) {
setupPathForAppDataInIntegrationTesting(di, directoryForIntegrationTesting);
}
setupPathForUserData(di);
registerAppPathsChannel(di);
},
instantiate: (di) =>
getAppPaths({ getAppPath: di.inject(getElectronAppPathInjectable) }),
injectionToken: appPathsInjectionToken,
lifecycle: lifecycleEnum.singleton,
});
export default appPathsInjectable;
const registerAppPathsChannel = (di: DependencyInjectionContainer) => {
const registerChannel = di.inject(registerChannelInjectable);
registerChannel(appPathsIpcChannel, () => di.inject(appPathsInjectable));
};
const setupPathForUserData = (di: DependencyInjectionContainer) => {
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
const appName = di.inject(appNameInjectable);
const getAppPath = di.inject(getElectronAppPathInjectable);
const appDataPath = getAppPath("appData");
setElectronAppPath("userData", path.join(appDataPath, appName));
};
const setupPathForAppDataInIntegrationTesting = (di: DependencyInjectionContainer, appDataPath: string) => {
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
setElectronAppPath("appData", appDataPath);
};

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
const directoryForIntegrationTestingInjectable = getInjectable({
instantiate: () => process.env.CICD,
lifecycle: lifecycleEnum.singleton,
});
export default directoryForIntegrationTestingInjectable;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { fromPairs } from "lodash/fp";
import { pathNames, PathName } from "../../common/app-paths/app-path-names";
import type { AppPaths } from "../../common/app-paths/app-path-injection-token";
interface Dependencies {
getAppPath: (name: PathName) => string
}
export const getAppPaths = ({ getAppPath }: Dependencies) =>
fromPairs(pathNames.map((name) => [name, getAppPath(name)])) as AppPaths;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { app } from "electron";
const electronAppInjectable = getInjectable({
instantiate: () => app,
lifecycle: lifecycleEnum.singleton,
causesSideEffects: false,
});
export default electronAppInjectable;

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import electronAppInjectable from "./electron-app/electron-app.injectable";
import { getElectronAppPath } from "./get-electron-app-path";
const getElectronAppPathInjectable = getInjectable({
instantiate: (di) =>
getElectronAppPath({ app: di.inject(electronAppInjectable) }),
lifecycle: lifecycleEnum.singleton,
});
export default getElectronAppPathInjectable;

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import electronAppInjectable from "./electron-app/electron-app.injectable";
import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { App } from "electron";
describe("get-electron-app-path", () => {
let getElectronAppPath: (name: string) => string | null;
beforeEach(() => {
const di = getDiForUnitTesting();
const appStub = {
getPath: (name: string) => {
if (name !== "some-existing-name") {
throw new Error("irrelevant");
}
return "some-existing-app-path";
},
} as App;
di.override(electronAppInjectable, () => appStub);
getElectronAppPath = di.inject(getElectronAppPathInjectable);
});
it("given app path exists, when called, returns app path", () => {
const actual = getElectronAppPath("some-existing-name");
expect(actual).toBe("some-existing-app-path");
});
it("given app path does not exist, when called, returns null", () => {
const actual = getElectronAppPath("some-non-existing-name");
expect(actual).toBe(null);
});
});

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { App } from "electron";
import type { PathName } from "../../../common/app-paths/app-path-names";
interface Dependencies {
app: App;
}
export const getElectronAppPath =
({ app }: Dependencies) =>
(name: PathName) : string | null => {
try {
return app.getPath(name);
} catch (e) {
return null;
}
};

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ipcMain } from "electron";
const ipcMainInjectable = getInjectable({
instantiate: () => ipcMain,
lifecycle: lifecycleEnum.singleton,
causesSideEffects: true,
});
export default ipcMainInjectable;

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import ipcMainInjectable from "./ipc-main/ipc-main.injectable";
import { registerChannel } from "./register-channel";
const registerChannelInjectable = getInjectable({
instantiate: (di) => registerChannel({
ipcMain: di.inject(ipcMainInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default registerChannelInjectable;

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { IpcMain } from "electron";
import type { Channel } from "../../../common/ipc-channel/channel";
interface Dependencies {
ipcMain: IpcMain;
}
export const registerChannel =
({ ipcMain }: Dependencies) =>
<TChannel extends Channel<TInstance>, TInstance>(
channel: TChannel,
getValue: () => TInstance,
) =>
ipcMain.handle(channel.name, getValue);

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { PathName } from "../../../common/app-paths/app-path-names";
import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable";
const setElectronAppPathInjectable = getInjectable({
instantiate: (di) => (name: PathName, path: string) : void =>
di.inject(electronAppInjectable).setPath(name, path),
lifecycle: lifecycleEnum.singleton,
});
export default setElectronAppPathInjectable;

View File

@ -22,13 +22,17 @@
import { ObservableMap } from "mobx"; import { ObservableMap } from "mobx";
import type { CatalogEntity } from "../../../common/catalog"; import type { CatalogEntity } from "../../../common/catalog";
import { loadFromOptions } from "../../../common/kube-helpers"; import { loadFromOptions } from "../../../common/kube-helpers";
import type { Cluster } from "../../cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import { computeDiff, configToModels } from "../kubeconfig-sync"; import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync-manager/kubeconfig-sync-manager";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import fs from "fs"; import fs from "fs";
import { ClusterStore } from "../../../common/cluster-store";
import { ClusterManager } from "../../cluster-manager"; import { ClusterManager } from "../../cluster-manager";
import { AppPaths } from "../../../common/app-paths"; import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import directoryForKubeConfigsInjectable
from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
@ -46,18 +50,28 @@ jest.mock("electron", () => ({
}, },
})); }));
AppPaths.init();
describe("kubeconfig-sync.source tests", () => { describe("kubeconfig-sync.source tests", () => {
beforeEach(() => { let computeDiff: ReturnType<typeof computeDiffFor>;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();
ClusterStore.createInstance();
await di.runSetups();
computeDiff = computeDiffFor({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
});
di.inject(clusterStoreInjectable);
ClusterManager.createInstance(); ClusterManager.createInstance();
}); });
afterEach(() => { afterEach(() => {
mockFs.restore(); mockFs.restore();
ClusterStore.resetInstance();
ClusterManager.resetInstance(); ClusterManager.resetInstance();
}); });

View File

@ -20,5 +20,4 @@
*/ */
export { syncWeblinks } from "./weblinks"; export { syncWeblinks } from "./weblinks";
export { KubeconfigSyncManager } from "./kubeconfig-sync";
export { syncGeneralEntities } from "./general"; export { syncGeneralEntities } from "./general";

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { KubeconfigSyncManager } from "./kubeconfig-sync-manager";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
const kubeconfigSyncManagerInjectable = getInjectable({
instantiate: (di) => new KubeconfigSyncManager({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
}),
lifecycle: lifecycleEnum.singleton,
});
export default kubeconfigSyncManagerInjectable;

View File

@ -20,25 +20,25 @@
*/ */
import { action, observable, IComputedValue, computed, ObservableMap, runInAction, makeObservable, observe } from "mobx"; import { action, observable, IComputedValue, computed, ObservableMap, runInAction, makeObservable, observe } from "mobx";
import type { CatalogEntity } from "../../common/catalog"; import type { CatalogEntity } from "../../../common/catalog";
import { catalogEntityRegistry } from "../../main/catalog"; import { catalogEntityRegistry } from "../../catalog";
import { FSWatcher, watch } from "chokidar"; import { FSWatcher, watch } from "chokidar";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import type stream from "stream"; import type stream from "stream";
import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop, Singleton, storedKubeConfigFolder } from "../../common/utils"; import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop } from "../../../common/utils";
import logger from "../logger"; import logger from "../../logger";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig } from "../../common/kube-helpers"; import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
import { Cluster } from "../cluster"; import { catalogEntityFromCluster, ClusterManager } from "../../cluster-manager";
import { catalogEntityFromCluster, ClusterManager } from "../cluster-manager"; import { UserStore } from "../../../common/user-store";
import { UserStore } from "../../common/user-store"; import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { ClusterStore } from "../../common/cluster-store";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { homedir } from "os"; import { homedir } from "os";
import globToRegExp from "glob-to-regexp"; import globToRegExp from "glob-to-regexp";
import { inspect } from "util"; import { inspect } from "util";
import type { UpdateClusterModel } from "../../common/cluster-types"; import type { ClusterModel, UpdateClusterModel } from "../../../common/cluster-types";
import type { Cluster } from "../../../common/cluster/cluster";
const logPrefix = "[KUBECONFIG-SYNC]:"; const logPrefix = "[KUBECONFIG-SYNC]:";
@ -63,16 +63,19 @@ const ignoreGlobs = [
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
export class KubeconfigSyncManager extends Singleton { interface Dependencies {
directoryForKubeConfigs: string
createCluster: (model: ClusterModel) => Cluster
}
const kubeConfigSyncName = "lens:kube-sync";
export class KubeconfigSyncManager {
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>(); protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
protected syncing = false; protected syncing = false;
protected syncListDisposer?: Disposer; protected syncListDisposer?: Disposer;
protected static readonly syncName = "lens:kube-sync"; constructor(private dependencies: Dependencies) {
constructor() {
super();
makeObservable(this); makeObservable(this);
} }
@ -86,7 +89,7 @@ export class KubeconfigSyncManager extends Singleton {
logger.info(`${logPrefix} starting requested syncs`); logger.info(`${logPrefix} starting requested syncs`);
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => ( catalogEntityRegistry.addComputedSource(kubeConfigSyncName, computed(() => (
Array.from(iter.flatMap( Array.from(iter.flatMap(
this.sources.values(), this.sources.values(),
([entities]) => entities.get(), ([entities]) => entities.get(),
@ -94,7 +97,7 @@ export class KubeconfigSyncManager extends Singleton {
))); )));
// This must be done so that c&p-ed clusters are visible // This must be done so that c&p-ed clusters are visible
this.startNewSync(storedKubeConfigFolder()); this.startNewSync(this.dependencies.directoryForKubeConfigs);
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) { for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
this.startNewSync(filePath); this.startNewSync(filePath);
@ -120,7 +123,7 @@ export class KubeconfigSyncManager extends Singleton {
this.stopOldSync(filePath); this.stopOldSync(filePath);
} }
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName); catalogEntityRegistry.removeSource(kubeConfigSyncName);
this.syncing = false; this.syncing = false;
} }
@ -131,7 +134,11 @@ export class KubeconfigSyncManager extends Singleton {
return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath }); return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath });
} }
this.sources.set(filePath, watchFileChanges(filePath)); this.sources.set(
filePath,
watchFileChanges(filePath, this.dependencies),
);
logger.info(`${logPrefix} starting sync of file/folder`, { filePath }); logger.info(`${logPrefix} starting sync of file/folder`, { filePath });
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) }); logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
} }
@ -170,7 +177,7 @@ type RootSourceValue = [Cluster, CatalogEntity];
type RootSource = ObservableMap<string, RootSourceValue>; type RootSource = ObservableMap<string, RootSourceValue>;
// exported for testing // exported for testing
export function computeDiff(contents: string, source: RootSource, filePath: string): void { export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Dependencies) => (contents: string, source: RootSource, filePath: string): void => {
runInAction(() => { runInAction(() => {
try { try {
const { config, error } = loadConfigFromString(contents); const { config, error } = loadConfigFromString(contents);
@ -212,7 +219,8 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
// 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 = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId });
const cluster = ClusterStore.getInstance().getById(clusterId) || createCluster({ ...model, id: clusterId });
if (!cluster.apiUrl) { if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error"); throw new Error("Cluster constructor failed, see above error");
@ -220,7 +228,7 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
const entity = catalogEntityFromCluster(cluster); const entity = catalogEntityFromCluster(cluster);
if (!filePath.startsWith(storedKubeConfigFolder())) { if (!filePath.startsWith(directoryForKubeConfigs)) {
entity.metadata.labels.file = filePath.replace(homedir(), "~"); entity.metadata.labels.file = filePath.replace(homedir(), "~");
} }
source.set(contextName, [cluster, entity]); source.set(contextName, [cluster, entity]);
@ -231,11 +239,12 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri
} }
} }
} catch (error) { } catch (error) {
console.log(error);
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath }); logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
source.clear(); // clear source if we have failed so as to not show outdated information source.clear(); // clear source if we have failed so as to not show outdated information
} }
}); });
} };
interface DiffChangedConfigArgs { interface DiffChangedConfigArgs {
filePath: string; filePath: string;
@ -244,7 +253,7 @@ interface DiffChangedConfigArgs {
maxAllowedFileReadSize: number; maxAllowedFileReadSize: number;
} }
function diffChangedConfig({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer { const diffChangedConfigFor = (dependencies: Dependencies) => ({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer => {
logger.debug(`${logPrefix} file changed`, { filePath }); logger.debug(`${logPrefix} file changed`, { filePath });
if (stats.size >= maxAllowedFileReadSize) { if (stats.size >= maxAllowedFileReadSize) {
@ -293,14 +302,14 @@ function diffChangedConfig({ filePath, source, stats, maxAllowedFileReadSize }:
}) })
.on("end", () => { .on("end", () => {
if (!closed) { if (!closed) {
computeDiff(fileString, source, filePath); computeDiff(dependencies)(fileString, source, filePath);
} }
}); });
return cleanup; return cleanup;
} };
function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, Disposer] { const watchFileChanges = (filePath: string, dependencies: Dependencies): [IComputedValue<CatalogEntity[]>, Disposer] => {
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>(); const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1])))); const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
@ -328,6 +337,8 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
atomic: 150, // for "atomic writes" atomic: 150, // for "atomic writes"
}); });
const diffChangedConfig = diffChangedConfigFor(dependencies);
watcher watcher
.on("change", (childFilePath, stats) => { .on("change", (childFilePath, stats) => {
const cleanup = cleanupFns.get(childFilePath); const cleanup = cleanupFns.get(childFilePath);
@ -378,4 +389,4 @@ function watchFileChanges(filePath: string): [IComputedValue<CatalogEntity[]>, D
return [derivedSource, () => { return [derivedSource, () => {
watcher?.close(); watcher?.close();
}]; }];
} };

View File

@ -20,7 +20,7 @@
*/ */
import type { RequestPromiseOptions } from "request-promise-native"; import type { RequestPromiseOptions } from "request-promise-native";
import type { Cluster } from "../cluster"; import type { Cluster } from "../../common/cluster/cluster";
import { k8sRequest } from "../k8s-request"; import { k8sRequest } from "../k8s-request";
export type ClusterDetectionResult = { export type ClusterDetectionResult = {

View File

@ -22,7 +22,7 @@
import { observable } from "mobx"; import { observable } from "mobx";
import type { ClusterMetadata } from "../../common/cluster-types"; import type { ClusterMetadata } from "../../common/cluster-types";
import { Singleton } from "../../common/utils"; import { Singleton } from "../../common/utils";
import type { Cluster } from "../cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector"; import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
export class DetectorRegistry extends Singleton { export class DetectorRegistry extends Singleton {

View File

@ -22,7 +22,7 @@
import "../common/cluster-ipc"; import "../common/cluster-ipc";
import type http from "http"; import type http from "http";
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx"; import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
import { Cluster } from "./cluster"; import { Cluster } from "../common/cluster/cluster";
import logger from "./logger"; import logger from "./logger";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { getClusterIdFromHost, Singleton } from "../common/utils"; import { getClusterIdFromHost, Singleton } from "../common/utils";
@ -30,7 +30,7 @@ import { catalogEntityRegistry } from "./catalog";
import { KubernetesCluster, KubernetesClusterPrometheusMetrics, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster"; import { KubernetesCluster, KubernetesClusterPrometheusMetrics, 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";
import { ClusterStore } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store/cluster-store";
import type { ClusterId } from "../common/cluster-types"; import type { ClusterId } from "../common/cluster-types";
const logPrefix = "[CLUSTER-MANAGER]:"; const logPrefix = "[CLUSTER-MANAGER]:";

View File

@ -19,15 +19,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"; import type { PrometheusProvider, PrometheusService } from "../prometheus/provider-registry";
import { PrometheusProviderRegistry } from "./prometheus/provider-registry"; import { PrometheusProviderRegistry } from "../prometheus/provider-registry";
import type { ClusterPrometheusPreferences } from "../common/cluster-types"; import type { ClusterPrometheusPreferences } from "../../common/cluster-types";
import type { Cluster } from "./cluster"; import type { Cluster } from "../../common/cluster/cluster";
import type httpProxy from "http-proxy"; import type httpProxy from "http-proxy";
import url, { UrlWithStringQuery } from "url"; import url, { UrlWithStringQuery } from "url";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import logger from "./logger"; import logger from "../logger";
import { KubeAuthProxy } from "./kube-auth-proxy"; import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
export interface PrometheusDetails { export interface PrometheusDetails {
prometheusPath: string; prometheusPath: string;
@ -41,6 +41,10 @@ interface PrometheusServicePreferences {
prefix: string; prefix: string;
} }
interface Dependencies {
createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy
}
export class ContextHandler { export class ContextHandler {
public clusterUrl: UrlWithStringQuery; public clusterUrl: UrlWithStringQuery;
protected kubeAuthProxy?: KubeAuthProxy; protected kubeAuthProxy?: KubeAuthProxy;
@ -48,7 +52,7 @@ export class ContextHandler {
protected prometheusProvider?: string; protected prometheusProvider?: string;
protected prometheus?: PrometheusServicePreferences; protected prometheus?: PrometheusServicePreferences;
constructor(protected cluster: Cluster) { constructor(private dependencies: Dependencies, protected cluster: Cluster) {
this.clusterUrl = url.parse(cluster.apiUrl); this.clusterUrl = url.parse(cluster.apiUrl);
this.setupPrometheus(cluster.preferences); this.setupPrometheus(cluster.preferences);
} }
@ -161,7 +165,7 @@ export class ContextHandler {
if (this.cluster.preferences.httpsProxy) { if (this.cluster.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy; proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
} }
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, proxyEnv); this.kubeAuthProxy = this.dependencies.createKubeAuthProxy(this.cluster, proxyEnv);
await this.kubeAuthProxy.run(); await this.kubeAuthProxy.run();
} }

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster";
import { ContextHandler } from "./context-handler";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
const createContextHandlerInjectable = getInjectable({
instantiate: (di) => {
const dependencies = {
createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable),
};
return (cluster: Cluster) => new ContextHandler(dependencies, cluster);
},
lifecycle: lifecycleEnum.singleton,
});
export default createContextHandlerInjectable;

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { ClusterModel } from "../../common/cluster-types";
import { Cluster } from "../../common/cluster/cluster";
import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable";
import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
const createClusterInjectable = getInjectable({
instantiate: (di) => {
const dependencies = {
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createKubeconfigManager: di.inject(createKubeconfigManagerInjectable),
createKubectl: di.inject(createKubectlInjectable),
createContextHandler: di.inject(createContextHandlerInjectable),
};
return (model: ClusterModel) => new Cluster(dependencies, model);
},
injectionToken: createClusterInjectionToken,
lifecycle: lifecycleEnum.singleton,
});
export default createClusterInjectable;

View File

@ -21,7 +21,7 @@
import { app } from "electron"; import { app } from "electron";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { appEventBus } from "../common/event-bus"; import { appEventBus } from "../common/app-event-bus/event-bus";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
import logger from "./logger"; import logger from "./logger";

View File

@ -26,6 +26,7 @@ export const getDi = () => {
const di = createContainer( const di = createContainer(
getRequireContextForMainCode, getRequireContextForMainCode,
getRequireContextForCommonExtensionCode, getRequireContextForCommonExtensionCode,
getRequireContextForCommonCode,
); );
setLegacyGlobalDiForExtensionApi(di); setLegacyGlobalDiForExtensionApi(di);
@ -38,3 +39,6 @@ const getRequireContextForMainCode = () =>
const getRequireContextForCommonExtensionCode = () => const getRequireContextForCommonExtensionCode = () =>
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/); require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
const getRequireContextForCommonCode = () =>
require.context("../common", true, /\.injectable\.(ts|tsx)$/);

View File

@ -21,20 +21,32 @@
import glob from "glob"; import glob from "glob";
import { memoize } from "lodash/fp"; import { memoize } from "lodash/fp";
import { kebabCase } from "lodash/fp";
import { import {
createContainer, createContainer,
ConfigurableDependencyInjectionContainer, ConfigurableDependencyInjectionContainer,
} from "@ogre-tools/injectable"; } from "@ogre-tools/injectable";
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
export const getDiForUnitTesting = () => { import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "./app-paths/app-name/app-name.injectable";
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
import writeJsonFileInjectable
from "../common/fs/write-json-file/write-json-file.injectable";
import readJsonFileInjectable
from "../common/fs/read-json-file/read-json-file.injectable";
export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false },
) => {
const di: ConfigurableDependencyInjectionContainer = createContainer(); const di: ConfigurableDependencyInjectionContainer = createContainer();
setLegacyGlobalDiForExtensionApi(di); setLegacyGlobalDiForExtensionApi(di);
getInjectableFilePaths() getInjectableFilePaths()
.map(key => { .map((key) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const injectable = require(key).default; const injectable = require(key).default;
@ -45,14 +57,34 @@ export const getDiForUnitTesting = () => {
}; };
}) })
.forEach(injectable => di.register(injectable)); .forEach((injectable) => di.register(injectable));
di.preventSideEffects(); di.preventSideEffects();
if (doGeneralOverrides) {
di.override(
getElectronAppPathInjectable,
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
);
di.override(setElectronAppPathInjectable, () => () => undefined);
di.override(appNameInjectable, () => "some-electron-app-name");
di.override(registerChannelInjectable, () => () => undefined);
di.override(writeJsonFileInjectable, () => () => {
throw new Error("Tried to write JSON file to file system without specifying explicit override.");
});
di.override(readJsonFileInjectable, () => () => {
throw new Error("Tried to read JSON file from file system without specifying explicit override.");
});
}
return di; return di;
}; };
const getInjectableFilePaths = memoize(() => [ const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }), ...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]); ]);

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { Cluster } from "../cluster"; import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger"; import logger from "../logger";
import { HelmRepoManager } from "./helm-repo-manager"; import { HelmRepoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager"; import { HelmChartManager } from "./helm-chart-manager";

Some files were not shown because too many files have changed in this diff Show More