mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
1 parent
e810eab39f
commit
b7974827d2
786 files changed
+14567
-24066
No files matched your search
@@ -1,9 +0,0 @@
|
||||
const userStore = {
|
||||
getPreferences: jest.fn(() => {
|
||||
return {
|
||||
downloadMirror: "default"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { userStore };
|
||||
@@ -1,14 +0,0 @@
|
||||
import { app, remote } from "electron"
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns app version correctly regardless of dev/prod mode and main/renderer differences
|
||||
*/
|
||||
export function getAppVersion(): string {
|
||||
// app is undefined when running in renderer
|
||||
let version = (app || remote.app).getVersion()
|
||||
if(process.env.NODE_ENV === 'development') {
|
||||
version = require("../../package.json").version
|
||||
}
|
||||
return version;
|
||||
}
|
||||
+18
-15
@@ -1,12 +1,12 @@
|
||||
import * as ElectronStore from "electron-store"
|
||||
import ElectronStore from "electron-store"
|
||||
import { Cluster, ClusterBaseInfo } from "../main/cluster";
|
||||
import { getAppVersion } from "./app-utils"
|
||||
import * as version200Beta2 from "./migrations/cluster-store/2.0.0-beta.2"
|
||||
import * as version241 from "./migrations/cluster-store/2.4.1"
|
||||
import * as version260Beta2 from "./migrations/cluster-store/2.6.0-beta.2"
|
||||
import * as version260Beta3 from "./migrations/cluster-store/2.6.0-beta.3"
|
||||
import * as version270Beta0 from "./migrations/cluster-store/2.7.0-beta.0"
|
||||
import * as version270Beta1 from "./migrations/cluster-store/2.7.0-beta.1"
|
||||
import * as version200Beta2 from "../migrations/cluster-store/2.0.0-beta.2"
|
||||
import * as version241 from "../migrations/cluster-store/2.4.1"
|
||||
import * as version260Beta2 from "../migrations/cluster-store/2.6.0-beta.2"
|
||||
import * as version260Beta3 from "../migrations/cluster-store/2.6.0-beta.3"
|
||||
import * as version270Beta0 from "../migrations/cluster-store/2.7.0-beta.0"
|
||||
import * as version270Beta1 from "../migrations/cluster-store/2.7.0-beta.1"
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
|
||||
export class ClusterStore {
|
||||
private static instance: ClusterStore;
|
||||
@@ -14,8 +14,10 @@ export class ClusterStore {
|
||||
|
||||
private constructor() {
|
||||
this.store = new ElectronStore({
|
||||
name: "lens-cluster-store",
|
||||
// @ts-ignore
|
||||
// fixme: tests are failed without "projectVersion"
|
||||
projectVersion: getAppVersion(),
|
||||
name: "lens-cluster-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
migrations: {
|
||||
"2.0.0-beta.2": version200Beta2.migration,
|
||||
@@ -58,7 +60,9 @@ export class ClusterStore {
|
||||
|
||||
public getCluster(id: string): Cluster {
|
||||
const cluster = this.getAllClusterObjects().find((cluster) => cluster.id === id)
|
||||
if (cluster) { return cluster}
|
||||
if (cluster) {
|
||||
return cluster
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -74,7 +78,8 @@ export class ClusterStore {
|
||||
}
|
||||
if (index === -1) {
|
||||
clusters.push(storable)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
clusters[index] = storable
|
||||
}
|
||||
this.store.set("clusters", clusters)
|
||||
@@ -97,7 +102,7 @@ export class ClusterStore {
|
||||
}
|
||||
|
||||
static getInstance(): ClusterStore {
|
||||
if(!ClusterStore.instance) {
|
||||
if (!ClusterStore.instance) {
|
||||
ClusterStore.instance = new ClusterStore();
|
||||
}
|
||||
return ClusterStore.instance;
|
||||
@@ -108,6 +113,4 @@ export class ClusterStore {
|
||||
}
|
||||
}
|
||||
|
||||
const clusterStore: ClusterStore = ClusterStore.getInstance();
|
||||
|
||||
export { clusterStore };
|
||||
export const clusterStore = ClusterStore.getInstance();
|
||||
@@ -0,0 +1,333 @@
|
||||
import mockFs from "mock-fs"
|
||||
import yaml from "js-yaml"
|
||||
import { ClusterStore } from "./cluster-store";
|
||||
import { Cluster } from "../main/cluster";
|
||||
|
||||
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
|
||||
console.log("");
|
||||
|
||||
describe("for an empty config", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("allows to store and retrieve a cluster", async () => {
|
||||
const cluster = new Cluster({
|
||||
id: 'foo',
|
||||
kubeConfig: 'kubeconfig string',
|
||||
preferences: {
|
||||
terminalCWD: '/tmp',
|
||||
icon: 'path to icon'
|
||||
}
|
||||
})
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
clusterStore.storeCluster(cluster);
|
||||
const storedCluster = clusterStore.getCluster(cluster.id);
|
||||
expect(storedCluster.kubeConfig).toBe(cluster.kubeConfig)
|
||||
expect(storedCluster.preferences.icon).toBe(cluster.preferences.icon)
|
||||
expect(storedCluster.preferences.terminalCWD).toBe(cluster.preferences.terminalCWD)
|
||||
expect(storedCluster.id).toBe(cluster.id)
|
||||
})
|
||||
|
||||
it("allows to delete a cluster", async () => {
|
||||
const cluster = new Cluster({
|
||||
id: 'foofoo',
|
||||
kubeConfig: 'kubeconfig string',
|
||||
preferences: {
|
||||
terminalCWD: '/tmp'
|
||||
}
|
||||
})
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
|
||||
clusterStore.storeCluster(cluster);
|
||||
|
||||
const storedCluster = clusterStore.getCluster(cluster.id);
|
||||
expect(storedCluster.id).toBe(cluster.id)
|
||||
|
||||
clusterStore.removeCluster(cluster.id);
|
||||
|
||||
expect(clusterStore.getCluster(cluster.id)).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe("for a config with existing clusters", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "99.99.99"
|
||||
}
|
||||
},
|
||||
clusters: [
|
||||
{
|
||||
id: 'cluster1',
|
||||
kubeConfig: 'foo',
|
||||
preferences: { terminalCWD: '/foo' }
|
||||
},
|
||||
{
|
||||
id: 'cluster2',
|
||||
kubeConfig: 'foo2',
|
||||
preferences: { terminalCWD: '/foo2' }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("allows to retrieve a cluster", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedCluster = clusterStore.getCluster('cluster1')
|
||||
expect(storedCluster.kubeConfig).toBe('foo')
|
||||
expect(storedCluster.preferences.terminalCWD).toBe('/foo')
|
||||
expect(storedCluster.id).toBe('cluster1')
|
||||
|
||||
const storedCluster2 = clusterStore.getCluster('cluster2')
|
||||
expect(storedCluster2.kubeConfig).toBe('foo2')
|
||||
expect(storedCluster2.preferences.terminalCWD).toBe('/foo2')
|
||||
expect(storedCluster2.id).toBe('cluster2')
|
||||
})
|
||||
|
||||
it("allows to delete a cluster", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
|
||||
clusterStore.removeCluster('cluster2')
|
||||
|
||||
// Verify the other cluster still exists:
|
||||
const storedCluster = clusterStore.getCluster('cluster1')
|
||||
expect(storedCluster.id).toBe('cluster1')
|
||||
|
||||
const storedCluster2 = clusterStore.getCluster('cluster2')
|
||||
expect(storedCluster2).toBe(null)
|
||||
})
|
||||
|
||||
it("allows to reload a cluster in-place", async () => {
|
||||
const cluster = new Cluster({
|
||||
id: 'cluster1',
|
||||
kubeConfig: 'kubeconfig string',
|
||||
preferences: {
|
||||
terminalCWD: '/tmp'
|
||||
}
|
||||
})
|
||||
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
clusterStore.reloadCluster(cluster)
|
||||
|
||||
expect(cluster.kubeConfig).toBe('foo')
|
||||
expect(cluster.preferences.terminalCWD).toBe('/foo')
|
||||
expect(cluster.id).toBe('cluster1')
|
||||
})
|
||||
|
||||
it("allows getting all the clusters", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedClusters = clusterStore.getAllClusters()
|
||||
|
||||
expect(storedClusters[0].id).toBe('cluster1')
|
||||
expect(storedClusters[0].preferences.terminalCWD).toBe('/foo')
|
||||
expect(storedClusters[0].kubeConfig).toBe('foo')
|
||||
|
||||
expect(storedClusters[1].id).toBe('cluster2')
|
||||
expect(storedClusters[1].preferences.terminalCWD).toBe('/foo2')
|
||||
expect(storedClusters[1].kubeConfig).toBe('foo2')
|
||||
})
|
||||
|
||||
it("allows storing the clusters in a different order", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedClusters = clusterStore.getAllClusters()
|
||||
|
||||
const reorderedClusters = [storedClusters[1], storedClusters[0]]
|
||||
clusterStore.storeClusters(reorderedClusters)
|
||||
const storedClusters2 = clusterStore.getAllClusters()
|
||||
|
||||
expect(storedClusters2[0].id).toBe('cluster2')
|
||||
expect(storedClusters2[1].id).toBe('cluster1')
|
||||
})
|
||||
})
|
||||
|
||||
describe("for a pre 2.0 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "1.0.0"
|
||||
}
|
||||
},
|
||||
cluster1: 'kubeconfig content'
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("migrates to modern format with kubeconfig under a key", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedCluster = clusterStore.store.get('clusters')[0]
|
||||
expect(storedCluster.kubeConfig).toBe('kubeconfig content')
|
||||
})
|
||||
})
|
||||
|
||||
describe("for a pre 2.4.1 config with an existing cluster", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.0.0-beta.2"
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: 'foo',
|
||||
online: true,
|
||||
accessible: false,
|
||||
failureReason: 'user error'
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("migrates to modern format throwing out the state related data", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedClusterData = clusterStore.store.get('clusters')[0]
|
||||
expect(storedClusterData.hasOwnProperty('online')).toBe(false)
|
||||
expect(storedClusterData.hasOwnProperty('accessible')).toBe(false)
|
||||
expect(storedClusterData.hasOwnProperty('failureReason')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("for a pre 2.6.0 config with a cluster that has arrays in auth config", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.4.1"
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: "apiVersion: v1\nclusters:\n- cluster:\n server: https://10.211.55.6:8443\n name: minikube\ncontexts:\n- context:\n cluster: minikube\n user: minikube\n name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n user:\n client-certificate: /Users/kimmo/.minikube/client.crt\n client-key: /Users/kimmo/.minikube/client.key\n auth-provider:\n config:\n access-token:\n - should be string\n expiry:\n - should be string\n"
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("replaces array format access token and expiry into string", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedClusterData = clusterStore.store.get('clusters')[0]
|
||||
const kc = yaml.safeLoad(storedClusterData.kubeConfig)
|
||||
expect(kc.users[0].user['auth-provider'].config['access-token']).toBe("should be string")
|
||||
expect(kc.users[0].user['auth-provider'].config['expiry']).toBe("should be string")
|
||||
})
|
||||
})
|
||||
|
||||
describe("for a pre 2.6.0 config with a cluster icon", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.4.1"
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: "foo",
|
||||
icon: "icon path",
|
||||
preferences: {
|
||||
terminalCWD: "/tmp"
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("moves the icon into preferences", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedClusterData = clusterStore.store.get('clusters')[0]
|
||||
expect(storedClusterData.hasOwnProperty('icon')).toBe(false)
|
||||
expect(storedClusterData.preferences.hasOwnProperty('icon')).toBe(true)
|
||||
expect(storedClusterData.preferences.icon).toBe("icon path")
|
||||
})
|
||||
})
|
||||
|
||||
describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'lens-cluster-store.json': JSON.stringify({
|
||||
__internal__: {
|
||||
migrations: {
|
||||
version: "2.6.6"
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: "foo",
|
||||
icon: "icon path",
|
||||
preferences: {
|
||||
terminalCWD: "/tmp"
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("adds cluster to default workspace", async () => {
|
||||
const clusterStore = ClusterStore.getInstance()
|
||||
const storedClusterData = clusterStore.store.get("clusters")[0]
|
||||
expect(storedClusterData.workspace).toBe('default')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
// Setup static folder for common assets
|
||||
|
||||
import path from "path";
|
||||
import { protocol } from "electron"
|
||||
import logger from "../main/logger";
|
||||
import { staticDir, staticProto, outDir } from "./vars";
|
||||
|
||||
export function registerStaticProtocol(rootFolder = staticDir) {
|
||||
const scheme = staticProto.replace("://", "");
|
||||
protocol.registerFileProtocol(scheme, (request, callback) => {
|
||||
const relativePath = request.url.replace(staticProto, "");
|
||||
const absPath = path.resolve(rootFolder, relativePath);
|
||||
callback(absPath);
|
||||
}, (error) => {
|
||||
logger.debug(`Failed to register protocol "${scheme}"`, error);
|
||||
})
|
||||
}
|
||||
|
||||
export function getStaticUrl(filePath: string) {
|
||||
return staticProto + filePath;
|
||||
}
|
||||
|
||||
export function getStaticPath(filePath: string) {
|
||||
return path.resolve(staticDir, filePath);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as request from "request"
|
||||
import { userStore } from "../common/user-store"
|
||||
import request from "request"
|
||||
import { userStore } from "./user-store"
|
||||
|
||||
export function globalRequestOpts(requestOpts: request.Options ) {
|
||||
const userPrefs = userStore.getPreferences()
|
||||
|
||||
+11
-3
@@ -1,6 +1,14 @@
|
||||
import * as winca from "win-ca/api"
|
||||
import "mac-ca"
|
||||
import { isMac, isWindows } from "./vars";
|
||||
import winca from "win-ca"
|
||||
import macca from "mac-ca"
|
||||
import logger from "../main/logger"
|
||||
|
||||
if (process.platform === "win32") {
|
||||
if (isMac) {
|
||||
for (const crt of macca.all()) {
|
||||
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`)
|
||||
logger.debug("Using host CA: " + attributes.join(","))
|
||||
}
|
||||
}
|
||||
if (isWindows) {
|
||||
winca.inject("+") // see: https://github.com/ukoloff/win-ca#caveats
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { machineIdSync } from 'node-machine-id'
|
||||
import { userStore } from "../common/user-store"
|
||||
import * as ua from "universal-analytics"
|
||||
import ua from "universal-analytics"
|
||||
import { machineIdSync } from "node-machine-id"
|
||||
import { userStore } from "./user-store"
|
||||
|
||||
const GA_ID = "UA-159377374-1"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as ElectronStore from "electron-store"
|
||||
import * as appUtil from "./app-utils"
|
||||
import * as version210Beta4 from "./migrations/user-store/2.1.0-beta.4"
|
||||
import ElectronStore from "electron-store"
|
||||
import * as version210Beta4 from "../migrations/user-store/2.1.0-beta.4"
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
|
||||
export interface User {
|
||||
id?: string;
|
||||
@@ -20,7 +20,9 @@ export class UserStore {
|
||||
|
||||
private constructor() {
|
||||
this.store = new ElectronStore({
|
||||
projectVersion: appUtil.getAppVersion(),
|
||||
// @ts-ignore
|
||||
// fixme: tests are failed without "projectVersion"
|
||||
projectVersion: getAppVersion(),
|
||||
migrations: {
|
||||
"2.1.0-beta.4": version210Beta4.migration,
|
||||
}
|
||||
@@ -68,7 +70,7 @@ export class UserStore {
|
||||
}
|
||||
|
||||
static getInstance(): UserStore {
|
||||
if(!UserStore.instance) {
|
||||
if (!UserStore.instance) {
|
||||
UserStore.instance = new UserStore();
|
||||
}
|
||||
return UserStore.instance;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import mockFs from "mock-fs"
|
||||
import { userStore, UserStore } from "./user-store"
|
||||
|
||||
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
|
||||
console.log("");
|
||||
|
||||
describe("for an empty config", () => {
|
||||
beforeEach(() => {
|
||||
UserStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'config.json': JSON.stringify({})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
const userStore = UserStore.getInstance()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("allows setting and retrieving lastSeenAppVersion", async () => {
|
||||
userStore.setLastSeenAppVersion("1.2.3");
|
||||
expect(userStore.lastSeenAppVersion()).toBe("1.2.3");
|
||||
})
|
||||
|
||||
it("allows adding and listing seen contexts", async () => {
|
||||
userStore.storeSeenContext(['foo'])
|
||||
expect(userStore.getSeenContexts().length).toBe(1)
|
||||
userStore.storeSeenContext(['foo', 'bar'])
|
||||
const seenContexts = userStore.getSeenContexts()
|
||||
expect(seenContexts.length).toBe(2) // check 'foo' isn't added twice
|
||||
expect(seenContexts[0]).toBe('foo')
|
||||
expect(seenContexts[1]).toBe('bar')
|
||||
})
|
||||
|
||||
it("allows setting and getting preferences", async () => {
|
||||
userStore.setPreferences({
|
||||
httpsProxy: 'abcd://defg',
|
||||
})
|
||||
const storedPreferences = userStore.getPreferences()
|
||||
expect(storedPreferences.httpsProxy).toBe('abcd://defg')
|
||||
expect(storedPreferences.colorTheme).toBe('dark') // defaults to dark
|
||||
userStore.setPreferences({
|
||||
colorTheme: 'light'
|
||||
})
|
||||
expect(userStore.getPreferences().colorTheme).toBe('light')
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrations", () => {
|
||||
beforeEach(() => {
|
||||
UserStore.resetInstance()
|
||||
const mockOpts = {
|
||||
'tmp': {
|
||||
'config.json': JSON.stringify({
|
||||
user: { username: 'foobar' },
|
||||
preferences: { colorTheme: 'light' },
|
||||
lastSeenAppVersion: '1.2.3'
|
||||
})
|
||||
}
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
const userStore = UserStore.getInstance()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore()
|
||||
})
|
||||
|
||||
it("sets last seen app version to 0.0.0", async () => {
|
||||
expect(userStore.lastSeenAppVersion()).toBe('0.0.0')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import packageInfo from "../../../package.json"
|
||||
|
||||
export function getAppVersion(): string {
|
||||
return packageInfo.version;
|
||||
}
|
||||
|
||||
export function getBundledKubectlVersion(): string {
|
||||
return packageInfo.config.bundledKubectlVersion;
|
||||
}
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
// Encode/decode utf-8 base64 string
|
||||
import * as Base64 from "crypto-js/enc-base64";
|
||||
import * as Utf8 from "crypto-js/enc-utf8";
|
||||
|
||||
export const base64 = {
|
||||
decode(data: string) {
|
||||
return Base64.parse(data).toString(Utf8);
|
||||
},
|
||||
encode(data: string) {
|
||||
return Utf8.parse(data).toString(Base64);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
// Convert object's keys to camelCase format
|
||||
import { camelCase, isPlainObject } from "lodash";
|
||||
|
||||
export function toCamelCase(obj: Record<string, any>): any {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(toCamelCase);
|
||||
}
|
||||
else if (isPlainObject(obj)) {
|
||||
return Object.keys(obj).reduce((result, key) => {
|
||||
const value = obj[key];
|
||||
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
|
||||
return result;
|
||||
}, {} as any);
|
||||
}
|
||||
else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Common utils (main/renderer)
|
||||
|
||||
export * from "./base64"
|
||||
export * from "./camelCase"
|
||||
export * from "./splitArray"
|
||||
@@ -0,0 +1,19 @@
|
||||
// Moved from dashboard/client/utils/arrays.ts
|
||||
/**
|
||||
* This function splits an array into two sub arrays on the first instance of
|
||||
* element (from the left). If the array does not contain the element. The
|
||||
* return value is defined to be `[array, [], false]`. If the element is in
|
||||
* the array then the return value is `[left, right, true]` where `left` is
|
||||
* the elements of `array` from `[0, index)` and `right` is `(index, length)`
|
||||
* @param array the full array to split into two sub-arrays
|
||||
* @param element the element in the middle of the array
|
||||
* @returns the left and right sub-arrays which when conjoined with `element`
|
||||
* is the same as `array`, and `true`
|
||||
*/
|
||||
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
|
||||
const index = array.indexOf(element);
|
||||
if (index < 0) {
|
||||
return [array, [], false];
|
||||
}
|
||||
return [array.slice(0, index), array.slice(index + 1, array.length), true]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { splitArray } from "./splitArray";
|
||||
|
||||
describe("split array on element tests", () => {
|
||||
test("empty array", () => {
|
||||
expect(splitArray([], 10)).toStrictEqual([[], [], false]);
|
||||
});
|
||||
|
||||
test("one element, not in array", () => {
|
||||
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
|
||||
});
|
||||
|
||||
test("ten elements, not in array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
|
||||
});
|
||||
|
||||
test("one elements, in array", () => {
|
||||
expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in front array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in middle array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in end array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// App's common configuration for any process (main, renderer, build pipeline, etc.)
|
||||
import path from "path";
|
||||
|
||||
// Temp
|
||||
export const reactAppName = "app_react"
|
||||
export const vueAppName = "app_vue"
|
||||
|
||||
// Flags
|
||||
export const isMac = process.platform === "darwin"
|
||||
export const isWindows = process.platform === "win32"
|
||||
export const isDebugging = process.env.DEBUG === "true";
|
||||
export const isProduction = process.env.NODE_ENV === "production"
|
||||
export const isDevelopment = isDebugging || !isProduction;
|
||||
export const buildVersion = process.env.BUILD_VERSION;
|
||||
export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
||||
|
||||
// Paths
|
||||
export const contextDir = process.cwd();
|
||||
export const staticDir = path.join(contextDir, "static");
|
||||
export const outDir = path.join(contextDir, "out");
|
||||
export const mainDir = path.join(contextDir, "src/main");
|
||||
export const rendererDir = path.join(contextDir, "src/renderer");
|
||||
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
||||
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
||||
|
||||
// Apis
|
||||
export const staticProto = "static://"
|
||||
|
||||
export const apiPrefix = {
|
||||
BASE: '/api',
|
||||
TERMINAL: '/api-terminal', // terminal api
|
||||
KUBE_BASE: '/api-kube', // kubernetes cluster api
|
||||
KUBE_HELM: '/api-helm', // helm charts api
|
||||
KUBE_RESOURCE_APPLIER: "/api-resource",
|
||||
};
|
||||
|
||||
// Links
|
||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues"
|
||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI"
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as ElectronStore from "electron-store"
|
||||
import ElectronStore from "electron-store"
|
||||
import { clusterStore } from "./cluster-store"
|
||||
|
||||
export interface WorkspaceData {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Feature, FeatureStatus } from "../main/feature"
|
||||
import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node"
|
||||
import * as semver from "semver"
|
||||
import semver from "semver"
|
||||
import { Cluster } from "../main/cluster";
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
|
||||
|
||||
+14
-20
@@ -1,19 +1,18 @@
|
||||
import { KubeConfig } from "@kubernetes/client-node"
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as http from "http"
|
||||
import http from "http"
|
||||
import { Cluster, ClusterBaseInfo } from "./cluster"
|
||||
import { clusterStore } from "../common/cluster-store"
|
||||
import * as k8s from "./k8s"
|
||||
import logger from "./logger"
|
||||
import { LensProxy } from "./proxy"
|
||||
import { app } from "electron"
|
||||
import * as path from "path"
|
||||
import path from "path"
|
||||
import { promises } from "fs"
|
||||
import { ensureDir } from "fs-extra"
|
||||
import * as filenamify from "filenamify"
|
||||
import filenamify from "filenamify"
|
||||
import { v4 as uuid } from "uuid"
|
||||
|
||||
declare const __static: string;
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
export type FeatureInstallRequest = {
|
||||
name: string;
|
||||
@@ -92,7 +91,7 @@ export class ClusterManager {
|
||||
reject("No cluster contexts defined")
|
||||
}
|
||||
configs.forEach(c => {
|
||||
k8s.valideConfig(c)
|
||||
k8s.validateConfig(c)
|
||||
const cluster = new Cluster({
|
||||
id: uuid(),
|
||||
port: this.port,
|
||||
@@ -117,15 +116,15 @@ export class ClusterManager {
|
||||
logger.debug(`IPC: addCluster`)
|
||||
const cluster = await this.addNewCluster(clusterData)
|
||||
return {
|
||||
addedCluster: this.clusterResponse(cluster),
|
||||
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => this.clusterResponse(cluster))
|
||||
addedCluster: cluster.toClusterInfo(),
|
||||
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => cluster.toClusterInfo())
|
||||
}
|
||||
});
|
||||
|
||||
this.promiseIpc.on("getClusters", async (workspaceId: string) => {
|
||||
logger.debug(`IPC: getClusters, workspace ${workspaceId}`)
|
||||
const workspaceClusters = Array.from(this.getClusters()).filter((cluster) => cluster.workspace === workspaceId)
|
||||
return workspaceClusters.map((cluster: Cluster) => this.clusterResponse(cluster))
|
||||
return workspaceClusters.map((cluster: Cluster) => cluster.toClusterInfo())
|
||||
});
|
||||
|
||||
this.promiseIpc.on("getCluster", async (id: string) => {
|
||||
@@ -133,7 +132,7 @@ export class ClusterManager {
|
||||
const cluster = this.getCluster(id)
|
||||
if (cluster) {
|
||||
await cluster.refreshCluster()
|
||||
return this.clusterResponse(cluster)
|
||||
return cluster.toClusterInfo()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -181,7 +180,7 @@ export class ClusterManager {
|
||||
if(!cluster.preferences) cluster.preferences = {};
|
||||
cluster.preferences.icon = clusterIcon
|
||||
clusterStore.storeCluster(cluster);
|
||||
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
|
||||
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
|
||||
} catch(error) {
|
||||
return {success: false, message: error}
|
||||
}
|
||||
@@ -193,7 +192,7 @@ export class ClusterManager {
|
||||
if (cluster && cluster.preferences) {
|
||||
cluster.preferences.icon = null;
|
||||
clusterStore.storeCluster(cluster)
|
||||
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
|
||||
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
|
||||
} else {
|
||||
return {success: false, message: "Cluster not found"}
|
||||
}
|
||||
@@ -202,7 +201,7 @@ export class ClusterManager {
|
||||
this.promiseIpc.on("refreshCluster", async (clusterId: string) => {
|
||||
const cluster = this.clusters.get(clusterId)
|
||||
await cluster.refreshCluster()
|
||||
return this.clusterResponse(cluster)
|
||||
return cluster.toClusterInfo()
|
||||
});
|
||||
|
||||
this.promiseIpc.on("stopCluster", (clusterId: string) => {
|
||||
@@ -217,7 +216,7 @@ export class ClusterManager {
|
||||
|
||||
this.promiseIpc.on("removeCluster", (ctx: string) => {
|
||||
logger.debug(`IPC: removeCluster: ${ctx}`)
|
||||
return this.removeCluster(ctx).map((cluster: Cluster) => this.clusterResponse(cluster))
|
||||
return this.removeCluster(ctx).map((cluster: Cluster) => cluster.toClusterInfo())
|
||||
});
|
||||
|
||||
this.promiseIpc.on("clusterStored", (clusterId: string) => {
|
||||
@@ -263,7 +262,7 @@ export class ClusterManager {
|
||||
cluster = this.clusters.get(clusterId)
|
||||
if (cluster) {
|
||||
// we need to swap path prefix so that request is proxied to kube api
|
||||
req.url = req.url.replace(`/${clusterId}`, "/api-kube")
|
||||
req.url = req.url.replace(`/${clusterId}`, apiPrefix.KUBE_BASE)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -274,11 +273,6 @@ export class ClusterManager {
|
||||
return cluster;
|
||||
}
|
||||
|
||||
// TODO: remove this
|
||||
protected clusterResponse(cluster: Cluster) {
|
||||
return cluster.toClusterInfo()
|
||||
}
|
||||
|
||||
protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise<string> {
|
||||
await ensureDir(ClusterManager.clusterIconDir)
|
||||
fileName = filenamify(cluster.contextName + "-" + fileName)
|
||||
|
||||
+31
-34
@@ -3,12 +3,13 @@ import { FeatureStatusMap } from "./feature"
|
||||
import * as k8s from "./k8s"
|
||||
import { clusterStore } from "../common/cluster-store"
|
||||
import logger from "./logger"
|
||||
import { KubeConfig, CoreV1Api, AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||
import * as fm from "./feature-manager";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as request from "request-promise-native"
|
||||
import request from "request-promise-native"
|
||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
enum ClusterStatus {
|
||||
AccessGranted = 2,
|
||||
@@ -123,16 +124,9 @@ export class Cluster implements ClusterInfo {
|
||||
this.contextHandler.setClusterPreferences(this.preferences)
|
||||
|
||||
const connectionStatus = await this.getConnectionStatus()
|
||||
if (connectionStatus == ClusterStatus.AccessGranted) {
|
||||
this.accessible = true
|
||||
} else {
|
||||
this.accessible = false
|
||||
}
|
||||
if (connectionStatus > ClusterStatus.Offline) {
|
||||
this.online = true
|
||||
} else {
|
||||
this.online = false
|
||||
}
|
||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||
this.online = connectionStatus > ClusterStatus.Offline;
|
||||
|
||||
if (this.accessible) {
|
||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||
this.features = await fm.getFeatures(this.contextHandler)
|
||||
@@ -146,7 +140,9 @@ export class Cluster implements ClusterInfo {
|
||||
|
||||
public updateKubeconfig(kubeconfig: string) {
|
||||
const storedCluster = clusterStore.getCluster(this.id)
|
||||
if (!storedCluster) { return }
|
||||
if (!storedCluster) {
|
||||
return
|
||||
}
|
||||
|
||||
this.kubeConfig = kubeconfig
|
||||
this.save()
|
||||
@@ -164,7 +160,7 @@ export class Cluster implements ClusterInfo {
|
||||
}
|
||||
|
||||
public toClusterInfo(): ClusterInfo {
|
||||
const clusterInfo: ClusterInfo = {
|
||||
return {
|
||||
id: this.id,
|
||||
workspace: this.workspace,
|
||||
url: this.url,
|
||||
@@ -179,20 +175,19 @@ export class Cluster implements ClusterInfo {
|
||||
isAdmin: this.isAdmin,
|
||||
features: this.features,
|
||||
kubeCtl: this.kubeCtl,
|
||||
kubeConfig: this.kubeConfig,
|
||||
kubeConfig: this.kubeConfig,
|
||||
preferences: this.preferences
|
||||
}
|
||||
return clusterInfo;
|
||||
}
|
||||
|
||||
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
|
||||
const options = Object.assign({
|
||||
json: true, timeout: 10000
|
||||
json: true,
|
||||
timeout: 10000
|
||||
}, (opts || {}))
|
||||
if (!options.headers) { options.headers = {} }
|
||||
options.headers.host = `${this.id}.localhost:${this.port}`
|
||||
|
||||
return request(`http://127.0.0.1:${this.port}/api-kube${path}`, options)
|
||||
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
|
||||
}
|
||||
|
||||
protected async getConnectionStatus() {
|
||||
@@ -201,21 +196,24 @@ export class Cluster implements ClusterInfo {
|
||||
this.version = response.gitVersion
|
||||
this.failureReason = null
|
||||
return ClusterStatus.AccessGranted;
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`)
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.failureReason = "Invalid credentials";
|
||||
return ClusterStatus.AccessDenied;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.failureReason = error.error || error.message;
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
} else if (error.failed === true) {
|
||||
}
|
||||
else if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.failureReason = "Connection timed out";
|
||||
return ClusterStatus.Offline;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.failureReason = "Failed to fetch credentials";
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
@@ -234,7 +232,7 @@ export class Cluster implements ClusterInfo {
|
||||
spec: { resourceAttributes }
|
||||
})
|
||||
return accessReview.body.status.allowed === true
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
|
||||
return false
|
||||
}
|
||||
@@ -258,10 +256,10 @@ export class Cluster implements ClusterInfo {
|
||||
else if (kubernetesVersion.includes("IKS")) {
|
||||
return "iks"
|
||||
}
|
||||
else if(this.apiUrl.endsWith("azmk8s.io")) {
|
||||
else if (this.apiUrl.endsWith("azmk8s.io")) {
|
||||
return "aks"
|
||||
}
|
||||
else if(this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
|
||||
else if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
|
||||
return "digitalocean"
|
||||
}
|
||||
else if (this.contextHandler.contextName.startsWith("minikube")) {
|
||||
@@ -274,11 +272,11 @@ export class Cluster implements ClusterInfo {
|
||||
return "vanilla"
|
||||
}
|
||||
|
||||
protected async getNodeCount() {
|
||||
protected async getNodeCount() {
|
||||
try {
|
||||
const response = await this.k8sRequest("/api/v1/nodes")
|
||||
return response.items.length
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.debug(`failed to request node list: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
@@ -294,18 +292,17 @@ export class Cluster implements ClusterInfo {
|
||||
const uniqEventSources = new Set();
|
||||
const warnings = response.body.items.filter(e => e.type !== 'Normal');
|
||||
for (const w of warnings) {
|
||||
if(w.involvedObject.kind === 'Pod') {
|
||||
if (w.involvedObject.kind === 'Pod') {
|
||||
try {
|
||||
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
|
||||
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
|
||||
if(k8s.podHasIssues(pod)) {
|
||||
if (k8s.podHasIssues(pod)) {
|
||||
uniqEventSources.add(w.involvedObject.uid);
|
||||
}
|
||||
continue;
|
||||
} catch (error) {
|
||||
continue;
|
||||
} catch (err) {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
uniqEventSources.add(w.involvedObject.uid);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-19
@@ -1,6 +1,5 @@
|
||||
import { KubeConfig, CoreV1Api } from "@kubernetes/client-node"
|
||||
import { readFileSync } from "fs"
|
||||
import * as http from "http"
|
||||
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"
|
||||
import http from "http"
|
||||
import { ServerOptions } from "http-proxy"
|
||||
import * as url from "url"
|
||||
import logger from "./logger"
|
||||
@@ -8,8 +7,7 @@ import { getFreePort } from "./port"
|
||||
import { KubeAuthProxy } from "./kube-auth-proxy"
|
||||
import { Cluster, ClusterPreferences } from "./cluster"
|
||||
import { prometheusProviders } from "../common/prometheus-providers"
|
||||
import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry"
|
||||
import { PrometheusLens } from "./prometheus/lens"
|
||||
import { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
|
||||
|
||||
export class ContextHandler {
|
||||
public contextName: string
|
||||
@@ -76,19 +74,21 @@ export class ContextHandler {
|
||||
if (clusterPreferences && clusterPreferences.prometheus) {
|
||||
const prom = clusterPreferences.prometheus
|
||||
this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}`
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.prometheusPath = null
|
||||
}
|
||||
if(clusterPreferences && clusterPreferences.clusterName) {
|
||||
if (clusterPreferences && clusterPreferences.clusterName) {
|
||||
this.clusterName = clusterPreferences.clusterName;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.clusterName = this.contextName;
|
||||
}
|
||||
}
|
||||
|
||||
protected async resolvePrometheusPath(): Promise<string> {
|
||||
const service = await this.getPrometheusService()
|
||||
return `${service.namespace}/services/${service.service}:${service.port}`
|
||||
const {service, namespace, port} = await this.getPrometheusService()
|
||||
return `${namespace}/services/${service}:${port}`
|
||||
}
|
||||
|
||||
public async getPrometheusProvider() {
|
||||
@@ -110,7 +110,8 @@ export class ContextHandler {
|
||||
const service = resolvedPrometheusServices.filter(n => n)[0]
|
||||
if (service) {
|
||||
return service
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return {
|
||||
id: "lens",
|
||||
namespace: "lens-metrics",
|
||||
@@ -128,7 +129,7 @@ export class ContextHandler {
|
||||
return this.prometheusPath
|
||||
}
|
||||
|
||||
public async getApiTarget(isWatchRequest = false) {
|
||||
public async getApiTarget(isWatchRequest = false): Promise<ServerOptions> {
|
||||
if (this.apiTarget && !isWatchRequest) {
|
||||
return this.apiTarget
|
||||
}
|
||||
@@ -140,7 +141,7 @@ export class ContextHandler {
|
||||
return apiTarget
|
||||
}
|
||||
|
||||
protected async newApiTarget(timeout: number) {
|
||||
protected async newApiTarget(timeout: number): Promise<ServerOptions> {
|
||||
return {
|
||||
changeOrigin: true,
|
||||
timeout: timeout,
|
||||
@@ -162,7 +163,7 @@ export class ContextHandler {
|
||||
let serverPort: number = null
|
||||
try {
|
||||
serverPort = await getFreePort()
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
throw(error)
|
||||
}
|
||||
@@ -178,7 +179,7 @@ export class ContextHandler {
|
||||
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
|
||||
try {
|
||||
await callback(this.cluster.kubeconfigPath())
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
throw(error)
|
||||
}
|
||||
}
|
||||
@@ -187,7 +188,7 @@ export class ContextHandler {
|
||||
if (!this.proxyServer) {
|
||||
const proxyPort = await this.resolveProxyPort()
|
||||
const proxyEnv = Object.assign({}, process.env)
|
||||
if (this.cluster.preferences && this.cluster.preferences.httpsProxy) {
|
||||
if (this.cluster?.preferences.httpsProxy) {
|
||||
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
|
||||
}
|
||||
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv)
|
||||
@@ -203,8 +204,6 @@ export class ContextHandler {
|
||||
}
|
||||
|
||||
public proxyServerError() {
|
||||
if (!this.proxyServer) { return null }
|
||||
|
||||
return this.proxyServer.lastError
|
||||
return this.proxyServer?.lastError || ""
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -1,5 +1,5 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path"
|
||||
import fs from "fs";
|
||||
import path from "path"
|
||||
import * as hb from "handlebars"
|
||||
import { ResourceApplier } from "./resource-applier"
|
||||
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node"
|
||||
@@ -99,6 +99,10 @@ export abstract class Feature {
|
||||
}
|
||||
|
||||
protected manifestPath() {
|
||||
return path.join(__dirname, '..', 'features', this.name);
|
||||
const devPath = path.join(__dirname, "..", 'src/features', this.name);
|
||||
if(fs.existsSync(devPath)) {
|
||||
return devPath;
|
||||
}
|
||||
return path.join(__dirname, "..", 'features', this.name);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "fs"
|
||||
import fs from "fs"
|
||||
|
||||
export function ensureDir(dirname: string) {
|
||||
if (!fs.existsSync(dirname)) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
|
||||
import logger from "./logger";
|
||||
|
||||
+10
-11
@@ -1,5 +1,7 @@
|
||||
import * as path from "path"
|
||||
import packageInfo from "../../package.json"
|
||||
import path from "path"
|
||||
import { LensBinary, LensBinaryOpts } from "./lens-binary"
|
||||
import { isProduction } from "../common/vars";
|
||||
|
||||
export class HelmCli extends LensBinary {
|
||||
|
||||
@@ -13,7 +15,7 @@ export class HelmCli extends LensBinary {
|
||||
super(opts)
|
||||
}
|
||||
|
||||
protected getTarName(): string|null {
|
||||
protected getTarName(): string | null {
|
||||
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
|
||||
}
|
||||
|
||||
@@ -26,19 +28,16 @@ export class HelmCli extends LensBinary {
|
||||
}
|
||||
|
||||
protected getOriginalBinaryPath() {
|
||||
return path.join(this.dirname, this.platformName+"-"+this.arch, this.originalBinaryName)
|
||||
return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName)
|
||||
}
|
||||
}
|
||||
|
||||
const helmVersion = require("../../package.json").config.bundledHelmVersion
|
||||
const isDevelopment = process.env.NODE_ENV !== "production"
|
||||
let baseDir: string = null
|
||||
const helmVersion = packageInfo.config.bundledHelmVersion;
|
||||
let baseDir = process.resourcesPath;
|
||||
|
||||
if(isDevelopment) {
|
||||
baseDir = path.join(process.cwd(), "binaries", "client")
|
||||
} else {
|
||||
baseDir = path.join(process.resourcesPath)
|
||||
if (!isProduction) {
|
||||
baseDir = path.join(process.cwd(), "binaries", "client");
|
||||
}
|
||||
|
||||
export const helmCli = new HelmCli(baseDir, helmVersion)
|
||||
export const helmCli = new HelmCli(baseDir, helmVersion);
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import * as tempy from "tempy";
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as camelcaseKeys from "camelcase-keys";
|
||||
import { promiseExec} from "./promise-exec"
|
||||
import { helmCli } from "./helm-cli";
|
||||
import { Cluster } from "./cluster";
|
||||
import { toCamelCase } from "../common/utils/camelCase";
|
||||
|
||||
export class HelmReleaseManager {
|
||||
|
||||
public async listReleases(pathToKubeconfig: string, namespace?: string) {
|
||||
const helm = await helmCli.binaryPath()
|
||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"
|
||||
const { stdout, stderr } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
|
||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
|
||||
|
||||
const output = JSON.parse(stdout)
|
||||
if (output.length == 0) {
|
||||
return output
|
||||
}
|
||||
const result: any = []
|
||||
output.forEach((release: any, index: number) => {
|
||||
output[index] = camelcaseKeys(release)
|
||||
output[index] = toCamelCase(release)
|
||||
});
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import logger from "./logger";
|
||||
import * as yaml from "js-yaml";
|
||||
import { promiseExec } from "./promise-exec";
|
||||
|
||||
+18
-11
@@ -1,7 +1,10 @@
|
||||
// Main process
|
||||
|
||||
import "../common/system-ca"
|
||||
import "../common/prometheus-providers"
|
||||
import { app, dialog, protocol } from "electron"
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as path from "path"
|
||||
import path from "path"
|
||||
import { format as formatUrl } from "url"
|
||||
import logger from "./logger"
|
||||
import initMenu from "./menu"
|
||||
@@ -15,20 +18,21 @@ import { shellSync } from "./shell-sync"
|
||||
import { getFreePort } from "./port"
|
||||
import { mangleProxyEnv } from "./proxy-env"
|
||||
import { findMainWebContents } from "./webcontents"
|
||||
import "../common/prometheus-providers"
|
||||
import { registerStaticProtocol } from "../common/register-static";
|
||||
import { isMac, vueAppName } from "../common/vars";
|
||||
|
||||
mangleProxyEnv()
|
||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
|
||||
}
|
||||
const isDevelopment = process.env.NODE_ENV !== "production"
|
||||
const promiseIpc = new PromiseIpc({ timeout: 2000 })
|
||||
|
||||
const promiseIpc = new PromiseIpc({ timeout: 2000 })
|
||||
let windowManager: WindowManager = null;
|
||||
let clusterManager: ClusterManager = null;
|
||||
let proxyServer: proxy.LensProxy = null;
|
||||
const vmURL = (isDevelopment) ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}` : formatUrl({
|
||||
pathname: path.join(__dirname, "index.html"),
|
||||
|
||||
const vmURL = formatUrl({
|
||||
pathname: path.join(__dirname, `${vueAppName}.html`),
|
||||
protocol: "file",
|
||||
slashes: true,
|
||||
})
|
||||
@@ -40,12 +44,15 @@ async function main() {
|
||||
updater.start();
|
||||
|
||||
tracker.event("app", "start");
|
||||
|
||||
registerStaticProtocol();
|
||||
protocol.registerFileProtocol('store', (request, callback) => {
|
||||
const url = request.url.substr(8)
|
||||
callback( path.normalize(`${app.getPath("userData")}/${url}`) )
|
||||
callback(path.normalize(`${app.getPath("userData")}/${url}`))
|
||||
}, (error) => {
|
||||
if (error) console.error('Failed to register protocol')
|
||||
})
|
||||
|
||||
let port: number = null
|
||||
// find free port
|
||||
try {
|
||||
@@ -80,7 +87,7 @@ async function main() {
|
||||
},
|
||||
showPreferencesHook: async () => {
|
||||
// IPC send needs webContents as we're sending it to renderer
|
||||
promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => {
|
||||
promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => {
|
||||
logger.debug("navigate: preferences IPC sent");
|
||||
})
|
||||
},
|
||||
@@ -103,10 +110,10 @@ async function main() {
|
||||
}
|
||||
|
||||
app.on("ready", main)
|
||||
app.on('window-all-closed', function() {
|
||||
app.on('window-all-closed', function () {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform != 'darwin') {
|
||||
if (!isMac) {
|
||||
app.quit();
|
||||
} else {
|
||||
windowManager = null
|
||||
@@ -116,7 +123,7 @@ app.on('window-all-closed', function() {
|
||||
app.on("activate", () => {
|
||||
if (!windowManager) {
|
||||
logger.debug("activate main window")
|
||||
windowManager = new WindowManager(false)
|
||||
windowManager = new WindowManager({ showSplash: false })
|
||||
windowManager.showMain(vmURL)
|
||||
}
|
||||
})
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import * as os from "os"
|
||||
import { all } from "q";
|
||||
import * as yaml from "js-yaml"
|
||||
import { V1beta1ValidatingWebhookConfiguration } from "@kubernetes/client-node";
|
||||
import logger from "./logger";
|
||||
|
||||
const kc = new k8s.KubeConfig()
|
||||
@@ -31,7 +29,7 @@ export function loadConfig(kubeconfig: string): k8s.KubeConfig {
|
||||
*
|
||||
* @param config KubeConfig to check
|
||||
*/
|
||||
export function valideConfig(config: k8s.KubeConfig): boolean {
|
||||
export function validateConfig(config: k8s.KubeConfig): boolean {
|
||||
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
|
||||
if(!config.users || config.users.length == 0) {
|
||||
throw new Error("No users provided in config")
|
||||
|
||||
@@ -28,9 +28,7 @@ export class KubeAuthProxy {
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.proxyProcess) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve()
|
||||
})
|
||||
return;
|
||||
}
|
||||
const proxyBin = await this.kubectl.kubectlPath()
|
||||
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => {
|
||||
@@ -43,11 +41,10 @@ export class KubeAuthProxy {
|
||||
}
|
||||
}
|
||||
})
|
||||
configWatcher.on("error", () => {})
|
||||
const clusterUrl = url.parse(this.cluster.apiUrl)
|
||||
let args = [
|
||||
"proxy",
|
||||
"-p", this.port.toString(),
|
||||
"--port", this.port.toString(),
|
||||
"--kubeconfig", this.cluster.kubeconfigPath(),
|
||||
"--accept-hosts", clusterUrl.hostname,
|
||||
]
|
||||
@@ -59,7 +56,9 @@ export class KubeAuthProxy {
|
||||
})
|
||||
this.proxyProcess.on("exit", (code) => {
|
||||
logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`)
|
||||
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((_) => {})
|
||||
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((err: Error) => {
|
||||
logger.debug("failed to send IPC log message: " + err.message)
|
||||
})
|
||||
this.proxyProcess = null
|
||||
configWatcher.close()
|
||||
})
|
||||
@@ -96,7 +95,7 @@ export class KubeAuthProxy {
|
||||
}
|
||||
|
||||
protected async sendIpcLogMessage(data: string, stream: string) {
|
||||
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data: data, stream: stream })
|
||||
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data, stream })
|
||||
}
|
||||
|
||||
public exit() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app } from "electron"
|
||||
import * as fs from "fs"
|
||||
import fs from "fs"
|
||||
import { ensureDir, randomFileName} from "./file-helpers"
|
||||
import logger from "./logger"
|
||||
|
||||
|
||||
+12
-14
@@ -1,17 +1,17 @@
|
||||
import { app, remote } from "electron"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import * as request from "request"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import request from "request"
|
||||
import { promiseExec} from "./promise-exec"
|
||||
import logger from "./logger"
|
||||
import { ensureDir, pathExists } from "fs-extra"
|
||||
import * as md5File from "md5-file"
|
||||
import { globalRequestOpts } from "../common/request"
|
||||
import * as lockFile from "proper-lockfile"
|
||||
import { helmCli } from "./helm-cli"
|
||||
import { userStore } from "../common/user-store"
|
||||
import { getBundledKubectlVersion} from "../common/utils/app-version"
|
||||
|
||||
const bundledVersion = require("../../package.json").config.bundledKubectlVersion
|
||||
const bundledVersion = getBundledKubectlVersion()
|
||||
const kubectlMap: Map<string, string> = new Map([
|
||||
["1.7", "1.8.15"],
|
||||
["1.8", "1.9.10"],
|
||||
@@ -195,16 +195,16 @@ export class Kubectl {
|
||||
const file = fs.createWriteStream(this.path)
|
||||
stream.on("complete", () => {
|
||||
logger.debug("kubectl binary download finished")
|
||||
file.end(() => {})
|
||||
file.end()
|
||||
})
|
||||
stream.on("error", (error) => {
|
||||
logger.error(error)
|
||||
fs.unlink(this.path, () => {})
|
||||
fs.unlink(this.path, null)
|
||||
reject(error)
|
||||
})
|
||||
file.on("close", () => {
|
||||
logger.debug("kubectl binary download closed")
|
||||
fs.chmod(this.path, 0o755, () => {})
|
||||
fs.chmod(this.path, 0o755, null)
|
||||
resolve()
|
||||
})
|
||||
stream.pipe(file)
|
||||
@@ -285,13 +285,11 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
protected getDownloadMirror() {
|
||||
if (process.platform == "darwin") {
|
||||
return packageMirrors.get("default") // MacOS packages are only available from default
|
||||
}
|
||||
const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror)
|
||||
if (mirror) { return mirror }
|
||||
|
||||
return packageMirrors.get("default")
|
||||
if (mirror) {
|
||||
return mirror
|
||||
}
|
||||
return packageMirrors.get("default") // MacOS packages are only available from default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import packageInfo from "../../package.json"
|
||||
import { bundledKubectl, Kubectl } from "../../src/main/kubectl";
|
||||
import { UserStore } from "../common/user-store";
|
||||
|
||||
jest.mock("../common/user-store", () => {
|
||||
const userStoreMock: Partial<UserStore> = {
|
||||
getPreferences() {
|
||||
return {
|
||||
downloadMirror: "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
userStore: userStoreMock,
|
||||
}
|
||||
})
|
||||
|
||||
describe("kubectlVersion", () => {
|
||||
it("returns bundled version if exactly same version used", async () => {
|
||||
const kubectl = new Kubectl(bundledKubectl.kubectlVersion)
|
||||
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
|
||||
})
|
||||
|
||||
it("returns bundled version if same major.minor version is used", async () => {
|
||||
const { bundledKubectlVersion } = packageInfo.config;
|
||||
const kubectl = new Kubectl(bundledKubectlVersion);
|
||||
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as http from "http";
|
||||
import http from "http";
|
||||
|
||||
export abstract class LensApi {
|
||||
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
|
||||
|
||||
+27
-20
@@ -1,9 +1,10 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import * as request from "request"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import request from "request"
|
||||
import logger from "./logger"
|
||||
import { ensureDir, pathExists } from "fs-extra"
|
||||
import * as tar from "tar"
|
||||
import { isWindows } from "../common/vars";
|
||||
|
||||
export type LensBinaryOpts = {
|
||||
version: string;
|
||||
@@ -12,6 +13,7 @@ export type LensBinaryOpts = {
|
||||
newBinaryName?: string;
|
||||
requestOpts?: request.Options;
|
||||
}
|
||||
|
||||
export class LensBinary {
|
||||
|
||||
public binaryVersion: string
|
||||
@@ -35,19 +37,21 @@ export class LensBinary {
|
||||
|
||||
let arch = null
|
||||
|
||||
if(process.arch == "x64") {
|
||||
if (process.arch == "x64") {
|
||||
arch = "amd64"
|
||||
} else if(process.arch == "x86" || process.arch == "ia32") {
|
||||
}
|
||||
else if (process.arch == "x86" || process.arch == "ia32") {
|
||||
arch = "386"
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
arch = process.arch
|
||||
}
|
||||
this.arch = arch
|
||||
this.platformName = process.platform === "win32" ? "windows" : process.platform
|
||||
this.platformName = isWindows ? "windows" : process.platform
|
||||
this.dirname = path.normalize(path.join(baseDir, this.binaryName))
|
||||
if (process.platform === "win32") {
|
||||
this.binaryName = this.binaryName+".exe"
|
||||
this.originalBinaryName = this.originalBinaryName+".exe"
|
||||
if (isWindows) {
|
||||
this.binaryName = this.binaryName + ".exe"
|
||||
this.originalBinaryName = this.originalBinaryName + ".exe"
|
||||
}
|
||||
const tarName = this.getTarName()
|
||||
if (tarName) {
|
||||
@@ -64,7 +68,7 @@ export class LensBinary {
|
||||
return this.getBinaryPath()
|
||||
}
|
||||
|
||||
protected getTarName(): string|null {
|
||||
protected getTarName(): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -88,7 +92,7 @@ export class LensBinary {
|
||||
try {
|
||||
await this.ensureBinary()
|
||||
return this.dirname
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return ""
|
||||
}
|
||||
@@ -101,10 +105,12 @@ export class LensBinary {
|
||||
|
||||
public async ensureBinary() {
|
||||
const isValid = await this.checkBinary()
|
||||
if(!isValid) {
|
||||
await this.downloadBinary().catch((error) => { logger.error(error) });
|
||||
if (!isValid) {
|
||||
await this.downloadBinary().catch((error) => {
|
||||
logger.error(error)
|
||||
});
|
||||
if (this.tarPath) await this.untarBinary()
|
||||
if(this.originalBinaryName != this.binaryName ) await this.renameBinary()
|
||||
if (this.originalBinaryName != this.binaryName) await this.renameBinary()
|
||||
logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
|
||||
}
|
||||
}
|
||||
@@ -127,7 +133,8 @@ export class LensBinary {
|
||||
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
@@ -135,7 +142,7 @@ export class LensBinary {
|
||||
}
|
||||
|
||||
protected async downloadBinary() {
|
||||
const binaryPath = this.tarPath || this.getBinaryPath()
|
||||
const binaryPath = this.tarPath || this.getBinaryPath()
|
||||
await ensureDir(this.getBinaryDir(), 0o755)
|
||||
|
||||
const file = fs.createWriteStream(binaryPath)
|
||||
@@ -152,18 +159,18 @@ export class LensBinary {
|
||||
|
||||
stream.on("complete", () => {
|
||||
logger.info(`Download of ${this.originalBinaryName} finished`)
|
||||
file.end(() => {})
|
||||
file.end()
|
||||
})
|
||||
|
||||
stream.on("error", (error) => {
|
||||
logger.error(error)
|
||||
fs.unlink(binaryPath, () => {})
|
||||
fs.unlink(binaryPath, null)
|
||||
throw(error)
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
file.on("close", () => {
|
||||
logger.debug(`${this.originalBinaryName} binary download closed`)
|
||||
if(!this.tarPath) fs.chmod(binaryPath, 0o755, () => {})
|
||||
if (!this.tarPath) fs.chmod(binaryPath, 0o755, null)
|
||||
resolve()
|
||||
})
|
||||
stream.pipe(file)
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
import * as winston from "winston"
|
||||
import winston from "winston"
|
||||
import { isDebugging } from "../common/vars";
|
||||
|
||||
const options = {
|
||||
colorize: true,
|
||||
handleExceptions: false,
|
||||
json: false,
|
||||
level: process.env.DEBUG === "true" ? "debug" : "info",
|
||||
level: isDebugging ? "debug" : "info",
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({
|
||||
|
||||
+18
-17
@@ -1,4 +1,7 @@
|
||||
import {app, dialog, Menu, MenuItemConstructorOptions, shell, webContents, BrowserWindow, MenuItem} from "electron"
|
||||
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
|
||||
import { isDevelopment, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
|
||||
|
||||
// todo: refactor + split menu sections to separated files, e.g. menus/file.menu.ts
|
||||
|
||||
export interface MenuOptions {
|
||||
logoutHook: any;
|
||||
@@ -10,7 +13,6 @@ export interface MenuOptions {
|
||||
}
|
||||
|
||||
function setClusterSettingsEnabled(enabled: boolean) {
|
||||
const isMac = process.platform === 'darwin';
|
||||
const menuIndex = isMac ? 1 : 0
|
||||
Menu.getApplicationMenu().items[menuIndex].submenu.items[1].enabled = enabled
|
||||
}
|
||||
@@ -21,7 +23,7 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
|
||||
]
|
||||
appDetails.push(`Copyright 2020 Lakend Labs, Inc.`)
|
||||
let title = "Lens"
|
||||
if (process.platform === "win32") {
|
||||
if (isWindows) {
|
||||
title = ` ${title}`
|
||||
}
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
@@ -40,9 +42,6 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
|
||||
* @param ipc the main promiceIpc handle. Needed to be able to hook IPC sending into logout click handler.
|
||||
*/
|
||||
export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
const isMac = process.platform === 'darwin';
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
const mt: MenuItemConstructorOptions[] = [];
|
||||
const macAppMenu: MenuItemConstructorOptions = {
|
||||
label: app.getName(),
|
||||
@@ -67,12 +66,12 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
{ role: 'quit' }
|
||||
]
|
||||
};
|
||||
if(isMac) {
|
||||
if (isMac) {
|
||||
mt.push(macAppMenu);
|
||||
}
|
||||
|
||||
let fileMenu: MenuItemConstructorOptions;
|
||||
if(isMac) {
|
||||
if (isMac) {
|
||||
fileMenu = {
|
||||
label: 'File',
|
||||
submenu: [{
|
||||
@@ -86,7 +85,8 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
fileMenu = {
|
||||
label: 'File',
|
||||
submenu: [
|
||||
@@ -134,27 +134,28 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
{
|
||||
label: 'Back',
|
||||
accelerator: 'CmdOrCtrl+[',
|
||||
click () {
|
||||
click() {
|
||||
webContents.getFocusedWebContents().executeJavaScript('window.history.back()')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Forward',
|
||||
accelerator: 'CmdOrCtrl+]',
|
||||
click () {
|
||||
click() {
|
||||
webContents.getFocusedWebContents().executeJavaScript('window.history.forward()')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click () {
|
||||
click() {
|
||||
webContents.getFocusedWebContents().reload()
|
||||
}
|
||||
},
|
||||
...(isDevelopment ? [
|
||||
{ role: 'toggleDevTools'} as MenuItemConstructorOptions,
|
||||
{ role: 'toggleDevTools' } as MenuItemConstructorOptions,
|
||||
{
|
||||
accelerator: "CmdOrCtrl+Shift+I",
|
||||
label: 'Open Dashboard Devtools',
|
||||
click() {
|
||||
webContents.getFocusedWebContents().openDevTools()
|
||||
@@ -183,20 +184,20 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
{
|
||||
label: 'Community Slack',
|
||||
click: async () => {
|
||||
shell.openExternal('https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI');
|
||||
shell.openExternal(slackUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue',
|
||||
click: async () => {
|
||||
shell.openExternal('https://github.com/lensapp/lens/issues');
|
||||
shell.openExternal(issuesTrackerUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "What's new?",
|
||||
click: opts.showWhatsNewHook,
|
||||
},
|
||||
...(process.platform !== "darwin" ? [{
|
||||
...(!isMac ? [{
|
||||
label: "About Lens",
|
||||
click: showAbout
|
||||
} as MenuItemConstructorOptions] : [])
|
||||
@@ -214,4 +215,4 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
|
||||
promiseIpc.on("disableClusterSettingsMenuItem", () => {
|
||||
setClusterSettingsEnabled(false)
|
||||
});
|
||||
};
|
||||
}
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import logger from "./logger"
|
||||
import { createServer } from "net"
|
||||
import { AddressInfo } from "net"
|
||||
import { createServer, AddressInfo } from "net"
|
||||
|
||||
const getNextAvailablePort = () => {
|
||||
logger.debug("getNextAvailablePort() start")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import { getFreePort } from "./port"
|
||||
|
||||
jest.mock("net", () => {
|
||||
return {
|
||||
createServer() {
|
||||
return new class MockServer extends EventEmitter {
|
||||
listen = jest.fn(() => {
|
||||
this.emit('listening')
|
||||
return this
|
||||
})
|
||||
address = () => {
|
||||
return { port: 12345 }
|
||||
}
|
||||
unref = jest.fn()
|
||||
close = jest.fn(cb => cb())
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
describe("getFreePort", () => {
|
||||
it("finds the next free port", async () => {
|
||||
return expect(getFreePort()).resolves.toEqual(expect.any(Number))
|
||||
})
|
||||
})
|
||||
+14
-27
@@ -1,5 +1,5 @@
|
||||
import * as http from "http";
|
||||
import * as httpProxy from "http-proxy";
|
||||
import http from "http";
|
||||
import httpProxy from "http-proxy";
|
||||
import { Socket } from "net";
|
||||
import * as url from "url";
|
||||
import * as WebSocket from "ws"
|
||||
@@ -8,6 +8,7 @@ import logger from "./logger"
|
||||
import * as shell from "./node-shell-session"
|
||||
import { ClusterManager } from "./cluster-manager"
|
||||
import { Router } from "./router"
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
export class LensProxy {
|
||||
public static readonly localShellSessions = true
|
||||
@@ -40,17 +41,15 @@ export class LensProxy {
|
||||
|
||||
protected buildProxyServer() {
|
||||
const proxy = this.createProxy();
|
||||
const proxyServer = http.createServer(function(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(proxy, req, res);
|
||||
}.bind(this));
|
||||
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
|
||||
});
|
||||
proxyServer.on("upgrade", (req: http.IncomingMessage, socket: Socket, head: Buffer) => {
|
||||
this.handleWsUpgrade(req, socket, head)
|
||||
}.bind(this));
|
||||
|
||||
});
|
||||
proxyServer.on("error", (err) => {
|
||||
logger.error(err)
|
||||
});
|
||||
|
||||
return proxyServer;
|
||||
}
|
||||
|
||||
@@ -64,11 +63,10 @@ export class LensProxy {
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
"Content-Type": "text/plain"
|
||||
})
|
||||
res.end(cluster.contextHandler.proxyServerError().toString())
|
||||
res.end(cluster.contextHandler.proxyServerError())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return
|
||||
}
|
||||
@@ -106,11 +104,11 @@ export class LensProxy {
|
||||
}
|
||||
|
||||
protected createWsListener() {
|
||||
const ws = new WebSocket.Server({ noServer: true})
|
||||
const ws = new WebSocket.Server({ noServer: true })
|
||||
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
||||
const contextHandler = cluster.contextHandler
|
||||
const nodeParam = this.getNodeParam(req.url)
|
||||
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
||||
|
||||
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => {
|
||||
return new Promise<boolean>(async (resolve, reject) => {
|
||||
@@ -120,26 +118,15 @@ export class LensProxy {
|
||||
})
|
||||
})
|
||||
})
|
||||
}).bind(this))
|
||||
}))
|
||||
return ws
|
||||
}
|
||||
|
||||
protected getNodeParam(requestUrl: string) {
|
||||
const reqUrl = url.parse(requestUrl, true)
|
||||
const urlParams = reqUrl.query
|
||||
let nodeParam: string = null
|
||||
for (const [key, value] of Object.entries(urlParams)) {
|
||||
if (key === "node") {
|
||||
nodeParam = value.toString()
|
||||
}
|
||||
}
|
||||
return nodeParam
|
||||
}
|
||||
|
||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
|
||||
if (req.url.startsWith("/api-kube/")) {
|
||||
const prefix = apiPrefix.KUBE_BASE;
|
||||
if (req.url.startsWith(prefix)) {
|
||||
delete req.headers.authorization
|
||||
req.url = req.url.replace("/api-kube", "")
|
||||
req.url = req.url.replace(prefix, "")
|
||||
const isWatchRequest = req.url.includes("watch=")
|
||||
return await contextHandler.getApiTarget(isWatchRequest)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { exec } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as path from "path";
|
||||
import path from "path";
|
||||
import * as tempy from "tempy";
|
||||
import logger from "./logger"
|
||||
import { Cluster } from "./cluster";
|
||||
|
||||
+9
-15
@@ -1,5 +1,8 @@
|
||||
import * as http from "http"
|
||||
import * as path from "path"
|
||||
import Call from "@hapi/call"
|
||||
import Subtext from "@hapi/subtext"
|
||||
import http from "http"
|
||||
import path from "path"
|
||||
import { readFile } from "fs"
|
||||
import { Cluster } from "./cluster"
|
||||
import { configRoute } from "./routes/config"
|
||||
import { helmApi } from "./helm-api"
|
||||
@@ -8,16 +11,7 @@ import { kubeconfigRoute } from "./routes/kubeconfig"
|
||||
import { metricsRoute } from "./routes/metrics"
|
||||
import { watchRoute } from "./routes/watch"
|
||||
import { portForwardRoute } from "./routes/port-forward"
|
||||
import { readFile } from "fs"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Call = require('@hapi/call');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Subtext = require('@hapi/subtext');
|
||||
|
||||
declare const __static: string;
|
||||
|
||||
const assetsPath = path.join(__static, "build/client")
|
||||
import { outDir, reactAppName } from "../common/vars";
|
||||
|
||||
const mimeTypes: {[key: string]: string} = {
|
||||
"html": "text/html",
|
||||
@@ -88,12 +82,12 @@ export class Router {
|
||||
return request
|
||||
}
|
||||
|
||||
protected handleStaticFile(file: string, response: http.ServerResponse) {
|
||||
const asset = path.join(assetsPath, file)
|
||||
protected handleStaticFile(filePath: string, response: http.ServerResponse) {
|
||||
const asset = path.resolve(outDir, filePath);
|
||||
readFile(asset, (err, data) => {
|
||||
if (err) {
|
||||
// default to index.html so that react routes work when page is refreshed
|
||||
this.handleStaticFile("index.html", response)
|
||||
this.handleStaticFile(`${reactAppName}.html`, response)
|
||||
} else {
|
||||
const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain";
|
||||
response.setHeader("Content-Type", type);
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { app } from "electron"
|
||||
import { CoreV1Api } from "@kubernetes/client-node"
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { userStore } from "../../common/user-store"
|
||||
import { getAppVersion } from "../../common/app-utils"
|
||||
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
|
||||
import { Cluster } from "../cluster"
|
||||
|
||||
export interface IConfigRoutePayload {
|
||||
kubeVersion?: string;
|
||||
clusterName?: string;
|
||||
lensVersion?: string;
|
||||
lensTheme?: string;
|
||||
username?: string;
|
||||
token?: string;
|
||||
allowedNamespaces?: string[];
|
||||
allowedResources?: string[];
|
||||
isClusterAdmin?: boolean;
|
||||
chartsEnabled: boolean;
|
||||
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
|
||||
}
|
||||
|
||||
// TODO: auto-populate all resources dynamically
|
||||
const apiResources = [
|
||||
{ resource: "configmaps" },
|
||||
@@ -44,12 +58,13 @@ async function getAllowedNamespaces(cluster: Cluster) {
|
||||
return namespaceList.body.items
|
||||
.filter((ns, i) => nsAccessStatuses[i])
|
||||
.map(ns => ns.metadata.name)
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
const kc = cluster.contextHandler.kc
|
||||
const ctx = kc.getContextObject(kc.currentContext)
|
||||
if (ctx.namespace) {
|
||||
return [ctx.namespace]
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -67,20 +82,19 @@ async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
|
||||
)
|
||||
return apiResources
|
||||
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigRoute extends LensApi {
|
||||
|
||||
public async routeConfig(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
const { params, response, cluster } = request
|
||||
|
||||
const namespaces = await getAllowedNamespaces(cluster)
|
||||
const data = {
|
||||
const data: IConfigRoutePayload = {
|
||||
clusterName: cluster.contextName,
|
||||
lensVersion: getAppVersion(),
|
||||
lensVersion: app.getVersion(),
|
||||
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
|
||||
kubeVersion: cluster.version,
|
||||
chartsEnabled: true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import * as requestPromise from "request-promise-native"
|
||||
import requestPromise from "request-promise-native"
|
||||
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry"
|
||||
import { apiPrefix } from "../../common/vars";
|
||||
|
||||
type MetricsQuery = string | string[] | {
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
[metricName: string]: string;
|
||||
}
|
||||
|
||||
@@ -11,13 +12,13 @@ class MetricsRoute extends LensApi {
|
||||
|
||||
public async routeMetrics(request: LensApiRequest) {
|
||||
const { response, cluster} = request
|
||||
const query: MetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
|
||||
const query: IMetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}`
|
||||
const headers = {
|
||||
"Host": `${cluster.id}.localhost:${cluster.port}`,
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
const queryParams: MetricsQuery = {}
|
||||
const queryParams: IMetricsQuery = {}
|
||||
request.query.forEach((value: string, key: string) => {
|
||||
queryParams[key] = value
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
|
||||
import { Watch, KubeConfig } from "@kubernetes/client-node"
|
||||
import { ServerResponse } from "http"
|
||||
import { Request } from "request"
|
||||
import logger from "../logger"
|
||||
@@ -41,7 +41,7 @@ class ApiWatcher {
|
||||
this.watchRequest.abort()
|
||||
}
|
||||
|
||||
private watchHandler(phase: string, obj: RuntimeRawExtension) {
|
||||
private watchHandler(phase: string, obj: any) {
|
||||
this.eventBuffer.push({
|
||||
type: phase,
|
||||
object: obj
|
||||
|
||||
+12
-15
@@ -1,13 +1,14 @@
|
||||
import * as pty from "node-pty"
|
||||
import * as WebSocket from "ws"
|
||||
import { EventEmitter } from "events";
|
||||
import * as path from "path"
|
||||
import shellEnv = require("shell-env")
|
||||
import path from "path"
|
||||
import shellEnv from "shell-env"
|
||||
import { app } from "electron"
|
||||
import { Kubectl } from "./kubectl"
|
||||
import { tracker } from "./tracker"
|
||||
import { Cluster, ClusterPreferences } from "./cluster"
|
||||
import { helmCli } from "./helm-cli"
|
||||
import { isWindows } from "../common/vars";
|
||||
|
||||
export class ShellSession extends EventEmitter {
|
||||
static shellEnvs: Map<string, any> = new Map()
|
||||
@@ -66,13 +67,10 @@ export class ShellSession extends EventEmitter {
|
||||
switch(path.basename(shell)) {
|
||||
case "powershell.exe":
|
||||
return ["-NoExit", "-command", `& {Set-Location $Env:USERPROFILE; $Env:PATH="${this.kubectlBinDir};${this.helmBinDir};$Env:PATH"}`]
|
||||
break
|
||||
case "bash":
|
||||
return ["--init-file", path.join(this.kubectlBinDir, '.bash_set_path')]
|
||||
break
|
||||
case "fish":
|
||||
return ["--login", "--init-command", `export PATH="${this.kubectlBinDir}:${this.helmBinDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPath}"`]
|
||||
break
|
||||
case "zsh":
|
||||
return ["--login"]
|
||||
default:
|
||||
@@ -99,7 +97,7 @@ export class ShellSession extends EventEmitter {
|
||||
const env = JSON.parse(JSON.stringify(await shellEnv()))
|
||||
const pathStr = [this.kubectlBinDir, this.helmBinDir, process.env.PATH].join(path.delimiter)
|
||||
|
||||
if(process.platform === "win32") {
|
||||
if(isWindows) {
|
||||
env["SystemRoot"] = process.env.SystemRoot
|
||||
env["PTYSHELL"] = "powershell.exe"
|
||||
env["PATH"] = pathStr
|
||||
@@ -131,14 +129,14 @@ export class ShellSession extends EventEmitter {
|
||||
|
||||
protected pipeStdout() {
|
||||
// send shell output to websocket
|
||||
this.shellProcess.on("data", ((data: string) => {
|
||||
this.shellProcess.onData(((data: string) => {
|
||||
this.sendResponse(data)
|
||||
}).bind(this));
|
||||
}));
|
||||
}
|
||||
|
||||
protected pipeStdin() {
|
||||
// write websocket messages to shellProcess
|
||||
this.websocket.on("message", function(data: string) {
|
||||
this.websocket.on("message", (data: string) => {
|
||||
if (!this.running) { return }
|
||||
|
||||
const message = Buffer.from(data.slice(1, data.length), "base64").toString()
|
||||
@@ -151,11 +149,10 @@ export class ShellSession extends EventEmitter {
|
||||
this.shellProcess.resize(resizeMsgObj["Width"], resizeMsgObj["Height"])
|
||||
break;
|
||||
case "9":
|
||||
this.token = message
|
||||
this.emit('newToken', this.token)
|
||||
this.emit('newToken', message)
|
||||
break;
|
||||
}
|
||||
}.bind(this))
|
||||
})
|
||||
}
|
||||
|
||||
protected exit(code = 1000) {
|
||||
@@ -164,10 +161,10 @@ export class ShellSession extends EventEmitter {
|
||||
}
|
||||
|
||||
protected closeWebsocketOnProcessExit() {
|
||||
this.shellProcess.on("exit", (code) => {
|
||||
this.shellProcess.onExit(({ exitCode }) => {
|
||||
this.running = false
|
||||
let timeout = 0
|
||||
if (code > 0) {
|
||||
if (exitCode > 0) {
|
||||
this.sendResponse("Terminal will auto-close in 15 seconds ...")
|
||||
timeout = 15*1000
|
||||
}
|
||||
@@ -186,7 +183,7 @@ export class ShellSession extends EventEmitter {
|
||||
protected killShellProcess(){
|
||||
if(this.running) {
|
||||
// On Windows we need to kill the shell process by pid, since Lens won't respond after a while if using `this.shellProcess.kill()`
|
||||
if (process.platform == "win32") {
|
||||
if (isWindows) {
|
||||
try {
|
||||
process.kill(this.shellProcess.pid)
|
||||
} catch(e) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import shellEnv = require("shell-env")
|
||||
import logger from "./logger"
|
||||
import * as os from "os";
|
||||
import shellEnv from "shell-env"
|
||||
import os from "os";
|
||||
|
||||
interface Env {
|
||||
[key: string]: string;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { BrowserWindow, shell, Menu } from "electron"
|
||||
import { BrowserWindow, shell } from "electron"
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as windowStateKeeper from "electron-window-state"
|
||||
import * as path from "path"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import { tracker } from "./tracker";
|
||||
|
||||
declare const __static: string;
|
||||
import { getStaticUrl } from "../common/register-static";
|
||||
|
||||
export class WindowManager {
|
||||
public mainWindow: BrowserWindow = null;
|
||||
@@ -12,7 +10,7 @@ export class WindowManager {
|
||||
protected promiseIpc: any
|
||||
protected windowState: windowStateKeeper.State;
|
||||
|
||||
constructor(showSplash = true) {
|
||||
constructor({ showSplash = true } = {}) {
|
||||
this.promiseIpc = new PromiseIpc({ timeout: 2000 })
|
||||
// Manage main window size&position with persistence
|
||||
this.windowState = windowStateKeeper({
|
||||
@@ -33,11 +31,12 @@ export class WindowManager {
|
||||
}
|
||||
})
|
||||
if (showSplash) {
|
||||
this.splashWindow.loadFile(path.join(__static, "/splash.html"))
|
||||
this.splashWindow.loadURL(getStaticUrl("splash.html"))
|
||||
this.splashWindow.show()
|
||||
}
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
show: false,
|
||||
x: this.windowState.x,
|
||||
y: this.windowState.y,
|
||||
width: this.windowState.width,
|
||||
@@ -48,7 +47,6 @@ export class WindowManager {
|
||||
nodeIntegration: true,
|
||||
webviewTag: true
|
||||
},
|
||||
show: false
|
||||
});
|
||||
|
||||
// Hook window state manager into window lifecycle
|
||||
@@ -68,22 +66,13 @@ export class WindowManager {
|
||||
shell.openExternal(link);
|
||||
})
|
||||
|
||||
// handle developer console
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
this.mainWindow.webContents.on("devtools-opened", () => {
|
||||
if (this.mainWindow.getBrowserView()) {
|
||||
this.mainWindow.getBrowserView().webContents.openDevTools({mode: "detach"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.mainWindow.on("focus", () => {
|
||||
tracker.event("app", "focus")
|
||||
})
|
||||
}
|
||||
|
||||
public showMain(url: string) {
|
||||
this.mainWindow.loadURL( url ).then(() => {
|
||||
this.mainWindow.loadURL(url).then(() => {
|
||||
this.splashWindow.hide()
|
||||
this.splashWindow.loadURL("data:text/html;charset=utf-8,").then(() => {
|
||||
this.splashWindow.close()
|
||||
|
||||
+5
-1
@@ -1,8 +1,12 @@
|
||||
/* Early store format had the kubeconfig directly under context name, this moves
|
||||
it under the kubeConfig key */
|
||||
|
||||
import { isTestEnv } from "../../common/vars";
|
||||
|
||||
export function migration(store: any) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.0.0-beta.2");
|
||||
if(!isTestEnv) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.0.0-beta.2");
|
||||
}
|
||||
for (const value of store) {
|
||||
const contextName = value[0];
|
||||
// Looping all the keys gives out the store internal stuff too...
|
||||
+6
-2
@@ -1,9 +1,13 @@
|
||||
// Cleans up a store that had the state related data stored
|
||||
import { isTestEnv } from "../../common/vars";
|
||||
|
||||
export function migration(store: any) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.4.1");
|
||||
if (!isTestEnv) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.4.1");
|
||||
}
|
||||
for (const value of store) {
|
||||
const contextName = value[0];
|
||||
if(contextName === "__internal__") continue;
|
||||
if (contextName === "__internal__") continue;
|
||||
const cluster = value[1];
|
||||
|
||||
store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {} });
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
// Move cluster icon from root to preferences
|
||||
import { isTestEnv } from "../../common/vars";
|
||||
|
||||
export function migration(store: any) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.2");
|
||||
if(!isTestEnv) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.2");
|
||||
}
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
if(clusterKey === "__internal__") continue
|
||||
+4
-1
@@ -1,8 +1,11 @@
|
||||
import * as yaml from "js-yaml"
|
||||
import { isTestEnv } from "../../common/vars";
|
||||
|
||||
// Convert access token and expiry from arrays into strings
|
||||
export function migration(store: any) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.3");
|
||||
if(!isTestEnv) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.6.0-beta.3");
|
||||
}
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
if(clusterKey === "__internal__") continue
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
// Add existing clusters to "default" workspace
|
||||
import { isTestEnv } from "../../common/vars";
|
||||
|
||||
export function migration(store: any) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.0");
|
||||
if(!isTestEnv) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.0");
|
||||
}
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
if(clusterKey === "__internal__") continue
|
||||
+4
-1
@@ -1,8 +1,11 @@
|
||||
// add id for clusters and store them to array
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { isTestEnv } from "../../common/vars";
|
||||
|
||||
export function migration(store: any) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.1");
|
||||
if(!isTestEnv) {
|
||||
console.log("CLUSTER STORE, MIGRATION: 2.7.0-beta.1");
|
||||
}
|
||||
const clusters: any[] = []
|
||||
for (const value of store) {
|
||||
const clusterKey = value[0];
|
||||
File renamed without changes.
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div class="draggable-top" />
|
||||
<div id="lens-container" />
|
||||
<div class="draggable-top" />
|
||||
<div class="main-view" :class="{ 'menu-visible': isMenuVisible }">
|
||||
<main-menu v-if="isMenuVisible" />
|
||||
<router-view />
|
||||
@@ -11,27 +11,28 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainMenu from "@/components/MainMenu/MainMenu";
|
||||
import BottomBar from "@/components/BottomBar/BottomBar";
|
||||
import MainMenu from "@/_vue/components/MainMenu/MainMenu";
|
||||
import BottomBar from "@/_vue/components/BottomBar/BottomBar";
|
||||
|
||||
export default {
|
||||
name: 'Lens',
|
||||
components: {
|
||||
BottomBar,
|
||||
MainMenu
|
||||
},
|
||||
computed:{
|
||||
isMenuVisible: function(){ return this.$store.getters.isMenuVisible }
|
||||
computed: {
|
||||
isMenuVisible: function () { return this.$store.getters.isMenuVisible }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#lens-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 70px;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.draggable-top {
|
||||
-webkit-app-region: drag;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,12 @@
|
||||
@import "custom";
|
||||
@import "~typeface-roboto/index.css";
|
||||
@import "~material-design-icons/iconfont/material-icons.css";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
@import "~bootstrap-vue/src/index";
|
||||
@import "~prismjs/themes/prism-tomorrow.css";
|
||||
@import "~vue-prism-editor/dist/VuePrismEditor.css";
|
||||
|
||||
@import 'custom.scss';
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import '~bootstrap-vue/src/index.scss';
|
||||
|
||||
@import 'fonts.scss';
|
||||
|
||||
html, body{
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
@@ -24,7 +25,7 @@ pre {
|
||||
color: $lens-text-color-light;
|
||||
}
|
||||
|
||||
#app{
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
& > .main-view{
|
||||
@@ -56,15 +57,6 @@ pre {
|
||||
}
|
||||
}
|
||||
|
||||
.draggable-top{
|
||||
-webkit-app-region: drag;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
|
||||
@@ -202,3 +194,13 @@ h1, h2, h3, h4, h5, h6{
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
#lens-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 70px;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+2
-2
@@ -111,8 +111,8 @@
|
||||
<script>
|
||||
import * as PrismEditor from 'vue-prism-editor'
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import { dumpConfigYaml } from "../../main/k8s"
|
||||
import ClustersMixin from "@/mixins/ClustersMixin";
|
||||
import { dumpConfigYaml } from "../../../main/k8s"
|
||||
import ClustersMixin from "@/_vue/mixins/ClustersMixin";
|
||||
|
||||
class ClusterAccessError extends Error {}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@
|
||||
|
||||
<script>
|
||||
import { v4 as uuid } from "uuid";
|
||||
import ClosePageButton from "@/components/common/ClosePageButton";
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'AddWorkspacePage',
|
||||
File renamed without changes.
+1
-1
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CubeSpinner from "@/components/CubeSpinner";
|
||||
import CubeSpinner from "@/_vue/components/CubeSpinner";
|
||||
import { remote, shell } from 'electron';
|
||||
export default {
|
||||
name: "ClusterPage",
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -70,7 +70,7 @@
|
||||
|
||||
<script>
|
||||
import { lstatSync } from "fs"
|
||||
import { prometheusProviders } from '../../../../common/prometheus-providers';
|
||||
import { prometheusProviders } from '../../../../../common/prometheus-providers';
|
||||
|
||||
export default {
|
||||
name: 'ClusterSettingsPreferences',
|
||||
+6
-6
@@ -39,13 +39,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CubeSpinner from "@/components/CubeSpinner";
|
||||
import ClusterSettingsGeneral from "./General";
|
||||
import ClusterSettingsOverview from "./Overview";
|
||||
import ClusterSettingsPreferences from "./Preferences";
|
||||
import ClusterSettingsFeatures from "./Features";
|
||||
import CubeSpinner from "@/_vue/components/CubeSpinner";
|
||||
import ClusterSettingsGeneral from "./General/index";
|
||||
import ClusterSettingsOverview from "./Overview/index";
|
||||
import ClusterSettingsPreferences from "./Preferences/index";
|
||||
import ClusterSettingsFeatures from "./Features/index";
|
||||
import hashicon from "../hashicon/hashicon";
|
||||
import ClosePageButton from "@/components/common/ClosePageButton";
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'ClusterSettingsPage',
|
||||
File renamed without changes.
+1
-1
@@ -54,7 +54,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClosePageButton from "@/components/common/ClosePageButton";
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'EditWorkspacePage',
|
||||
File renamed without changes.
+1
-1
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClustersMixin from "@/mixins/ClustersMixin";
|
||||
import ClustersMixin from "@/_vue/mixins/ClustersMixin";
|
||||
import { newContexts } from '@kubernetes/client-node/dist/config_types';
|
||||
export default {
|
||||
name: "AddClusterMenuItem",
|
||||
File renamed without changes.
+44
-43
@@ -19,14 +19,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClusterMenuItem from "@/components/MainMenu/ClusterMenuItem";
|
||||
import AddClusterMenuItem from "@/components/MainMenu/AddClusterMenuItem";
|
||||
import ClusterMenuItem from "@/_vue/components/MainMenu/ClusterMenuItem";
|
||||
import AddClusterMenuItem from "@/_vue/components/MainMenu/AddClusterMenuItem";
|
||||
import draggable from 'vuedraggable'
|
||||
import { clusterStore } from "@/../common/cluster-store";
|
||||
import * as os from "os";
|
||||
import { clusterStore } from "../../../../common/cluster-store"
|
||||
import { isMac } from "../../../../common/vars"
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { Menu, MenuItem } = remote
|
||||
const {remote} = require('electron')
|
||||
const {Menu, MenuItem} = remote
|
||||
|
||||
export default {
|
||||
name: "MainMenu",
|
||||
@@ -35,30 +35,27 @@ export default {
|
||||
AddClusterMenuItem,
|
||||
draggable
|
||||
},
|
||||
data(){
|
||||
data() {
|
||||
return {
|
||||
computedPaddingTop: "15px"
|
||||
}
|
||||
},
|
||||
computed : {
|
||||
menuAvailable: function(){
|
||||
return this.$store.getters.isLoggedIn && this.$store.getters.isEulaAccepted
|
||||
},
|
||||
computed: {
|
||||
clusters: {
|
||||
get: function() {
|
||||
get: function () {
|
||||
return this.$store.getters.clusters;
|
||||
},
|
||||
set: function(clusters) {
|
||||
set: function (clusters) {
|
||||
this.$store.commit("updateClusters", clusters);
|
||||
clusterStore.storeClusters(clusters);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isActive: function( id ){
|
||||
isActive: function (id) {
|
||||
return id === this.$route.params.id;
|
||||
},
|
||||
addCluster: async function(){
|
||||
addCluster: async function () {
|
||||
this.$router.push({
|
||||
name: "add-cluster-page",
|
||||
params: {
|
||||
@@ -67,8 +64,8 @@ export default {
|
||||
}).catch(err => {})
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
if (os.platform() == "darwin") {
|
||||
mounted: function () {
|
||||
if (isMac) {
|
||||
this.computedPaddingTop = "25px";
|
||||
}
|
||||
}
|
||||
@@ -76,32 +73,36 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.main-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 20px;
|
||||
width: 70px;
|
||||
padding-top: 15px;
|
||||
background: #252729;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
.menu-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 6px 15px 6px 15px;
|
||||
color: #87909c;
|
||||
opacity: 0.3;
|
||||
&.active {
|
||||
opacity: 1;
|
||||
.main-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 20px;
|
||||
width: 70px;
|
||||
padding-top: 15px;
|
||||
background: #252729;
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
&.online {
|
||||
opacity: 1;
|
||||
|
||||
.menu-item {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 6px 15px 6px 15px;
|
||||
color: #87909c;
|
||||
opacity: 0.3;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.online {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+3
-3
@@ -135,9 +135,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as request from 'request-promise-native'
|
||||
import { globalRequestOpts } from "../../common/request"
|
||||
import ClosePageButton from "@/components/common/ClosePageButton";
|
||||
import request from 'request-promise-native'
|
||||
import { globalRequestOpts } from "../../../common/request"
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
|
||||
export default {
|
||||
name: 'PreferencesPage',
|
||||
+5
-4
@@ -28,16 +28,17 @@
|
||||
|
||||
<script>
|
||||
import marked from 'marked'
|
||||
import fs from 'fs'
|
||||
import * as path from "path"
|
||||
import {readFileSync} from 'fs'
|
||||
import { getStaticPath } from "../../../common/register-static"
|
||||
|
||||
export default {
|
||||
name: 'WhatsNewPage',
|
||||
data() {
|
||||
let releaseNotes = getStaticPath("RELEASE_NOTES.md");
|
||||
let content = marked(readFileSync(releaseNotes, 'utf8'));
|
||||
return {
|
||||
error: "",
|
||||
// eslint-disable-next-line
|
||||
content: marked(fs.readFileSync(path.join(__static, "RELEASE_NOTES.md"), 'utf8')),
|
||||
content: content,
|
||||
}
|
||||
},
|
||||
components: {},
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClosePageButton from "@/components/common/ClosePageButton";
|
||||
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
|
||||
export default {
|
||||
name: 'WorkspacesPage',
|
||||
components: {
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,47 @@
|
||||
import "../../common/system-ca"
|
||||
import "./assets/css/app.scss"
|
||||
import "prismjs";
|
||||
import "prismjs/components/prism-yaml"
|
||||
import { remote } from "electron"
|
||||
import Vue from 'vue'
|
||||
import VueElectron from 'vue-electron'
|
||||
import BootstrapVue from 'bootstrap-vue'
|
||||
import { PromiseIpc } from 'electron-promise-ipc'
|
||||
import { Tracker } from "../../common/tracker"
|
||||
import App from './App'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
const tracker = new Tracker(remote.app);
|
||||
const promiseIpc = new PromiseIpc({maxTimeoutMs: 6000});
|
||||
|
||||
promiseIpc.on('navigate', async (view) => {
|
||||
router.push(view).catch(err => {})
|
||||
});
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.use(VueElectron)
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
Vue.mixin({
|
||||
created: function () {
|
||||
this.$promiseIpc = promiseIpc;
|
||||
this.$tracker = tracker;
|
||||
}
|
||||
})
|
||||
|
||||
// any initialization we want to do for app state
|
||||
setTimeout(() => {
|
||||
store.dispatch('init', ).catch((error) => {
|
||||
console.error(error)
|
||||
}).finally(() => {
|
||||
/* eslint-disable no-new */
|
||||
console.log("start vue")
|
||||
new Vue({
|
||||
components: { App },
|
||||
store,
|
||||
router,
|
||||
template: '<App/>'
|
||||
}).$mount('#app')
|
||||
})
|
||||
}, 0)
|
||||
File renamed without changes.
@@ -1,70 +1,68 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import store from "../store/index.js";
|
||||
import store from "../store";
|
||||
import { whatsNew } from './routeguard'
|
||||
|
||||
// Route Guard
|
||||
import * as guard from './routeguard'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(Router);
|
||||
|
||||
const router = new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'landing-page',
|
||||
component: require('@/components/LandingPage').default,
|
||||
component: require('@/_vue/components/LandingPage').default,
|
||||
meta: {
|
||||
routeguard: [
|
||||
// guards in priority order; the first one to catch will trigger something
|
||||
guard.whatsNew,
|
||||
whatsNew,
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/preferences',
|
||||
name: 'preferences-page',
|
||||
component: require('@/components/PreferencesPage').default,
|
||||
component: require('@/_vue/components/PreferencesPage').default,
|
||||
},
|
||||
{
|
||||
path: '/workspaces',
|
||||
name: 'workspaces-page',
|
||||
component: require('@/components/WorkspacesPage').default,
|
||||
component: require('@/_vue/components/WorkspacesPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/add-workspace',
|
||||
name: 'add-workspace-page',
|
||||
component: require('@/components/AddWorkspacePage').default,
|
||||
component: require('@/_vue/components/AddWorkspacePage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/edit-workspace',
|
||||
name: 'edit-workspace-page',
|
||||
component: require('@/components/EditWorkspacePage').default,
|
||||
component: require('@/_vue/components/EditWorkspacePage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/clusters/:id',
|
||||
name: 'cluster-page',
|
||||
component: require('@/components/ClusterPage').default,
|
||||
component: require('@/_vue/components/ClusterPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/clusters/:id/settings',
|
||||
name: 'cluster-settings-page',
|
||||
component: require('@/components/ClusterSettings').default,
|
||||
component: require('@/_vue/components/ClusterSettings').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/add-cluster",
|
||||
name: "add-cluster-page",
|
||||
component: require('@/components/AddClusterPage').default,
|
||||
component: require('@/_vue/components/AddClusterPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "/whats-new",
|
||||
name: "whats-new-page",
|
||||
component: require('@/components/WhatsNewPage').default,
|
||||
component: require('@/_vue/components/WhatsNewPage').default,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
File renamed without changes.
@@ -1,19 +1,19 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { userStore } from "../../common/user-store"
|
||||
import semver from "semver"
|
||||
import { userStore } from "../../../common/user-store"
|
||||
import { getAppVersion } from "../../../common/utils/app-version"
|
||||
import KubeContexts from './modules/kube-contexts'
|
||||
import Clusters from './modules/clusters'
|
||||
import HelmRepos from './modules/helm-repos'
|
||||
import Workspaces from './modules/workspaces'
|
||||
import * as semver from "semver"
|
||||
import * as appUtil from "../../common/app-utils"
|
||||
|
||||
// promise ipc
|
||||
import { PromiseIpc } from 'electron-promise-ipc'
|
||||
const promiseIpc = new PromiseIpc( { maxTimeoutMs: 120000 } );
|
||||
|
||||
// tracker
|
||||
import { Tracker } from "../../common/tracker"
|
||||
import { Tracker } from "../../../common/tracker"
|
||||
import { remote } from "electron"
|
||||
const tracker = new Tracker(remote.app);
|
||||
|
||||
@@ -77,7 +77,7 @@ export default new Vuex.Store({
|
||||
},
|
||||
async updateLastSeenAppVersion({commit, state}) {
|
||||
tracker.event("app", "whats-new-seen")
|
||||
commit("updateLastSeenAppVersion", appUtil.getAppVersion())
|
||||
commit("updateLastSeenAppVersion", getAppVersion())
|
||||
}
|
||||
},
|
||||
getters : {
|
||||
@@ -87,7 +87,7 @@ export default new Vuex.Store({
|
||||
return state.hud.isMenuVisible && !getters.showWhatsNew;
|
||||
},
|
||||
showWhatsNew: function(state) {
|
||||
return semver.gt(appUtil.getAppVersion(), state.lastSeenAppVersion);
|
||||
return semver.gt(getAppVersion(), state.lastSeenAppVersion);
|
||||
},
|
||||
preferences: state => state.preferences,
|
||||
}
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import Vue from "vue"
|
||||
import { ClusterInfo } from "../../../main/cluster"
|
||||
import { ClusterInfo } from "../../../../main/cluster"
|
||||
import { MutationTree, ActionTree, GetterTree } from "vuex"
|
||||
import { PromiseIpc } from 'electron-promise-ipc'
|
||||
import { Tracker } from "../../../common/tracker"
|
||||
import { Tracker } from "../../../../common/tracker"
|
||||
import { remote } from "electron"
|
||||
import { clusterStore } from "../../../common/cluster-store"
|
||||
import { Workspace } from "../../../common/workspace-store"
|
||||
import { clusterStore } from "../../../../common/cluster-store"
|
||||
import { Workspace } from "../../../../common/workspace-store"
|
||||
|
||||
const promiseIpc = new PromiseIpc( { maxTimeoutMs: 120000 } );
|
||||
const tracker = new Tracker(remote.app);
|
||||
@@ -88,7 +88,7 @@ const actions: ActionTree<ClusterState, any> = {
|
||||
await promiseIpc.send("removeCluster", cluster.id).catch((error: Error) => {
|
||||
return false;
|
||||
})
|
||||
tracker.event("cluster", "remove´");
|
||||
tracker.event("cluster", "remove");
|
||||
await dispatch("refreshClusters", getters.currentWorkspace)
|
||||
return true;
|
||||
},
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import Vue from "vue"
|
||||
import { MutationTree, ActionTree, GetterTree } from "vuex"
|
||||
import { HelmRepo, repoManager } from "../../../main/helm-repo-manager"
|
||||
import { HelmRepo, repoManager } from "../../../../main/helm-repo-manager"
|
||||
|
||||
export interface HelmRepoState {
|
||||
repos: HelmRepo[];
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import { splitConfig, dumpConfigYaml } from "../../../main/k8s"
|
||||
import { splitConfig, dumpConfigYaml } from "../../../../main/k8s"
|
||||
|
||||
const state = {
|
||||
availableKubeContexts: []
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { MutationTree, ActionTree, GetterTree } from "vuex"
|
||||
import { workspaceStore, Workspace, WorkspaceData } from "../../../common/workspace-store"
|
||||
import { workspaceStore, Workspace, WorkspaceData } from "../../../../common/workspace-store"
|
||||
|
||||
export interface WorkspaceState {
|
||||
workspaces: Array<Workspace>;
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { KubeObjectStore } from "../kube-object.store";
|
||||
import type { KubeObjectDetailsProps, KubeObjectListLayoutProps, KubeObjectMenuProps } from "../components/kube-object";
|
||||
import type React from "react";
|
||||
|
||||
import { observable } from "mobx";
|
||||
import { autobind } from "../utils/autobind";
|
||||
import { KubeApi } from "./kube-api";
|
||||
|
||||
export interface ApiComponents {
|
||||
List?: React.ComponentType<KubeObjectListLayoutProps>;
|
||||
Menu?: React.ComponentType<KubeObjectMenuProps>;
|
||||
Details?: React.ComponentType<KubeObjectDetailsProps>;
|
||||
}
|
||||
|
||||
@autobind()
|
||||
export class ApiManager {
|
||||
private apis = observable.map<string, KubeApi>();
|
||||
private stores = observable.map<KubeApi, KubeObjectStore>();
|
||||
private views = observable.map<KubeApi, ApiComponents>();
|
||||
|
||||
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
|
||||
if (typeof pathOrCallback === "string") {
|
||||
return this.apis.get(pathOrCallback) || this.apis.get(KubeApi.parseApi(pathOrCallback).apiBase);
|
||||
}
|
||||
|
||||
return Array.from(this.apis.values()).find(pathOrCallback);
|
||||
}
|
||||
|
||||
registerApi(apiBase: string, api: KubeApi) {
|
||||
if (!this.apis.has(apiBase)) {
|
||||
this.apis.set(apiBase, api);
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveApi(api: string | KubeApi): KubeApi {
|
||||
if (typeof api === "string") return this.getApi(api)
|
||||
return api;
|
||||
}
|
||||
|
||||
unregisterApi(api: string | KubeApi) {
|
||||
if (typeof api === "string") this.apis.delete(api);
|
||||
else {
|
||||
const apis = Array.from(this.apis.entries());
|
||||
const entry = apis.find(entry => entry[1] === api);
|
||||
if (entry) this.unregisterApi(entry[0]);
|
||||
}
|
||||
}
|
||||
|
||||
registerStore(api: KubeApi, store: KubeObjectStore) {
|
||||
this.stores.set(api, store);
|
||||
}
|
||||
|
||||
getStore(api: string | KubeApi): KubeObjectStore {
|
||||
return this.stores.get(this.resolveApi(api));
|
||||
}
|
||||
|
||||
registerViews(api: KubeApi | KubeApi[], views: ApiComponents) {
|
||||
if (Array.isArray(api)) {
|
||||
api.forEach(api => this.registerViews(api, views));
|
||||
return;
|
||||
}
|
||||
const currentViews = this.views.get(api) || {};
|
||||
this.views.set(api, {
|
||||
...currentViews,
|
||||
...views,
|
||||
});
|
||||
}
|
||||
|
||||
getViews(api: string | KubeApi): ApiComponents {
|
||||
return this.views.get(this.resolveApi(api)) || {}
|
||||
}
|
||||
}
|
||||
|
||||
export const apiManager = new ApiManager();
|
||||
@@ -0,0 +1,261 @@
|
||||
// Kubernetes certificate management controller apis
|
||||
// Reference: https://docs.cert-manager.io/en/latest/reference/index.html
|
||||
// API docs: https://docs.cert-manager.io/en/latest/reference/api-docs/index.html
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { ISecretRef, secretsApi } from "./secret.api";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export class Certificate extends KubeObject {
|
||||
static kind = "Certificate"
|
||||
|
||||
spec: {
|
||||
secretName: string;
|
||||
commonName?: string;
|
||||
dnsNames?: string[];
|
||||
organization?: string[];
|
||||
ipAddresses?: string[];
|
||||
duration?: string;
|
||||
renewBefore?: string;
|
||||
isCA?: boolean;
|
||||
keySize?: number;
|
||||
keyAlgorithm?: "rsa" | "ecdsa";
|
||||
issuerRef: {
|
||||
kind?: string;
|
||||
name: string;
|
||||
};
|
||||
acme?: {
|
||||
config: {
|
||||
domains: string[];
|
||||
http01: {
|
||||
ingress?: string;
|
||||
ingressClass?: string;
|
||||
};
|
||||
dns01?: {
|
||||
provider: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
}
|
||||
status: {
|
||||
conditions?: {
|
||||
lastTransitionTime: string; // 2019-06-04T07:35:58Z,
|
||||
message: string; // Certificate is up to date and has not expired,
|
||||
reason: string; // Ready,
|
||||
status: string; // True,
|
||||
type: string; // Ready
|
||||
}[];
|
||||
notAfter: string; // 2019-11-01T05:36:27Z
|
||||
lastFailureTime?: string;
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
const { isCA, acme } = this.spec;
|
||||
if (isCA) return "CA"
|
||||
if (acme) return "ACME"
|
||||
}
|
||||
|
||||
getCommonName() {
|
||||
return this.spec.commonName || ""
|
||||
}
|
||||
|
||||
getIssuerName() {
|
||||
return this.spec.issuerRef.name;
|
||||
}
|
||||
|
||||
getSecretName() {
|
||||
return this.spec.secretName;
|
||||
}
|
||||
|
||||
getIssuerDetailsUrl() {
|
||||
return getDetailsUrl(issuersApi.getUrl({
|
||||
namespace: this.getNs(),
|
||||
name: this.getIssuerName(),
|
||||
}))
|
||||
}
|
||||
|
||||
getSecretDetailsUrl() {
|
||||
return getDetailsUrl(secretsApi.getUrl({
|
||||
namespace: this.getNs(),
|
||||
name: this.getSecretName(),
|
||||
}))
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
const { conditions = [] } = this.status;
|
||||
return conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
return {
|
||||
...condition,
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Issuer extends KubeObject {
|
||||
static kind = "Issuer"
|
||||
|
||||
spec: {
|
||||
acme?: {
|
||||
email: string;
|
||||
server: string;
|
||||
skipTLSVerify?: boolean;
|
||||
privateKeySecretRef: ISecretRef;
|
||||
solvers?: {
|
||||
dns01?: {
|
||||
cnameStrategy: string;
|
||||
acmedns?: {
|
||||
host: string;
|
||||
accountSecretRef: ISecretRef;
|
||||
};
|
||||
akamai?: {
|
||||
accessTokenSecretRef: ISecretRef;
|
||||
clientSecretSecretRef: ISecretRef;
|
||||
clientTokenSecretRef: ISecretRef;
|
||||
serviceConsumerDomain: string;
|
||||
};
|
||||
azuredns?: {
|
||||
clientID: string;
|
||||
clientSecretSecretRef: ISecretRef;
|
||||
hostedZoneName: string;
|
||||
resourceGroupName: string;
|
||||
subscriptionID: string;
|
||||
tenantID: string;
|
||||
};
|
||||
clouddns?: {
|
||||
project: string;
|
||||
serviceAccountSecretRef: ISecretRef;
|
||||
};
|
||||
cloudflare?: {
|
||||
email: string;
|
||||
apiKeySecretRef: ISecretRef;
|
||||
};
|
||||
digitalocean?: {
|
||||
tokenSecretRef: ISecretRef;
|
||||
};
|
||||
rfc2136?: {
|
||||
nameserver: string;
|
||||
tsigAlgorithm: string;
|
||||
tsigKeyName: string;
|
||||
tsigSecretSecretRef: ISecretRef;
|
||||
};
|
||||
route53?: {
|
||||
accessKeyID: string;
|
||||
hostedZoneID: string;
|
||||
region: string;
|
||||
secretAccessKeySecretRef: ISecretRef;
|
||||
};
|
||||
webhook?: {
|
||||
config: object; // arbitrary json
|
||||
groupName: string;
|
||||
solverName: string;
|
||||
};
|
||||
};
|
||||
http01?: {
|
||||
ingress: {
|
||||
class: string;
|
||||
name: string;
|
||||
serviceType: string;
|
||||
};
|
||||
};
|
||||
selector?: {
|
||||
dnsNames: string[];
|
||||
matchLabels: {
|
||||
[label: string]: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
};
|
||||
ca?: {
|
||||
secretName: string;
|
||||
};
|
||||
vault?: {
|
||||
path: string;
|
||||
server: string;
|
||||
caBundle: string; // <base64 encoded caBundle PEM file>
|
||||
auth: {
|
||||
appRole: {
|
||||
path: string;
|
||||
roleId: string;
|
||||
secretRef: ISecretRef;
|
||||
};
|
||||
};
|
||||
};
|
||||
selfSigned?: {};
|
||||
venafi?: {
|
||||
zone: string;
|
||||
cloud?: {
|
||||
apiTokenSecretRef: ISecretRef;
|
||||
};
|
||||
tpp?: {
|
||||
url: string;
|
||||
caBundle: string; // <base64 encoded caBundle PEM file>
|
||||
credentialsRef: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
status: {
|
||||
acme?: {
|
||||
uri: string;
|
||||
};
|
||||
conditions?: {
|
||||
lastTransitionTime: string; // 2019-06-05T07:10:42Z,
|
||||
message: string; // The ACME account was registered with the ACME server,
|
||||
reason: string; // ACMEAccountRegistered,
|
||||
status: string; // True,
|
||||
type: string; // Ready
|
||||
}[];
|
||||
}
|
||||
|
||||
getType() {
|
||||
const { acme, ca, selfSigned, vault, venafi } = this.spec;
|
||||
if (acme) return "ACME"
|
||||
if (ca) return "CA"
|
||||
if (selfSigned) return "SelfSigned"
|
||||
if (vault) return "Vault"
|
||||
if (venafi) return "Venafi"
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
const { conditions = [] } = this.status;
|
||||
return conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
return {
|
||||
...condition,
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterIssuer extends Issuer {
|
||||
static kind = "ClusterIssuer"
|
||||
}
|
||||
|
||||
export const certificatesApi = new KubeApi({
|
||||
kind: Certificate.kind,
|
||||
apiBase: "/apis/cert-manager.io/v1alpha2/certificates",
|
||||
isNamespaced: true,
|
||||
objectConstructor: Certificate,
|
||||
});
|
||||
|
||||
export const issuersApi = new KubeApi({
|
||||
kind: Issuer.kind,
|
||||
apiBase: "/apis/cert-manager.io/v1alpha2/issuers",
|
||||
isNamespaced: true,
|
||||
objectConstructor: Issuer,
|
||||
});
|
||||
|
||||
export const clusterIssuersApi = new KubeApi({
|
||||
kind: ClusterIssuer.kind,
|
||||
apiBase: "/apis/cert-manager.io/v1alpha2/clusterissuers",
|
||||
isNamespaced: false,
|
||||
objectConstructor: ClusterIssuer,
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { RoleBinding } from "./role-binding.api";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export class ClusterRoleBinding extends RoleBinding {
|
||||
static kind = "ClusterRoleBinding"
|
||||
}
|
||||
|
||||
export const clusterRoleBindingApi = new KubeApi({
|
||||
kind: ClusterRoleBinding.kind,
|
||||
apiBase: "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings",
|
||||
isNamespaced: false,
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
Loaded 100 of 786 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user