@ -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
.babelrc
Normal 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"
|
||||
]
|
||||
}
|
||||
@ -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: [
|
||||
|
||||
|
Before Width: | Height: | Size: 754 KiB After Width: | Height: | Size: 754 KiB |
2
.gitignore
vendored
@ -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
|
||||
|
||||
2
.yarnrc
@ -1,3 +1,3 @@
|
||||
disturl "https://atom.io/download/electron"
|
||||
target "6.1.10"
|
||||
target "6.1.12"
|
||||
runtime "electron"
|
||||
|
||||
51
Makefile
@ -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/*
|
||||
|
||||
22
README.md
@ -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.
|
||||
|
||||
[](https://youtu.be/04v2ODsmtIs)
|
||||
[](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.
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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') },
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"macros",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
]
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
.idea
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
wireframes/
|
||||
backup
|
||||
npm-debug.log
|
||||
.vscode
|
||||
.env
|
||||
/tslint.json
|
||||
*.DS_Store
|
||||
docker-compose.yml
|
||||
13
dashboard/.gitignore
vendored
@ -1,13 +0,0 @@
|
||||
.idea
|
||||
node_modules
|
||||
build/
|
||||
dist/
|
||||
wireframes/
|
||||
backup
|
||||
npm-debug.log
|
||||
.vscode
|
||||
dump.rdb
|
||||
*.env
|
||||
/tslint.json
|
||||
*.DS_Store
|
||||
locales/_build/
|
||||
@ -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")
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
@ -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');
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
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 |
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,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"
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export interface IClusterInfo {
|
||||
kubeVersion?: string;
|
||||
clusterName?: string;
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
[metricName: string]: string | object;
|
||||
}
|
||||
@ -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;
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"extends": "../client/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../build",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "esnext",
|
||||
"sourceMap": false,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"include": [
|
||||
"./app.ts"
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
],
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
const Enzyme = require("enzyme");
|
||||
const Adapter = require("enzyme-adapter-react-16");
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "../client/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"target": "es6",
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -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
dashboard/yarn.lock
@ -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)
|
||||
2490
locales/en/messages.po
Normal file
2473
locales/fi/messages.po
Normal file
2498
locales/ru/messages.po
Normal file
318
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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))
|
||||
})
|
||||
})
|
||||
@ -1,9 +0,0 @@
|
||||
const userStore = {
|
||||
getPreferences: jest.fn(() => {
|
||||
return {
|
||||
downloadMirror: "default"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export { userStore };
|
||||
@ -1,14 +0,0 @@
|
||||
import { app, remote } from "electron"
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns app version correctly regardless of dev/prod mode and main/renderer differences
|
||||
*/
|
||||
export function getAppVersion(): string {
|
||||
// app is undefined when running in renderer
|
||||
let version = (app || remote.app).getVersion()
|
||||
if(process.env.NODE_ENV === 'development') {
|
||||
version = require("../../package.json").version
|
||||
}
|
||||
return version;
|
||||
}
|
||||
@ -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
src/common/register-static.ts
Normal 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);
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
import * as winca from "win-ca/api"
|
||||
import "mac-ca"
|
||||
import { isMac, isWindows } from "./vars";
|
||||
import winca from "win-ca"
|
||||
import macca from "mac-ca"
|
||||
import logger from "../main/logger"
|
||||
|
||||
if (process.platform === "win32") {
|
||||
if (isMac) {
|
||||
for (const crt of macca.all()) {
|
||||
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`)
|
||||
logger.debug("Using host CA: " + attributes.join(","))
|
||||
}
|
||||
}
|
||||
if (isWindows) {
|
||||
winca.inject("+") // see: https://github.com/ukoloff/win-ca#caveats
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { machineIdSync } from 'node-machine-id'
|
||||
import { userStore } from "../common/user-store"
|
||||
import * as ua from "universal-analytics"
|
||||
import ua from "universal-analytics"
|
||||
import { machineIdSync } from "node-machine-id"
|
||||
import { userStore } from "./user-store"
|
||||
|
||||
const GA_ID = "UA-159377374-1"
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as ElectronStore from "electron-store"
|
||||
import * as appUtil from "./app-utils"
|
||||
import * as version210Beta4 from "./migrations/user-store/2.1.0-beta.4"
|
||||
import ElectronStore from "electron-store"
|
||||
import * as version210Beta4 from "../migrations/user-store/2.1.0-beta.4"
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
|
||||
export interface User {
|
||||
id?: string;
|
||||
@ -20,7 +20,9 @@ export class UserStore {
|
||||
|
||||
private constructor() {
|
||||
this.store = new ElectronStore({
|
||||
projectVersion: appUtil.getAppVersion(),
|
||||
// @ts-ignore
|
||||
// fixme: tests are failed without "projectVersion"
|
||||
projectVersion: getAppVersion(),
|
||||
migrations: {
|
||||
"2.1.0-beta.4": version210Beta4.migration,
|
||||
}
|
||||
@ -68,7 +70,7 @@ export class UserStore {
|
||||
}
|
||||
|
||||
static getInstance(): UserStore {
|
||||
if(!UserStore.instance) {
|
||||
if (!UserStore.instance) {
|
||||
UserStore.instance = new UserStore();
|
||||
}
|
||||
return UserStore.instance;
|
||||
|
||||
@ -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
src/common/utils/app-version.ts
Normal 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;
|
||||
}
|
||||
18
src/common/utils/camelCase.ts
Normal 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
src/common/utils/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// Common utils (main/renderer)
|
||||
|
||||
export * from "./base64"
|
||||
export * from "./camelCase"
|
||||
export * from "./splitArray"
|
||||
@ -1,5 +1,6 @@
|
||||
// Moved from dashboard/client/utils/arrays.ts
|
||||
/**
|
||||
* This function splits an array into two sub arrays on the first instance of
|
||||
* This function splits an array into two sub arrays on the first instance of
|
||||
* element (from the left). If the array does not contain the element. The
|
||||
* return value is defined to be `[array, [], false]`. If the element is in
|
||||
* the array then the return value is `[left, right, true]` where `left` is
|
||||
@ -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
src/common/utils/splitArray_test.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { splitArray } from "./splitArray";
|
||||
|
||||
describe("split array on element tests", () => {
|
||||
test("empty array", () => {
|
||||
expect(splitArray([], 10)).toStrictEqual([[], [], false]);
|
||||
});
|
||||
|
||||
test("one element, not in array", () => {
|
||||
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
|
||||
});
|
||||
|
||||
test("ten elements, not in array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
|
||||
});
|
||||
|
||||
test("one elements, in array", () => {
|
||||
expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in front array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in middle array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
|
||||
});
|
||||
|
||||
test("ten elements, in end array", () => {
|
||||
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
|
||||
});
|
||||
});
|
||||
39
src/common/vars.ts
Normal file
@ -0,0 +1,39 @@
|
||||
// App's common configuration for any process (main, renderer, build pipeline, etc.)
|
||||
import path from "path";
|
||||
|
||||
// Temp
|
||||
export const reactAppName = "app_react"
|
||||
export const vueAppName = "app_vue"
|
||||
|
||||
// Flags
|
||||
export const isMac = process.platform === "darwin"
|
||||
export const isWindows = process.platform === "win32"
|
||||
export const isDebugging = process.env.DEBUG === "true";
|
||||
export const isProduction = process.env.NODE_ENV === "production"
|
||||
export const isDevelopment = isDebugging || !isProduction;
|
||||
export const buildVersion = process.env.BUILD_VERSION;
|
||||
export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
||||
|
||||
// Paths
|
||||
export const contextDir = process.cwd();
|
||||
export const staticDir = path.join(contextDir, "static");
|
||||
export const outDir = path.join(contextDir, "out");
|
||||
export const mainDir = path.join(contextDir, "src/main");
|
||||
export const rendererDir = path.join(contextDir, "src/renderer");
|
||||
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
||||
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
||||
|
||||
// Apis
|
||||
export const staticProto = "static://"
|
||||
|
||||
export const apiPrefix = {
|
||||
BASE: '/api',
|
||||
TERMINAL: '/api-terminal', // terminal api
|
||||
KUBE_BASE: '/api-kube', // kubernetes cluster api
|
||||
KUBE_HELM: '/api-helm', // helm charts api
|
||||
KUBE_RESOURCE_APPLIER: "/api-resource",
|
||||
};
|
||||
|
||||
// Links
|
||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues"
|
||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI"
|
||||
@ -1,4 +1,4 @@
|
||||
import * as ElectronStore from "electron-store"
|
||||
import ElectronStore from "electron-store"
|
||||
import { clusterStore } from "./cluster-store"
|
||||
|
||||
export interface WorkspaceData {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Feature, FeatureStatus } from "../main/feature"
|
||||
import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node"
|
||||
import * as semver from "semver"
|
||||
import semver from "semver"
|
||||
import { Cluster } from "../main/cluster";
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -3,12 +3,13 @@ import { FeatureStatusMap } from "./feature"
|
||||
import * as k8s from "./k8s"
|
||||
import { clusterStore } from "../common/cluster-store"
|
||||
import logger from "./logger"
|
||||
import { KubeConfig, CoreV1Api, AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||
import * as fm from "./feature-manager";
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { PromiseIpc } from "electron-promise-ipc"
|
||||
import * as request from "request-promise-native"
|
||||
import request from "request-promise-native"
|
||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
enum ClusterStatus {
|
||||
AccessGranted = 2,
|
||||
@ -123,16 +124,9 @@ export class Cluster implements ClusterInfo {
|
||||
this.contextHandler.setClusterPreferences(this.preferences)
|
||||
|
||||
const connectionStatus = await this.getConnectionStatus()
|
||||
if (connectionStatus == ClusterStatus.AccessGranted) {
|
||||
this.accessible = true
|
||||
} else {
|
||||
this.accessible = false
|
||||
}
|
||||
if (connectionStatus > ClusterStatus.Offline) {
|
||||
this.online = true
|
||||
} else {
|
||||
this.online = false
|
||||
}
|
||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||
this.online = connectionStatus > ClusterStatus.Offline;
|
||||
|
||||
if (this.accessible) {
|
||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||
this.features = await fm.getFeatures(this.contextHandler)
|
||||
@ -146,7 +140,9 @@ export class Cluster implements ClusterInfo {
|
||||
|
||||
public updateKubeconfig(kubeconfig: string) {
|
||||
const storedCluster = clusterStore.getCluster(this.id)
|
||||
if (!storedCluster) { return }
|
||||
if (!storedCluster) {
|
||||
return
|
||||
}
|
||||
|
||||
this.kubeConfig = kubeconfig
|
||||
this.save()
|
||||
@ -164,7 +160,7 @@ export class Cluster implements ClusterInfo {
|
||||
}
|
||||
|
||||
public toClusterInfo(): ClusterInfo {
|
||||
const clusterInfo: ClusterInfo = {
|
||||
return {
|
||||
id: this.id,
|
||||
workspace: this.workspace,
|
||||
url: this.url,
|
||||
@ -179,20 +175,19 @@ export class Cluster implements ClusterInfo {
|
||||
isAdmin: this.isAdmin,
|
||||
features: this.features,
|
||||
kubeCtl: this.kubeCtl,
|
||||
kubeConfig: this.kubeConfig,
|
||||
kubeConfig: this.kubeConfig,
|
||||
preferences: this.preferences
|
||||
}
|
||||
return clusterInfo;
|
||||
}
|
||||
|
||||
protected async k8sRequest(path: string, opts?: request.RequestPromiseOptions) {
|
||||
const options = Object.assign({
|
||||
json: true, timeout: 10000
|
||||
json: true,
|
||||
timeout: 10000
|
||||
}, (opts || {}))
|
||||
if (!options.headers) { options.headers = {} }
|
||||
options.headers.host = `${this.id}.localhost:${this.port}`
|
||||
|
||||
return request(`http://127.0.0.1:${this.port}/api-kube${path}`, options)
|
||||
return request(`http://127.0.0.1:${this.port}${apiPrefix.KUBE_BASE}${path}`, options)
|
||||
}
|
||||
|
||||
protected async getConnectionStatus() {
|
||||
@ -201,21 +196,24 @@ export class Cluster implements ClusterInfo {
|
||||
this.version = response.gitVersion
|
||||
this.failureReason = null
|
||||
return ClusterStatus.AccessGranted;
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(`Failed to connect to cluster ${this.contextName}: ${JSON.stringify(error)}`)
|
||||
if (error.statusCode) {
|
||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||
this.failureReason = "Invalid credentials";
|
||||
return ClusterStatus.AccessDenied;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.failureReason = error.error || error.message;
|
||||
return ClusterStatus.Offline;
|
||||
}
|
||||
} else if (error.failed === true) {
|
||||
}
|
||||
else if (error.failed === true) {
|
||||
if (error.timedOut === true) {
|
||||
this.failureReason = "Connection timed out";
|
||||
return ClusterStatus.Offline;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
this.failureReason = "Failed to fetch credentials";
|
||||
return ClusterStatus.AccessDenied;
|
||||
}
|
||||
@ -234,7 +232,7 @@ export class Cluster implements ClusterInfo {
|
||||
spec: { resourceAttributes }
|
||||
})
|
||||
return accessReview.body.status.allowed === true
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.error(`failed to request selfSubjectAccessReview: ${error.message}`)
|
||||
return false
|
||||
}
|
||||
@ -258,10 +256,10 @@ export class Cluster implements ClusterInfo {
|
||||
else if (kubernetesVersion.includes("IKS")) {
|
||||
return "iks"
|
||||
}
|
||||
else if(this.apiUrl.endsWith("azmk8s.io")) {
|
||||
else if (this.apiUrl.endsWith("azmk8s.io")) {
|
||||
return "aks"
|
||||
}
|
||||
else if(this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
|
||||
else if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) {
|
||||
return "digitalocean"
|
||||
}
|
||||
else if (this.contextHandler.contextName.startsWith("minikube")) {
|
||||
@ -274,11 +272,11 @@ export class Cluster implements ClusterInfo {
|
||||
return "vanilla"
|
||||
}
|
||||
|
||||
protected async getNodeCount() {
|
||||
protected async getNodeCount() {
|
||||
try {
|
||||
const response = await this.k8sRequest("/api/v1/nodes")
|
||||
return response.items.length
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
logger.debug(`failed to request node list: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
@ -294,18 +292,17 @@ export class Cluster implements ClusterInfo {
|
||||
const uniqEventSources = new Set();
|
||||
const warnings = response.body.items.filter(e => e.type !== 'Normal');
|
||||
for (const w of warnings) {
|
||||
if(w.involvedObject.kind === 'Pod') {
|
||||
if (w.involvedObject.kind === 'Pod') {
|
||||
try {
|
||||
const pod = (await client.readNamespacedPod(w.involvedObject.name, w.involvedObject.namespace)).body;
|
||||
logger.debug(`checking pod ${w.involvedObject.namespace}/${w.involvedObject.name}`)
|
||||
if(k8s.podHasIssues(pod)) {
|
||||
if (k8s.podHasIssues(pod)) {
|
||||
uniqEventSources.add(w.involvedObject.uid);
|
||||
}
|
||||
continue;
|
||||
} catch (error) {
|
||||
continue;
|
||||
} catch (err) {
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
uniqEventSources.add(w.involvedObject.uid);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 || ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path"
|
||||
import fs from "fs";
|
||||
import path from "path"
|
||||
import * as hb from "handlebars"
|
||||
import { ResourceApplier } from "./resource-applier"
|
||||
import { KubeConfig, CoreV1Api, Watch } from "@kubernetes/client-node"
|
||||
@ -99,6 +99,10 @@ export abstract class Feature {
|
||||
}
|
||||
|
||||
protected manifestPath() {
|
||||
return path.join(__dirname, '..', 'features', this.name);
|
||||
const devPath = path.join(__dirname, "..", 'src/features', this.name);
|
||||
if(fs.existsSync(devPath)) {
|
||||
return devPath;
|
||||
}
|
||||
return path.join(__dirname, "..", 'features', this.name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as fs from "fs"
|
||||
import fs from "fs"
|
||||
|
||||
export function ensureDir(dirname: string) {
|
||||
if (!fs.existsSync(dirname)) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import { HelmRepo, HelmRepoManager } from "./helm-repo-manager"
|
||||
import logger from "./logger";
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import * as path from "path"
|
||||
import packageInfo from "../../package.json"
|
||||
import path from "path"
|
||||
import { LensBinary, LensBinaryOpts } from "./lens-binary"
|
||||
import { isProduction } from "../common/vars";
|
||||
|
||||
export class HelmCli extends LensBinary {
|
||||
|
||||
@ -13,7 +15,7 @@ export class HelmCli extends LensBinary {
|
||||
super(opts)
|
||||
}
|
||||
|
||||
protected getTarName(): string|null {
|
||||
protected getTarName(): string | null {
|
||||
return `${this.binaryName}-v${this.binaryVersion}-${this.platformName}-${this.arch}.tar.gz`
|
||||
}
|
||||
|
||||
@ -26,19 +28,16 @@ export class HelmCli extends LensBinary {
|
||||
}
|
||||
|
||||
protected getOriginalBinaryPath() {
|
||||
return path.join(this.dirname, this.platformName+"-"+this.arch, this.originalBinaryName)
|
||||
return path.join(this.dirname, this.platformName + "-" + this.arch, this.originalBinaryName)
|
||||
}
|
||||
}
|
||||
|
||||
const helmVersion = require("../../package.json").config.bundledHelmVersion
|
||||
const isDevelopment = process.env.NODE_ENV !== "production"
|
||||
let baseDir: string = null
|
||||
const helmVersion = packageInfo.config.bundledHelmVersion;
|
||||
let baseDir = process.resourcesPath;
|
||||
|
||||
if(isDevelopment) {
|
||||
baseDir = path.join(process.cwd(), "binaries", "client")
|
||||
} else {
|
||||
baseDir = path.join(process.resourcesPath)
|
||||
if (!isProduction) {
|
||||
baseDir = path.join(process.cwd(), "binaries", "client");
|
||||
}
|
||||
|
||||
export const helmCli = new HelmCli(baseDir, helmVersion)
|
||||
export const helmCli = new HelmCli(baseDir, helmVersion);
|
||||
|
||||
|
||||
@ -1,25 +1,24 @@
|
||||
import * as tempy from "tempy";
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as camelcaseKeys from "camelcase-keys";
|
||||
import { promiseExec} from "./promise-exec"
|
||||
import { helmCli } from "./helm-cli";
|
||||
import { Cluster } from "./cluster";
|
||||
import { toCamelCase } from "../common/utils/camelCase";
|
||||
|
||||
export class HelmReleaseManager {
|
||||
|
||||
public async listReleases(pathToKubeconfig: string, namespace?: string) {
|
||||
const helm = await helmCli.binaryPath()
|
||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"
|
||||
const { stdout, stderr } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
|
||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr)})
|
||||
|
||||
const output = JSON.parse(stdout)
|
||||
if (output.length == 0) {
|
||||
return output
|
||||
}
|
||||
const result: any = []
|
||||
output.forEach((release: any, index: number) => {
|
||||
output[index] = camelcaseKeys(release)
|
||||
output[index] = toCamelCase(release)
|
||||
});
|
||||
return output
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import logger from "./logger";
|
||||
import * as yaml from "js-yaml";
|
||||
import { promiseExec } from "./promise-exec";
|
||||
|
||||
@ -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,8 +1,6 @@
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
import * as os from "os"
|
||||
import { all } from "q";
|
||||
import * as yaml from "js-yaml"
|
||||
import { V1beta1ValidatingWebhookConfiguration } from "@kubernetes/client-node";
|
||||
import logger from "./logger";
|
||||
|
||||
const kc = new k8s.KubeConfig()
|
||||
@ -31,7 +29,7 @@ export function loadConfig(kubeconfig: string): k8s.KubeConfig {
|
||||
*
|
||||
* @param config KubeConfig to check
|
||||
*/
|
||||
export function valideConfig(config: k8s.KubeConfig): boolean {
|
||||
export function validateConfig(config: k8s.KubeConfig): boolean {
|
||||
logger.debug(`validating kube config: ${JSON.stringify(config)}`)
|
||||
if(!config.users || config.users.length == 0) {
|
||||
throw new Error("No users provided in config")
|
||||
|
||||
@ -28,9 +28,7 @@ export class KubeAuthProxy {
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.proxyProcess) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve()
|
||||
})
|
||||
return;
|
||||
}
|
||||
const proxyBin = await this.kubectl.kubectlPath()
|
||||
const configWatcher = watch(this.cluster.kubeconfigPath(), (eventType: string, filename: string) => {
|
||||
@ -43,11 +41,10 @@ export class KubeAuthProxy {
|
||||
}
|
||||
}
|
||||
})
|
||||
configWatcher.on("error", () => {})
|
||||
const clusterUrl = url.parse(this.cluster.apiUrl)
|
||||
let args = [
|
||||
"proxy",
|
||||
"-p", this.port.toString(),
|
||||
"--port", this.port.toString(),
|
||||
"--kubeconfig", this.cluster.kubeconfigPath(),
|
||||
"--accept-hosts", clusterUrl.hostname,
|
||||
]
|
||||
@ -59,7 +56,9 @@ export class KubeAuthProxy {
|
||||
})
|
||||
this.proxyProcess.on("exit", (code) => {
|
||||
logger.error(`proxy ${this.cluster.contextName} exited with code ${code}`)
|
||||
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((_) => {})
|
||||
this.sendIpcLogMessage( `proxy exited with code ${code}`, "stderr").catch((err: Error) => {
|
||||
logger.debug("failed to send IPC log message: " + err.message)
|
||||
})
|
||||
this.proxyProcess = null
|
||||
configWatcher.close()
|
||||
})
|
||||
@ -96,7 +95,7 @@ export class KubeAuthProxy {
|
||||
}
|
||||
|
||||
protected async sendIpcLogMessage(data: string, stream: string) {
|
||||
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data: data, stream: stream })
|
||||
await this.promiseIpc.send(`kube-auth:${this.cluster.id}`, findMainWebContents(), { data, stream })
|
||||
}
|
||||
|
||||
public exit() {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { app } from "electron"
|
||||
import * as fs from "fs"
|
||||
import fs from "fs"
|
||||
import { ensureDir, randomFileName} from "./file-helpers"
|
||||
import logger from "./logger"
|
||||
|
||||
|
||||
@ -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
src/main/kubectl_spec.ts
Normal 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,4 +1,4 @@
|
||||
import * as http from "http";
|
||||
import http from "http";
|
||||
|
||||
export abstract class LensApi {
|
||||
protected respondJson(res: http.ServerResponse, content: {}, status = 200) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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,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
src/main/port_spec.ts
Normal 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))
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,5 @@
|
||||
import * as http from "http";
|
||||
import * as httpProxy from "http-proxy";
|
||||
import http from "http";
|
||||
import httpProxy from "http-proxy";
|
||||
import { Socket } from "net";
|
||||
import * as url from "url";
|
||||
import * as WebSocket from "ws"
|
||||
@ -8,6 +8,7 @@ import logger from "./logger"
|
||||
import * as shell from "./node-shell-session"
|
||||
import { ClusterManager } from "./cluster-manager"
|
||||
import { Router } from "./router"
|
||||
import { apiPrefix } from "../common/vars";
|
||||
|
||||
export class LensProxy {
|
||||
public static readonly localShellSessions = true
|
||||
@ -40,17 +41,15 @@ export class LensProxy {
|
||||
|
||||
protected buildProxyServer() {
|
||||
const proxy = this.createProxy();
|
||||
const proxyServer = http.createServer(function(req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const proxyServer = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
this.handleRequest(proxy, req, res);
|
||||
}.bind(this));
|
||||
proxyServer.on("upgrade", function(req: http.IncomingMessage, socket: Socket, head: Buffer) {
|
||||
});
|
||||
proxyServer.on("upgrade", (req: http.IncomingMessage, socket: Socket, head: Buffer) => {
|
||||
this.handleWsUpgrade(req, socket, head)
|
||||
}.bind(this));
|
||||
|
||||
});
|
||||
proxyServer.on("error", (err) => {
|
||||
logger.error(err)
|
||||
});
|
||||
|
||||
return proxyServer;
|
||||
}
|
||||
|
||||
@ -64,11 +63,10 @@ export class LensProxy {
|
||||
res.writeHead(proxyRes.statusCode, {
|
||||
"Content-Type": "text/plain"
|
||||
})
|
||||
res.end(cluster.contextHandler.proxyServerError().toString())
|
||||
res.end(cluster.contextHandler.proxyServerError())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method !== "GET") {
|
||||
return
|
||||
}
|
||||
@ -106,11 +104,11 @@ export class LensProxy {
|
||||
}
|
||||
|
||||
protected createWsListener() {
|
||||
const ws = new WebSocket.Server({ noServer: true})
|
||||
const ws = new WebSocket.Server({ noServer: true })
|
||||
ws.on("connection", ((con: WebSocket, req: http.IncomingMessage) => {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req)
|
||||
const contextHandler = cluster.contextHandler
|
||||
const nodeParam = this.getNodeParam(req.url)
|
||||
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
||||
|
||||
contextHandler.withTemporaryKubeconfig((kubeconfigPath) => {
|
||||
return new Promise<boolean>(async (resolve, reject) => {
|
||||
@ -120,26 +118,15 @@ export class LensProxy {
|
||||
})
|
||||
})
|
||||
})
|
||||
}).bind(this))
|
||||
}))
|
||||
return ws
|
||||
}
|
||||
|
||||
protected getNodeParam(requestUrl: string) {
|
||||
const reqUrl = url.parse(requestUrl, true)
|
||||
const urlParams = reqUrl.query
|
||||
let nodeParam: string = null
|
||||
for (const [key, value] of Object.entries(urlParams)) {
|
||||
if (key === "node") {
|
||||
nodeParam = value.toString()
|
||||
}
|
||||
}
|
||||
return nodeParam
|
||||
}
|
||||
|
||||
protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise<httpProxy.ServerOptions> {
|
||||
if (req.url.startsWith("/api-kube/")) {
|
||||
const prefix = apiPrefix.KUBE_BASE;
|
||||
if (req.url.startsWith(prefix)) {
|
||||
delete req.headers.authorization
|
||||
req.url = req.url.replace("/api-kube", "")
|
||||
req.url = req.url.replace(prefix, "")
|
||||
const isWatchRequest = req.url.includes("watch=")
|
||||
return await contextHandler.getApiTarget(isWatchRequest)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { exec } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import fs from "fs";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as path from "path";
|
||||
import path from "path";
|
||||
import * as tempy from "tempy";
|
||||
import logger from "./logger"
|
||||
import { Cluster } from "./cluster";
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import * as http from "http"
|
||||
import * as path from "path"
|
||||
import Call from "@hapi/call"
|
||||
import Subtext from "@hapi/subtext"
|
||||
import http from "http"
|
||||
import path from "path"
|
||||
import { readFile } from "fs"
|
||||
import { Cluster } from "./cluster"
|
||||
import { configRoute } from "./routes/config"
|
||||
import { helmApi } from "./helm-api"
|
||||
@ -8,16 +11,7 @@ import { kubeconfigRoute } from "./routes/kubeconfig"
|
||||
import { metricsRoute } from "./routes/metrics"
|
||||
import { watchRoute } from "./routes/watch"
|
||||
import { portForwardRoute } from "./routes/port-forward"
|
||||
import { readFile } from "fs"
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Call = require('@hapi/call');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Subtext = require('@hapi/subtext');
|
||||
|
||||
declare const __static: string;
|
||||
|
||||
const assetsPath = path.join(__static, "build/client")
|
||||
import { outDir, reactAppName } from "../common/vars";
|
||||
|
||||
const mimeTypes: {[key: string]: string} = {
|
||||
"html": "text/html",
|
||||
@ -88,12 +82,12 @@ export class Router {
|
||||
return request
|
||||
}
|
||||
|
||||
protected handleStaticFile(file: string, response: http.ServerResponse) {
|
||||
const asset = path.join(assetsPath, file)
|
||||
protected handleStaticFile(filePath: string, response: http.ServerResponse) {
|
||||
const asset = path.resolve(outDir, filePath);
|
||||
readFile(asset, (err, data) => {
|
||||
if (err) {
|
||||
// default to index.html so that react routes work when page is refreshed
|
||||
this.handleStaticFile("index.html", response)
|
||||
this.handleStaticFile(`${reactAppName}.html`, response)
|
||||
} else {
|
||||
const type = mimeTypes[path.extname(asset).slice(1)] || "text/plain";
|
||||
response.setHeader("Content-Type", type);
|
||||
|
||||
@ -1,10 +1,24 @@
|
||||
import { app } from "electron"
|
||||
import { CoreV1Api } from "@kubernetes/client-node"
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { userStore } from "../../common/user-store"
|
||||
import { getAppVersion } from "../../common/app-utils"
|
||||
import { CoreV1Api, AuthorizationV1Api } from "@kubernetes/client-node"
|
||||
import { Cluster } from "../cluster"
|
||||
|
||||
export interface IConfigRoutePayload {
|
||||
kubeVersion?: string;
|
||||
clusterName?: string;
|
||||
lensVersion?: string;
|
||||
lensTheme?: string;
|
||||
username?: string;
|
||||
token?: string;
|
||||
allowedNamespaces?: string[];
|
||||
allowedResources?: string[];
|
||||
isClusterAdmin?: boolean;
|
||||
chartsEnabled: boolean;
|
||||
kubectlAccess?: boolean; // User accessed via kubectl-lens plugin
|
||||
}
|
||||
|
||||
// TODO: auto-populate all resources dynamically
|
||||
const apiResources = [
|
||||
{ resource: "configmaps" },
|
||||
@ -44,12 +58,13 @@ async function getAllowedNamespaces(cluster: Cluster) {
|
||||
return namespaceList.body.items
|
||||
.filter((ns, i) => nsAccessStatuses[i])
|
||||
.map(ns => ns.metadata.name)
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
const kc = cluster.contextHandler.kc
|
||||
const ctx = kc.getContextObject(kc.currentContext)
|
||||
if (ctx.namespace) {
|
||||
return [ctx.namespace]
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@ -67,20 +82,19 @@ async function getAllowedResources(cluster: Cluster, namespaces: string[]) {
|
||||
)
|
||||
return apiResources
|
||||
.filter((resource, i) => resourceAccessStatuses[i]).map(apiResource => apiResource.resource)
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigRoute extends LensApi {
|
||||
|
||||
public async routeConfig(request: LensApiRequest) {
|
||||
const { params, response, cluster} = request
|
||||
const { params, response, cluster } = request
|
||||
|
||||
const namespaces = await getAllowedNamespaces(cluster)
|
||||
const data = {
|
||||
const data: IConfigRoutePayload = {
|
||||
clusterName: cluster.contextName,
|
||||
lensVersion: getAppVersion(),
|
||||
lensVersion: app.getVersion(),
|
||||
lensTheme: `kontena-${userStore.getPreferences().colorTheme}`,
|
||||
kubeVersion: cluster.version,
|
||||
chartsEnabled: true,
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import * as requestPromise from "request-promise-native"
|
||||
import requestPromise from "request-promise-native"
|
||||
import { PrometheusProviderRegistry, PrometheusProvider, PrometheusNodeQuery, PrometheusClusterQuery, PrometheusPodQuery, PrometheusPvcQuery, PrometheusIngressQuery, PrometheusQueryOpts} from "../prometheus/provider-registry"
|
||||
import { apiPrefix } from "../../common/vars";
|
||||
|
||||
type MetricsQuery = string | string[] | {
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
[metricName: string]: string;
|
||||
}
|
||||
|
||||
@ -11,13 +12,13 @@ class MetricsRoute extends LensApi {
|
||||
|
||||
public async routeMetrics(request: LensApiRequest) {
|
||||
const { response, cluster} = request
|
||||
const query: MetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}/api-kube`
|
||||
const query: IMetricsQuery = request.payload;
|
||||
const serverUrl = `http://127.0.0.1:${cluster.port}${apiPrefix.KUBE_BASE}`
|
||||
const headers = {
|
||||
"Host": `${cluster.id}.localhost:${cluster.port}`,
|
||||
"Content-type": "application/json",
|
||||
}
|
||||
const queryParams: MetricsQuery = {}
|
||||
const queryParams: IMetricsQuery = {}
|
||||
request.query.forEach((value: string, key: string) => {
|
||||
queryParams[key] = value
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { LensApiRequest } from "../router"
|
||||
import { LensApi } from "../lens-api"
|
||||
import { Watch, KubeConfig, RuntimeRawExtension } from "@kubernetes/client-node"
|
||||
import { Watch, KubeConfig } from "@kubernetes/client-node"
|
||||
import { ServerResponse } from "http"
|
||||
import { Request } from "request"
|
||||
import logger from "../logger"
|
||||
@ -41,7 +41,7 @@ class ApiWatcher {
|
||||
this.watchRequest.abort()
|
||||
}
|
||||
|
||||
private watchHandler(phase: string, obj: RuntimeRawExtension) {
|
||||
private watchHandler(phase: string, obj: any) {
|
||||
this.eventBuffer.push({
|
||||
type: phase,
|
||||
object: obj
|
||||
|
||||