Lens restructure (#540)

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

No files matched your search

+4 -5
View File
@@ -35,7 +35,7 @@ jobs:
path: $(YARN_CACHE_FOLDER)
cacheHitVar: CACHE_RESTORED
displayName: Cache Yarn packages
- script: make deps
- script: make install-deps
displayName: Install dependencies
- script: make integration-win
displayName: Run integration tests
@@ -72,10 +72,8 @@ jobs:
tar -xzf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" -C /
displayName: "Unpack cache"
condition: eq(variables.CACHE_RESTORED, 'true')
- script: make deps
- script: make install-deps
displayName: Install dependencies
- script: make lint
displayName: Lint
- script: make test
displayName: Run tests
- script: make integration-mac
@@ -119,7 +117,7 @@ jobs:
tar -xzf "$AZURE_CACHE_FOLDER/yarn-cache.tar.gz" -C /
displayName: "Unpack cache"
condition: eq(variables.CACHE_RESTORED, 'true')
- script: make deps
- script: make install-deps
displayName: Install dependencies
- script: make lint
displayName: Lint
@@ -130,6 +128,7 @@ jobs:
sudo apt-get install libgconf-2-4 conntrack -y
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
export CHANGE_MINIKUBE_NONE_USER=true
sudo minikube start --driver=none
displayName: Install integration test dependencies
- script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
+12
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"
]
}
+6 -3
View File
@@ -29,7 +29,7 @@ module.exports = {
files: [
"build/*.ts",
"src/**/*.ts",
"spec/**/*.ts"
"integration/**/*.ts"
],
parser: "@typescript-eslint/parser",
extends: [
@@ -43,13 +43,16 @@ module.exports = {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "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]
},
},
{
files: [
"dashboard/**/*.ts",
"dashboard/**/*.tsx",
"src/renderer/**/*.tsx",
],
parser: "@typescript-eslint/parser",
extends: [
File renamed without changes.
+2
View File
@@ -1,4 +1,5 @@
dist/
out/
node_modules/
.DS_Store
yarn-error.log
@@ -7,3 +8,4 @@ tmp/
static/build/client/
binaries/client/
binaries/server/
locales/**/**.js
+1 -1
View File
@@ -1,3 +1,3 @@
disturl "https://atom.io/download/electron"
target "6.1.10"
target "6.1.12"
runtime "electron"
+23 -28
View File
@@ -4,15 +4,30 @@ else
DETECTED_OS := $(shell uname)
endif
.PHONY: dev build test clean
.PHONY: init dev build test clean
init: download-bins install-deps compile-dev
echo "Init done"
download-bins:
yarn download:bins
yarn download-bins
dev: app-deps dashboard-deps
yarn dev
install-deps:
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:
yarn build:linux
@@ -26,19 +41,10 @@ integration-win:
yarn build:win
yarn integration
lint:
yarn lint
yarn lint-dashboard
test-app:
yarn test
deps: app-deps dashboard-deps
app-deps:
yarn install --frozen-lockfile
build: build-dashboard app-deps
build: install-deps
yarn install
ifeq "$(DETECTED_OS)" "Windows"
yarn dist:win
@@ -46,18 +52,7 @@ else
yarn dist
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:
rm -rf binaries/client/*
rm -rf dist/*
rm -rf out/*
+14 -8
View File
@@ -6,7 +6,7 @@
Lens is the only IDE you’ll 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?
@@ -23,19 +23,25 @@ Lens is the only IDE you’ll 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).
Alternatively on Mac:
```
brew cask install lens
```
## 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 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
Bug reports and pull requests are welcome on GitHub at https://github.com/lensapp/lens.
+2 -1
View File
@@ -3,7 +3,8 @@ module.exports = {
match: jest.fn(),
app: {
getVersion: jest.fn().mockReturnValue("3.0.0"),
getPath: jest.fn().mockReturnValue("/foo/bar")
getPath: jest.fn().mockReturnValue("tmp"),
getLocale: jest.fn().mockRejectedValue("en"),
},
remote: {
app: {
+7 -6
View File
@@ -1,9 +1,10 @@
import * as request from "request"
import * as fs from "fs"
import packageInfo from "../package.json"
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 * as md5File from "md5-file"
import * as requestPromise from "request-promise-native"
import * as path from "path"
import path from "path"
class KubectlDownloader {
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 downloads = [
{ platform: 'linux', arch: 'amd64', target: path.join(baseDir, 'linux', 'x64', 'kubectl') },
-10
View File
@@ -1,10 +0,0 @@
{
"plugins": [
"macros",
"@babel/plugin-transform-runtime",
],
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
-12
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
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/
-18
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"
}
]
}
@@ -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();
});
});
@@ -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")
},
};
-43
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);
-117
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

@@ -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

-30
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__"
]
}
@@ -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]);
});
});
@@ -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)
});
});
@@ -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);
});
});
-18
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;
}
}
-20
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>
-1
View File
@@ -1 +0,0 @@
/* eslint-disable */module.exports={localeData:{"plurals":function(n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n10==1&&n100!=11?"one":n10==2&&n100!=12?"two":n10==3&&n100!=13?"few":"other";return n==1&&v0?"one":"other"}},messages:{"(as a percentage of request)":"(as a percentage of request)","(empty) (Allowing the specific traffic to all pods in this namespace)":"(empty) (Allowing the specific traffic to all pods in this namespace)","<0>Filtered</0>: {itemsCount} / {allItemsCount}":["<0>Filtered</0>: ",["itemsCount"]," / ",["allItemsCount"]],"<0>Your browser does not support all Lens features. </0> Please consider using another browser.":"<0>Your browser does not support all Lens features. </0> Please consider using another browser.","<0>{0}</0> successfully created":["<0>",["0"],"</0> successfully created"],"API Group":"API Group","Access Control":"Access Control","Access Modes":"Access Modes","Account Name":"Account Name","Active":"Active","Add Namespace":"Add Namespace","Add RoleBinding":"Add RoleBinding","Add bindings to {name}":["Add bindings to ",["name"]],"Add field":"Add field","Additional Printer Columns":"Additional Printer Columns","Addresses":"Addresses","Affinities":"Affinities","Age":"Age","All groups":"All groups","All logs":"All logs","All namespaces":"All namespaces","Allocatable":"Allocatable","Allow Privilege Escalation":"Allow Privilege Escalation","Allowed CSI Drivers":"Allowed CSI Drivers","Allowed Capabilities":"Allowed Capabilities","Allowed Flex Volumes":"Allowed Flex Volumes","Allowed Host Paths":"Allowed Host Paths","Allowed Proc Mount Types":"Allowed Proc Mount Types","Allowed Runtime Class Names":"Allowed Runtime Class Names","Allowed Unsafe Sysctls":"Allowed Unsafe Sysctls","Annotations":"Annotations","Api Groups":"Api Groups","App Version":"App Version","App crash at <0>{pageUrl}</0>":["App crash at <0>",["pageUrl"],"</0>"],"Applying..":"Applying..","Apps":"Apps","Are you sure you want to drain <0>{nodeName}</0>?":["Are you sure you want to drain <0>",["nodeName"],"</0>?"],"Arguments":"Arguments","Auth App Role":"Auth App Role","Back":"Back","Backends":"Backends","Binding":"Binding","Binding targets":"Binding targets","Bindings":"Bindings","Build version":"Build version","Bytes consumed on this filesystem":"Bytes consumed on this filesystem","Bytes received by all containers":"Bytes received by all containers","Bytes sent by Ingress controller with error status":"Bytes sent by Ingress controller with error status","Bytes sent by Ingress controller with successful status":"Bytes sent by Ingress controller with successful status","Bytes sent, status 2xx":"Bytes sent, status 2xx","Bytes sent, status 5xx":"Bytes sent, status 5xx","Bytes transmitted from all containers":"Bytes transmitted from all containers","CA Bundle":"CA Bundle","CPU":"CPU","CPU capacity":"CPU capacity","CPU cores usage":"CPU cores usage","CPU limits":"CPU limits","CPU requests":"CPU requests","CPU:":"CPU:","Cancel":"Cancel","Capacity":"Capacity","Certificates":"Certificates","Chart":"Chart","Chart Release <0>{0}</0> successfully created.":["Chart Release <0>",["0"],"</0> successfully created."],"Chart: {0}":["Chart: ",["0"]],"Charts":"Charts","Checking update":"Checking update","Claim":"Claim","Claim Name":"Claim Name","Close":"Close","Close (Ctrl+W)":"Close (Ctrl+W)","Cloud API Token Secret":"Cloud API Token Secret","Cluster":"Cluster","Cluster IP":"Cluster IP","Cluster Issuers":"Cluster Issuers","Command":"Command","Common Name":"Common Name","Compact view":"Compact view","Completions":"Completions","Component stack":"Component stack","Condition":"Condition","Conditions":"Conditions","Config Maps":"Config Maps","Config copied to clipboard":"Config copied to clipboard","ConfigMap <0>{0}</0> successfully updated.":["ConfigMap <0>",["0"],"</0> successfully updated."],"ConfigMaps":"ConfigMaps","Configuration":"Configuration","Connection":"Connection","Container":"Container","Container CPU cores usage":"Container CPU cores usage","Container CPU requests":"Container CPU requests","Container memory limits":"Container memory limits","Container memory requests":"Container memory requests","Container memory usage":"Container memory usage","Container runtime":"Container runtime","Containers":"Containers","Context":"Context","Controlled By":"Controlled By","Controlled by":"Controlled by","Conversion":"Conversion","Copy to clipboard":"Copy to clipboard","Cordon":"Cordon","Count":"Count","Create":"Create","Create Namespace":"Create Namespace","Create ResourceQuota":"Create ResourceQuota","Create Role":"Create Role","Create Secret":"Create Secret","Create Service Account":"Create Service Account","Create new ResourceQuota":"Create new ResourceQuota","Create new Role":"Create new Role","Create new RoleBinding":"Create new RoleBinding","Create new Secret":"Create new Secret","Create new Service Account":"Create new Service Account","Create resourcLine truncated
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
/* eslint-disable */module.exports={localeData:{"plurals":function(n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],i10=i.slice(-1),i100=i.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11?"one":v0&&i10>=2&&i10<=4&&(i100<12||i100>14)?"few":v0&&i10==0||v0&&i10>=5&&i10<=9||v0&&i100>=11&&i100<=14?"many":"other"}},messages:{"(as a percentage of request)":"(as a percentage of request)","(empty) (Allowing the specific traffic to all pods in this namespace)":"(\u041F\u0443\u0441\u0442\u043E) (\u0414\u043E\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0441\u044F \u0442\u0440\u0430\u0444\u0438\u043A \u043A\u043E \u0432\u0441\u0435\u043C \u043F\u043E\u0434\u0430\u043C \u0432 \u0434\u0430\u043D\u043D\u043E\u0439 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0438\u043C\u0435\u043D)","<0>Filtered</0>: {itemsCount} / {allItemsCount}":["<0>\u041E\u0442\u0444\u0438\u043B\u044C\u0442\u0440\u043E\u0432\u0430\u043D\u043E</0>: ",["itemsCount"]," / ",["allItemsCount"]],"<0>Your browser does not support all Lens features. </0> Please consider using another browser.":"<0>\u0412\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0441\u0435 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u0438 Lens. </0> \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430 \u0440\u0430\u0441\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435 \u0434\u0440\u0443\u0433\u043E\u0433\u043E \u0441\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0433\u043E \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430.","<0>{0}</0> successfully created":["<0>",["0"],"</0> successfully created"],"API Group":"API Group","Access Control":"\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C \u0434\u043E\u0441\u0442\u0443\u043F\u0430","Access Modes":"\u0420\u0435\u0436\u0438\u043C\u044B \u0434\u043E\u0441\u0442\u0443\u043F\u0430","Account Name":"\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430","Active":"\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0439","Add Namespace":"\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C Namespace","Add RoleBinding":"\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0443 \u0440\u043E\u043B\u0435\u0439","Add bindings to {name}":["\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u043A ",["name"]],"Add field":"\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u043E\u043B\u0435","Additional Printer Columns":"Additional Printer Columns","Addresses":"\u0410\u0434\u0440\u0435\u0441\u0430","Affinities":"\u0410\u0444\u0444\u0438\u043D\u0438\u0442\u0435\u0442\u044B","Age":"\u0412\u043E\u0437\u0440\u0430\u0441\u0442","All groups":"All groups","All logs":"\u0412\u0441\u0435 \u043B\u043E\u0433\u0438","All namespaces":"All namespaces","Allocatable":"Allocatable","Allow Privilege Escalation":"Allow Privilege Escalation","Allowed CSI Drivers":"Allowed CSI Drivers","Allowed Capabilities":"Allowed Capabilities","Allowed Flex Volumes":"Allowed Flex Volumes","Allowed Host Paths":"Allowed Host Paths","Allowed Proc Mount Types":"Allowed Proc Mount Types","Allowed Runtime Class Names":"Allowed Runtime Class Names","Allowed Unsafe Sysctls":"Allowed Unsafe Sysctls","Annotations":"\u0410\u043D\u043D\u043E\u0442\u0430\u0446\u0438\u0438","Api Groups":"API \u0433\u0440\u0443\u043F\u043F\u044B","App Version":"\u0412\u0435\u0440\u0441\u0438\u044F \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F","App crash at <0>{pageUrl}</0>":["\u0421\u0431\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043D\u0430 <0>",["pageUrl"],"</0>"],"Applying..":"\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u0438\u0435..","Apps":"\u041F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F","Are you sure you want to drain <0>{nodeName}</0>?":["\u0412\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443 drain \u0434\u043B\u044F \u043D\u043E\u0434\u044B <0>",["nodeName"],"</0>?"],"Arguments":"\u0410\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u044B","Auth App Role":"Auth App Role","Back":"\u041D\u0430\u0437\u0430\u0434","Backends":"\u0411\u044D\u043A\u0435\u043D\u0434\u044B","Binding":"\u041F\u0440\u0438\u0432\u044F\u0437\u043A\u0430","Binding targets":"\u0426\u0435\u043B\u0438 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438","Bindings":"\u041F\u0440\u0438\u0432\u044F\u0437\u043A\u0438","Build version":"\u0412\u0435\u0440\u0441\u0438\u044F \u0431\u0438\u043B\u0434\u0430","Bytes consumed on this filesystem":"\u0411\u0430\u0439\u0442\u044B, \u043F\u043E\u0442\u0440\u0435\u0431\u043B\u044F\u0435\u043C\u044B\u0435 \u0432 \u0444\u0430\u0439\u043B\u043E\u0432\u043E\u0439 \u0441\u0438\u0441\u0442\u0435\u043C\u0435","Bytes received by all containerLine truncated
File diff suppressed because it is too large. Load diff
-129
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"
}
}
-4
View File
@@ -1,4 +0,0 @@
export interface IClusterInfo {
kubeVersion?: string;
clusterName?: string;
}
-12
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
}
-14
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[];
}
-4
View File
@@ -1,4 +0,0 @@
export type IMetricsQuery = string | string[] | {
[metricName: string]: string | object;
}
-74
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;
-14
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"
]
}
-28
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"
],
};
-4
View File
@@ -1,4 +0,0 @@
const Enzyme = require("enzyme");
const Adapter = require("enzyme-adapter-react-16");
Enzyme.configure({ adapter: new Adapter() });
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "../client/tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"target": "es6",
}
}
-125
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);
-144
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",
}),
],
}
};
-10991
View File
File diff suppressed because it is too large. Load diff
@@ -13,7 +13,7 @@ case "darwin":
break
}
export function setup() {
export function setup(): Application {
return new Application({
// path to electron app
args: [],
@@ -36,7 +36,11 @@ describe("app start", () => {
beforeEach(async () => {
app = util.setup()
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.waitUntilWindowLoaded()
}, 20000)
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+205 -113
View File
@@ -1,15 +1,43 @@
{
"name": "kontena-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": {
"name": "Lakend Labs, Inc.",
"email": "info@lakendlabs.com"
},
"copyright": "© 2020, Lakend Labs, Inc.",
"license": "MIT",
"description": "Lens - The Kubernetes IDE",
"version": "3.6.0-dev",
"main": "main.ts",
"scripts": {
"dev": "concurrently -k \"yarn dev-run -C\" \"yarn dev:main\" \"yarn dev:renderer\"",
"dev-run": "nodemon --watch out/main.* --exec \"electron --inspect .\" $@",
"dev-test": "yarn test --watch",
"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": {
"bundledKubectlVersion": "1.17.4",
"bundledHelmVersion": "3.2.4"
@@ -17,6 +45,32 @@
"engines": {
"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": {
"afterSign": "build/notarize.js",
"extraResources": [
@@ -25,6 +79,11 @@
"to": "features/",
"filter": "**/*"
},
{
"from": "locales/",
"to": "locales/",
"filter": "**/*.js"
},
{
"from": "static/",
"to": "static/",
@@ -95,138 +154,171 @@
"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": {
"@hapi/call": "^6.0.1",
"@hapi/subtext": "^6.1.2",
"@kubernetes/client-node": "0.11.1",
"@types/cookie": "^0.3.3",
"@types/fs-extra": "^8.0.0",
"@hapi/call": "^8.0.0",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.12.0",
"@types/crypto-js": "^3.1.47",
"@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/tar": "^4.0.3",
"camelcase-keys": "^6.1.1",
"cookie": "^0.4.0",
"electron-promise-ipc": "^1.3.0",
"electron-store": "^5.0.0",
"electron-updater": "^4.1.2",
"crypto-js": "^4.0.0",
"electron-promise-ipc": "^2.1.0",
"electron-store": "^5.2.0",
"electron-updater": "^4.3.1",
"electron-window-state": "^5.0.3",
"filenamify": "^4.1.0",
"handlebars": "4.1.2",
"http-proxy": "^1.18.0",
"http-proxy-middleware": "^0.19.2",
"https-proxy-agent": "^3.0.1",
"jsonwebtoken": "^8.5.1",
"fs-extra": "^9.0.1",
"handlebars": "^4.7.6",
"http-proxy": "^1.18.1",
"js-yaml": "^3.14.0",
"jsonpath": "^1.0.2",
"lodash": "^4.17.15",
"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-pty": "^0.9.0",
"on-change": "^1.6.2",
"openid-client": "^3.15.2",
"proper-lockfile": "^4.1.1",
"request": "^2.88.0",
"request-promise-native": "^ 1.0.7",
"semver": "^6.3.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"semver": "^7.3.2",
"shell-env": "^3.0.0",
"shelljs": "^0.8.3",
"source-map-support": "^0.5.13",
"ssl-root-cas": "^1.3.1",
"tar": "^5.0.5",
"tar": "^6.0.2",
"tcp-port-used": "^1.0.1",
"tempy": "0.3.0",
"tempy": "^0.5.0",
"universal-analytics": "^0.4.20",
"uuid": "^3.3.3",
"v-clipboard": "^2.2.2",
"vuex": "^3.1.1",
"win-ca": "^3.1.1",
"uuid": "^8.1.0",
"win-ca": "^3.2.0",
"winston": "^3.2.1",
"ws": "^7.1.2"
"ws": "^7.3.0"
},
"devDependencies": {
"@types/ejs": "^2.6.3",
"@types/electron-window-state": "^2.0.31",
"@babel/core": "^7.10.2",
"@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/http-proxy": "^1.17.0",
"@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.5",
"@types/md5-file": "^4.0.0",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.7.2",
"@types/request": "2.47.0",
"@types/request-promise-native": "1.0.16",
"@types/semver": "5.5.0",
"@types/shelljs": "^0.8.5",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/html-webpack-plugin": "^3.2.3",
"@types/jest": "^25.2.3",
"@types/material-ui": "^0.21.7",
"@types/md5-file": "^4.0.2",
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@types/react-router-dom": "^5.1.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/tempy": "0.1.0",
"@types/universal-analytics": "^0.4.3",
"@types/uuid": "^3.4.5",
"@types/tempy": "^0.3.0",
"@types/terser-webpack-plugin": "^3.0.0",
"@types/universal-analytics": "^0.4.4",
"@types/uuid": "^8.0.0",
"@types/webdriverio": "^4.13.0",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.0-rc.28",
"concurrently": "^5.1.0",
"css-loader": "^3.2.0",
"electron": "6.1.10",
"electron-builder": "^22.4.0",
"electron-notarize": "^0.2.1",
"electron-webpack": "^2.7.4",
"electron-webpack-ts": "^3.2.0",
"eslint": "^6.3.0",
"eslint-plugin-vue": "^5.2.3",
"@types/webpack": "^4.41.17",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^1.7.1",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"ace-builds": "^1.4.11",
"ansi_up": "^4.0.4",
"babel-core": "^7.0.0-beta.3",
"babel-loader": "^8.1.0",
"babel-plugin-macros": "^2.8.0",
"babel-runtime": "^6.26.0",
"bootstrap": "^4.5.0",
"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",
"jest": "^24.9.0",
"js-yaml": "^3.13.1",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"marked": "^0.7.0",
"hoist-non-react-statics": "^3.3.2",
"html-webpack-plugin": "^4.3.0",
"identity-obj-proxy": "^3.0.0",
"include-media": "^1.4.9",
"jest": "^26.0.1",
"make-plural": "^6.2.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-sass": "^4.12.0",
"patch-package": "^6.2.0",
"postinstall-postinstall": "^2.0.0",
"prismjs": "^1.17.1",
"sass-loader": "^8.0.0",
"node-sass": "^4.14.1",
"nodemon": "^2.0.4",
"patch-package": "^6.2.2",
"path-to-regexp": "^6.1.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",
"ts-jest": "^24.1.0",
"ts-loader": "^6.0.4",
"ts-node": "^8.4.1",
"style-loader": "^1.2.1",
"terser-webpack-plugin": "^3.0.3",
"ts-jest": "^26.1.0",
"ts-loader": "^7.0.5",
"ts-node": "^8.10.2",
"typeface-roboto": "^0.0.75",
"typescript": "^3.7.0",
"vue": "^2.6.10",
"typescript": "^3.9.5",
"url-loader": "^4.1.0",
"vue": "^2.6.11",
"vue-electron": "^1.0.6",
"vue-loader": "^15.7.1",
"vue-prism-editor": "^0.3.0",
"vue-router": "^3.1.2",
"vue-template-compiler": "^2.6.10",
"vue-loader": "^15.9.2",
"vue-prism-editor": "^0.6.1",
"vue-router": "^3.3.2",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.11",
"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"
}
}
-17
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)
})
})
-32
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))
})
})
-9
View File
@@ -1,9 +0,0 @@
const userStore = {
getPreferences: jest.fn(() => {
return {
downloadMirror: "default"
}
})
}
export { userStore };
-14
View File
@@ -1,14 +0,0 @@
import { app, remote } from "electron"
/**
*
* @returns app version correctly regardless of dev/prod mode and main/renderer differences
*/
export function getAppVersion(): string {
// app is undefined when running in renderer
let version = (app || remote.app).getVersion()
if(process.env.NODE_ENV === 'development') {
version = require("../../package.json").version
}
return version;
}
+18 -15
View File
@@ -1,12 +1,12 @@
import * as ElectronStore from "electron-store"
import ElectronStore from "electron-store"
import { Cluster, ClusterBaseInfo } from "../main/cluster";
import { getAppVersion } from "./app-utils"
import * as version200Beta2 from "./migrations/cluster-store/2.0.0-beta.2"
import * as version241 from "./migrations/cluster-store/2.4.1"
import * as version260Beta2 from "./migrations/cluster-store/2.6.0-beta.2"
import * as version260Beta3 from "./migrations/cluster-store/2.6.0-beta.3"
import * as version270Beta0 from "./migrations/cluster-store/2.7.0-beta.0"
import * as version270Beta1 from "./migrations/cluster-store/2.7.0-beta.1"
import * as version200Beta2 from "../migrations/cluster-store/2.0.0-beta.2"
import * as version241 from "../migrations/cluster-store/2.4.1"
import * as version260Beta2 from "../migrations/cluster-store/2.6.0-beta.2"
import * as version260Beta3 from "../migrations/cluster-store/2.6.0-beta.3"
import * as version270Beta0 from "../migrations/cluster-store/2.7.0-beta.0"
import * as version270Beta1 from "../migrations/cluster-store/2.7.0-beta.1"
import { getAppVersion } from "./utils/app-version";
export class ClusterStore {
private static instance: ClusterStore;
@@ -14,8 +14,10 @@ export class ClusterStore {
private constructor() {
this.store = new ElectronStore({
name: "lens-cluster-store",
// @ts-ignore
// fixme: tests are failed without "projectVersion"
projectVersion: getAppVersion(),
name: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
migrations: {
"2.0.0-beta.2": version200Beta2.migration,
@@ -58,7 +60,9 @@ export class ClusterStore {
public getCluster(id: string): Cluster {
const cluster = this.getAllClusterObjects().find((cluster) => cluster.id === id)
if (cluster) { return cluster}
if (cluster) {
return cluster
}
return null
}
@@ -74,7 +78,8 @@ export class ClusterStore {
}
if (index === -1) {
clusters.push(storable)
} else {
}
else {
clusters[index] = storable
}
this.store.set("clusters", clusters)
@@ -97,7 +102,7 @@ export class ClusterStore {
}
static getInstance(): ClusterStore {
if(!ClusterStore.instance) {
if (!ClusterStore.instance) {
ClusterStore.instance = new ClusterStore();
}
return ClusterStore.instance;
@@ -108,6 +113,4 @@ export class ClusterStore {
}
}
const clusterStore: ClusterStore = ClusterStore.getInstance();
export { clusterStore };
export const clusterStore = ClusterStore.getInstance();
@@ -1,22 +1,11 @@
import * as mockFs from "mock-fs"
import * as yaml from "js-yaml"
jest.mock("electron", () => {
return {
app: {
getVersion: () => '99.99.99',
getPath: () => 'tmp',
getLocale: () => 'en'
}
}
})
import mockFs from "mock-fs"
import yaml from "js-yaml"
import { ClusterStore } from "./cluster-store";
import { Cluster } from "../main/cluster";
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
console.log("");
import { ClusterStore } from "../../../src/common/cluster-store"
import { Cluster } from "../../../src/main/cluster"
describe("for an empty config", () => {
beforeEach(() => {
ClusterStore.resetInstance()
+25
View File
@@ -0,0 +1,25 @@
// Setup static folder for common assets
import path from "path";
import { protocol } from "electron"
import logger from "../main/logger";
import { staticDir, staticProto, outDir } from "./vars";
export function registerStaticProtocol(rootFolder = staticDir) {
const scheme = staticProto.replace("://", "");
protocol.registerFileProtocol(scheme, (request, callback) => {
const relativePath = request.url.replace(staticProto, "");
const absPath = path.resolve(rootFolder, relativePath);
callback(absPath);
}, (error) => {
logger.debug(`Failed to register protocol "${scheme}"`, error);
})
}
export function getStaticUrl(filePath: string) {
return staticProto + filePath;
}
export function getStaticPath(filePath: string) {
return path.resolve(staticDir, filePath);
}
+2 -2
View File
@@ -1,5 +1,5 @@
import * as request from "request"
import { userStore } from "../common/user-store"
import request from "request"
import { userStore } from "./user-store"
export function globalRequestOpts(requestOpts: request.Options ) {
const userPrefs = userStore.getPreferences()
+11 -3
View File
@@ -1,6 +1,14 @@
import * as winca from "win-ca/api"
import "mac-ca"
import { isMac, isWindows } from "./vars";
import winca from "win-ca"
import macca from "mac-ca"
import logger from "../main/logger"
if (process.platform === "win32") {
if (isMac) {
for (const crt of macca.all()) {
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`)
logger.debug("Using host CA: " + attributes.join(","))
}
}
if (isWindows) {
winca.inject("+") // see: https://github.com/ukoloff/win-ca#caveats
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { machineIdSync } from 'node-machine-id'
import { userStore } from "../common/user-store"
import * as ua from "universal-analytics"
import ua from "universal-analytics"
import { machineIdSync } from "node-machine-id"
import { userStore } from "./user-store"
const GA_ID = "UA-159377374-1"
+7 -5
View File
@@ -1,6 +1,6 @@
import * as ElectronStore from "electron-store"
import * as appUtil from "./app-utils"
import * as version210Beta4 from "./migrations/user-store/2.1.0-beta.4"
import ElectronStore from "electron-store"
import * as version210Beta4 from "../migrations/user-store/2.1.0-beta.4"
import { getAppVersion } from "./utils/app-version";
export interface User {
id?: string;
@@ -20,7 +20,9 @@ export class UserStore {
private constructor() {
this.store = new ElectronStore({
projectVersion: appUtil.getAppVersion(),
// @ts-ignore
// fixme: tests are failed without "projectVersion"
projectVersion: getAppVersion(),
migrations: {
"2.1.0-beta.4": version210Beta4.migration,
}
@@ -68,7 +70,7 @@ export class UserStore {
}
static getInstance(): UserStore {
if(!UserStore.instance) {
if (!UserStore.instance) {
UserStore.instance = new UserStore();
}
return UserStore.instance;
@@ -1,21 +1,9 @@
import * as mockFs from "mock-fs"
import * as yaml from "js-yaml"
jest.mock("electron", () => {
return {
app: {
getVersion: () => '99.99.99',
getPath: () => 'tmp',
getLocale: () => 'en'
}
}
})
import mockFs from "mock-fs"
import { userStore, UserStore } from "./user-store"
// Console.log needs to be called before fs-mocks, see https://github.com/tschaub/mock-fs/issues/234
console.log("");
import { userStore, User, UserPreferences, UserStore } from "../../../src/common/user-store"
describe("for an empty config", () => {
beforeEach(() => {
UserStore.resetInstance()
+9
View File
@@ -0,0 +1,9 @@
import packageInfo from "../../../package.json"
export function getAppVersion(): string {
return packageInfo.version;
}
export function getBundledKubectlVersion(): string {
return packageInfo.config.bundledKubectlVersion;
}
File renamed without changes.
+18
View File
@@ -0,0 +1,18 @@
// Convert object's keys to camelCase format
import { camelCase, isPlainObject } from "lodash";
export function toCamelCase(obj: Record<string, any>): any {
if (Array.isArray(obj)) {
return obj.map(toCamelCase);
}
else if (isPlainObject(obj)) {
return Object.keys(obj).reduce((result, key) => {
const value = obj[key];
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
return result;
}, {} as any);
}
else {
return obj;
}
}
+5
View File
@@ -0,0 +1,5 @@
// Common utils (main/renderer)
export * from "./base64"
export * from "./camelCase"
export * from "./splitArray"
@@ -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
* 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`
* 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);
if (index < 0) {
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]
}
+31
View File
@@ -0,0 +1,31 @@
import { splitArray } from "./splitArray";
describe("split array on element tests", () => {
test("empty array", () => {
expect(splitArray([], 10)).toStrictEqual([[], [], false]);
});
test("one element, not in array", () => {
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
});
test("ten elements, not in array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
});
test("one elements, in array", () => {
expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
});
test("ten elements, in front array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
});
test("ten elements, in middle array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
});
test("ten elements, in end array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
});
});
+39
View File
@@ -0,0 +1,39 @@
// App's common configuration for any process (main, renderer, build pipeline, etc.)
import path from "path";
// Temp
export const reactAppName = "app_react"
export const vueAppName = "app_vue"
// Flags
export const isMac = process.platform === "darwin"
export const isWindows = process.platform === "win32"
export const isDebugging = process.env.DEBUG === "true";
export const isProduction = process.env.NODE_ENV === "production"
export const isDevelopment = isDebugging || !isProduction;
export const buildVersion = process.env.BUILD_VERSION;
export const isTestEnv = !!process.env.JEST_WORKER_ID;
// Paths
export const contextDir = process.cwd();
export const staticDir = path.join(contextDir, "static");
export const outDir = path.join(contextDir, "out");
export const mainDir = path.join(contextDir, "src/main");
export const rendererDir = path.join(contextDir, "src/renderer");
export const htmlTemplate = path.resolve(rendererDir, "template.html");
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
// Apis
export const staticProto = "static://"
export const apiPrefix = {
BASE: '/api',
TERMINAL: '/api-terminal', // terminal api
KUBE_BASE: '/api-kube', // kubernetes cluster api
KUBE_HELM: '/api-helm', // helm charts api
KUBE_RESOURCE_APPLIER: "/api-resource",
};
// Links
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues"
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI"
+1 -1
View File
@@ -1,4 +1,4 @@
import * as ElectronStore from "electron-store"
import ElectronStore from "electron-store"
import { clusterStore } from "./cluster-store"
export interface WorkspaceData {
+1 -1
View File
@@ -1,6 +1,6 @@
import { Feature, FeatureStatus } from "../main/feature"
import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node"
import * as semver from "semver"
import semver from "semver"
import { Cluster } from "../main/cluster";
import * as k8s from "@kubernetes/client-node"
+14 -20
View File
@@ -1,19 +1,18 @@
import { KubeConfig } from "@kubernetes/client-node"
import { PromiseIpc } from "electron-promise-ipc"
import * as http from "http"
import http from "http"
import { Cluster, ClusterBaseInfo } from "./cluster"
import { clusterStore } from "../common/cluster-store"
import * as k8s from "./k8s"
import logger from "./logger"
import { LensProxy } from "./proxy"
import { app } from "electron"
import * as path from "path"
import path from "path"
import { promises } from "fs"
import { ensureDir } from "fs-extra"
import * as filenamify from "filenamify"
import filenamify from "filenamify"
import { v4 as uuid } from "uuid"
declare const __static: string;
import { apiPrefix } from "../common/vars";
export type FeatureInstallRequest = {
name: string;
@@ -92,7 +91,7 @@ export class ClusterManager {
reject("No cluster contexts defined")
}
configs.forEach(c => {
k8s.valideConfig(c)
k8s.validateConfig(c)
const cluster = new Cluster({
id: uuid(),
port: this.port,
@@ -117,15 +116,15 @@ export class ClusterManager {
logger.debug(`IPC: addCluster`)
const cluster = await this.addNewCluster(clusterData)
return {
addedCluster: this.clusterResponse(cluster),
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => this.clusterResponse(cluster))
addedCluster: cluster.toClusterInfo(),
allClusters: Array.from(this.getClusters()).map((cluster: Cluster) => cluster.toClusterInfo())
}
});
this.promiseIpc.on("getClusters", async (workspaceId: string) => {
logger.debug(`IPC: getClusters, workspace ${workspaceId}`)
const workspaceClusters = Array.from(this.getClusters()).filter((cluster) => cluster.workspace === workspaceId)
return workspaceClusters.map((cluster: Cluster) => this.clusterResponse(cluster))
return workspaceClusters.map((cluster: Cluster) => cluster.toClusterInfo())
});
this.promiseIpc.on("getCluster", async (id: string) => {
@@ -133,7 +132,7 @@ export class ClusterManager {
const cluster = this.getCluster(id)
if (cluster) {
await cluster.refreshCluster()
return this.clusterResponse(cluster)
return cluster.toClusterInfo()
} else {
return null
}
@@ -181,7 +180,7 @@ export class ClusterManager {
if(!cluster.preferences) cluster.preferences = {};
cluster.preferences.icon = clusterIcon
clusterStore.storeCluster(cluster);
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
} catch(error) {
return {success: false, message: error}
}
@@ -193,7 +192,7 @@ export class ClusterManager {
if (cluster && cluster.preferences) {
cluster.preferences.icon = null;
clusterStore.storeCluster(cluster)
return {success: true, cluster: this.clusterResponse(cluster), message: ""}
return {success: true, cluster: cluster.toClusterInfo(), message: ""}
} else {
return {success: false, message: "Cluster not found"}
}
@@ -202,7 +201,7 @@ export class ClusterManager {
this.promiseIpc.on("refreshCluster", async (clusterId: string) => {
const cluster = this.clusters.get(clusterId)
await cluster.refreshCluster()
return this.clusterResponse(cluster)
return cluster.toClusterInfo()
});
this.promiseIpc.on("stopCluster", (clusterId: string) => {
@@ -217,7 +216,7 @@ export class ClusterManager {
this.promiseIpc.on("removeCluster", (ctx: string) => {
logger.debug(`IPC: removeCluster: ${ctx}`)
return this.removeCluster(ctx).map((cluster: Cluster) => this.clusterResponse(cluster))
return this.removeCluster(ctx).map((cluster: Cluster) => cluster.toClusterInfo())
});
this.promiseIpc.on("clusterStored", (clusterId: string) => {
@@ -263,7 +262,7 @@ export class ClusterManager {
cluster = this.clusters.get(clusterId)
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, "/api-kube")
req.url = req.url.replace(`/${clusterId}`, apiPrefix.KUBE_BASE)
}
}
} else {
@@ -274,11 +273,6 @@ export class ClusterManager {
return cluster;
}
// TODO: remove this
protected clusterResponse(cluster: Cluster) {
return cluster.toClusterInfo()
}
protected async uploadClusterIcon(cluster: Cluster, fileName: string, src: string): Promise<string> {
await ensureDir(ClusterManager.clusterIconDir)
fileName = filenamify(cluster.contextName + "-" + fileName)
+30 -33
View File
@@ -3,12 +3,13 @@ import { FeatureStatusMap } from "./feature"
import * as k8s from "./k8s"
import { clusterStore } from "../common/cluster-store"
import logger from "./logger"
import { KubeConfig, CoreV1Api, AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
import * as fm from "./feature-manager";
import { Kubectl } from "./kubectl";
import { PromiseIpc } from "electron-promise-ipc"
import * as request from "request-promise-native"
import request from "request-promise-native"
import { KubeconfigManager } from "./kubeconfig-manager"
import { apiPrefix } from "../common/vars";
enum ClusterStatus {
AccessGranted = 2,
@@ -123,16 +124,9 @@ export class Cluster implements ClusterInfo {
this.contextHandler.setClusterPreferences(this.preferences)
const connectionStatus = await this.getConnectionStatus()
if (connectionStatus == ClusterStatus.AccessGranted) {
this.accessible = true
} else {
this.accessible = false
}
if (connectionStatus > ClusterStatus.Offline) {
this.online = true
} else {
this.online = false
}
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
this.online = connectionStatus > ClusterStatus.Offline;
if (this.accessible) {
this.distribution = this.detectKubernetesDistribution(this.version)
this.features = await fm.getFeatures(this.contextHandler)
@@ -146,7 +140,9 @@ export class Cluster implements ClusterInfo {
public updateKubeconfig(kubeconfig: string) {
const storedCluster = clusterStore.getCluster(this.id)
if (!storedCluster) { return }
if (!storedCluster) {
return
}
this.kubeConfig = kubeconfig
this.save()
@@ -164,7 +160,7 @@ export class Cluster implements ClusterInfo {
}
public toClusterInfo(): ClusterInfo {
const clusterInfo: ClusterInfo = {
return {
id: this.id,
workspace: this.workspace,
url: this.url,
@@ -182,17 +178,16 @@ export class Cluster implements ClusterInfo {
kubeConfig: this.kubeConfig,
preferences: this.preferences
}
return clusterInfo;
}
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
const options = Object.assign({
json: true, timeout: 10000
json: true,
timeout: 10000
}, (opts || {}))
if (!options.headers) { options.headers = {} }
options.headers.host = `${this.id}.localhost:${this.port}`
return request(`http://127.0.0.1:${this.port}/api-kube${path}`, options)
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
}
protected async getConnectionStatus() {
@@ -201,21 +196,24 @@ export class Cluster implements ClusterInfo {
this.version = response.gitVersion
this.failureReason = null
return ClusterStatus.AccessGranted;
} catch(error) {
} catch (error) {
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`)
if (error.statusCode) {
if (error.statusCode >= 400 && error.statusCode < 500) {
this.failureReason = "Invalid credentials";
return ClusterStatus.AccessDenied;
} else {
}
else {
this.failureReason = error.error || error.message;
return ClusterStatus.Offline;
}
} else if (error.failed === true) {
}
else if (error.failed === true) {
if (error.timedOut === true) {
this.failureReason = "Connection timed out";
return ClusterStatus.Offline;
} else {
}
else {
this.failureReason = "Failed to fetch credentials";
return ClusterStatus.AccessDenied;
}
@@ -234,7 +232,7 @@ export class Cluster implements ClusterInfo {
spec: { resourceAttributes }
})
return accessReview.body.status.allowed === true
} catch(error) {
} catch (error) {
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
return false
}
@@ -258,10 +256,10 @@ export class Cluster implements ClusterInfo {
else if (kubernetesVersion.includes("IKS")) {
return "iks"
}
else if(this.apiUrl.endsWith("azmk8s.io")) {
else if (this.apiUrl.endsWith("azmk8s.io")) {
return "aks"
}
else if(this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
else if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
return "digitalocean"
}
else if (this.contextHandler.contextName.startsWith("minikube")) {
@@ -274,11 +272,11 @@ export class Cluster implements ClusterInfo {
return "vanilla"
}
protected async getNodeCount() {
protected async getNodeCount() {
try {
const response = await this.k8sRequest("/api/v1/nodes")
return response.items.length
} catch(error) {
} catch (error) {
logger.debug(`failed to request node list: ${error.message}`)
return null
}
@@ -294,18 +292,17 @@ export class Cluster implements ClusterInfo {
const uniqEventSources = new Set();
const warnings = response.body.items.filter(e => e.type !== 'Normal');
for (const w of warnings) {
if(w.involvedObject.kind === 'Pod') {
if (w.involvedObject.kind === 'Pod') {
try {
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
if(k8s.podHasIssues(pod)) {
if (k8s.podHasIssues(pod)) {
uniqEventSources.add(w.involvedObject.uid);
}
continue;
} catch (error) {
continue;
} catch (err) {
}
} else {
}
else {
uniqEventSources.add(w.involvedObject.uid);
}
}
+18 -19
View File
@@ -1,6 +1,5 @@
import { KubeConfig, CoreV1Api } from "@kubernetes/client-node"
import { readFileSync } from "fs"
import * as http from "http"
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node"
import http from "http"
import { ServerOptions } from "http-proxy"
import * as url from "url"
import logger from "./logger"
@@ -8,8 +7,7 @@ import { getFreePort } from "./port"
import { KubeAuthProxy } from "./kube-auth-proxy"
import { Cluster, ClusterPreferences } from "./cluster"
import { prometheusProviders } from "../common/prometheus-providers"
import { PrometheusService, PrometheusProvider } from "./prometheus/provider-registry"
import { PrometheusLens } from "./prometheus/lens"
import { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"
export class ContextHandler {
public contextName: string
@@ -76,19 +74,21 @@ export class ContextHandler {
if (clusterPreferences && clusterPreferences.prometheus) {
const prom = clusterPreferences.prometheus
this.prometheusPath = `${prom.namespace}/services/${prom.service}:${prom.port}`
} else {
}
else {
this.prometheusPath = null
}
if(clusterPreferences && clusterPreferences.clusterName) {
if (clusterPreferences && clusterPreferences.clusterName) {
this.clusterName = clusterPreferences.clusterName;
} else {
}
else {
this.clusterName = this.contextName;
}
}
protected async resolvePrometheusPath(): Promise<string> {
const service = await this.getPrometheusService()
return `${service.namespace}/services/${service.service}:${service.port}`
const {service, namespace, port} = await this.getPrometheusService()
return `${namespace}/services/${service}:${port}`
}
public async getPrometheusProvider() {
@@ -110,7 +110,8 @@ export class ContextHandler {
const service = resolvedPrometheusServices.filter(n => n)[0]
if (service) {
return service
} else {
}
else {
return {
id: "lens",
namespace: "lens-metrics",
@@ -128,7 +129,7 @@ export class ContextHandler {
return this.prometheusPath
}
public async getApiTarget(isWatchRequest = false) {
public async getApiTarget(isWatchRequest = false): Promise<ServerOptions> {
if (this.apiTarget && !isWatchRequest) {
return this.apiTarget
}
@@ -140,7 +141,7 @@ export class ContextHandler {
return apiTarget
}
protected async newApiTarget(timeout: number) {
protected async newApiTarget(timeout: number): Promise<ServerOptions> {
return {
changeOrigin: true,
timeout: timeout,
@@ -162,7 +163,7 @@ export class ContextHandler {
let serverPort: number = null
try {
serverPort = await getFreePort()
} catch(error) {
} catch (error) {
logger.error(error)
throw(error)
}
@@ -178,7 +179,7 @@ export class ContextHandler {
public async withTemporaryKubeconfig(callback: (kubeconfig: string) => Promise<any>) {
try {
await callback(this.cluster.kubeconfigPath())
} catch(error) {
} catch (error) {
throw(error)
}
}
@@ -187,7 +188,7 @@ export class ContextHandler {
if (!this.proxyServer) {
const proxyPort = await this.resolveProxyPort()
const proxyEnv = Object.assign({}, process.env)
if (this.cluster.preferences && this.cluster.preferences.httpsProxy) {
if (this.cluster?.preferences.httpsProxy) {
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy
}
this.proxyServer = new KubeAuthProxy(this.cluster, proxyPort, proxyEnv)
@@ -203,8 +204,6 @@ export class ContextHandler {
}
public proxyServerError() {
if (!this.proxyServer) { return null }
return this.proxyServer.lastError
return this.proxyServer?.lastError || ""
}
}
+7 -3
View File
@@ -1,5 +1,5 @@
import * as fs from "fs";
import * as path from "path"
import fs from "fs";
import path from "path"
import * as hb from "handlebars"
import { ResourceApplier } from "./resource-applier"
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node"
@@ -99,6 +99,10 @@ export abstract class Feature {
}
protected manifestPath() {
return path.join(__dirname, '..', 'features', this.name);
const devPath = path.join(__dirname, "..", 'src/features', this.name);
if(fs.existsSync(devPath)) {
return devPath;
}
return path.join(__dirname, "..", 'features', this.name);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import * as fs from "fs"
import fs from "fs"
export function ensureDir(dirname: string) {
if (!fs.existsSync(dirname)) {
+1 -1
View File
@@ -1,4 +1,4 @@
import * as fs from "fs";
import fs from "fs";
import * as yaml from "js-yaml";
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
import logger from "./logger";
+10 -11
View File
@@ -1,5 +1,7 @@
import * as path from "path"
import packageInfo from "../../package.json"
import path from "path"
import { LensBinary, LensBinaryOpts } from "./lens-binary"
import { isProduction } from "../common/vars";
export class HelmCli extends LensBinary {
@@ -13,7 +15,7 @@ export class HelmCli extends LensBinary {
super(opts)
}
protected getTarName(): string|null {
protected getTarName(): string | null {
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
}
@@ -26,19 +28,16 @@ export class HelmCli extends LensBinary {
}
protected getOriginalBinaryPath() {
return path.join(this.dirname, this.platformName+"-"+this.arch, this.originalBinaryName)
return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName)
}
}
const helmVersion = require("../../package.json").config.bundledHelmVersion
const isDevelopment = process.env.NODE_ENV !== "production"
let baseDir: string = null
const helmVersion = packageInfo.config.bundledHelmVersion;
let baseDir = process.resourcesPath;
if(isDevelopment) {
baseDir = path.join(process.cwd(), "binaries", "client")
} else {
baseDir = path.join(process.resourcesPath)
if (!isProduction) {
baseDir = path.join(process.cwd(), "binaries", "client");
}
export const helmCli = new HelmCli(baseDir, helmVersion)
export const helmCli = new HelmCli(baseDir, helmVersion);
+4 -5
View File
@@ -1,25 +1,24 @@
import * as tempy from "tempy";
import * as fs from "fs";
import fs from "fs";
import * as yaml from "js-yaml";
import * as camelcaseKeys from "camelcase-keys";
import { promiseExec} from "./promise-exec"
import { helmCli } from "./helm-cli";
import { Cluster } from "./cluster";
import { toCamelCase } from "../common/utils/camelCase";
export class HelmReleaseManager {
public async listReleases(pathToKubeconfig: string, namespace?: string) {
const helm = await helmCli.binaryPath()
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"
const { stdout, stderr } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
const output = JSON.parse(stdout)
if (output.length == 0) {
return output
}
const result: any = []
output.forEach((release: any, index: number) => {
output[index] = camelcaseKeys(release)
output[index] = toCamelCase(release)
});
return output
}
+1 -1
View File
@@ -1,4 +1,4 @@
import * as fs from "fs";
import fs from "fs";
import logger from "./logger";
import * as yaml from "js-yaml";
import { promiseExec } from "./promise-exec";
+18 -11
View File
@@ -1,7 +1,10 @@
// Main process
import "../common/system-ca"
import "../common/prometheus-providers"
import { app, dialog, protocol } from "electron"
import { PromiseIpc } from "electron-promise-ipc"
import * as path from "path"
import path from "path"
import { format as formatUrl } from "url"
import logger from "./logger"
import initMenu from "./menu"
@@ -15,20 +18,21 @@ import { shellSync } from "./shell-sync"
import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env"
import { findMainWebContents } from "./webcontents"
import "../common/prometheus-providers"
import { registerStaticProtocol } from "../common/register-static";
import { isMac, vueAppName } from "../common/vars";
mangleProxyEnv()
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
}
const isDevelopment = process.env.NODE_ENV !== "production"
const promiseIpc = new PromiseIpc({ timeout: 2000 })
const promiseIpc = new PromiseIpc({ timeout: 2000 })
let windowManager: WindowManager = null;
let clusterManager: ClusterManager = null;
let proxyServer: proxy.LensProxy = null;
const vmURL = (isDevelopment) ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}` : formatUrl({
pathname: path.join(__dirname, "index.html"),
const vmURL = formatUrl({
pathname: path.join(__dirname, `${vueAppName}.html`),
protocol: "file",
slashes: true,
})
@@ -40,12 +44,15 @@ async function main() {
updater.start();
tracker.event("app", "start");
registerStaticProtocol();
protocol.registerFileProtocol('store', (request, callback) => {
const url = request.url.substr(8)
callback( path.normalize(`${app.getPath("userData")}/${url}`) )
callback(path.normalize(`${app.getPath("userData")}/${url}`))
}, (error) => {
if (error) console.error('Failed to register protocol')
})
let port: number = null
// find free port
try {
@@ -80,7 +87,7 @@ async function main() {
},
showPreferencesHook: async () => {
// IPC send needs webContents as we're sending it to renderer
promiseIpc.send('navigate', findMainWebContents(), {name: 'preferences-page'}).then((data: any) => {
promiseIpc.send('navigate', findMainWebContents(), { name: 'preferences-page' }).then((data: any) => {
logger.debug("navigate: preferences IPC sent");
})
},
@@ -103,10 +110,10 @@ async function main() {
}
app.on("ready", main)
app.on('window-all-closed', function() {
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform != 'darwin') {
if (!isMac) {
app.quit();
} else {
windowManager = null
@@ -116,7 +123,7 @@ app.on('window-all-closed', function() {
app.on("activate", () => {
if (!windowManager) {
logger.debug("activate main window")
windowManager = new WindowManager(false)
windowManager = new WindowManager({ showSplash: false })
windowManager.showMain(vmURL)
}
})
+1 -3
View File
@@ -1,8 +1,6 @@
import * as k8s from "@kubernetes/client-node"
import * as os from "os"
import { all } from "q";
import * as yaml from "js-yaml"
import { V1beta1ValidatingWebhookConfiguration } from "@kubernetes/client-node";
import logger from "./logger";
const kc = new k8s.KubeConfig()
@@ -31,7 +29,7 @@ export function loadConfig(kubeconfig: string): k8s.KubeConfig {
*
* @param config KubeConfig to check
*/
export function valideConfig(config: k8s.KubeConfig): boolean {
export function validateConfig(config: k8s.KubeConfig): boolean {
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
if(!config.users || config.users.length == 0) {
throw new Error("No users provided in config")
+6 -7
View File
@@ -28,9 +28,7 @@ export class KubeAuthProxy {
public async run(): Promise<void> {
if (this.proxyProcess) {
return new Promise((resolve, reject) => {
resolve()
})
return;
}
const proxyBin = await this.kubectl.kubectlPath()
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => {
@@ -43,11 +41,10 @@ export class KubeAuthProxy {
}
}
})
configWatcher.on("error", () => {})
const clusterUrl = url.parse(this.cluster.apiUrl)
let args = [
"proxy",
"-p", this.port.toString(),
"--port", this.port.toString(),
"--kubeconfig", this.cluster.kubeconfigPath(),
"--accept-hosts", clusterUrl.hostname,
]
@@ -59,7 +56,9 @@ export class KubeAuthProxy {
})
this.proxyProcess.on("exit", (code) => {
logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`)
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((_) => {})
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((err: Error) => {
logger.debug("failed to send IPC log message: " + err.message)
})
this.proxyProcess = null
configWatcher.close()
})
@@ -96,7 +95,7 @@ export class KubeAuthProxy {
}
protected async sendIpcLogMessage(data: string, stream: string) {
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data: data, stream: stream })
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data, stream })
}
public exit() {
+1 -1
View File
@@ -1,5 +1,5 @@
import { app } from "electron"
import * as fs from "fs"
import fs from "fs"
import { ensureDir, randomFileName} from "./file-helpers"
import logger from "./logger"
+12 -14
View File
@@ -1,17 +1,17 @@
import { app, remote } from "electron"
import * as path from "path"
import * as fs from "fs"
import * as request from "request"
import path from "path"
import fs from "fs"
import request from "request"
import { promiseExec} from "./promise-exec"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import * as md5File from "md5-file"
import { globalRequestOpts } from "../common/request"
import * as lockFile from "proper-lockfile"
import { helmCli } from "./helm-cli"
import { userStore } from "../common/user-store"
import { getBundledKubectlVersion} from "../common/utils/app-version"
const bundledVersion = require("../../package.json").config.bundledKubectlVersion
const bundledVersion = getBundledKubectlVersion()
const kubectlMap: Map<string, string> = new Map([
["1.7", "1.8.15"],
["1.8", "1.9.10"],
@@ -195,16 +195,16 @@ export class Kubectl {
const file = fs.createWriteStream(this.path)
stream.on("complete", () => {
logger.debug("kubectl binary download finished")
file.end(() => {})
file.end()
})
stream.on("error", (error) => {
logger.error(error)
fs.unlink(this.path, () => {})
fs.unlink(this.path, null)
reject(error)
})
file.on("close", () => {
logger.debug("kubectl binary download closed")
fs.chmod(this.path, 0o755, () => {})
fs.chmod(this.path, 0o755, null)
resolve()
})
stream.pipe(file)
@@ -285,13 +285,11 @@ export class Kubectl {
}
protected getDownloadMirror() {
if (process.platform == "darwin") {
return packageMirrors.get("default") // MacOS packages are only available from default
}
const mirror = packageMirrors.get(userStore.getPreferences().downloadMirror)
if (mirror) { return mirror }
return packageMirrors.get("default")
if (mirror) {
return mirror
}
return packageMirrors.get("default") // MacOS packages are only available from default
}
}
+29
View File
@@ -0,0 +1,29 @@
import packageInfo from "../../package.json"
import { bundledKubectl, Kubectl } from "../../src/main/kubectl";
import { UserStore } from "../common/user-store";
jest.mock("../common/user-store", () => {
const userStoreMock: Partial<UserStore> = {
getPreferences() {
return {
downloadMirror: "default"
}
}
}
return {
userStore: userStoreMock,
}
})
describe("kubectlVersion", () => {
it("returns bundled version if exactly same version used", async () => {
const kubectl = new Kubectl(bundledKubectl.kubectlVersion)
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
})
it("returns bundled version if same major.minor version is used", async () => {
const { bundledKubectlVersion } = packageInfo.config;
const kubectl = new Kubectl(bundledKubectlVersion);
expect(kubectl.kubectlVersion).toBe(bundledKubectl.kubectlVersion)
})
})
+1 -1
View File
@@ -1,4 +1,4 @@
import * as http from "http";
import http from "http";
export abstract class LensApi {
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
+27 -20
View File
@@ -1,9 +1,10 @@
import * as path from "path"
import * as fs from "fs"
import * as request from "request"
import path from "path"
import fs from "fs"
import request from "request"
import logger from "./logger"
import { ensureDir, pathExists } from "fs-extra"
import * as tar from "tar"
import { isWindows } from "../common/vars";
export type LensBinaryOpts = {
version: string;
@@ -12,6 +13,7 @@ export type LensBinaryOpts = {
newBinaryName?: string;
requestOpts?: request.Options;
}
export class LensBinary {
public binaryVersion: string
@@ -35,19 +37,21 @@ export class LensBinary {
let arch = null
if(process.arch == "x64") {
if (process.arch == "x64") {
arch = "amd64"
} else if(process.arch == "x86" || process.arch == "ia32") {
}
else if (process.arch == "x86" || process.arch == "ia32") {
arch = "386"
} else {
}
else {
arch = process.arch
}
this.arch = arch
this.platformName = process.platform === "win32" ? "windows" : process.platform
this.platformName = isWindows ? "windows" : process.platform
this.dirname = path.normalize(path.join(baseDir, this.binaryName))
if (process.platform === "win32") {
this.binaryName = this.binaryName+".exe"
this.originalBinaryName = this.originalBinaryName+".exe"
if (isWindows) {
this.binaryName = this.binaryName + ".exe"
this.originalBinaryName = this.originalBinaryName + ".exe"
}
const tarName = this.getTarName()
if (tarName) {
@@ -64,7 +68,7 @@ export class LensBinary {
return this.getBinaryPath()
}
protected getTarName(): string|null {
protected getTarName(): string | null {
return null
}
@@ -88,7 +92,7 @@ export class LensBinary {
try {
await this.ensureBinary()
return this.dirname
} catch(err) {
} catch (err) {
logger.error(err)
return ""
}
@@ -101,10 +105,12 @@ export class LensBinary {
public async ensureBinary() {
const isValid = await this.checkBinary()
if(!isValid) {
await this.downloadBinary().catch((error) => { logger.error(error) });
if (!isValid) {
await this.downloadBinary().catch((error) => {
logger.error(error)
});
if (this.tarPath) await this.untarBinary()
if(this.originalBinaryName != this.binaryName ) await this.renameBinary()
if (this.originalBinaryName != this.binaryName) await this.renameBinary()
logger.info(`${this.originalBinaryName} has been downloaded to ${this.getBinaryPath()}`)
}
}
@@ -127,7 +133,8 @@ export class LensBinary {
fs.rename(this.getOriginalBinaryPath(), this.getBinaryPath(), (err) => {
if (err) {
reject(err)
} else {
}
else {
resolve()
}
})
@@ -135,7 +142,7 @@ export class LensBinary {
}
protected async downloadBinary() {
const binaryPath = this.tarPath || this.getBinaryPath()
const binaryPath = this.tarPath || this.getBinaryPath()
await ensureDir(this.getBinaryDir(), 0o755)
const file = fs.createWriteStream(binaryPath)
@@ -152,18 +159,18 @@ export class LensBinary {
stream.on("complete", () => {
logger.info(`Download of ${this.originalBinaryName} finished`)
file.end(() => {})
file.end()
})
stream.on("error", (error) => {
logger.error(error)
fs.unlink(binaryPath, () => {})
fs.unlink(binaryPath, null)
throw(error)
})
return new Promise((resolve, reject) => {
file.on("close", () => {
logger.debug(`${this.originalBinaryName} binary download closed`)
if(!this.tarPath) fs.chmod(binaryPath, 0o755, () => {})
if (!this.tarPath) fs.chmod(binaryPath, 0o755, null)
resolve()
})
stream.pipe(file)
+3 -2
View File
@@ -1,10 +1,11 @@
import * as winston from "winston"
import winston from "winston"
import { isDebugging } from "../common/vars";
const options = {
colorize: true,
handleExceptions: false,
json: false,
level: process.env.DEBUG === "true" ? "debug" : "info",
level: isDebugging ? "debug" : "info",
}
const logger = winston.createLogger({
+18 -17
View File
@@ -1,4 +1,7 @@
import {app, dialog, Menu, MenuItemConstructorOptions, shell, webContents, BrowserWindow, MenuItem} from "electron"
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
import { isDevelopment, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
// todo: refactor + split menu sections to separated files, e.g. menus/file.menu.ts
export interface MenuOptions {
logoutHook: any;
@@ -10,7 +13,6 @@ export interface MenuOptions {
}
function setClusterSettingsEnabled(enabled: boolean) {
const isMac = process.platform === 'darwin';
const menuIndex = isMac ? 1 : 0
Menu.getApplicationMenu().items[menuIndex].submenu.items[1].enabled = enabled
}
@@ -21,7 +23,7 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
]
appDetails.push(`Copyright 2020 Lakend Labs, Inc.`)
let title = "Lens"
if (process.platform === "win32") {
if (isWindows) {
title = ` ${title}`
}
dialog.showMessageBoxSync(browserWindow, {
@@ -40,9 +42,6 @@ function showAbout(_menuitem: MenuItem, browserWindow: BrowserWindow) {
* @param ipc the main promiceIpc handle. Needed to be able to hook IPC sending into logout click handler.
*/
export default function initMenu(opts: MenuOptions, promiseIpc: any) {
const isMac = process.platform === 'darwin';
const isDevelopment = process.env.NODE_ENV === 'development';
const mt: MenuItemConstructorOptions[] = [];
const macAppMenu: MenuItemConstructorOptions = {
label: app.getName(),
@@ -67,12 +66,12 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{ role: 'quit' }
]
};
if(isMac) {
if (isMac) {
mt.push(macAppMenu);
}
let fileMenu: MenuItemConstructorOptions;
if(isMac) {
if (isMac) {
fileMenu = {
label: 'File',
submenu: [{
@@ -86,7 +85,8 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
}
]
}
} else {
}
else {
fileMenu = {
label: 'File',
submenu: [
@@ -134,27 +134,28 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{
label: 'Back',
accelerator: 'CmdOrCtrl+[',
click () {
click() {
webContents.getFocusedWebContents().executeJavaScript('window.history.back()')
}
},
{
label: 'Forward',
accelerator: 'CmdOrCtrl+]',
click () {
click() {
webContents.getFocusedWebContents().executeJavaScript('window.history.forward()')
}
},
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click () {
click() {
webContents.getFocusedWebContents().reload()
}
},
...(isDevelopment ? [
{ role: 'toggleDevTools'} as MenuItemConstructorOptions,
{ role: 'toggleDevTools' } as MenuItemConstructorOptions,
{
accelerator: "CmdOrCtrl+Shift+I",
label: 'Open Dashboard Devtools',
click() {
webContents.getFocusedWebContents().openDevTools()
@@ -183,20 +184,20 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
{
label: 'Community Slack',
click: async () => {
shell.openExternal('https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI');
shell.openExternal(slackUrl);
},
},
{
label: 'Report an Issue',
click: async () => {
shell.openExternal('https://github.com/lensapp/lens/issues');
shell.openExternal(issuesTrackerUrl);
},
},
{
label: "What's new?",
click: opts.showWhatsNewHook,
},
...(process.platform !== "darwin" ? [{
...(!isMac ? [{
label: "About Lens",
click: showAbout
} as MenuItemConstructorOptions] : [])
@@ -214,4 +215,4 @@ export default function initMenu(opts: MenuOptions, promiseIpc: any) {
promiseIpc.on("disableClusterSettingsMenuItem", () => {
setClusterSettingsEnabled(false)
});
};
}
+1 -2
View File
@@ -1,6 +1,5 @@
import logger from "./logger"
import { createServer } from "net"
import { AddressInfo } from "net"
import { createServer, AddressInfo } from "net"
const getNextAvailablePort = () => {
logger.debug("getNextAvailablePort() start")
+26
View File
@@ -0,0 +1,26 @@
import { EventEmitter } from 'events'
import { getFreePort } from "./port"
jest.mock("net", () => {
return {
createServer() {
return new class MockServer extends EventEmitter {
listen = jest.fn(() => {
this.emit('listening')
return this
})
address = () => {
return { port: 12345 }
}
unref = jest.fn()
close = jest.fn(cb => cb())
}
},
}
});
describe("getFreePort", () => {
it("finds the next free port", async () => {
return expect(getFreePort()).resolves.toEqual(expect.any(Number))
})
})
+14 -27
View File
@@ -1,5 +1,5 @@
import * as http from "http";
import * as httpProxy from "http-proxy";
import http from "http";
import httpProxy from "http-proxy";
import { Socket } from "net";
import * as url from "url";
import * as WebSocket from "ws"
@@ -8,6 +8,7 @@ import logger from "./logger"
import * as shell from "./node-shell-session"
import { ClusterManager } from "./cluster-manager"
import { Router } from "./router"
import { apiPrefix } from "../common/vars";
export class LensProxy {
public static readonly localShellSessions = true
@@ -40,17 +41,15 @@ export class LensProxy {
protected buildProxyServer() {
const proxy = this.createProxy();
const proxyServer = http.createServer(function(req: http.IncomingMessage, res: http.ServerResponse) {
const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
this.handleRequest(proxy, req, res);
}.bind(this));
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
});
proxyServer.on("upgrade", (req: http.IncomingMessage, socket: Socket, head: Buffer) => {
this.handleWsUpgrade(req, socket, head)
}.bind(this));
});
proxyServer.on("error", (err) => {
logger.error(err)
});
return proxyServer;
}
@@ -64,11 +63,10 @@ export class LensProxy {
res.writeHead(proxyRes.statusCode, {
"Content-Type": "text/plain"
})
res.end(cluster.contextHandler.proxyServerError().toString())
res.end(cluster.contextHandler.proxyServerError())
return
}
}
if (req.method !== "GET") {
return
}
@@ -106,11 +104,11 @@ export class LensProxy {
}
protected createWsListener() {
const ws = new WebSocket.Server({ noServer: true})
const ws = new WebSocket.Server({ noServer: true })
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => {
const cluster = this.clusterManager.getClusterForRequest(req)
const contextHandler = cluster.contextHandler
const nodeParam = this.getNodeParam(req.url)
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => {
return new Promise<boolean>(async (resolve, reject) => {
@@ -120,26 +118,15 @@ export class LensProxy {
})
})
})
}).bind(this))
}))
return ws
}
protected getNodeParam(requestUrl: string) {
const reqUrl = url.parse(requestUrl, true)
const urlParams = reqUrl.query
let nodeParam: string = null
for (const [key, value] of Object.entries(urlParams)) {
if (key === "node") {
nodeParam = value.toString()
}
}
return nodeParam
}
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
if (req.url.startsWith("/api-kube/")) {
const prefix = apiPrefix.KUBE_BASE;
if (req.url.startsWith(prefix)) {
delete req.headers.authorization
req.url = req.url.replace("/api-kube", "")
req.url = req.url.replace(prefix, "")
const isWatchRequest = req.url.includes("watch=")
return await contextHandler.getApiTarget(isWatchRequest)
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { exec } from "child_process";
import * as fs from "fs";
import fs from "fs";
import * as yaml from "js-yaml";
import * as path from "path";
import path from "path";
import * as tempy from "tempy";
import logger from "./logger"
import { Cluster } from "./cluster";
+9 -15
View File
@@ -1,5 +1,8 @@
import * as http from "http"
import * as path from "path"
import Call from "@hapi/call"
import Subtext from "@hapi/subtext"
import http from "http"
import path from "path"
import { readFile } from "fs"
import { Cluster } from "./cluster"
import { configRoute } from "./routes/config"
import { helmApi } from "./helm-api"
@@ -8,16 +11,7 @@ import { kubeconfigRoute } from "./routes/kubeconfig"
import { metricsRoute } from "./routes/metrics"
import { watchRoute } from "./routes/watch"
import { portForwardRoute } from "./routes/port-forward"
import { readFile } from "fs"
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Call = require('@hapi/call');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Subtext = require('@hapi/subtext');
declare const __static: string;
const assetsPath = path.join(__static, "build/client")
import { outDir, reactAppName } from "../common/vars";
const mimeTypes: {[key: string]: string} = {
"html": "text/html",
@@ -88,12 +82,12 @@ export class Router {
return request
}
protected handleStaticFile(file: string, response: http.ServerResponse) {
const asset = path.join(assetsPath, file)
protected handleStaticFile(filePath: string, response: http.ServerResponse) {
const asset = path.resolve(outDir, filePath);
readFile(asset, (err, data) => {
if (err) {
// default to index.html so that react routes work when page is refreshed
this.handleStaticFile("index.html", response)
this.handleStaticFile(`${reactAppName}.html`, response)
} else {
const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain";
response.setHeader("Content-Type", type);
+23 -9
View File
@@ -1,10 +1,24 @@
import { app } from "electron"
import { CoreV1Api } from "@kubernetes/client-node"
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { userStore } from "../../common/user-store"
import { getAppVersion } from "../../common/app-utils"
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
import { Cluster } from "../cluster"
export interface IConfigRoutePayload {
kubeVersion?: string;
clusterName?: string;
lensVersion?: string;
lensTheme?: string;
username?: string;
token?: string;
allowedNamespaces?: string[];
allowedResources?: string[];
isClusterAdmin?: boolean;
chartsEnabled: boolean;
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
}
// TODO: auto-populate all resources dynamically
const apiResources = [
{ resource: "configmaps" },
@@ -44,12 +58,13 @@ async function getAllowedNamespaces(cluster: Cluster) {
return namespaceList.body.items
.filter((ns, i) => nsAccessStatuses[i])
.map(ns => ns.metadata.name)
} catch(error) {
} catch (error) {
const kc = cluster.contextHandler.kc
const ctx = kc.getContextObject(kc.currentContext)
if (ctx.namespace) {
return [ctx.namespace]
} else {
}
else {
return []
}
}
@@ -67,20 +82,19 @@ async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
)
return apiResources
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
} catch(error) {
} catch (error) {
return []
}
}
class ConfigRoute extends LensApi {
public async routeConfig(request: LensApiRequest) {
const { params, response, cluster} = request
const { params, response, cluster } = request
const namespaces = await getAllowedNamespaces(cluster)
const data = {
const data: IConfigRoutePayload = {
clusterName: cluster.contextName,
lensVersion: getAppVersion(),
lensVersion: app.getVersion(),
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
kubeVersion: cluster.version,
chartsEnabled: true,
+6 -5
View File
@@ -1,9 +1,10 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import * as requestPromise from "request-promise-native"
import requestPromise from "request-promise-native"
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry"
import { apiPrefix } from "../../common/vars";
type MetricsQuery = string | string[] | {
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;
}
@@ -11,13 +12,13 @@ class MetricsRoute extends LensApi {
public async routeMetrics(request: LensApiRequest) {
const { response, cluster} = request
const query: MetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
const query: IMetricsQuery = request.payload;
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}`
const headers = {
"Host": `${cluster.id}.localhost:${cluster.port}`,
"Content-type": "application/json",
}
const queryParams: MetricsQuery = {}
const queryParams: IMetricsQuery = {}
request.query.forEach((value: string, key: string) => {
queryParams[key] = value
})
+2 -2
View File
@@ -1,6 +1,6 @@
import { LensApiRequest } from "../router"
import { LensApi } from "../lens-api"
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
import { Watch, KubeConfig } from "@kubernetes/client-node"
import { ServerResponse } from "http"
import { Request } from "request"
import logger from "../logger"
@@ -41,7 +41,7 @@ class ApiWatcher {
this.watchRequest.abort()
}
private watchHandler(phase: string, obj: RuntimeRawExtension) {
private watchHandler(phase: string, obj: any) {
this.eventBuffer.push({
type: phase,
object: obj
Loaded 100 of 786 files, more files were not shown because too many files have changed in this diff. Show more