Lens restructure (#540)

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman authored and GitHub committed 2020-06-30 14:35:16 +03:00
1 parent e810eab39f
commit b7974827d2
786 files changed
+14567 -24066

No files matched your search

-9
View File
@@ -1,9 +0,0 @@
const userStore = {
getPreferences: jest.fn(() => {
return {
downloadMirror: "default"
}
})
}
export { userStore };
-14
View File
@@ -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
View File
@@ -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();
+333
View File
@@ -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')
})
})
+25
View File
@@ -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);
}
+2 -2
View File
@@ -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
View File
@@ -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
}
+3 -3
View File
@@ -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"
+7 -5
View File
@@ -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;
+75
View File
@@ -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')
})
})
+9
View File
@@ -0,0 +1,9 @@
import packageInfo from "../../../package.json"
export function getAppVersion(): string {
return packageInfo.version;
}
export function getBundledKubectlVersion(): string {
return packageInfo.config.bundledKubectlVersion;
}
+12
View File
@@ -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);
}
};
+18
View File
@@ -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;
}
}
+5
View File
@@ -0,0 +1,5 @@
// Common utils (main/renderer)
export * from "./base64"
export * from "./camelCase"
export * from "./splitArray"
+19
View File
@@ -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]
}
+31
View File
@@ -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]);
});
});
+39
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import * as ElectronStore from "electron-store"
import ElectronStore from "electron-store"
import { clusterStore } from "./cluster-store"
export interface WorkspaceData {
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
import * as fs from "fs"
import fs from "fs"
export function ensureDir(dirname: string) {
if (!fs.existsSync(dirname)) {
+1 -1
View File
@@ -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
View File
@@ -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);
+4 -5
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+6 -7
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
}
}
+29
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+26
View File
@@ -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
View File
@@ -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)
}
+2 -2
View File
@@ -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
View File
@@ -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);
+23 -9
View File
@@ -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,
+6 -5
View File
@@ -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
})
+2 -2
View File
@@ -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
View File
@@ -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) {
+2 -3
View File
@@ -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;
+7 -18
View File
@@ -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()
@@ -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...
@@ -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 || {} });
@@ -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
@@ -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
@@ -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
@@ -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];
@@ -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.
@@ -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 {}
@@ -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',
@@ -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",
@@ -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',
@@ -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',
@@ -54,7 +54,7 @@
</template>
<script>
import ClosePageButton from "@/components/common/ClosePageButton";
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
export default {
name: 'EditWorkspacePage',
@@ -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",
@@ -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>
@@ -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',
@@ -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: {},
@@ -38,7 +38,7 @@
</template>
<script>
import ClosePageButton from "@/components/common/ClosePageButton";
import ClosePageButton from "@/_vue/components/common/ClosePageButton";
export default {
name: 'WorkspacesPage',
components: {
+47
View File
@@ -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)
@@ -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,
},
{
@@ -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,
}
@@ -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,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,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,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>;
+74
View File
@@ -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