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

Lens restructure (#540)

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2020-06-30 14:35:16 +03:00 committed by GitHub
parent e810eab39f
commit b7974827d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
786 changed files with 14568 additions and 24067 deletions

View File

@ -35,7 +35,7 @@ jobs:
path: $(YARN_CACHE_FOLDER) path: $(YARN_CACHE_FOLDER)
cacheHitVar: CACHE_RESTORED cacheHitVar: CACHE_RESTORED
displayName: Cache Yarn packages displayName: Cache Yarn packages
- script: make deps - script: make install-deps
displayName: Install dependencies displayName: Install dependencies
- script: make integration-win - script: make integration-win
displayName: Run integration tests displayName: Run integration tests
@ -72,10 +72,8 @@ jobs:
tar -xzf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" -C / tar -xzf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" -C /
displayName: "Unpack cache" displayName: "Unpack cache"
condition: eq(variables.CACHE_RESTORED, 'true') condition: eq(variables.CACHE_RESTORED, 'true')
- script: make deps - script: make install-deps
displayName: Install dependencies displayName: Install dependencies
- script: make lint
displayName: Lint
- script: make test - script: make test
displayName: Run tests displayName: Run tests
- script: make integration-mac - script: make integration-mac
@ -119,7 +117,7 @@ jobs:
tar -xzf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" -C / tar -xzf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" -C /
displayName: "Unpack cache" displayName: "Unpack cache"
condition: eq(variables.CACHE_RESTORED, 'true') condition: eq(variables.CACHE_RESTORED, 'true')
- script: make deps - script: make install-deps
displayName: Install dependencies displayName: Install dependencies
- script: make lint - script: make lint
displayName: Lint displayName: Lint
@ -130,6 +128,7 @@ jobs:
sudo apt-get install libgconf-2-4 conntrack -y sudo apt-get install libgconf-2-4 conntrack -y
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube sudo install minikube-linux-amd64 /usr/local/bin/minikube
export CHANGE_MINIKUBE_NONE_USER=true
sudo minikube start --driver=none sudo minikube start --driver=none
displayName: Install integration test dependencies displayName: Install integration test dependencies
- script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux - script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux

12
.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@lingui/babel-preset-react"
],
"plugins": [
"macros",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime"
]
}

View File

@ -29,7 +29,7 @@ module.exports = {
files: [ files: [
"build/*.ts", "build/*.ts",
"src/**/*.ts", "src/**/*.ts",
"spec/**/*.ts" "integration/**/*.ts"
], ],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
extends: [ extends: [
@ -43,13 +43,16 @@ module.exports = {
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-interface": "off",
"indent": ["error", 2] "indent": ["error", 2]
}, },
}, },
{ {
files: [ files: [
"dashboard/**/*.ts", "src/renderer/**/*.tsx",
"dashboard/**/*.tsx",
], ],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
extends: [ extends: [

View File

Before

Width:  |  Height:  |  Size: 754 KiB

After

Width:  |  Height:  |  Size: 754 KiB

2
.gitignore vendored
View File

@ -1,4 +1,5 @@
dist/ dist/
out/
node_modules/ node_modules/
.DS_Store .DS_Store
yarn-error.log yarn-error.log
@ -7,3 +8,4 @@ tmp/
static/build/client/ static/build/client/
binaries/client/ binaries/client/
binaries/server/ binaries/server/
locales/**/**.js

View File

@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron" disturl "https://atom.io/download/electron"
target "6.1.10" target "6.1.12"
runtime "electron" runtime "electron"

View File

@ -4,15 +4,30 @@ else
DETECTED_OS := $(shell uname) DETECTED_OS := $(shell uname)
endif endif
.PHONY: dev build test clean .PHONY: init dev build test clean
init: download-bins install-deps compile-dev
echo "Init done"
download-bins: download-bins:
yarn download:bins yarn download-bins
dev: app-deps dashboard-deps install-deps:
yarn dev yarn install --frozen-lockfile
test: test-app test-dashboard compile-dev:
yarn compile:main --cache
yarn compile:renderer --cache
dev:
test -f out/main.js || make init
yarn dev # run electron and watch files
lint:
yarn lint
test:
yarn test
integration-linux: integration-linux:
yarn build:linux yarn build:linux
@ -26,19 +41,10 @@ integration-win:
yarn build:win yarn build:win
yarn integration yarn integration
lint:
yarn lint
yarn lint-dashboard
test-app: test-app:
yarn test yarn test
deps: app-deps dashboard-deps build: install-deps
app-deps:
yarn install --frozen-lockfile
build: build-dashboard app-deps
yarn install yarn install
ifeq "$(DETECTED_OS)" "Windows" ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win yarn dist:win
@ -46,18 +52,7 @@ else
yarn dist yarn dist
endif endif
dashboard-deps:
cd dashboard && yarn install --frozen-lockfile
clean-dashboard:
rm -rf dashboard/build/ && rm -rf static/build/client
test-dashboard: dashboard-deps
cd dashboard && yarn test
build-dashboard: dashboard-deps clean-dashboard
export NODE_ENV=production
cd dashboard && yarn build
clean: clean:
rm -rf binaries/client/*
rm -rf dist/* rm -rf dist/*
rm -rf out/*

View File

@ -6,7 +6,7 @@
Lens is the only IDE youll ever need to take control of your Kubernetes clusters. It is a standalone application for MacOS, Windows and Linux operating systems. It is open source and free. Lens is the only IDE youll ever need to take control of your Kubernetes clusters. It is a standalone application for MacOS, Windows and Linux operating systems. It is open source and free.
[![Screenshot](./images/screenshot.png)](https://youtu.be/04v2ODsmtIs) [![Screenshot](.github/screenshot.png)](https://youtu.be/04v2ODsmtIs)
## What makes Lens special? ## What makes Lens special?
@ -23,19 +23,25 @@ Lens is the only IDE youll ever need to take control of your Kubernetes clust
Download a pre-built package from the [releases](https://github.com/lensapp/lens/releases) page. Lens can be also installed via [snapcraft](https://snapcraft.io/kontena-lens) (Linux only). Download a pre-built package from the [releases](https://github.com/lensapp/lens/releases) page. Lens can be also installed via [snapcraft](https://snapcraft.io/kontena-lens) (Linux only).
Alternatively on Mac:
```
brew cask install lens
```
## Development ## Development
> Prerequisities: Nodejs v12, make, yarn > Prerequisites: Nodejs v12, make, yarn
* `make download-bins` - downloads bundled binaries to dev environment * `make init` - initial compilation, installing deps, etc.
* `make dev` - builds and starts the app * `make dev` - builds and starts the app
* `make test` - run tests * `make test` - run tests
## Development (advanced)
Allows faster separately re-run some of involved processes:
1. `yarn dev:main` compiles electron's main process and watch files
1. `yarn dev:renderer:vue` compiles electron's renderer vue-part
1. `yarn dev:renderer:react` compiles electron's renderer react-part
1. `yarn dev-run` runs app in dev-mode and restarts when electron's main process file has changed
Alternatively to compile both render parts in single command use `yarn dev:renderer`
## Contributing ## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/lensapp/lens. Bug reports and pull requests are welcome on GitHub at https://github.com/lensapp/lens.

View File

@ -3,7 +3,8 @@ module.exports = {
match: jest.fn(), match: jest.fn(),
app: { app: {
getVersion: jest.fn().mockReturnValue("3.0.0"), getVersion: jest.fn().mockReturnValue("3.0.0"),
getPath: jest.fn().mockReturnValue("/foo/bar") getPath: jest.fn().mockReturnValue("tmp"),
getLocale: jest.fn().mockRejectedValue("en"),
}, },
remote: { remote: {
app: { app: {

View File

@ -1,9 +1,10 @@
import * as request from "request" import packageInfo from "../package.json"
import * as fs from "fs" import fs from "fs"
import request from "request"
import md5File from "md5-file"
import requestPromise from "request-promise-native"
import { ensureDir, pathExists } from "fs-extra" import { ensureDir, pathExists } from "fs-extra"
import * as md5File from "md5-file" import path from "path"
import * as requestPromise from "request-promise-native"
import * as path from "path"
class KubectlDownloader { class KubectlDownloader {
public kubectlVersion: string public kubectlVersion: string
@ -86,7 +87,7 @@ class KubectlDownloader {
} }
} }
const downloadVersion: string = require("../package.json").config.bundledKubectlVersion const downloadVersion = packageInfo.config.bundledKubectlVersion;
const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client') const baseDir = path.join(process.env.INIT_CWD, 'binaries', 'client')
const downloads = [ const downloads = [
{ platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') }, { platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') },

View File

@ -1,10 +0,0 @@
{
"plugins": [
"macros",
"@babel/plugin-transform-runtime",
],
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}

View File

@ -1,12 +0,0 @@
.idea
node_modules/
build/
dist/
wireframes/
backup
npm-debug.log
.vscode
.env
/tslint.json
*.DS_Store
docker-compose.yml

13
dashboard/.gitignore vendored
View File

@ -1,13 +0,0 @@
.idea
node_modules
build/
dist/
wireframes/
backup
npm-debug.log
.vscode
dump.rdb
*.env
/tslint.json
*.DS_Store
locales/_build/

View File

@ -1,18 +0,0 @@
{
"locales": ["en", "ru"],
"sourceLocale": "en",
"fallbackLocale": "en",
"compileNamespace": "cjs",
"format": "po",
"extractBabelOptions": {
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
},
"catalogs": [
{
"path": "./locales/{locale}/messages",
"include": "./client"
}
]
}

View File

@ -1,47 +0,0 @@
import { CronJob } from "../";
//jest.mock('../../../components/+login/auth.store.ts', () => 'authStore');
jest.mock('../../kube-watch-api.ts', () => 'kube-watch-api');
const cronJob = new CronJob({
metadata: {
name: "hello",
namespace: "default",
selfLink: "/apis/batch/v1beta1/namespaces/default/cronjobs/hello",
uid: "cd3af13f-0b70-11ea-93da-9600002795a0",
resourceVersion: "51394448",
creationTimestamp: "2019-11-20T08:36:09Z",
},
spec: {
schedule: "30 06 31 12 *",
concurrencyPolicy: "Allow",
suspend: false,
},
status: {}
} as any)
describe("Check for CronJob schedule never run", () => {
test("Should be false with normal schedule", () => {
expect(cronJob.isNeverRun()).toBeFalsy();
});
test("Should be false with other normal schedule", () => {
cronJob.spec.schedule = "0 1 * * *";
expect(cronJob.isNeverRun()).toBeFalsy();
});
test("Should be true with date 31 of February", () => {
cronJob.spec.schedule = "30 06 31 2 *"
expect(cronJob.isNeverRun()).toBeTruthy();
});
test("Should be true with date 32 of July", () => {
cronJob.spec.schedule = "0 30 06 32 7 *"
expect(cronJob.isNeverRun()).toBeTruthy();
});
test("Should be false with predefined schedule", () => {
cronJob.spec.schedule = "@hourly";
expect(cronJob.isNeverRun()).toBeFalsy();
});
});

View File

@ -1,9 +0,0 @@
// App configuration api
import { apiBase } from "../index";
import { IConfig } from "../../../server/common/config";
export const configApi = {
getConfig() {
return apiBase.get<IConfig>("/config")
},
};

View File

@ -1,43 +0,0 @@
import { JsonApi, JsonApiErrorParsed } from "./json-api";
import { KubeJsonApi } from "./kube-json-api";
import { Notifications } from "../components/notifications";
import { clientVars } from "../../server/config";
//-- JSON HTTP APIS
export const apiBase = new JsonApi({
debug: !clientVars.IS_PRODUCTION,
apiPrefix: clientVars.API_PREFIX.BASE,
});
export const apiKube = new KubeJsonApi({
debug: !clientVars.IS_PRODUCTION,
apiPrefix: clientVars.API_PREFIX.KUBE_BASE,
});
export const apiKubeUsers = new KubeJsonApi({
debug: !clientVars.IS_PRODUCTION,
apiPrefix: clientVars.API_PREFIX.KUBE_USERS,
});
export const apiKubeHelm = new KubeJsonApi({
debug: !clientVars.IS_PRODUCTION,
apiPrefix: clientVars.API_PREFIX.KUBE_HELM,
});
export const apiKubeResourceApplier = new KubeJsonApi({
debug: !clientVars.IS_PRODUCTION,
apiPrefix: clientVars.API_PREFIX.KUBE_RESOURCE_APPLIER,
});
// Common handler for HTTP api errors
function onApiError(error: JsonApiErrorParsed, res: Response) {
switch (res.status) {
case 403:
error.isUsedForNotification = true;
Notifications.error(error);
break;
}
}
apiBase.onError.addListener(onApiError);
apiKube.onError.addListener(onApiError);
apiKubeUsers.onError.addListener(onApiError);
apiKubeHelm.onError.addListener(onApiError);
apiKubeResourceApplier.onError.addListener(onApiError);

View File

@ -1,117 +0,0 @@
// Custom fonts, bundled with app
// Material Design Icons
// https://material.io/resources/icons/
// https://github.com/google/material-design-icons/tree/master/iconfont
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'), local('MaterialIcons-Regular'),
url("fonts/MaterialIcons-Regular.woff2") format("woff2");
}
// Google fonts
// https://fonts.google.com/
// Download & generate styles:
// https://google-webfonts-helper.herokuapp.com/fonts/roboto?subsets=latin,cyrillic
/* roboto-100 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: local('Roboto Thin'), local('Roboto-Thin'),
url('fonts/roboto-v20-cyrillic_latin-100.woff2') format('woff2');
}
/* roboto-100italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 100;
src: local('Roboto Thin Italic'), local('Roboto-ThinItalic'),
url('fonts/roboto-v20-cyrillic_latin-100italic.woff2') format('woff2');
}
/* roboto-300 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url('fonts/roboto-v20-cyrillic_latin-300.woff2') format('woff2');
}
/* roboto-300italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url('fonts/roboto-v20-cyrillic_latin-300italic.woff2') format('woff2');
}
/* roboto-regular - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'),
url('fonts/roboto-v20-cyrillic_latin-regular.woff2') format('woff2');
}
/* roboto-italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: local('Roboto Italic'), local('Roboto-Italic'),
url('fonts/roboto-v20-cyrillic_latin-italic.woff2') format('woff2');
}
/* roboto-500 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'),
url('fonts/roboto-v20-cyrillic_latin-500.woff2') format('woff2');
}
/* roboto-500italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 500;
src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'),
url('fonts/roboto-v20-cyrillic_latin-500italic.woff2') format('woff2');
}
/* roboto-700 - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url('fonts/roboto-v20-cyrillic_latin-700.woff2') format('woff2');
}
/* roboto-700italic - cyrillic_latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url('fonts/roboto-v20-cyrillic_latin-700italic.woff2') format('woff2');
}
// Patched Roboto Mono font with icons
// https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/RobotoMono
/* RobotoMono Windows Compatible for using in terminal */
@font-face {
font-family: 'RobotoMono';
src: local('RobotoMono'),
url('fonts/roboto-mono-nerd.ttf') format('truetype');
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1 +0,0 @@
<svg enable-background="new 0 0 1034.7 1034.7" viewBox="0 0 1034.7 1034.7" xmlns="http://www.w3.org/2000/svg"><path d="m744 451.8v261.6l-75.5 43.3v-87.2l-75.5-43.3 75.5-43.3v-174.4l75.5 43.3v-261.6l-226.7-130.5-226.6 130.5v261.6l226.6-130.5 151.1 87.2-75.5 43.3v87.2l-151.1 87.2v-174.4l-75.5 43.3v261.6l-75.5-43.3v-261.6l-226.7 130.5v261.6l226.6 130.5 226.6-130.5-75.5-43.3v-87.2l75.5-43.3 75.5 43.3v87.2l-75.5 43.9 226.7 130.5 226.6-130.5v-261.6z"/></svg>

Before

Width:  |  Height:  |  Size: 456 B

View File

@ -1,30 +0,0 @@
{
"compilerOptions": {
"baseUrl": "../",
"rootDir": "../",
"outDir": "../build",
"jsx": "preserve",
"target": "es2016",
"lib": ["esnext", "dom", "dom.iterable"],
"module": "esnext",
"moduleResolution": "node",
"sourceMap": true,
"noImplicitAny": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"traceResolution": false,
"resolveJsonModule": true,
"paths": {
"@lingui/macro": [
"node_modules/@types/lingui__macro"
]
}
},
"exclude": [
"../test",
"**/__tests__"
]
}

View File

@ -1,31 +0,0 @@
import { split } from "../arrays";
describe("split array on element tests", () => {
test("empty array", () => {
expect(split([], 10)).toStrictEqual([[], [], false]);
});
test("one element, not in array", () => {
expect(split([1], 10)).toStrictEqual([[1], [], false]);
});
test("ten elements, not in array", () => {
expect(split([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(split([1], 1)).toStrictEqual([[], [], true]);
});
test("ten elements, in front array", () => {
expect(split([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(split([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(split([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
});
});

View File

@ -1,22 +0,0 @@
import { cpuUnitsToNumber } from "../convertCpu";
jest.mock('../../api/index', () => 'apiKube');
jest.mock('../../config.store', () => 'configStore');
describe("k8s CPU units conversion", () => {
test("Convert normal, nano(n), micro(u), milli(m) units to cores number", () => {
const units = [
"0.5",
"100m", // 0.1
"930000n", // 0.00093
"3028u", // 0.003028
]
const cpuCores = units.map(unit => cpuUnitsToNumber(unit))
const expected = [
0.5,
0.1,
0.00093,
0.003028
]
expect(cpuCores).toEqual(expected)
});
});

View File

@ -1,89 +0,0 @@
import { bytesToUnits, unitsToBytes } from "../convertMemory";
jest.mock('../../api/index', () => 'apiKube');
jest.mock('../../config.store', () => 'configStore');
describe("Kubernetes units conversion", () => {
test("Convert bytes to units", () => {
const bytes = [
128,
2048, // 2Ki
2097152, // 2Mi
4596968000, // 4.2Gi
4596968000000, // 4.1Ti
1.2384898975269E+15 // 1.1Pi
]
const units = bytes.map(byte => bytesToUnits(byte))
const expected = [
"128B",
"2.0Ki",
"2.0Mi",
"4.3Gi",
"4.2Ti",
"1.1Pi"
]
expect(units).toEqual(expected)
});
test("Convert bytes to units with decimal precision", () => {
const bytes = [
2107152, // 2.010Mi
4596968000, // 4.281Gi
]
const units = bytes.map(byte => bytesToUnits(byte, 3))
const expected = [
"2.010Mi",
"4.281Gi"
]
expect(units).toEqual(expected)
})
test("Convert 0 to bytes", () => {
expect(bytesToUnits(0)).toEqual("N/A");
});
test("Convert full units to bytes", () => {
const units = [
"128",
"22Ki", // 22528
"17.2Mi", // 18035507
"7.99Gi", // 8579197173
"2Ti", // 2199023255552
"1Pi", // 1125899906842624
]
const expected = [
128,
22528,
18035507,
8579197173,
2199023255552,
1125899906842624
]
const bytes = units.map(unitsToBytes)
expect(bytes).toEqual(expected)
});
test("Convert shorten units to bytes", () => {
const units = [
"128",
"22K", // 22528
"17.2M", // 18035507
"7.99G", // 8579197173
"2T", // 2199023255552
"1P", // 1125899906842624
]
const expected = [
128,
22528,
18035507,
8579197173,
2199023255552,
1125899906842624
]
const bytes = units.map(unitsToBytes)
expect(bytes).toEqual(expected)
});
test("Convert strange unit to bytes", () => {
expect(unitsToBytes("sss")).toEqual(NaN);
});
});

View File

@ -1,18 +0,0 @@
// Convert object's keys to camelCase format
import { camelCase, isPlainObject } from "lodash";
export function toCamelCase(data: any): any {
if (Array.isArray(data)) {
return data.map(toCamelCase);
}
else if (isPlainObject(data)) {
return Object.keys(data).reduce<any>((result, key) => {
const value = data[key];
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
return result;
}, {});
}
else {
return data;
}
}

View File

@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lens - The Kubernetes IDE</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" sizes="180x180" href="<%= require('./client/favicon/apple-touch-icon.png') %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= require('./client/favicon/favicon-32x32.png') %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= require('./client/favicon/favicon-16x16.png') %>">
<link rel="icon" type="image/png" sizes="512x512" href="<%= require('./client/favicon/android-chrome-512x512.png') %>">
<link rel="mask-icon" color="#5bbad5" href="<%= require('./client/favicon/safari-pinned-tab.svg') %>">
<link rel="preload" as="font" href="<%= require('./client/components/fonts/roboto-mono-nerd.ttf') %>" type="font/ttf" crossorigin>
<link rel="preload" as="font" href="<%= require('./client/components/fonts/MaterialIcons-Regular.woff2') %>" type="font/woff2" crossorigin>
</head>
<body>
<div id="app"></div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -1,129 +0,0 @@
{
"name": "lens-app-dashboard",
"version": "0.0.0",
"scripts": {
"dev": "webpack-cli --watch --cache --progress --output-path ../static/build/client/",
"build": "webpack -p --progress --output-path ../static/build/client/",
"test": "jest --config './test/jest.config.js'",
"add-locale": "lingui add-locale",
"lingui-extract": "lingui extract --clean",
"lingui-compile": "lingui compile"
},
"dependencies": {
"axios": "^0.19.0",
"chalk": "^2.4.2",
"compare-versions": "^3.6.0",
"compression": "^1.7.4",
"cookie-session": "^1.3.3",
"cors": "^2.8.5",
"crypto-js": "^3.1.9-1",
"dotenv": "^8.2.0",
"ip": "^1.1.5",
"js-yaml": "^3.13.1",
"jsonpath": "^1.0.2",
"lodash": "^4.17.15",
"morgan": "^1.9.1"
},
"devDependencies": {
"@babel/core": "^7.7.2",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "=7.9.0",
"@babel/preset-react": "^7.7.0",
"@babel/preset-typescript": "^7.8.3",
"@babel/runtime": "^7.7.2",
"@lingui/cli": "^3.0.0-7",
"@lingui/loader": "^3.0.0-7",
"@lingui/macro": "^3.0.0-7",
"@lingui/react": "^3.0.0-7",
"@material-ui/core": "^4.6.0",
"@types/chart.js": "^2.9.1",
"@types/color": "^3.0.0",
"@types/compression": "^1.0.1",
"@types/cookie-session": "^2.0.37",
"@types/cors": "^2.8.6",
"@types/crypto-js": "^3.1.43",
"@types/dompurify": "^2.0.0",
"@types/dotenv": "^8.2.0",
"@types/enzyme": "^3.10.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/express": "^4.17.2",
"@types/helmet": "^0.0.45",
"@types/history": "^4.7.3",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.1",
"@types/http-proxy-middleware": "^0.19.3",
"@types/ip": "^1.1.0",
"@types/jest": "^24.0.22",
"@types/js-yaml": "^3.12.1",
"@types/jsonpath": "^0.2.0",
"@types/lingui__macro": "^2.7.3",
"@types/lodash": "^4.14.146",
"@types/marked": "^0.7.0",
"@types/material-ui": "^0.21.7",
"@types/mini-css-extract-plugin": "^0.8.0",
"@types/morgan": "^1.7.37",
"@types/node": "^12.12.7",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.4",
"@types/react-router-dom": "^5.1.2",
"@types/react-select": "^3.0.8",
"@types/react-window": "^1.8.1",
"@types/terser-webpack-plugin": "^2.2.0",
"@types/webpack": "^4.39.8",
"ace-builds": "^1.4.7",
"ansi_up": "^4.0.4",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^8.0.6",
"babel-plugin-macros": "^2.6.1",
"chart.js": "^2.9.2",
"color": "^3.1.2",
"commander": "^4.0.1",
"concurrently": "^5.1.0",
"css-element-queries": "^1.2.1",
"css-loader": "^3.2.0",
"dompurify": "^2.0.7",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"file-loader": "^4.2.0",
"flex.box": "^3.4.4",
"fs-extra": "^8.1.0",
"hoist-non-react-statics": "^3.3.0",
"html-webpack-plugin": "3.2.0",
"identity-obj-proxy": "^3.0.0",
"include-media": "^1.4.9",
"jest": "^24.9.0",
"marked": "^0.7.0",
"mini-css-extract-plugin": "^0.8.0",
"mobx": "^5.15.0",
"mobx-observable-history": "^1.0.0",
"mobx-react": "^6.1.4",
"moment": "^2.24.0",
"node-sass": "^4.13.0",
"nodemon": "^1.19.4",
"path-to-regexp": "^3.2.0",
"pkg": "^4.4.4",
"raw-loader": "^3.1.0",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"react-router-dom": "^5.1.2",
"react-select": "^3.0.8",
"react-window": "^1.8.5",
"sass-loader": "^8.0.0",
"style-loader": "^1.0.0",
"ts-jest": "^24.1.0",
"ts-loader": "^6.2.1",
"ts-node": "^8.5.0",
"typescript": "^3.7.2",
"url-loader": "^2.2.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"xterm": "^4.4.0-vscode1",
"xterm-addon-fit": "^0.3.0",
"yargs": "^14.2.0"
}
}

View File

@ -1,4 +0,0 @@
export interface IClusterInfo {
kubeVersion?: string;
clusterName?: string;
}

View File

@ -1,12 +0,0 @@
import { IClusterInfo } from "../common/cluster";
export interface IConfig extends Partial<IClusterInfo> {
lensVersion?: string;
lensTheme?: string;
username?: string;
token?: string;
allowedNamespaces?: string[];
allowedResources?: string[];
isClusterAdmin?: boolean;
chartsEnabled: boolean;
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
}

View File

@ -1,14 +0,0 @@
export interface IKubeWatchEvent<T = any> {
type: "ADDED" | "MODIFIED" | "DELETED";
object?: T;
}
export interface IKubeWatchRouteEvent {
type: "STREAM_END";
url: string;
status: number;
}
export interface IKubeWatchRouteQuery {
api: string | string[];
}

View File

@ -1,4 +0,0 @@
export type IMetricsQuery = string | string[] | {
[metricName: string]: string | object;
}

View File

@ -1,74 +0,0 @@
// Server-side config
export const CLIENT_DIR = "client";
export const BUILD_DIR = "build";
export const IS_PRODUCTION = process.env.NODE_ENV === "production";
export const KUBERNETES_SERVICE_HOST = process.env.KUBERNETES_SERVICE_HOST || "kubernetes";
export const KUBERNETES_SERVICE_PORT = Number(process.env.KUBERNETES_SERVICE_PORT || 443);
export const KUBERNETES_SERVICE_URL = `https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}`;
export const config = {
IS_PRODUCTION: IS_PRODUCTION,
LENS_VERSION: process.env.LENS_VERSION,
LENS_THEME: process.env.LENS_THEME,
BUILD_VERSION: process.env.BUILD_VERSION,
API_PREFIX: {
BASE: '/api', // local express.js server api
TERMINAL: '/api-terminal', // terminal api
KUBE_BASE: '/api-kube', // kubernetes cluster api
KUBE_USERS: '/api-users', // users & groups api
KUBE_HELM: '/api-helm', // helm charts api middleware
KUBE_RESOURCE_APPLIER: "/api-resource",
},
// express.js port
LOCAL_SERVER_PORT: Number(process.env.LOCAL_SERVER_PORT || 8889),
WEBPACK_DEV_SERVER_PORT: Number(process.env.LOCAL_SERVER_PORT || 8080),
// session
SESSION_NAME: process.env.SESSION_NAME || "lens-s3ss10n",
SESSION_SECRET: process.env.SESSION_SECRET || "k0nt3n@-s3cr3t-key",
// kubernetes apis
KUBE_CLUSTER_NAME: process.env.KUBE_CLUSTER_NAME,
KUBE_CLUSTER_URL: process.env.KUBE_CLUSTER_URL || KUBERNETES_SERVICE_URL,
KUBE_USERS_URL: process.env.KUBE_USERS_URL || `http://localhost:9999`,
KUBE_TERMINAL_URL: process.env.KUBE_TERMINAL_URL || `http://localhost:9998`,
KUBE_HELM_URL: process.env.KUBE_HELM_URL || `http://localhost:9292`,
KUBE_RESOURCE_APPLIER_URL: process.env.KUBE_RESOURCE_APPLIER_URL || `http://localhost:9393`,
KUBE_METRICS_URL: process.env.KUBE_METRICS_URL || `http://localhost:9090`, // rbac-proxy-url
// flags define visibility of some ui-parts and pages in dashboard
USER_MANAGEMENT_ENABLED: JSON.parse(process.env.USER_MANAGEMENT_ENABLED || "false"),
CHARTS_ENABLED: JSON.parse(process.env.CHARTS_ENABLED || "false"),
// namespaces
LENS_NAMESPACE: process.env.LENS_NAMESPACE || "kontena-lens",
STATS_NAMESPACE: process.env.STATS_NAMESPACE || "kontena-stats",
SERVICE_ACCOUNT_TOKEN: process.env.SERVICE_ACCOUNT_TOKEN
|| null,
KUBERNETES_CA_CERT: process.env.KUBERNETES_CA_CERT,
KUBERNETES_CLIENT_CERT: process.env.KUBERNETES_CLIENT_CERT || "",
KUBERNETES_CLIENT_KEY: process.env.KUBERNETES_CLIENT_KEY || "",
KUBERNETES_TLS_SKIP: JSON.parse(process.env.KUBERNETES_TLS_SKIP || "false"),
KUBERNETES_NAMESPACE: process.env.KUBERNETES_NAMESPACE || "", // default allowed namespace
}
export function isSecure() {
return IS_PRODUCTION ? !config.KUBERNETES_TLS_SKIP : false;
}
export default config;
// Client-side process.env, must be provided by webpack.DefinePlugin
export const clientVars = {
BUILD_VERSION: config.BUILD_VERSION,
IS_PRODUCTION: config.IS_PRODUCTION,
API_PREFIX: config.API_PREFIX,
LOCAL_SERVER_PORT: config.LOCAL_SERVER_PORT,
}
export type IClientVars = typeof clientVars;

View File

@ -1,14 +0,0 @@
{
"extends": "../client/tsconfig.json",
"compilerOptions": {
"outDir": "../build",
"module": "commonjs",
"moduleResolution": "node",
"target": "esnext",
"sourceMap": false,
"esModuleInterop": true
},
"include": [
"./app.ts"
]
}

View File

@ -1,28 +0,0 @@
module.exports = {
transform: {
"^.+\\.tsx?$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json"
],
testPathIgnorePatterns: [
"/node_modules/"
],
moduleNameMapper: {
"\\.(scss)$": "identity-obj-proxy",
},
moduleDirectories: ["node_modules"],
setupFilesAfterEnv: ["./setup-tests.js"],
globals: {
"ts-jest": {
"tsConfig": "./test/tsconfig.json"
}
},
roots: [
"../client"
],
};

View File

@ -1,4 +0,0 @@
const Enzyme = require("enzyme");
const Adapter = require("enzyme-adapter-react-16");
Enzyme.configure({ adapter: new Adapter() });

View File

@ -1,8 +0,0 @@
{
"extends": "../client/tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"target": "es6",
}
}

View File

@ -1,125 +0,0 @@
// Get kubernetes services and port-forward them to pods at localhost
// To be used in development only
import * as yargs from "yargs"
import * as concurrently from "concurrently"
import chalk from "chalk";
import { find } from "lodash"
import { execSync } from "child_process"
import { Pod } from "../client/api/endpoints/pods.api";
import { Service } from "../client/api/endpoints/service.api";
import config from "../server/config";
var { LOCAL_SERVER_PORT, WEBPACK_DEV_SERVER_PORT, KUBE_TERMINAL_URL, KUBE_METRICS_URL } = config;
var terminalPort = +KUBE_TERMINAL_URL.match(/\d+$/)[0];
var metricsPort = +KUBE_METRICS_URL.match(/\d+$/)[0];
// Configure default options
var { namespaces, portOverride, skipServices, verbose } = yargs.options({
namespaces: {
alias: "n",
describe: "Namespaces to search Services & Pods. Example: --namespaces name1 name2 etc",
array: true,
default: [
"kontena-lens",
"kontena-stats",
],
},
verbose: {
describe: "Show extra logs output. Example: --verbose",
boolean: true,
},
skipServices: {
alias: "s",
describe: "Services to skip. Example: --skipServices myService otherName",
array: true,
default: [],
},
portOverride: {
alias: "o",
describe: "Override local ports. Example: --portOverride.serviceName 1000",
default: {
"dashboard": terminalPort, // terminal is running in dashboard pod's container
"rbac-proxy": metricsPort, // replace default "http" port
"prometheus": metricsPort + 1, // keep available metrics service for testing PromQL results
}
},
}).argv;
interface IServiceForward {
namespace: string;
serviceName: string;
podName: string;
port: number;
localPort?: number;
}
function getServices() {
var forwards: IServiceForward[] = [];
// Search Pod by Service.spec.selector for kubectl port-forward commands
namespaces.forEach(namespace => {
var pods = JSON.parse(execSync(`kubectl get pods -n ${namespace} -o json`).toString());
var services = JSON.parse(execSync(`kubectl get services -n ${namespace} -o json`).toString());
services.items.forEach((service: Service) => {
var serviceName = service.metadata.name;
var port = service.spec.ports && service.spec.ports[0].targetPort;
var podSelector = service.spec.selector;
var pod: Pod = find(pods.items, {
metadata: {
labels: podSelector
}
});
var podName = pod ? pod.metadata.name : null;
var localPort = portOverride[serviceName] || port;
var skipByName = skipServices.includes(serviceName);
var skipByPort = ["http", WEBPACK_DEV_SERVER_PORT, LOCAL_SERVER_PORT].includes(localPort);
if (skipByName || skipByPort || !podName) {
var getReason = () => {
if (skipByName) return "service is excluded in configuration"
if (skipByPort) return "local port already in use"
if (!podName) return `pod not found, selector: ${JSON.stringify(podSelector)}`
};
console.info(
chalk.yellow(
`Skip service: ${chalk.bold(`${namespace}/${serviceName}`)} (${getReason()})`,
`Ports (local/remote): ${chalk.bold(`${localPort}/${port}`)}`,
`Pod: ${chalk.bold(podName)}`
),
)
}
else {
forwards.push({
namespace, serviceName, podName,
port, localPort,
});
}
});
});
return forwards;
}
// Run
var services = getServices();
var commands = services.map(({ podName, localPort, port, namespace }: IServiceForward) => {
return `kubectl port-forward -n ${namespace} ${podName} ${localPort}:${port}`
});
services.forEach(({ serviceName, namespace, podName, port, localPort }, index) => {
console.log(
chalk.blueBright.bold(`[${index + 1}] Port-forward`),
`http://${serviceName}.${namespace}.svc.cluster.local -> http://localhost:${localPort}`,
`(Pod: ${chalk.bold(podName)})`,
);
});
if (verbose) {
console.log(
chalk.bold.grey('Commands:'),
chalk.grey(JSON.stringify(commands, null, 2)),
);
}
concurrently(commands, {
restartTries: 1000,
restartDelay: 1000 * 60,
}).catch(Function);

View File

@ -1,144 +0,0 @@
import * as path from "path";
import * as webpack from "webpack";
import * as HtmlWebpackPlugin from "html-webpack-plugin";
import * as MiniCssExtractPlugin from "mini-css-extract-plugin";
import * as TerserWebpackPlugin from "terser-webpack-plugin";
import { BUILD_DIR, CLIENT_DIR, clientVars, config } from "./server/config"
export default () => {
const { IS_PRODUCTION } = config;
const srcDir = path.resolve(process.cwd(), CLIENT_DIR);
const buildDir = path.resolve(process.cwd(), BUILD_DIR, CLIENT_DIR);
const tsConfigClientFile = path.resolve(srcDir, "tsconfig.json");
const sassCommonVarsFile = "./components/vars.scss"; // needs to be relative for Windows
return {
entry: {
app: path.resolve(srcDir, "components/app.tsx"),
},
output: {
path: buildDir,
publicPath: '/',
filename: '[name].js',
chunkFilename: 'chunks/[name].js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json']
},
mode: IS_PRODUCTION ? "production" : "development",
devtool: IS_PRODUCTION ? "" : "cheap-module-eval-source-map",
optimization: {
minimize: IS_PRODUCTION,
minimizer: [
...(!IS_PRODUCTION ? [] : [
new TerserWebpackPlugin({
cache: true,
parallel: true,
terserOptions: {
mangle: true,
compress: true,
keep_classnames: true,
keep_fnames: true,
},
extractComments: {
condition: "some",
banner: [
`Lens - The Kubernetes IDE. Copyright ${new Date().getFullYear()} by Lakend Labs, Inc. All rights reserved.`
].join("\n")
}
})
]),
],
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: [
"babel-loader",
{
loader: 'ts-loader',
options: {
configFile: tsConfigClientFile
}
}
]
},
{
test: /\.(jpg|png|svg|map|ico)$/,
use: 'file-loader?name=assets/[name]-[hash:6].[ext]'
},
{
test: /\.(ttf|eot|woff2?)$/,
use: 'file-loader?name=fonts/[name].[ext]'
},
{
test: /\.ya?ml$/,
use: "yml-loader"
},
{
test: /\.s?css$/,
use: [
IS_PRODUCTION ? MiniCssExtractPlugin.loader : {
loader: "style-loader",
options: {}
},
{
loader: "css-loader",
options: {
sourceMap: !IS_PRODUCTION
},
},
{
loader: "sass-loader",
options: {
sourceMap: !IS_PRODUCTION,
prependData: '@import "' + sassCommonVarsFile + '";',
sassOptions: {
includePaths: [srcDir]
},
}
},
]
}
]
},
plugins: [
...(IS_PRODUCTION ? [] : [
new webpack.HotModuleReplacementPlugin(),
]),
new webpack.DefinePlugin({
process: {
env: JSON.stringify(clientVars)
},
}),
// don't include all moment.js locales by default
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new HtmlWebpackPlugin({
template: 'index.html',
inject: true,
hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
}),
],
}
};

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ case "darwin":
break break
} }
export function setup() { export function setup(): Application {
return new Application({ return new Application({
// path to electron app // path to electron app
args: [], args: [],

View File

@ -36,7 +36,11 @@ describe("app start", () => {
beforeEach(async () => { beforeEach(async () => {
app = util.setup() app = util.setup()
await app.start() await app.start()
const windowCount = await app.client.getWindowCount() await app.client.waitUntilWindowLoaded()
let windowCount = await app.client.getWindowCount()
while (windowCount > 1) {
windowCount = await app.client.getWindowCount()
}
await app.client.windowByIndex(windowCount - 1) await app.client.windowByIndex(windowCount - 1)
await app.client.waitUntilWindowLoaded() await app.client.waitUntilWindowLoaded()
}, 20000) }, 20000)

2490
locales/en/messages.po Normal file

File diff suppressed because it is too large Load Diff

2473
locales/fi/messages.po Normal file

File diff suppressed because it is too large Load Diff

2498
locales/ru/messages.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,43 @@
{ {
"name": "kontena-lens", "name": "kontena-lens",
"productName": "Lens", "productName": "Lens",
"description": "Lens - The Kubernetes IDE",
"version": "3.6.0-dev",
"main": "out/main.js",
"copyright": "© 2020, Lakend Labs, Inc.",
"license": "MIT",
"author": { "author": {
"name": "Lakend Labs, Inc.", "name": "Lakend Labs, Inc.",
"email": "info@lakendlabs.com" "email": "info@lakendlabs.com"
}, },
"copyright": "© 2020, Lakend Labs, Inc.", "scripts": {
"license": "MIT", "dev": "concurrently -k \"yarn dev-run -C\" \"yarn dev:main\" \"yarn dev:renderer\"",
"description": "Lens - The Kubernetes IDE", "dev-run": "nodemon --watch out/main.* --exec \"electron --inspect .\" $@",
"version": "3.6.0-dev", "dev-test": "yarn test --watch",
"main": "main.ts", "dev:main": "env DEBUG=true yarn compile:main --watch $@",
"dev:renderer": "env DEBUG=true yarn compile:renderer --watch $@",
"dev:renderer:react": "yarn dev:renderer --config-name react $@",
"dev:renderer:vue": "yarn dev:renderer --config-name vue $@",
"compile": "concurrently \"yarn i18n:compile\" \"yarn compile:main -p\" \"yarn compile:renderer -p\"",
"compile:main": "webpack --progress --config webpack.main.ts",
"compile:renderer": "webpack --progress --config webpack.renderer.ts",
"compile:dll": "webpack --config webpack.dll.ts",
"build:linux": "yarn compile && electron-builder --linux --dir -c.productName=LensDev",
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=LensDev",
"build:win": "yarn compile && electron-builder --win --dir -c.productName=LensDev",
"test": "jest --env=jsdom src $@",
"integration": "jest --coverage integration $@",
"dist": "yarn compile && electron-builder -p onTag",
"dist:win": "yarn compile && electron-builder -p onTag --x64 --ia32",
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null",
"postinstall": "patch-package",
"i18n:extract": "lingui extract",
"i18n:compile": "lingui compile",
"download-bins": "concurrently yarn:download:*",
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts",
"lint": "eslint $@ --ext js,ts,tsx,vue --max-warnings=0 src/"
},
"config": { "config": {
"bundledKubectlVersion": "1.17.4", "bundledKubectlVersion": "1.17.4",
"bundledHelmVersion": "3.2.4" "bundledHelmVersion": "3.2.4"
@ -17,6 +45,32 @@
"engines": { "engines": {
"node": ">=12.0 <13.0" "node": ">=12.0 <13.0"
}, },
"lingui": {
"locales": [
"en",
"ru",
"fi"
],
"format": "po",
"sourceLocale": "en",
"fallbackLocale": "en",
"compileNamespace": "cjs",
"catalogs": [
{
"path": "./locales/{locale}/messages",
"include": "./src/renderer"
}
]
},
"jest": {
"testRegex": ".*_(spec|test)\\.[jt]sx?$",
"collectCoverage": false,
"verbose": true,
"testEnvironment": "node",
"transform": {
"^.+\\.tsx?$": "ts-jest"
}
},
"build": { "build": {
"afterSign": "build/notarize.js", "afterSign": "build/notarize.js",
"extraResources": [ "extraResources": [
@ -25,6 +79,11 @@
"to": "features/", "to": "features/",
"filter": "**/*" "filter": "**/*"
}, },
{
"from": "locales/",
"to": "locales/",
"filter": "**/*.js"
},
{ {
"from": "static/", "from": "static/",
"to": "static/", "to": "static/",
@ -95,138 +154,171 @@
"confinement": "classic" "confinement": "classic"
} }
}, },
"jest": {
"collectCoverage": true,
"testRegex": "spec/.*_(spec)\\.[jt]sx?$",
"verbose": true,
"testEnvironment": "node",
"transform": {
"^.+\\.tsx?$": "ts-jest"
}
},
"scripts": {
"dev": "concurrently -n app,dash \"yarn dev-electron\" \"yarn dev-dashboard\"",
"dev-dashboard": "cd dashboard && yarn dev",
"dev-electron": "electron-webpack dev",
"compile": "yarn download:bins && electron-webpack",
"build:linux": "yarn compile && electron-builder --linux --dir -c.productName=LensDev",
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=LensDev",
"build:win": "yarn compile && electron-builder --win --dir -c.productName=LensDev",
"dist": "yarn compile && electron-builder -p onTag",
"dist:win": "yarn compile && electron-builder -p onTag --x64 --ia32",
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null",
"lint": "eslint $@ --ext js,ts,vue --max-warnings=0 src/",
"lint-dashboard": "eslint $@ --ext js,ts,tsx --max-warnings=0 dashboard/client dashboard/server",
"postinstall": "patch-package",
"test": "node_modules/.bin/jest spec/src/",
"integration": "node_modules/.bin/jest spec/integration/",
"download:bins": "concurrently \"yarn download:kubectl\" \"yarn download:helm\"",
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
"download:helm": "yarn run ts-node build/download_helm.ts"
},
"dependencies": { "dependencies": {
"@hapi/call": "^6.0.1", "@hapi/call": "^8.0.0",
"@hapi/subtext": "^6.1.2", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "0.11.1", "@kubernetes/client-node": "^0.12.0",
"@types/cookie": "^0.3.3", "@types/crypto-js": "^3.1.47",
"@types/fs-extra": "^8.0.0", "@types/electron-window-state": "^2.0.34",
"@types/fs-extra": "^9.0.1",
"@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.4",
"@types/jsonpath": "^0.2.0",
"@types/lodash": "^4.14.155",
"@types/marked": "^0.7.4",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.12.45",
"@types/proper-lockfile": "^4.1.1", "@types/proper-lockfile": "^4.1.1",
"@types/tar": "^4.0.3", "@types/tar": "^4.0.3",
"camelcase-keys": "^6.1.1", "crypto-js": "^4.0.0",
"cookie": "^0.4.0", "electron-promise-ipc": "^2.1.0",
"electron-promise-ipc": "^1.3.0", "electron-store": "^5.2.0",
"electron-store": "^5.0.0", "electron-updater": "^4.3.1",
"electron-updater": "^4.1.2",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"filenamify": "^4.1.0", "filenamify": "^4.1.0",
"handlebars": "4.1.2", "fs-extra": "^9.0.1",
"http-proxy": "^1.18.0", "handlebars": "^4.7.6",
"http-proxy-middleware": "^0.19.2", "http-proxy": "^1.18.1",
"https-proxy-agent": "^3.0.1", "js-yaml": "^3.14.0",
"jsonwebtoken": "^8.5.1", "jsonpath": "^1.0.2",
"lodash": "^4.17.15",
"mac-ca": "^1.0.4", "mac-ca": "^1.0.4",
"md5-file": "^4.0.0", "marked": "^1.1.0",
"md5-file": "^5.0.0",
"mock-fs": "^4.12.0",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"on-change": "^1.6.2", "openid-client": "^3.15.2",
"proper-lockfile": "^4.1.1", "proper-lockfile": "^4.1.1",
"request": "^2.88.0", "request": "^2.88.2",
"request-promise-native": "^ 1.0.7", "request-promise-native": "^1.0.8",
"semver": "^6.3.0", "semver": "^7.3.2",
"shell-env": "^3.0.0", "shell-env": "^3.0.0",
"shelljs": "^0.8.3", "tar": "^6.0.2",
"source-map-support": "^0.5.13",
"ssl-root-cas": "^1.3.1",
"tar": "^5.0.5",
"tcp-port-used": "^1.0.1", "tcp-port-used": "^1.0.1",
"tempy": "0.3.0", "tempy": "^0.5.0",
"universal-analytics": "^0.4.20", "universal-analytics": "^0.4.20",
"uuid": "^3.3.3", "uuid": "^8.1.0",
"v-clipboard": "^2.2.2", "win-ca": "^3.2.0",
"vuex": "^3.1.1",
"win-ca": "^3.1.1",
"winston": "^3.2.1", "winston": "^3.2.1",
"ws": "^7.1.2" "ws": "^7.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/ejs": "^2.6.3", "@babel/core": "^7.10.2",
"@types/electron-window-state": "^2.0.31", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.10.2",
"@babel/preset-react": "^7.10.1",
"@babel/preset-typescript": "^7.10.1",
"@lingui/babel-preset-react": "^2.9.1",
"@lingui/cli": "^3.0.0-13",
"@lingui/loader": "^3.0.0-13",
"@lingui/macro": "^3.0.0-13",
"@lingui/react": "^3.0.0-13",
"@material-ui/core": "^4.10.1",
"@types/chart.js": "^2.9.21",
"@types/circular-dependency-plugin": "^5.0.1",
"@types/color": "^3.0.1",
"@types/dompurify": "^2.0.2",
"@types/hapi": "^18.0.3", "@types/hapi": "^18.0.3",
"@types/http-proxy": "^1.17.0", "@types/hoist-non-react-statics": "^3.3.1",
"@types/jest": "^24.0.18", "@types/html-webpack-plugin": "^3.2.3",
"@types/jsonwebtoken": "^8.3.5", "@types/jest": "^25.2.3",
"@types/md5-file": "^4.0.0", "@types/material-ui": "^0.21.7",
"@types/mock-fs": "^4.10.0", "@types/md5-file": "^4.0.2",
"@types/node": "^12.7.2", "@types/mini-css-extract-plugin": "^0.9.1",
"@types/request": "2.47.0", "@types/react": "^16.9.35",
"@types/request-promise-native": "1.0.16", "@types/react-dom": "^16.9.8",
"@types/semver": "5.5.0", "@types/react-router-dom": "^5.1.5",
"@types/shelljs": "^0.8.5", "@types/react-select": "^3.0.13",
"@types/react-window": "^1.8.2",
"@types/request": "^2.48.5",
"@types/request-promise-native": "^1.0.17",
"@types/semver": "^7.2.0",
"@types/shelljs": "^0.8.8",
"@types/tcp-port-used": "^1.0.0", "@types/tcp-port-used": "^1.0.0",
"@types/tempy": "0.1.0", "@types/tempy": "^0.3.0",
"@types/universal-analytics": "^0.4.3", "@types/terser-webpack-plugin": "^3.0.0",
"@types/uuid": "^3.4.5", "@types/universal-analytics": "^0.4.4",
"@types/uuid": "^8.0.0",
"@types/webdriverio": "^4.13.0", "@types/webdriverio": "^4.13.0",
"@typescript-eslint/eslint-plugin": "^2.7.0", "@types/webpack": "^4.41.17",
"@typescript-eslint/parser": "^2.7.0", "@types/webpack-env": "^1.15.2",
"bootstrap": "^4.3.1", "@types/webpack-node-externals": "^1.7.1",
"bootstrap-vue": "^2.0.0-rc.28", "@typescript-eslint/eslint-plugin": "^3.4.0",
"concurrently": "^5.1.0", "@typescript-eslint/parser": "^3.4.0",
"css-loader": "^3.2.0", "ace-builds": "^1.4.11",
"electron": "6.1.10", "ansi_up": "^4.0.4",
"electron-builder": "^22.4.0", "babel-core": "^7.0.0-beta.3",
"electron-notarize": "^0.2.1", "babel-loader": "^8.1.0",
"electron-webpack": "^2.7.4", "babel-plugin-macros": "^2.8.0",
"electron-webpack-ts": "^3.2.0", "babel-runtime": "^6.26.0",
"eslint": "^6.3.0", "bootstrap": "^4.5.0",
"eslint-plugin-vue": "^5.2.3", "bootstrap-vue": "^2.15.0",
"chart.js": "^2.9.3",
"circular-dependency-plugin": "^5.2.0",
"color": "^3.1.2",
"concurrently": "^5.2.0",
"css-element-queries": "^1.2.3",
"css-loader": "^3.5.3",
"dompurify": "^2.0.11",
"electron": "^6.1.12",
"electron-builder": "^22.7.0",
"electron-notarize": "^0.3.0",
"eslint": "^7.3.1",
"eslint-plugin-vue": "^6.2.2",
"file-loader": "^6.0.0",
"flex.box": "^3.4.4",
"fork-ts-checker-webpack-plugin": "^5.0.0",
"hashicon": "^0.3.0", "hashicon": "^0.3.0",
"jest": "^24.9.0", "hoist-non-react-statics": "^3.3.2",
"js-yaml": "^3.13.1", "html-webpack-plugin": "^4.3.0",
"less": "^3.10.3", "identity-obj-proxy": "^3.0.0",
"less-loader": "^5.0.0", "include-media": "^1.4.9",
"marked": "^0.7.0", "jest": "^26.0.1",
"make-plural": "^6.2.1",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"mock-fs": "^4.10.3", "mini-css-extract-plugin": "^0.9.0",
"mobx": "^5.15.4",
"mobx-observable-history": "^1.0.3",
"mobx-react": "^6.2.2",
"moment": "^2.26.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-sass": "^4.12.0", "node-sass": "^4.14.1",
"patch-package": "^6.2.0", "nodemon": "^2.0.4",
"postinstall-postinstall": "^2.0.0", "patch-package": "^6.2.2",
"prismjs": "^1.17.1", "path-to-regexp": "^6.1.0",
"sass-loader": "^8.0.0", "postinstall-postinstall": "^2.1.0",
"prismjs": "^1.20.0",
"raw-loader": "^4.0.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-select": "^3.1.0",
"react-window": "^1.8.5",
"sass-loader": "^8.0.2",
"spectron": "^8.0.0", "spectron": "^8.0.0",
"ts-jest": "^24.1.0", "style-loader": "^1.2.1",
"ts-loader": "^6.0.4", "terser-webpack-plugin": "^3.0.3",
"ts-node": "^8.4.1", "ts-jest": "^26.1.0",
"ts-loader": "^7.0.5",
"ts-node": "^8.10.2",
"typeface-roboto": "^0.0.75", "typeface-roboto": "^0.0.75",
"typescript": "^3.7.0", "typescript": "^3.9.5",
"vue": "^2.6.10", "url-loader": "^4.1.0",
"vue": "^2.6.11",
"vue-electron": "^1.0.6", "vue-electron": "^1.0.6",
"vue-loader": "^15.7.1", "vue-loader": "^15.9.2",
"vue-prism-editor": "^0.3.0", "vue-prism-editor": "^0.6.1",
"vue-router": "^3.1.2", "vue-router": "^3.3.2",
"vue-template-compiler": "^2.6.10", "vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"vuedraggable": "^2.23.2", "vuedraggable": "^2.23.2",
"webpack": "~4.35.3" "vuex": "^3.4.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-node-externals": "^1.7.2",
"xterm": "^4.6.0",
"xterm-addon-fit": "^0.4.0"
} }
} }

View File

@ -1,17 +0,0 @@
jest.mock("electron")
jest.mock("../../../src/common/user-store")
import { Kubectl, bundledKubectl } from "../../../src/main/kubectl"
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 kubectl = new Kubectl("1.17.0")
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
})
})

View File

@ -1,32 +0,0 @@
import { EventEmitter } from 'events'
class MockServer extends EventEmitter {
listen = jest.fn((obj) => {
this.emit('listening', {})
return this
})
address = () => { return { port: 12345 }}
unref = jest.fn()
close = jest.fn((cb) => {
cb()
})
}
// eslint-disable-next-line @typescript-eslint/no-var-requires
const net = require("net")
jest.mock("net")
import * as port from "../../../src/main/port"
describe("getFreePort", () => {
beforeEach(() => {
net.createServer.mockReturnValue(new MockServer)
})
afterEach(() => {
jest.clearAllMocks()
})
it("finds the next free port", async () => {
return expect(port.getFreePort()).resolves.toEqual(expect.any(Number))
})
})

View File

@ -1,9 +0,0 @@
const userStore = {
getPreferences: jest.fn(() => {
return {
downloadMirror: "default"
}
})
}
export { userStore };

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;
}

View File

@ -1,12 +1,12 @@
import * as ElectronStore from "electron-store" import ElectronStore from "electron-store"
import { Cluster, ClusterBaseInfo } from "../main/cluster"; 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 version200Beta2 from "./migrations/cluster-store/2.0.0-beta.2" import * as version241 from "../migrations/cluster-store/2.4.1"
import * as version241 from "./migrations/cluster-store/2.4.1" import * as version260Beta2 from "../migrations/cluster-store/2.6.0-beta.2"
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 version260Beta3 from "./migrations/cluster-store/2.6.0-beta.3" import * as version270Beta0 from "../migrations/cluster-store/2.7.0-beta.0"
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 version270Beta1 from "./migrations/cluster-store/2.7.0-beta.1" import { getAppVersion } from "./utils/app-version";
export class ClusterStore { export class ClusterStore {
private static instance: ClusterStore; private static instance: ClusterStore;
@ -14,8 +14,10 @@ export class ClusterStore {
private constructor() { private constructor() {
this.store = new ElectronStore({ this.store = new ElectronStore({
name: "lens-cluster-store", // @ts-ignore
// fixme: tests are failed without "projectVersion"
projectVersion: getAppVersion(), projectVersion: getAppVersion(),
name: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: { migrations: {
"2.0.0-beta.2": version200Beta2.migration, "2.0.0-beta.2": version200Beta2.migration,
@ -58,7 +60,9 @@ export class ClusterStore {
public getCluster(id: string): Cluster { public getCluster(id: string): Cluster {
const cluster = this.getAllClusterObjects().find((cluster) => cluster.id === id) const cluster = this.getAllClusterObjects().find((cluster) => cluster.id === id)
if (cluster) { return cluster} if (cluster) {
return cluster
}
return null return null
} }
@ -74,7 +78,8 @@ export class ClusterStore {
} }
if (index === -1) { if (index === -1) {
clusters.push(storable) clusters.push(storable)
} else { }
else {
clusters[index] = storable clusters[index] = storable
} }
this.store.set("clusters", clusters) this.store.set("clusters", clusters)
@ -97,7 +102,7 @@ export class ClusterStore {
} }
static getInstance(): ClusterStore { static getInstance(): ClusterStore {
if(!ClusterStore.instance) { if (!ClusterStore.instance) {
ClusterStore.instance = new ClusterStore(); ClusterStore.instance = new ClusterStore();
} }
return ClusterStore.instance; return ClusterStore.instance;
@ -108,6 +113,4 @@ export class ClusterStore {
} }
} }
const clusterStore: ClusterStore = ClusterStore.getInstance(); export const clusterStore = ClusterStore.getInstance();
export { clusterStore };

View File

@ -1,22 +1,11 @@
import * as mockFs from "mock-fs" import mockFs from "mock-fs"
import * as yaml from "js-yaml" import yaml from "js-yaml"
import { ClusterStore } from "./cluster-store";
jest.mock("electron", () => { import { Cluster } from "../main/cluster";
return {
app: {
getVersion: () => '99.99.99',
getPath: () => 'tmp',
getLocale: () => 'en'
}
}
})
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234 // Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
console.log(""); console.log("");
import { ClusterStore } from "../../../src/common/cluster-store"
import { Cluster } from "../../../src/main/cluster"
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance() ClusterStore.resetInstance()

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);
}

View File

@ -1,5 +1,5 @@
import * as request from "request" import request from "request"
import { userStore } from "../common/user-store" import { userStore } from "./user-store"
export function globalRequestOpts(requestOpts: request.Options ) { export function globalRequestOpts(requestOpts: request.Options ) {
const userPrefs = userStore.getPreferences() const userPrefs = userStore.getPreferences()

View File

@ -1,6 +1,14 @@
import * as winca from "win-ca/api" import { isMac, isWindows } from "./vars";
import "mac-ca" 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 winca.inject("+") // see: https://github.com/ukoloff/win-ca#caveats
} }

View File

@ -1,6 +1,6 @@
import { machineIdSync } from 'node-machine-id' import ua from "universal-analytics"
import { userStore } from "../common/user-store" import { machineIdSync } from "node-machine-id"
import * as ua from "universal-analytics" import { userStore } from "./user-store"
const GA_ID = "UA-159377374-1" const GA_ID = "UA-159377374-1"

View File

@ -1,6 +1,6 @@
import * as ElectronStore from "electron-store" import ElectronStore from "electron-store"
import * as appUtil from "./app-utils" import * as version210Beta4 from "../migrations/user-store/2.1.0-beta.4"
import * as version210Beta4 from "./migrations/user-store/2.1.0-beta.4" import { getAppVersion } from "./utils/app-version";
export interface User { export interface User {
id?: string; id?: string;
@ -20,7 +20,9 @@ export class UserStore {
private constructor() { private constructor() {
this.store = new ElectronStore({ this.store = new ElectronStore({
projectVersion: appUtil.getAppVersion(), // @ts-ignore
// fixme: tests are failed without "projectVersion"
projectVersion: getAppVersion(),
migrations: { migrations: {
"2.1.0-beta.4": version210Beta4.migration, "2.1.0-beta.4": version210Beta4.migration,
} }
@ -68,7 +70,7 @@ export class UserStore {
} }
static getInstance(): UserStore { static getInstance(): UserStore {
if(!UserStore.instance) { if (!UserStore.instance) {
UserStore.instance = new UserStore(); UserStore.instance = new UserStore();
} }
return UserStore.instance; return UserStore.instance;

View File

@ -1,21 +1,9 @@
import * as mockFs from "mock-fs" import mockFs from "mock-fs"
import * as yaml from "js-yaml" import { userStore, UserStore } from "./user-store"
jest.mock("electron", () => {
return {
app: {
getVersion: () => '99.99.99',
getPath: () => 'tmp',
getLocale: () => 'en'
}
}
})
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234 // Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
console.log(""); console.log("");
import { userStore, User, UserPreferences, UserStore } from "../../../src/common/user-store"
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEach(() => { beforeEach(() => {
UserStore.resetInstance() UserStore.resetInstance()

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;
}

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;
}
}

View File

@ -0,0 +1,5 @@
// Common utils (main/renderer)
export * from "./base64"
export * from "./camelCase"
export * from "./splitArray"

View File

@ -1,3 +1,4 @@
// Moved from dashboard/client/utils/arrays.ts
/** /**
* This function splits an array into two sub arrays on the first instance of * 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 * element (from the left). If the array does not contain the element. The
@ -9,11 +10,10 @@
* @returns the left and right sub-arrays which when conjoined with `element` * @returns the left and right sub-arrays which when conjoined with `element`
* is the same as `array`, and `true` * is the same as `array`, and `true`
*/ */
export function split<T>(array: Array<T>, element: T): [Array<T>, Array<T>, boolean] { export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
const index = array.indexOf(element); const index = array.indexOf(element);
if (index < 0) { if (index < 0) {
return [array, [], false]; return [array, [], false];
} }
return [array.slice(0, index), array.slice(index + 1, array.length), true]
return [array.slice(0, index), array.slice(index+1, array.length), true]
} }

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
src/common/vars.ts Normal file
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"

View File

@ -1,4 +1,4 @@
import * as ElectronStore from "electron-store" import ElectronStore from "electron-store"
import { clusterStore } from "./cluster-store" import { clusterStore } from "./cluster-store"
export interface WorkspaceData { export interface WorkspaceData {

View File

@ -1,6 +1,6 @@
import { Feature, FeatureStatus } from "../main/feature" import { Feature, FeatureStatus } from "../main/feature"
import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node" import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node"
import * as semver from "semver" import semver from "semver"
import { Cluster } from "../main/cluster"; import { Cluster } from "../main/cluster";
import * as k8s from "@kubernetes/client-node" import * as k8s from "@kubernetes/client-node"

View File

@ -1,19 +1,18 @@
import { KubeConfig } from "@kubernetes/client-node" import { KubeConfig } from "@kubernetes/client-node"
import { PromiseIpc } from "electron-promise-ipc" import { PromiseIpc } from "electron-promise-ipc"
import * as http from "http" import http from "http"
import { Cluster, ClusterBaseInfo } from "./cluster" import { Cluster, ClusterBaseInfo } from "./cluster"
import { clusterStore } from "../common/cluster-store" import { clusterStore } from "../common/cluster-store"
import * as k8s from "./k8s" import * as k8s from "./k8s"
import logger from "./logger" import logger from "./logger"
import { LensProxy } from "./proxy" import { LensProxy } from "./proxy"
import { app } from "electron" import { app } from "electron"
import * as path from "path" import path from "path"
import { promises } from "fs" import { promises } from "fs"
import { ensureDir } from "fs-extra" import { ensureDir } from "fs-extra"
import * as filenamify from "filenamify" import filenamify from "filenamify"
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import { apiPrefix } from "../common/vars";
declare const __static: string;
export type FeatureInstallRequest = { export type FeatureInstallRequest = {
name: string; name: string;
@ -92,7 +91,7 @@ export class ClusterManager {
reject("No cluster contexts defined") reject("No cluster contexts defined")
} }
configs.forEach(c => { configs.forEach(c => {
k8s.valideConfig(c) k8s.validateConfig(c)
const cluster = new Cluster({ const cluster = new Cluster({
id: uuid(), id: uuid(),
port: this.port, port: this.port,
@ -117,15 +116,15 @@ export class ClusterManager {
logger.debug(`IPC: addCluster`) logger.debug(`IPC: addCluster`)
const cluster = await this.addNewCluster(clusterData) const cluster = await this.addNewCluster(clusterData)
return { return {
addedCluster: this.clusterResponse(cluster), addedCluster: cluster.toClusterInfo(),
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => this.clusterResponse(cluster)) allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => cluster.toClusterInfo())
} }
}); });
this.promiseIpc.on("getClusters", async (workspaceId: string) => { this.promiseIpc.on("getClusters", async (workspaceId: string) => {
logger.debug(`IPC: getClusters, workspace ${workspaceId}`) logger.debug(`IPC: getClusters, workspace ${workspaceId}`)
const workspaceClusters = Array.from(this.getClusters()).filter((cluster) => cluster.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) => { this.promiseIpc.on("getCluster", async (id: string) => {
@ -133,7 +132,7 @@ export class ClusterManager {
const cluster = this.getCluster(id) const cluster = this.getCluster(id)
if (cluster) { if (cluster) {
await cluster.refreshCluster() await cluster.refreshCluster()
return this.clusterResponse(cluster) return cluster.toClusterInfo()
} else { } else {
return null return null
} }
@ -181,7 +180,7 @@ export class ClusterManager {
if(!cluster.preferences) cluster.preferences = {}; if(!cluster.preferences) cluster.preferences = {};
cluster.preferences.icon = clusterIcon cluster.preferences.icon = clusterIcon
clusterStore.storeCluster(cluster); clusterStore.storeCluster(cluster);
return {success: true, cluster: this.clusterResponse(cluster), message: ""} return {success: true, cluster: cluster.toClusterInfo(), message: ""}
} catch(error) { } catch(error) {
return {success: false, message: error} return {success: false, message: error}
} }
@ -193,7 +192,7 @@ export class ClusterManager {
if (cluster && cluster.preferences) { if (cluster && cluster.preferences) {
cluster.preferences.icon = null; cluster.preferences.icon = null;
clusterStore.storeCluster(cluster) clusterStore.storeCluster(cluster)
return {success: true, cluster: this.clusterResponse(cluster), message: ""} return {success: true, cluster: cluster.toClusterInfo(), message: ""}
} else { } else {
return {success: false, message: "Cluster not found"} return {success: false, message: "Cluster not found"}
} }
@ -202,7 +201,7 @@ export class ClusterManager {
this.promiseIpc.on("refreshCluster", async (clusterId: string) => { this.promiseIpc.on("refreshCluster", async (clusterId: string) => {
const cluster = this.clusters.get(clusterId) const cluster = this.clusters.get(clusterId)
await cluster.refreshCluster() await cluster.refreshCluster()
return this.clusterResponse(cluster) return cluster.toClusterInfo()
}); });
this.promiseIpc.on("stopCluster", (clusterId: string) => { this.promiseIpc.on("stopCluster", (clusterId: string) => {
@ -217,7 +216,7 @@ export class ClusterManager {
this.promiseIpc.on("removeCluster", (ctx: string) => { this.promiseIpc.on("removeCluster", (ctx: string) => {
logger.debug(`IPC: removeCluster: ${ctx}`) 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) => { this.promiseIpc.on("clusterStored", (clusterId: string) => {
@ -263,7 +262,7 @@ export class ClusterManager {
cluster = this.clusters.get(clusterId) cluster = this.clusters.get(clusterId)
if (cluster) { if (cluster) {
// we need to swap path prefix so that request is proxied to kube api // 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 { } else {
@ -274,11 +273,6 @@ export class ClusterManager {
return cluster; return cluster;
} }
// TODO: remove this
protected clusterResponse(cluster: Cluster) {
return cluster.toClusterInfo()
}
protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise<string> { protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise<string> {
await ensureDir(ClusterManager.clusterIconDir) await ensureDir(ClusterManager.clusterIconDir)
fileName = filenamify(cluster.contextName + "-" + fileName) fileName = filenamify(cluster.contextName + "-" + fileName)

View File

@ -3,12 +3,13 @@ import { FeatureStatusMap } from "./feature"
import * as k8s from "./k8s" import * as k8s from "./k8s"
import { clusterStore } from "../common/cluster-store" import { clusterStore } from "../common/cluster-store"
import logger from "./logger" 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 * as fm from "./feature-manager";
import { Kubectl } from "./kubectl"; import { Kubectl } from "./kubectl";
import { PromiseIpc } from "electron-promise-ipc" 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 { KubeconfigManager } from "./kubeconfig-manager"
import { apiPrefix } from "../common/vars";
enum ClusterStatus { enum ClusterStatus {
AccessGranted = 2, AccessGranted = 2,
@ -123,16 +124,9 @@ export class Cluster implements ClusterInfo {
this.contextHandler.setClusterPreferences(this.preferences) this.contextHandler.setClusterPreferences(this.preferences)
const connectionStatus = await this.getConnectionStatus() const connectionStatus = await this.getConnectionStatus()
if (connectionStatus == ClusterStatus.AccessGranted) { this.accessible = connectionStatus == ClusterStatus.AccessGranted;
this.accessible = true this.online = connectionStatus > ClusterStatus.Offline;
} else {
this.accessible = false
}
if (connectionStatus > ClusterStatus.Offline) {
this.online = true
} else {
this.online = false
}
if (this.accessible) { if (this.accessible) {
this.distribution = this.detectKubernetesDistribution(this.version) this.distribution = this.detectKubernetesDistribution(this.version)
this.features = await fm.getFeatures(this.contextHandler) this.features = await fm.getFeatures(this.contextHandler)
@ -146,7 +140,9 @@ export class Cluster implements ClusterInfo {
public updateKubeconfig(kubeconfig: string) { public updateKubeconfig(kubeconfig: string) {
const storedCluster = clusterStore.getCluster(this.id) const storedCluster = clusterStore.getCluster(this.id)
if (!storedCluster) { return } if (!storedCluster) {
return
}
this.kubeConfig = kubeconfig this.kubeConfig = kubeconfig
this.save() this.save()
@ -164,7 +160,7 @@ export class Cluster implements ClusterInfo {
} }
public toClusterInfo(): ClusterInfo { public toClusterInfo(): ClusterInfo {
const clusterInfo: ClusterInfo = { return {
id: this.id, id: this.id,
workspace: this.workspace, workspace: this.workspace,
url: this.url, url: this.url,
@ -179,20 +175,19 @@ export class Cluster implements ClusterInfo {
isAdmin: this.isAdmin, isAdmin: this.isAdmin,
features: this.features, features: this.features,
kubeCtl: this.kubeCtl, kubeCtl: this.kubeCtl,
kubeConfig: this.kubeConfig, kubeConfig: this.kubeConfig,
preferences: this.preferences preferences: this.preferences
} }
return clusterInfo;
} }
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) { protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
const options = Object.assign({ const options = Object.assign({
json: true, timeout: 10000 json: true,
timeout: 10000
}, (opts || {})) }, (opts || {}))
if (!options.headers) { options.headers = {} } if (!options.headers) { options.headers = {} }
options.headers.host = `${this.id}.localhost:${this.port}` options.headers.host = `${this.id}.localhost:${this.port}`
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
return request(`http://127.0.0.1:${this.port}/api-kube${path}`, options)
} }
protected async getConnectionStatus() { protected async getConnectionStatus() {
@ -201,21 +196,24 @@ export class Cluster implements ClusterInfo {
this.version = response.gitVersion this.version = response.gitVersion
this.failureReason = null this.failureReason = null
return ClusterStatus.AccessGranted; return ClusterStatus.AccessGranted;
} catch(error) { } catch (error) {
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`) logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`)
if (error.statusCode) { if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) { if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials"; this.failureReason = "Invalid credentials";
return ClusterStatus.AccessDenied; return ClusterStatus.AccessDenied;
} else { }
else {
this.failureReason = error.error || error.message; this.failureReason = error.error || error.message;
return ClusterStatus.Offline; return ClusterStatus.Offline;
} }
} else if (error.failed === true) { }
else if (error.failed === true) {
if (error.timedOut === true) { if (error.timedOut === true) {
this.failureReason = "Connection timed out"; this.failureReason = "Connection timed out";
return ClusterStatus.Offline; return ClusterStatus.Offline;
} else { }
else {
this.failureReason = "Failed to fetch credentials"; this.failureReason = "Failed to fetch credentials";
return ClusterStatus.AccessDenied; return ClusterStatus.AccessDenied;
} }
@ -234,7 +232,7 @@ export class Cluster implements ClusterInfo {
spec: { resourceAttributes } spec: { resourceAttributes }
}) })
return accessReview.body.status.allowed === true return accessReview.body.status.allowed === true
} catch(error) { } catch (error) {
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`) logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
return false return false
} }
@ -258,10 +256,10 @@ export class Cluster implements ClusterInfo {
else if (kubernetesVersion.includes("IKS")) { else if (kubernetesVersion.includes("IKS")) {
return "iks" return "iks"
} }
else if(this.apiUrl.endsWith("azmk8s.io")) { else if (this.apiUrl.endsWith("azmk8s.io")) {
return "aks" return "aks"
} }
else if(this.apiUrl.endsWith("k8s.ondigitalocean.com")) { else if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
return "digitalocean" return "digitalocean"
} }
else if (this.contextHandler.contextName.startsWith("minikube")) { else if (this.contextHandler.contextName.startsWith("minikube")) {
@ -274,11 +272,11 @@ export class Cluster implements ClusterInfo {
return "vanilla" return "vanilla"
} }
protected async getNodeCount() { protected async getNodeCount() {
try { try {
const response = await this.k8sRequest("/api/v1/nodes") const response = await this.k8sRequest("/api/v1/nodes")
return response.items.length return response.items.length
} catch(error) { } catch (error) {
logger.debug(`failed to request node list: ${error.message}`) logger.debug(`failed to request node list: ${error.message}`)
return null return null
} }
@ -294,18 +292,17 @@ export class Cluster implements ClusterInfo {
const uniqEventSources = new Set(); const uniqEventSources = new Set();
const warnings = response.body.items.filter(e => e.type !== 'Normal'); const warnings = response.body.items.filter(e => e.type !== 'Normal');
for (const w of warnings) { for (const w of warnings) {
if(w.involvedObject.kind === 'Pod') { if (w.involvedObject.kind === 'Pod') {
try { try {
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body; const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`) logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
if(k8s.podHasIssues(pod)) { if (k8s.podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid); uniqEventSources.add(w.involvedObject.uid);
} }
continue; } catch (err) {
} catch (error) {
continue;
} }
} else { }
else {
uniqEventSources.add(w.involvedObject.uid); uniqEventSources.add(w.involvedObject.uid);
} }
} }

View File

@ -1,6 +1,5 @@
import { KubeConfig, CoreV1Api } from "@kubernetes/client-node" import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"
import { readFileSync } from "fs" import http from "http"
import * as http from "http"
import { ServerOptions } from "http-proxy" import { ServerOptions } from "http-proxy"
import * as url from "url" import * as url from "url"
import logger from "./logger" import logger from "./logger"
@ -8,8 +7,7 @@ import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy" import { KubeAuthProxy } from "./kube-auth-proxy"
import { Cluster, ClusterPreferences } from "./cluster" import { Cluster, ClusterPreferences } from "./cluster"
import { prometheusProviders } from "../common/prometheus-providers" import { prometheusProviders } from "../common/prometheus-providers"
import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry" import { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
import { PrometheusLens } from "./prometheus/lens"
export class ContextHandler { export class ContextHandler {
public contextName: string public contextName: string
@ -76,19 +74,21 @@ export class ContextHandler {
if (clusterPreferences && clusterPreferences.prometheus) { if (clusterPreferences && clusterPreferences.prometheus) {
const prom = clusterPreferences.prometheus const prom = clusterPreferences.prometheus
this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}` this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}`
} else { }
else {
this.prometheusPath = null this.prometheusPath = null
} }
if(clusterPreferences && clusterPreferences.clusterName) { if (clusterPreferences && clusterPreferences.clusterName) {
this.clusterName = clusterPreferences.clusterName; this.clusterName = clusterPreferences.clusterName;
} else { }
else {
this.clusterName = this.contextName; this.clusterName = this.contextName;
} }
} }
protected async resolvePrometheusPath(): Promise<string> { protected async resolvePrometheusPath(): Promise<string> {
const service = await this.getPrometheusService() const {service, namespace, port} = await this.getPrometheusService()
return `${service.namespace}/services/${service.service}:${service.port}` return `${namespace}/services/${service}:${port}`
} }
public async getPrometheusProvider() { public async getPrometheusProvider() {
@ -110,7 +110,8 @@ export class ContextHandler {
const service = resolvedPrometheusServices.filter(n => n)[0] const service = resolvedPrometheusServices.filter(n => n)[0]
if (service) { if (service) {
return service return service
} else { }
else {
return { return {
id: "lens", id: "lens",
namespace: "lens-metrics", namespace: "lens-metrics",
@ -128,7 +129,7 @@ export class ContextHandler {
return this.prometheusPath return this.prometheusPath
} }
public async getApiTarget(isWatchRequest = false) { public async getApiTarget(isWatchRequest = false): Promise<ServerOptions> {
if (this.apiTarget && !isWatchRequest) { if (this.apiTarget && !isWatchRequest) {
return this.apiTarget return this.apiTarget
} }
@ -140,7 +141,7 @@ export class ContextHandler {
return apiTarget return apiTarget
} }
protected async newApiTarget(timeout: number) { protected async newApiTarget(timeout: number): Promise<ServerOptions> {
return { return {
changeOrigin: true, changeOrigin: true,
timeout: timeout, timeout: timeout,
@ -162,7 +163,7 @@ export class ContextHandler {
let serverPort: number = null let serverPort: number = null
try { try {
serverPort = await getFreePort() serverPort = await getFreePort()
} catch(error) { } catch (error) {
logger.error(error) logger.error(error)
throw(error) throw(error)
} }
@ -178,7 +179,7 @@ export class ContextHandler {
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) { public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
try { try {
await callback(this.cluster.kubeconfigPath()) await callback(this.cluster.kubeconfigPath())
} catch(error) { } catch (error) {
throw(error) throw(error)
} }
} }
@ -187,7 +188,7 @@ export class ContextHandler {
if (!this.proxyServer) { if (!this.proxyServer) {
const proxyPort = await this.resolveProxyPort() const proxyPort = await this.resolveProxyPort()
const proxyEnv = Object.assign({}, process.env) 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 proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
} }
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv) this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv)
@ -203,8 +204,6 @@ export class ContextHandler {
} }
public proxyServerError() { public proxyServerError() {
if (!this.proxyServer) { return null } return this.proxyServer?.lastError || ""
return this.proxyServer.lastError
} }
} }

View File

@ -1,5 +1,5 @@
import * as fs from "fs"; import fs from "fs";
import * as path from "path" import path from "path"
import * as hb from "handlebars" import * as hb from "handlebars"
import { ResourceApplier } from "./resource-applier" import { ResourceApplier } from "./resource-applier"
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node" import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node"
@ -99,6 +99,10 @@ export abstract class Feature {
} }
protected manifestPath() { 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);
} }
} }

View File

@ -1,4 +1,4 @@
import * as fs from "fs" import fs from "fs"
export function ensureDir(dirname: string) { export function ensureDir(dirname: string) {
if (!fs.existsSync(dirname)) { if (!fs.existsSync(dirname)) {

View File

@ -1,4 +1,4 @@
import * as fs from "fs"; import fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager" import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
import logger from "./logger"; import logger from "./logger";

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 { LensBinary, LensBinaryOpts } from "./lens-binary"
import { isProduction } from "../common/vars";
export class HelmCli extends LensBinary { export class HelmCli extends LensBinary {
@ -13,7 +15,7 @@ export class HelmCli extends LensBinary {
super(opts) super(opts)
} }
protected getTarName(): string|null { protected getTarName(): string | null {
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz` return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
} }
@ -26,19 +28,16 @@ export class HelmCli extends LensBinary {
} }
protected getOriginalBinaryPath() { 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 helmVersion = packageInfo.config.bundledHelmVersion;
const isDevelopment = process.env.NODE_ENV !== "production" let baseDir = process.resourcesPath;
let baseDir: string = null
if(isDevelopment) { if (!isProduction) {
baseDir = path.join(process.cwd(), "binaries", "client") baseDir = path.join(process.cwd(), "binaries", "client");
} else {
baseDir = path.join(process.resourcesPath)
} }
export const helmCli = new HelmCli(baseDir, helmVersion) export const helmCli = new HelmCli(baseDir, helmVersion);

View File

@ -1,25 +1,24 @@
import * as tempy from "tempy"; import * as tempy from "tempy";
import * as fs from "fs"; import fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as camelcaseKeys from "camelcase-keys";
import { promiseExec} from "./promise-exec" import { promiseExec} from "./promise-exec"
import { helmCli } from "./helm-cli"; import { helmCli } from "./helm-cli";
import { Cluster } from "./cluster"; import { Cluster } from "./cluster";
import { toCamelCase } from "../common/utils/camelCase";
export class HelmReleaseManager { export class HelmReleaseManager {
public async listReleases(pathToKubeconfig: string, namespace?: string) { public async listReleases(pathToKubeconfig: string, namespace?: string) {
const helm = await helmCli.binaryPath() const helm = await helmCli.binaryPath()
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces" 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) const output = JSON.parse(stdout)
if (output.length == 0) { if (output.length == 0) {
return output return output
} }
const result: any = []
output.forEach((release: any, index: number) => { output.forEach((release: any, index: number) => {
output[index] = camelcaseKeys(release) output[index] = toCamelCase(release)
}); });
return output return output
} }

View File

@ -1,4 +1,4 @@
import * as fs from "fs"; import fs from "fs";
import logger from "./logger"; import logger from "./logger";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { promiseExec } from "./promise-exec"; import { promiseExec } from "./promise-exec";

View File

@ -1,7 +1,10 @@
// Main process
import "../common/system-ca" import "../common/system-ca"
import "../common/prometheus-providers"
import { app, dialog, protocol } from "electron" import { app, dialog, protocol } from "electron"
import { PromiseIpc } from "electron-promise-ipc" import { PromiseIpc } from "electron-promise-ipc"
import * as path from "path" import path from "path"
import { format as formatUrl } from "url" import { format as formatUrl } from "url"
import logger from "./logger" import logger from "./logger"
import initMenu from "./menu" import initMenu from "./menu"
@ -15,20 +18,21 @@ import { shellSync } from "./shell-sync"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env" import { mangleProxyEnv } from "./proxy-env"
import { findMainWebContents } from "./webcontents" import { findMainWebContents } from "./webcontents"
import "../common/prometheus-providers" import { registerStaticProtocol } from "../common/register-static";
import { isMac, vueAppName } from "../common/vars";
mangleProxyEnv() mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") { if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = 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 windowManager: WindowManager = null;
let clusterManager: ClusterManager = null; let clusterManager: ClusterManager = null;
let proxyServer: proxy.LensProxy = 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", protocol: "file",
slashes: true, slashes: true,
}) })
@ -40,12 +44,15 @@ async function main() {
updater.start(); updater.start();
tracker.event("app", "start"); tracker.event("app", "start");
registerStaticProtocol();
protocol.registerFileProtocol('store', (request, callback) => { protocol.registerFileProtocol('store', (request, callback) => {
const url = request.url.substr(8) const url = request.url.substr(8)
callback( path.normalize(`${app.getPath("userData")}/${url}`) ) callback(path.normalize(`${app.getPath("userData")}/${url}`))
}, (error) => { }, (error) => {
if (error) console.error('Failed to register protocol') if (error) console.error('Failed to register protocol')
}) })
let port: number = null let port: number = null
// find free port // find free port
try { try {
@ -80,7 +87,7 @@ async function main() {
}, },
showPreferencesHook: async () => { showPreferencesHook: async () => {
// IPC send needs webContents as we're sending it to renderer // 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"); logger.debug("navigate: preferences IPC sent");
}) })
}, },
@ -103,10 +110,10 @@ async function main() {
} }
app.on("ready", 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 // On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q // to stay active until the user quits explicitly with Cmd + Q
if (process.platform != 'darwin') { if (!isMac) {
app.quit(); app.quit();
} else { } else {
windowManager = null windowManager = null
@ -116,7 +123,7 @@ app.on('window-all-closed', function() {
app.on("activate", () => { app.on("activate", () => {
if (!windowManager) { if (!windowManager) {
logger.debug("activate main window") logger.debug("activate main window")
windowManager = new WindowManager(false) windowManager = new WindowManager({ showSplash: false })
windowManager.showMain(vmURL) windowManager.showMain(vmURL)
} }
}) })

View File

@ -1,8 +1,6 @@
import * as k8s from "@kubernetes/client-node" import * as k8s from "@kubernetes/client-node"
import * as os from "os" import * as os from "os"
import { all } from "q";
import * as yaml from "js-yaml" import * as yaml from "js-yaml"
import { V1beta1ValidatingWebhookConfiguration } from "@kubernetes/client-node";
import logger from "./logger"; import logger from "./logger";
const kc = new k8s.KubeConfig() const kc = new k8s.KubeConfig()
@ -31,7 +29,7 @@ export function loadConfig(kubeconfig: string): k8s.KubeConfig {
* *
* @param config KubeConfig to check * @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)}`) logger.debug(`validating kube config: ${JSON.stringify(config)}`)
if(!config.users || config.users.length == 0) { if(!config.users || config.users.length == 0) {
throw new Error("No users provided in config") throw new Error("No users provided in config")

View File

@ -28,9 +28,7 @@ export class KubeAuthProxy {
public async run(): Promise<void> { public async run(): Promise<void> {
if (this.proxyProcess) { if (this.proxyProcess) {
return new Promise((resolve, reject) => { return;
resolve()
})
} }
const proxyBin = await this.kubectl.kubectlPath() const proxyBin = await this.kubectl.kubectlPath()
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => { 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) const clusterUrl = url.parse(this.cluster.apiUrl)
let args = [ let args = [
"proxy", "proxy",
"-p", this.port.toString(), "--port", this.port.toString(),
"--kubeconfig", this.cluster.kubeconfigPath(), "--kubeconfig", this.cluster.kubeconfigPath(),
"--accept-hosts", clusterUrl.hostname, "--accept-hosts", clusterUrl.hostname,
] ]
@ -59,7 +56,9 @@ export class KubeAuthProxy {
}) })
this.proxyProcess.on("exit", (code) => { this.proxyProcess.on("exit", (code) => {
logger.error(`proxy ${this.cluster.contextName} exited with code ${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 this.proxyProcess = null
configWatcher.close() configWatcher.close()
}) })
@ -96,7 +95,7 @@ export class KubeAuthProxy {
} }
protected async sendIpcLogMessage(data: string, stream: string) { 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() { public exit() {

View File

@ -1,5 +1,5 @@
import { app } from "electron" import { app } from "electron"
import * as fs from "fs" import fs from "fs"
import { ensureDir, randomFileName} from "./file-helpers" import { ensureDir, randomFileName} from "./file-helpers"
import logger from "./logger" import logger from "./logger"

View File

@ -1,17 +1,17 @@
import { app, remote } from "electron" import { app, remote } from "electron"
import * as path from "path" import path from "path"
import * as fs from "fs" import fs from "fs"
import * as request from "request" import request from "request"
import { promiseExec} from "./promise-exec" import { promiseExec} from "./promise-exec"
import logger from "./logger" import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra" import { ensureDir, pathExists } from "fs-extra"
import * as md5File from "md5-file"
import { globalRequestOpts } from "../common/request" import { globalRequestOpts } from "../common/request"
import * as lockFile from "proper-lockfile" import * as lockFile from "proper-lockfile"
import { helmCli } from "./helm-cli" import { helmCli } from "./helm-cli"
import { userStore } from "../common/user-store" 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([ const kubectlMap: Map<string, string> = new Map([
["1.7", "1.8.15"], ["1.7", "1.8.15"],
["1.8", "1.9.10"], ["1.8", "1.9.10"],
@ -195,16 +195,16 @@ export class Kubectl {
const file = fs.createWriteStream(this.path) const file = fs.createWriteStream(this.path)
stream.on("complete", () => { stream.on("complete", () => {
logger.debug("kubectl binary download finished") logger.debug("kubectl binary download finished")
file.end(() => {}) file.end()
}) })
stream.on("error", (error) => { stream.on("error", (error) => {
logger.error(error) logger.error(error)
fs.unlink(this.path, () => {}) fs.unlink(this.path, null)
reject(error) reject(error)
}) })
file.on("close", () => { file.on("close", () => {
logger.debug("kubectl binary download closed") logger.debug("kubectl binary download closed")
fs.chmod(this.path, 0o755, () => {}) fs.chmod(this.path, 0o755, null)
resolve() resolve()
}) })
stream.pipe(file) stream.pipe(file)
@ -285,13 +285,11 @@ export class Kubectl {
} }
protected getDownloadMirror() { protected getDownloadMirror() {
if (process.platform == "darwin") {
return packageMirrors.get("default") // MacOS packages are only available from default
}
const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror) const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror)
if (mirror) { return mirror } if (mirror) {
return mirror
return packageMirrors.get("default") }
return packageMirrors.get("default") // MacOS packages are only available from default
} }
} }

29
src/main/kubectl_spec.ts Normal file
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)
})
})

View File

@ -1,4 +1,4 @@
import * as http from "http"; import http from "http";
export abstract class LensApi { export abstract class LensApi {
protected respondJson(res: http.ServerResponse, content: {}, status = 200) { protected respondJson(res: http.ServerResponse, content: {}, status = 200) {

View File

@ -1,9 +1,10 @@
import * as path from "path" import path from "path"
import * as fs from "fs" import fs from "fs"
import * as request from "request" import request from "request"
import logger from "./logger" import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra" import { ensureDir, pathExists } from "fs-extra"
import * as tar from "tar" import * as tar from "tar"
import { isWindows } from "../common/vars";
export type LensBinaryOpts = { export type LensBinaryOpts = {
version: string; version: string;
@ -12,6 +13,7 @@ export type LensBinaryOpts = {
newBinaryName?: string; newBinaryName?: string;
requestOpts?: request.Options; requestOpts?: request.Options;
} }
export class LensBinary { export class LensBinary {
public binaryVersion: string public binaryVersion: string
@ -35,19 +37,21 @@ export class LensBinary {
let arch = null let arch = null
if(process.arch == "x64") { if (process.arch == "x64") {
arch = "amd64" arch = "amd64"
} else if(process.arch == "x86" || process.arch == "ia32") { }
else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386" arch = "386"
} else { }
else {
arch = process.arch arch = process.arch
} }
this.arch = 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)) this.dirname = path.normalize(path.join(baseDir, this.binaryName))
if (process.platform === "win32") { if (isWindows) {
this.binaryName = this.binaryName+".exe" this.binaryName = this.binaryName + ".exe"
this.originalBinaryName = this.originalBinaryName+".exe" this.originalBinaryName = this.originalBinaryName + ".exe"
} }
const tarName = this.getTarName() const tarName = this.getTarName()
if (tarName) { if (tarName) {
@ -64,7 +68,7 @@ export class LensBinary {
return this.getBinaryPath() return this.getBinaryPath()
} }
protected getTarName(): string|null { protected getTarName(): string | null {
return null return null
} }
@ -88,7 +92,7 @@ export class LensBinary {
try { try {
await this.ensureBinary() await this.ensureBinary()
return this.dirname return this.dirname
} catch(err) { } catch (err) {
logger.error(err) logger.error(err)
return "" return ""
} }
@ -101,10 +105,12 @@ export class LensBinary {
public async ensureBinary() { public async ensureBinary() {
const isValid = await this.checkBinary() const isValid = await this.checkBinary()
if(!isValid) { if (!isValid) {
await this.downloadBinary().catch((error) => { logger.error(error) }); await this.downloadBinary().catch((error) => {
logger.error(error)
});
if (this.tarPath) await this.untarBinary() 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()}`) logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
} }
} }
@ -127,7 +133,8 @@ export class LensBinary {
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => { fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
if (err) { if (err) {
reject(err) reject(err)
} else { }
else {
resolve() resolve()
} }
}) })
@ -135,7 +142,7 @@ export class LensBinary {
} }
protected async downloadBinary() { protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath() const binaryPath = this.tarPath || this.getBinaryPath()
await ensureDir(this.getBinaryDir(), 0o755) await ensureDir(this.getBinaryDir(), 0o755)
const file = fs.createWriteStream(binaryPath) const file = fs.createWriteStream(binaryPath)
@ -152,18 +159,18 @@ export class LensBinary {
stream.on("complete", () => { stream.on("complete", () => {
logger.info(`Download of ${this.originalBinaryName} finished`) logger.info(`Download of ${this.originalBinaryName} finished`)
file.end(() => {}) file.end()
}) })
stream.on("error", (error) => { stream.on("error", (error) => {
logger.error(error) logger.error(error)
fs.unlink(binaryPath, () => {}) fs.unlink(binaryPath, null)
throw(error) throw(error)
}) })
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
file.on("close", () => { file.on("close", () => {
logger.debug(`${this.originalBinaryName} binary download closed`) logger.debug(`${this.originalBinaryName} binary download closed`)
if(!this.tarPath) fs.chmod(binaryPath, 0o755, () => {}) if (!this.tarPath) fs.chmod(binaryPath, 0o755, null)
resolve() resolve()
}) })
stream.pipe(file) stream.pipe(file)

View File

@ -1,10 +1,11 @@
import * as winston from "winston" import winston from "winston"
import { isDebugging } from "../common/vars";
const options = { const options = {
colorize: true, colorize: true,
handleExceptions: false, handleExceptions: false,
json: false, json: false,
level: process.env.DEBUG === "true" ? "debug" : "info", level: isDebugging ? "debug" : "info",
} }
const logger = winston.createLogger({ const logger = winston.createLogger({

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 { export interface MenuOptions {
logoutHook: any; logoutHook: any;
@ -10,7 +13,6 @@ export interface MenuOptions {
} }
function setClusterSettingsEnabled(enabled: boolean) { function setClusterSettingsEnabled(enabled: boolean) {
const isMac = process.platform === 'darwin';
const menuIndex = isMac ? 1 : 0 const menuIndex = isMac ? 1 : 0
Menu.getApplicationMenu().items[menuIndex].submenu.items[1].enabled = enabled 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.`) appDetails.push(`Copyright 2020 Lakend Labs, Inc.`)
let title = "Lens" let title = "Lens"
if (process.platform === "win32") { if (isWindows) {
title = ` ${title}` title = ` ${title}`
} }
dialog.showMessageBoxSync(browserWindow, { 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. * @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) { export default function initMenu(opts: MenuOptions, promiseIpc: any) {
const isMac = process.platform === 'darwin';
const isDevelopment = process.env.NODE_ENV === 'development';
const mt: MenuItemConstructorOptions[] = []; const mt: MenuItemConstructorOptions[] = [];
const macAppMenu: MenuItemConstructorOptions = { const macAppMenu: MenuItemConstructorOptions = {
label: app.getName(), label: app.getName(),
@ -67,12 +66,12 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{ role: 'quit' } { role: 'quit' }
] ]
}; };
if(isMac) { if (isMac) {
mt.push(macAppMenu); mt.push(macAppMenu);
} }
let fileMenu: MenuItemConstructorOptions; let fileMenu: MenuItemConstructorOptions;
if(isMac) { if (isMac) {
fileMenu = { fileMenu = {
label: 'File', label: 'File',
submenu: [{ submenu: [{
@ -86,7 +85,8 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
} }
] ]
} }
} else { }
else {
fileMenu = { fileMenu = {
label: 'File', label: 'File',
submenu: [ submenu: [
@ -134,27 +134,28 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{ {
label: 'Back', label: 'Back',
accelerator: 'CmdOrCtrl+[', accelerator: 'CmdOrCtrl+[',
click () { click() {
webContents.getFocusedWebContents().executeJavaScript('window.history.back()') webContents.getFocusedWebContents().executeJavaScript('window.history.back()')
} }
}, },
{ {
label: 'Forward', label: 'Forward',
accelerator: 'CmdOrCtrl+]', accelerator: 'CmdOrCtrl+]',
click () { click() {
webContents.getFocusedWebContents().executeJavaScript('window.history.forward()') webContents.getFocusedWebContents().executeJavaScript('window.history.forward()')
} }
}, },
{ {
label: 'Reload', label: 'Reload',
accelerator: 'CmdOrCtrl+R', accelerator: 'CmdOrCtrl+R',
click () { click() {
webContents.getFocusedWebContents().reload() webContents.getFocusedWebContents().reload()
} }
}, },
...(isDevelopment ? [ ...(isDevelopment ? [
{ role: 'toggleDevTools'} as MenuItemConstructorOptions, { role: 'toggleDevTools' } as MenuItemConstructorOptions,
{ {
accelerator: "CmdOrCtrl+Shift+I",
label: 'Open Dashboard Devtools', label: 'Open Dashboard Devtools',
click() { click() {
webContents.getFocusedWebContents().openDevTools() webContents.getFocusedWebContents().openDevTools()
@ -183,20 +184,20 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{ {
label: 'Community Slack', label: 'Community Slack',
click: async () => { click: async () => {
shell.openExternal('https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI'); shell.openExternal(slackUrl);
}, },
}, },
{ {
label: 'Report an Issue', label: 'Report an Issue',
click: async () => { click: async () => {
shell.openExternal('https://github.com/lensapp/lens/issues'); shell.openExternal(issuesTrackerUrl);
}, },
}, },
{ {
label: "What's new?", label: "What's new?",
click: opts.showWhatsNewHook, click: opts.showWhatsNewHook,
}, },
...(process.platform !== "darwin" ? [{ ...(!isMac ? [{
label: "About Lens", label: "About Lens",
click: showAbout click: showAbout
} as MenuItemConstructorOptions] : []) } as MenuItemConstructorOptions] : [])
@ -214,4 +215,4 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
promiseIpc.on("disableClusterSettingsMenuItem", () => { promiseIpc.on("disableClusterSettingsMenuItem", () => {
setClusterSettingsEnabled(false) setClusterSettingsEnabled(false)
}); });
}; }

View File

@ -1,6 +1,5 @@
import logger from "./logger" import logger from "./logger"
import { createServer } from "net" import { createServer, AddressInfo } from "net"
import { AddressInfo } from "net"
const getNextAvailablePort = () => { const getNextAvailablePort = () => {
logger.debug("getNextAvailablePort() start") logger.debug("getNextAvailablePort() start")

26
src/main/port_spec.ts Normal file
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))
})
})

View File

@ -1,5 +1,5 @@
import * as http from "http"; import http from "http";
import * as httpProxy from "http-proxy"; import httpProxy from "http-proxy";
import { Socket } from "net"; import { Socket } from "net";
import * as url from "url"; import * as url from "url";
import * as WebSocket from "ws" import * as WebSocket from "ws"
@ -8,6 +8,7 @@ import logger from "./logger"
import * as shell from "./node-shell-session" import * as shell from "./node-shell-session"
import { ClusterManager } from "./cluster-manager" import { ClusterManager } from "./cluster-manager"
import { Router } from "./router" import { Router } from "./router"
import { apiPrefix } from "../common/vars";
export class LensProxy { export class LensProxy {
public static readonly localShellSessions = true public static readonly localShellSessions = true
@ -40,17 +41,15 @@ export class LensProxy {
protected buildProxyServer() { protected buildProxyServer() {
const proxy = this.createProxy(); 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); 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) this.handleWsUpgrade(req, socket, head)
}.bind(this)); });
proxyServer.on("error", (err) => { proxyServer.on("error", (err) => {
logger.error(err) logger.error(err)
}); });
return proxyServer; return proxyServer;
} }
@ -64,11 +63,10 @@ export class LensProxy {
res.writeHead(proxyRes.statusCode, { res.writeHead(proxyRes.statusCode, {
"Content-Type": "text/plain" "Content-Type": "text/plain"
}) })
res.end(cluster.contextHandler.proxyServerError().toString()) res.end(cluster.contextHandler.proxyServerError())
return return
} }
} }
if (req.method !== "GET") { if (req.method !== "GET") {
return return
} }
@ -106,11 +104,11 @@ export class LensProxy {
} }
protected createWsListener() { protected createWsListener() {
const ws = new WebSocket.Server({ noServer: true}) const ws = new WebSocket.Server({ noServer: true })
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => { ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req) const cluster = this.clusterManager.getClusterForRequest(req)
const contextHandler = cluster.contextHandler const contextHandler = cluster.contextHandler
const nodeParam = this.getNodeParam(req.url) const nodeParam = url.parse(req.url, true).query["node"]?.toString();
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => { contextHandler.withTemporaryKubeconfig((kubeconfigPath) => {
return new Promise<boolean>(async (resolve, reject) => { return new Promise<boolean>(async (resolve, reject) => {
@ -120,26 +118,15 @@ export class LensProxy {
}) })
}) })
}) })
}).bind(this)) }))
return ws 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> { 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 delete req.headers.authorization
req.url = req.url.replace("/api-kube", "") req.url = req.url.replace(prefix, "")
const isWatchRequest = req.url.includes("watch=") const isWatchRequest = req.url.includes("watch=")
return await contextHandler.getApiTarget(isWatchRequest) return await contextHandler.getApiTarget(isWatchRequest)
} }

View File

@ -1,7 +1,7 @@
import { exec } from "child_process"; import { exec } from "child_process";
import * as fs from "fs"; import fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import * as path from "path"; import path from "path";
import * as tempy from "tempy"; import * as tempy from "tempy";
import logger from "./logger" import logger from "./logger"
import { Cluster } from "./cluster"; import { Cluster } from "./cluster";

View File

@ -1,5 +1,8 @@
import * as http from "http" import Call from "@hapi/call"
import * as path from "path" import Subtext from "@hapi/subtext"
import http from "http"
import path from "path"
import { readFile } from "fs"
import { Cluster } from "./cluster" import { Cluster } from "./cluster"
import { configRoute } from "./routes/config" import { configRoute } from "./routes/config"
import { helmApi } from "./helm-api" import { helmApi } from "./helm-api"
@ -8,16 +11,7 @@ import { kubeconfigRoute } from "./routes/kubeconfig"
import { metricsRoute } from "./routes/metrics" import { metricsRoute } from "./routes/metrics"
import { watchRoute } from "./routes/watch" import { watchRoute } from "./routes/watch"
import { portForwardRoute } from "./routes/port-forward" import { portForwardRoute } from "./routes/port-forward"
import { readFile } from "fs" import { outDir, reactAppName } from "../common/vars";
// 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")
const mimeTypes: {[key: string]: string} = { const mimeTypes: {[key: string]: string} = {
"html": "text/html", "html": "text/html",
@ -88,12 +82,12 @@ export class Router {
return request return request
} }
protected handleStaticFile(file: string, response: http.ServerResponse) { protected handleStaticFile(filePath: string, response: http.ServerResponse) {
const asset = path.join(assetsPath, file) const asset = path.resolve(outDir, filePath);
readFile(asset, (err, data) => { readFile(asset, (err, data) => {
if (err) { if (err) {
// default to index.html so that react routes work when page is refreshed // default to index.html so that react routes work when page is refreshed
this.handleStaticFile("index.html", response) this.handleStaticFile(`${reactAppName}.html`, response)
} else { } else {
const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain"; const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain";
response.setHeader("Content-Type", type); response.setHeader("Content-Type", type);

View File

@ -1,10 +1,24 @@
import { app } from "electron"
import { CoreV1Api } from "@kubernetes/client-node"
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" import { LensApi } from "../lens-api"
import { userStore } from "../../common/user-store" import { userStore } from "../../common/user-store"
import { getAppVersion } from "../../common/app-utils"
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
import { Cluster } from "../cluster" 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 // TODO: auto-populate all resources dynamically
const apiResources = [ const apiResources = [
{ resource: "configmaps" }, { resource: "configmaps" },
@ -44,12 +58,13 @@ async function getAllowedNamespaces(cluster: Cluster) {
return namespaceList.body.items return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i]) .filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name) .map(ns => ns.metadata.name)
} catch(error) { } catch (error) {
const kc = cluster.contextHandler.kc const kc = cluster.contextHandler.kc
const ctx = kc.getContextObject(kc.currentContext) const ctx = kc.getContextObject(kc.currentContext)
if (ctx.namespace) { if (ctx.namespace) {
return [ctx.namespace] return [ctx.namespace]
} else { }
else {
return [] return []
} }
} }
@ -67,20 +82,19 @@ async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
) )
return apiResources return apiResources
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource) .filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
} catch(error) { } catch (error) {
return [] return []
} }
} }
class ConfigRoute extends LensApi { class ConfigRoute extends LensApi {
public async routeConfig(request: LensApiRequest) { public async routeConfig(request: LensApiRequest) {
const { params, response, cluster} = request const { params, response, cluster } = request
const namespaces = await getAllowedNamespaces(cluster) const namespaces = await getAllowedNamespaces(cluster)
const data = { const data: IConfigRoutePayload = {
clusterName: cluster.contextName, clusterName: cluster.contextName,
lensVersion: getAppVersion(), lensVersion: app.getVersion(),
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`, lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
kubeVersion: cluster.version, kubeVersion: cluster.version,
chartsEnabled: true, chartsEnabled: true,

View File

@ -1,9 +1,10 @@
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" 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 { 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; [metricName: string]: string;
} }
@ -11,13 +12,13 @@ class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest) { public async routeMetrics(request: LensApiRequest) {
const { response, cluster} = request const { response, cluster} = request
const query: MetricsQuery = request.payload; const query: IMetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube` const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}`
const headers = { const headers = {
"Host": `${cluster.id}.localhost:${cluster.port}`, "Host": `${cluster.id}.localhost:${cluster.port}`,
"Content-type": "application/json", "Content-type": "application/json",
} }
const queryParams: MetricsQuery = {} const queryParams: IMetricsQuery = {}
request.query.forEach((value: string, key: string) => { request.query.forEach((value: string, key: string) => {
queryParams[key] = value queryParams[key] = value
}) })

View File

@ -1,6 +1,6 @@
import { LensApiRequest } from "../router" import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api" 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 { ServerResponse } from "http"
import { Request } from "request" import { Request } from "request"
import logger from "../logger" import logger from "../logger"
@ -41,7 +41,7 @@ class ApiWatcher {
this.watchRequest.abort() this.watchRequest.abort()
} }
private watchHandler(phase: string, obj: RuntimeRawExtension) { private watchHandler(phase: string, obj: any) {
this.eventBuffer.push({ this.eventBuffer.push({
type: phase, type: phase,
object: obj object: obj

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