Merge branch 'master' into tag-exposed-extension-stores-as-beta
@ -41,10 +41,12 @@ jobs:
|
|||||||
displayName: Generate npm package
|
displayName: Generate npm package
|
||||||
- script: make -j2 build-extensions
|
- script: make -j2 build-extensions
|
||||||
displayName: Build bundled extensions
|
displayName: Build bundled extensions
|
||||||
- script: make integration-win
|
- script: make test
|
||||||
displayName: Run integration tests
|
displayName: Run tests
|
||||||
- script: make test-extensions
|
- script: make test-extensions
|
||||||
displayName: Run In-tree Extension tests
|
displayName: Run In-tree Extension tests
|
||||||
|
- script: make integration-win
|
||||||
|
displayName: Run integration tests
|
||||||
- script: make build
|
- script: make build
|
||||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||||
displayName: Build
|
displayName: Build
|
||||||
|
|||||||
28
.eslintrc.js
@ -4,6 +4,7 @@ module.exports = {
|
|||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
"**/node_modules/**/*",
|
"**/node_modules/**/*",
|
||||||
"**/dist/**/*",
|
"**/dist/**/*",
|
||||||
|
"**/static/**/*",
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
react: {
|
react: {
|
||||||
@ -49,6 +50,15 @@ module.exports = {
|
|||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"error",
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "block-like" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "function" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "class" },
|
||||||
|
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
|
||||||
|
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -94,6 +104,15 @@ module.exports = {
|
|||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"error",
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "block-like" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "function" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "class" },
|
||||||
|
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
|
||||||
|
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -146,6 +165,15 @@ module.exports = {
|
|||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
"prefer-template": "error",
|
"prefer-template": "error",
|
||||||
"template-curly-spacing": "error",
|
"template-curly-spacing": "error",
|
||||||
|
"padding-line-between-statements": [
|
||||||
|
"error",
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "return" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "block-like" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "function" },
|
||||||
|
{ "blankLine": "always", "prev": "*", "next": "class" },
|
||||||
|
{ "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" },
|
||||||
|
{ "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"]},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -17,14 +17,15 @@ export async function generateTrayIcon(
|
|||||||
outputFilename += shouldUseDarkColors ? "_dark" : "";
|
outputFilename += shouldUseDarkColors ? "_dark" : "";
|
||||||
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
|
dpiSuffix = dpiSuffix !== "1x" ? `@${dpiSuffix}` : "";
|
||||||
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
|
const pngIconDestPath = path.resolve(outputFolder, `${outputFilename}${dpiSuffix}.png`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Modify .SVG colors
|
// Modify .SVG colors
|
||||||
const trayIconColor = shouldUseDarkColors ? "white" : "black";
|
const trayIconColor = shouldUseDarkColors ? "white" : "black";
|
||||||
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
|
const svgDom = await jsdom.JSDOM.fromFile(svgIconPath);
|
||||||
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
const svgRoot = svgDom.window.document.body.getElementsByTagName("svg")[0];
|
||||||
|
|
||||||
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
|
svgRoot.innerHTML += `<style>* {fill: ${trayIconColor} !important;}</style>`;
|
||||||
const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
|
const svgIconBuffer = Buffer.from(svgRoot.outerHTML);
|
||||||
|
|
||||||
// Resize and convert to .PNG
|
// Resize and convert to .PNG
|
||||||
const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
|
const pngIconBuffer: Buffer = await sharp(svgIconBuffer)
|
||||||
.resize({ width: pixelSize, height: pixelSize })
|
.resize({ width: pixelSize, height: pixelSize })
|
||||||
@ -45,6 +46,7 @@ const iconSizes: Record<string, number> = {
|
|||||||
"2x": 32,
|
"2x": 32,
|
||||||
"3x": 48,
|
"3x": 48,
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
|
Object.entries(iconSizes).forEach(([dpiSuffix, pixelSize]) => {
|
||||||
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
|
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: false });
|
||||||
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
|
generateTrayIcon({ dpiSuffix, pixelSize, shouldUseDarkColors: true });
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class KubectlDownloader {
|
|||||||
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
|
constructor(clusterVersion: string, platform: string, arch: string, target: string) {
|
||||||
this.kubectlVersion = clusterVersion;
|
this.kubectlVersion = clusterVersion;
|
||||||
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
|
const binaryName = platform === "windows" ? "kubectl.exe" : "kubectl";
|
||||||
|
|
||||||
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
|
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${this.kubectlVersion}/bin/${platform}/${arch}/${binaryName}`;
|
||||||
this.dirname = path.dirname(target);
|
this.dirname = path.dirname(target);
|
||||||
this.path = target;
|
this.path = target;
|
||||||
@ -30,16 +31,20 @@ class KubectlDownloader {
|
|||||||
if (response.headers["etag"]) {
|
if (response.headers["etag"]) {
|
||||||
return response.headers["etag"].replace(/"/g, "");
|
return response.headers["etag"].replace(/"/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkBinary() {
|
public async checkBinary() {
|
||||||
const exists = await pathExists(this.path);
|
const exists = await pathExists(this.path);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
const hash = md5File.sync(this.path);
|
const hash = md5File.sync(this.path);
|
||||||
const etag = await this.urlEtag();
|
const etag = await this.urlEtag();
|
||||||
|
|
||||||
if(hash == etag) {
|
if(hash == etag) {
|
||||||
console.log("Kubectl md5sum matches the remote etag");
|
console.log("Kubectl md5sum matches the remote etag");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,13 +57,16 @@ class KubectlDownloader {
|
|||||||
|
|
||||||
public async downloadKubectl() {
|
public async downloadKubectl() {
|
||||||
const exists = await this.checkBinary();
|
const exists = await this.checkBinary();
|
||||||
|
|
||||||
if(exists) {
|
if(exists) {
|
||||||
console.log("Already exists and is valid");
|
console.log("Already exists and is valid");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ensureDir(path.dirname(this.path), 0o755);
|
await ensureDir(path.dirname(this.path), 0o755);
|
||||||
|
|
||||||
const file = fs.createWriteStream(this.path);
|
const file = fs.createWriteStream(this.path);
|
||||||
|
|
||||||
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
console.log(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||||
const requestOpts: request.UriOptions & request.CoreOptions = {
|
const requestOpts: request.UriOptions & request.CoreOptions = {
|
||||||
uri: this.url,
|
uri: this.url,
|
||||||
@ -78,6 +86,7 @@ class KubectlDownloader {
|
|||||||
fs.unlink(this.path, () => {});
|
fs.unlink(this.path, () => {});
|
||||||
throw(error);
|
throw(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
file.on("close", () => {
|
file.on("close", () => {
|
||||||
console.log("kubectl binary download closed");
|
console.log("kubectl binary download closed");
|
||||||
@ -103,6 +112,7 @@ const downloads = [
|
|||||||
downloads.forEach((dlOpts) => {
|
downloads.forEach((dlOpts) => {
|
||||||
console.log(dlOpts);
|
console.log(dlOpts);
|
||||||
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
const downloader = new KubectlDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
||||||
|
|
||||||
console.log(`Downloading: ${JSON.stringify(dlOpts)}`);
|
console.log(`Downloading: ${JSON.stringify(dlOpts)}`);
|
||||||
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
|
downloader.downloadKubectl().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,9 +2,11 @@ const { notarize } = require("electron-notarize");
|
|||||||
|
|
||||||
exports.default = async function notarizing(context) {
|
exports.default = async function notarizing(context) {
|
||||||
const { electronPlatformName, appOutDir } = context;
|
const { electronPlatformName, appOutDir } = context;
|
||||||
|
|
||||||
if (electronPlatformName !== "darwin") {
|
if (electronPlatformName !== "darwin") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.env.APPLEID || !process.env.APPLEIDPASS) {
|
if (!process.env.APPLEID || !process.env.APPLEIDPASS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 478 B After Width: | Height: | Size: 478 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 448 B |
|
Before Width: | Height: | Size: 973 B |
|
Before Width: | Height: | Size: 1.4 KiB |
@ -225,7 +225,7 @@ import { HelpIcon, HelpPage } from "./page"
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
export default class HelpExtension extends LensRendererExtension {
|
export default class HelpExtension extends LensRendererExtension {
|
||||||
clusterPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
id: "help",
|
id: "help",
|
||||||
components: {
|
components: {
|
||||||
@ -379,6 +379,74 @@ The `uninstall()` method is implemented in the example above by utilizing the [`
|
|||||||
|
|
||||||
The `updateStatus()` method is implemented above by using the [`K8sApi`](tbd) as well, this time to get information from the `example-pod` pod, in particular to determine if it is installed, what version is associated with it, and if it can be upgraded. How the status is updated for a specific cluster feature is up to the implementation.
|
The `updateStatus()` method is implemented above by using the [`K8sApi`](tbd) as well, this time to get information from the `example-pod` pod, in particular to determine if it is installed, what version is associated with it, and if it can be upgraded. How the status is updated for a specific cluster feature is up to the implementation.
|
||||||
|
|
||||||
|
### `appPreferences`
|
||||||
|
|
||||||
|
The Preferences page is a built-in global page. Extensions can add custom preferences to the Preferences page, thus providing a single location for users to configure global options, for Lens and extensions alike. The following example demonstrates adding a custom preference:
|
||||||
|
|
||||||
|
``` typescript
|
||||||
|
import { LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
import { ExamplePreference, ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default class ExampleRendererExtension extends LensRendererExtension {
|
||||||
|
|
||||||
|
@observable preference: ExamplePreference = { enabled: false };
|
||||||
|
|
||||||
|
appPreferences = [
|
||||||
|
{
|
||||||
|
title: "Example Preferences",
|
||||||
|
components: {
|
||||||
|
Input: () => <ExamplePreferenceInput preference={this.preference}/>,
|
||||||
|
Hint: () => <ExamplePreferenceHint/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
App preferences are objects matching the `AppPreferenceRegistration` interface. The `title` field specifies the text to show as the heading on the Preferences page. The `components` field specifies two `React.Component` objects defining the interface for the preference. `Input` should specify an interactive input element for your preference and `Hint` should provide descriptive information for the preference, which is shown below the `Input` element. `ExamplePreferenceInput` expects its React props set to an `ExamplePreference` instance, which is how `ExampleRendererExtension` handles the state of the preference input. `ExampleRendererExtension` has the field `preference`, which is provided to `ExamplePreferenceInput` when it is created. In this example `ExamplePreferenceInput`, `ExamplePreferenceHint`, and `ExamplePreference` are defined in `./src/example-preference.tsx`:
|
||||||
|
|
||||||
|
``` typescript
|
||||||
|
import { Component } from "@k8slens/extensions";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type ExamplePreference = {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ExamplePreferenceInput extends React.Component<{preference: ExamplePreference}, {}> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { preference } = this.props;
|
||||||
|
return (
|
||||||
|
<Component.Checkbox
|
||||||
|
label="I understand appPreferences"
|
||||||
|
value={preference.enabled}
|
||||||
|
onChange={v => { preference.enabled = v; }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExamplePreferenceHint extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<span>This is an example of an appPreference for extensions.</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ExamplePreferenceInput` implements a simple checkbox (using Lens' `Component.Checkbox`). It provides `label` as the text to display next to the checkbox and an `onChange` function, which reacts to the checkbox state change. The checkbox's `value` is initially set to `preference.enabled`. `ExamplePreferenceInput` is defined with React props of `ExamplePreference` type, which has a single field, `enabled`. This is used to indicate the state of the preference, and is bound to the checkbox state in `onChange`. `ExamplePreferenceHint` is a simple text span. Note that the input and the hint could comprise of more sophisticated elements, according to the needs of the extension.
|
||||||
|
|
||||||
|
Note that the above example introduces decorators `observable` and `observer` from the [`mobx`](https://mobx.js.org/README.html) and [`mobx-react`](https://github.com/mobxjs/mobx-react#mobx-react) packages. `mobx` simplifies state management and without it this example would not visually update the checkbox properly when the user activates it. [Lens uses `mobx` extensively for state management](../working-with-mobx) of its own UI elements and it is recommended that extensions rely on it too. Alternatively, React's state management can be used instead, though `mobx` is typically simpler to use.
|
||||||
|
|
||||||
|
Also note that an extension's state data can be managed using an `ExtensionStore` object, which conveniently handles persistence and synchronization. The example above defined an `ExamplePreference` type to hold the extension's state to simplify the code for this guide, but it is recommended to manage your extension's state data using [`ExtensionStore`](../stores#extensionstore)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
*********************************************************************
|
*********************************************************************
|
||||||
WIP below!
|
WIP below!
|
||||||
@ -386,62 +454,36 @@ WIP below!
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### `appPreferences`
|
|
||||||
|
|
||||||
The Preferences page is essentially a global page. Extensions can add custom preferences to the Preferences page, thus providing a single location for users to configure global, for Lens and extensions alike.
|
|
||||||
|
|
||||||
``` typescript
|
|
||||||
import React from "react"
|
|
||||||
import { LensRendererExtension } from "@k8slens/extensions"
|
|
||||||
import { myCustomPreferencesStore } from "./src/my-custom-preferences-store"
|
|
||||||
import { MyCustomPreferenceHint, MyCustomPreferenceInput } from "./src/my-custom-preference"
|
|
||||||
|
|
||||||
|
|
||||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
|
||||||
appPreferences = [
|
|
||||||
{
|
|
||||||
title: "My Custom Preference",
|
|
||||||
components: {
|
|
||||||
Hint: () => <MyCustomPreferenceHint/>,
|
|
||||||
Input: () => <MyCustomPreferenceInput store={myCustomPreferencesStore}/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `statusBarItems`
|
### `statusBarItems`
|
||||||
|
|
||||||
The Status bar is the blue strip along the bottom of the Lens UI. Status bar items are `React.ReactNode` types, which can be used to convey status information, or act as a link to a global page.
|
The Status bar is the blue strip along the bottom of the Lens UI. Status bar items are `React.ReactNode` types, which can be used to convey status information, or act as a link to a global page, or even an external page.
|
||||||
|
|
||||||
The following example adds a status bar item definition, as well as a global page definition, to a `LensRendererExtension` subclass, and configures the status bar item to navigate to the global upon a mouse click:
|
The following example adds a status bar item definition, as well as a global page definition, to a `LensRendererExtension` subclass, and configures the status bar item to navigate to the global page upon a mouse click:
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
import { LensRendererExtension, Navigation } from '@k8slens/extensions';
|
import { LensRendererExtension } from '@k8slens/extensions';
|
||||||
import { MyStatusBarIcon, MyPage } from './page';
|
import { HelpIcon, HelpPage } from "./page"
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default class ExtensionRenderer extends LensRendererExtension {
|
export default class HelpExtension extends LensRendererExtension {
|
||||||
globalPages = [
|
globalPages = [
|
||||||
{
|
{
|
||||||
path: "/my-extension-path",
|
id: "help",
|
||||||
hideInMenu: true,
|
|
||||||
components: {
|
components: {
|
||||||
Page: () => <MyPage extension={this} />,
|
Page: () => <HelpPage extension={this}/>,
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
statusBarItems = [
|
statusBarItems = [
|
||||||
{
|
{
|
||||||
item: (
|
item: (
|
||||||
<div
|
<div
|
||||||
className="flex align-center gaps hover-highlight"
|
className="flex align-center gaps"
|
||||||
onClick={() => Navigation.navigate(this.globalPages[0].path)}
|
onClick={() => this.navigate("help")}
|
||||||
>
|
>
|
||||||
<MyStatusBarIcon />
|
<HelpIcon />
|
||||||
<span>My Status Bar Item</span>
|
My Status Bar Item
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
---
|
---
|
||||||
WIP
|
WIP
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# Stores
|
||||||
|
|
||||||
|
## ClusterStore
|
||||||
|
|
||||||
|
## WorkspaceStore
|
||||||
|
|
||||||
|
## ExtensionStore
|
||||||
@ -3,21 +3,24 @@
|
|||||||
The features that Lens includes out-of-the-box are just the start.
|
The features that Lens includes out-of-the-box are just the start.
|
||||||
Lens extensions let you add new features to your installation to support your workflow.
|
Lens extensions let you add new features to your installation to support your workflow.
|
||||||
Rich extensibility model lets extension authors plug directly into the Lens UI and contribute functionality through the same APIs used by Lens itself.
|
Rich extensibility model lets extension authors plug directly into the Lens UI and contribute functionality through the same APIs used by Lens itself.
|
||||||
|
The start using Lens Extensions go to **File** (or **Lens** on macOS) > **Extensions** in the application menu.
|
||||||
|
This is the `Extensions` management page where all the management of the extensions you want to use is done.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installing an Extension
|
## Installing an Extension
|
||||||
|
|
||||||
You can install a dowloaded extension .tgz package by going to **File** > **Extensions** (**Lens** > **Extensions** on Mac). Alternatively you can point an URL to .tgz file. An installed extension is enabled automatically.
|
There are three ways to install extensions.
|
||||||
|
If you have the extension as a `.tgz` file then dragging and dropping it in the extension management page will install it for you.
|
||||||
|
If it is hosted on the web, you can paste the URL and click `Install` and Lens will download and install it.
|
||||||
|
The third way is to move the extension into your `~/.k8slens/extensions` (or `C:\Users\<user>\.k8slens\extensions`) folder and Lens will automatically detect it and install the extension.
|
||||||
|
|
||||||
## Enabling an Extension
|
## Enabling or Disabling an Extension
|
||||||
|
|
||||||
Go to **File** > **Extensions** (**Lens** > **Extensions** on Mac) and click "Enable" button.
|
Go to the extension management page and click either the `Enable` or `Disable` buttons.
|
||||||
|
Extensions will be enabled by default when you first install them.
|
||||||
## Disabling an Extension
|
A disabled extension is not loaded by Lens and is not run.
|
||||||
|
|
||||||
Go to **File** > **Extensions** (**Lens** > **Extensions** on Mac) and click "Disable" button.
|
|
||||||
|
|
||||||
## Uninstalling an Extension
|
## Uninstalling an Extension
|
||||||
|
|
||||||
Go to **File** > **Extensions** (**Lens** > **Extensions** on Mac) and click "Uninstall" button.
|
If, for whatever reason, you wish to remove the installation of an extension simple click the `Uninstall` button. This will remove all the files that Lens would need to run the extension.
|
||||||
|
|||||||
2
extensions/example-extension/package-lock.json
generated
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "extension-example",
|
"name": "example-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export function ExampleIcon(props: Component.IconProps) {
|
|||||||
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
export class ExamplePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
deactivate = () => {
|
deactivate = () => {
|
||||||
const { extension } = this.props;
|
const { extension } = this.props;
|
||||||
|
|
||||||
extension.disable();
|
extension.disable();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ export class ExamplePage extends React.Component<{ extension: LensRendererExtens
|
|||||||
const doodleStyle = {
|
const doodleStyle = {
|
||||||
width: "200px"
|
width: "200px"
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex column gaps align-flex-start">
|
<div className="flex column gaps align-flex-start">
|
||||||
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
|
<div style={doodleStyle}><CoffeeDoodle accent="#3d90ce" /></div>
|
||||||
|
|||||||
@ -4,10 +4,12 @@ export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatu
|
|||||||
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
||||||
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
|
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object);
|
||||||
const warnings = events.filter(evt => evt.isWarning());
|
const warnings = events.filter(evt => evt.isWarning());
|
||||||
|
|
||||||
if (!events.length || !warnings.length) {
|
if (!events.length || !warnings.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const event = [...warnings, ...events][0]; // get latest event
|
const event = [...warnings, ...events][0]; // get latest event
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
||||||
text: `${event.message}`,
|
text: `${event.message}`,
|
||||||
@ -22,10 +24,12 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
|
|||||||
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
||||||
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
|
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod);
|
||||||
const warnings = events.filter(evt => evt.isWarning());
|
const warnings = events.filter(evt => evt.isWarning());
|
||||||
|
|
||||||
if (!events.length || !warnings.length) {
|
if (!events.length || !warnings.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const event = [...warnings, ...events][0]; // get latest event
|
const event = [...warnings, ...events][0]; // get latest event
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
||||||
text: `${event.message}`,
|
text: `${event.message}`,
|
||||||
@ -37,13 +41,16 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
|
|||||||
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
|
||||||
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
|
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob);
|
||||||
const warnings = events.filter(evt => evt.isWarning());
|
const warnings = events.filter(evt => evt.isWarning());
|
||||||
|
|
||||||
if (cronJob.isNeverRun()) {
|
if (cronJob.isNeverRun()) {
|
||||||
events = events.filter(event => event.reason != "FailedNeedsStart");
|
events = events.filter(event => event.reason != "FailedNeedsStart");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!events.length || !warnings.length) {
|
if (!events.length || !warnings.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const event = [...warnings, ...events][0]; // get latest event
|
const event = [...warnings, ...events][0]; // get latest event
|
||||||
|
|
||||||
return {
|
return {
|
||||||
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
level: K8sApi.KubeObjectStatusLevel.WARNING,
|
||||||
text: `${event.message}`,
|
text: `${event.message}`,
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
// Check if there are storageclasses
|
// Check if there are storageclasses
|
||||||
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
|
||||||
const scs = await storageClassApi.list();
|
const scs = await storageClassApi.list();
|
||||||
|
|
||||||
this.templateContext.persistence.enabled = scs.some(sc => (
|
this.templateContext.persistence.enabled = scs.some(sc => (
|
||||||
sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" ||
|
sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" ||
|
||||||
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
|
||||||
@ -69,6 +70,7 @@ export class MetricsFeature extends ClusterFeature.Feature {
|
|||||||
try {
|
try {
|
||||||
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
|
||||||
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
|
||||||
|
|
||||||
if (prometheus?.kind) {
|
if (prometheus?.kind) {
|
||||||
this.status.installed = true;
|
this.status.installed = true;
|
||||||
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
|
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node
|
|||||||
|
|
||||||
export function NodeMenu(props: NodeMenuProps) {
|
export function NodeMenu(props: NodeMenuProps) {
|
||||||
const { object: node, toolbar } = props;
|
const { object: node, toolbar } = props;
|
||||||
|
|
||||||
if (!node) return null;
|
if (!node) return null;
|
||||||
const nodeName = node.getName();
|
const nodeName = node.getName();
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ export function NodeMenu(props: NodeMenuProps) {
|
|||||||
|
|
||||||
const drain = () => {
|
const drain = () => {
|
||||||
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
|
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
|
||||||
|
|
||||||
Component.ConfirmDialog.open({
|
Component.ConfirmDialog.open({
|
||||||
ok: () => sendToTerminal(command),
|
ok: () => sendToTerminal(command),
|
||||||
labelOk: `Drain Node`,
|
labelOk: `Drain Node`,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
showLogs(container: K8sApi.IPodContainer) {
|
showLogs(container: K8sApi.IPodContainer) {
|
||||||
Navigation.hideDetails();
|
Navigation.hideDetails();
|
||||||
const pod = this.props.object;
|
const pod = this.props.object;
|
||||||
|
|
||||||
Component.createPodLogsTab({
|
Component.createPodLogsTab({
|
||||||
pod,
|
pod,
|
||||||
containers: pod.getContainers(),
|
containers: pod.getContainers(),
|
||||||
@ -22,7 +23,9 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
const { object: pod, toolbar } = this.props;
|
const { object: pod, toolbar } = this.props;
|
||||||
const containers = pod.getAllContainers();
|
const containers = pod.getAllContainers();
|
||||||
const statuses = pod.getContainerStatuses();
|
const statuses = pod.getContainerStatuses();
|
||||||
|
|
||||||
if (!containers.length) return null;
|
if (!containers.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||||
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
|
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
|
||||||
@ -40,6 +43,7 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
|
|||||||
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
|
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
|
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
|
||||||
{brick}
|
{brick}
|
||||||
|
|||||||
@ -12,9 +12,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
const { object: pod } = this.props;
|
const { object: pod } = this.props;
|
||||||
const containerParam = container ? `-c ${container}` : "";
|
const containerParam = container ? `-c ${container}` : "";
|
||||||
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`;
|
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`;
|
||||||
|
|
||||||
if (window.navigator.platform !== "Win32") {
|
if (window.navigator.platform !== "Win32") {
|
||||||
command = `exec ${command}`;
|
command = `exec ${command}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pod.getSelectedNodeOs() === "windows") {
|
if (pod.getSelectedNodeOs() === "windows") {
|
||||||
command = `${command} powershell`;
|
command = `${command} powershell`;
|
||||||
} else {
|
} else {
|
||||||
@ -34,7 +36,9 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
render() {
|
render() {
|
||||||
const { object, toolbar } = this.props;
|
const { object, toolbar } = this.props;
|
||||||
const containers = object.getRunningContainers();
|
const containers = object.getRunningContainers();
|
||||||
|
|
||||||
if (!containers.length) return null;
|
if (!containers.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||||
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
||||||
@ -46,6 +50,7 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
|
|||||||
{
|
{
|
||||||
containers.map(container => {
|
containers.map(container => {
|
||||||
const { name } = container;
|
const { name } = container;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
|
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
|
||||||
<Component.StatusBrick/>
|
<Component.StatusBrick/>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { TelemetryPreferencesStore } from "./telemetry-preferences-store";
|
|||||||
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
|
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { telemetry } = this.props;
|
const { telemetry } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component.Checkbox
|
<Component.Checkbox
|
||||||
label="Allow telemetry & usage tracking"
|
label="Allow telemetry & usage tracking"
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export class Tracker extends Util.Singleton {
|
|||||||
this.anonymousId = machineIdSync();
|
this.anonymousId = machineIdSync();
|
||||||
this.os = this.resolveOS();
|
this.os = this.resolveOS();
|
||||||
this.userAgent = `Lens ${App.version} (${this.os})`;
|
this.userAgent = `Lens ${App.version} (${this.os})`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false });
|
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -49,18 +50,22 @@ export class Tracker extends Util.Singleton {
|
|||||||
const handler = (ev: EventBus.AppEvent) => {
|
const handler = (ev: EventBus.AppEvent) => {
|
||||||
this.event(ev.name, ev.action, ev.params);
|
this.event(ev.name, ev.action, ev.params);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.eventHandlers.push(handler);
|
this.eventHandlers.push(handler);
|
||||||
EventBus.appEventBus.addListener(handler);
|
EventBus.appEventBus.addListener(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
watchExtensions() {
|
watchExtensions() {
|
||||||
let previousExtensions = App.getEnabledExtensions();
|
let previousExtensions = App.getEnabledExtensions();
|
||||||
|
|
||||||
this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => {
|
this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => {
|
||||||
const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x));
|
const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x));
|
||||||
|
|
||||||
removedExtensions.forEach(ext => {
|
removedExtensions.forEach(ext => {
|
||||||
this.event("extension", "disable", { extension: ext });
|
this.event("extension", "disable", { extension: ext });
|
||||||
});
|
});
|
||||||
const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x));
|
const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x));
|
||||||
|
|
||||||
newExtensions.forEach(ext => {
|
newExtensions.forEach(ext => {
|
||||||
this.event("extension", "enable", { extension: ext });
|
this.event("extension", "enable", { extension: ext });
|
||||||
});
|
});
|
||||||
@ -82,6 +87,7 @@ export class Tracker extends Util.Singleton {
|
|||||||
for (const handler of this.eventHandlers) {
|
for (const handler of this.eventHandlers) {
|
||||||
EventBus.appEventBus.removeListener(handler);
|
EventBus.appEventBus.removeListener(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.reportInterval) {
|
if (this.reportInterval) {
|
||||||
clearInterval(this.reportInterval);
|
clearInterval(this.reportInterval);
|
||||||
}
|
}
|
||||||
@ -125,12 +131,14 @@ export class Tracker extends Util.Singleton {
|
|||||||
|
|
||||||
protected resolveOS() {
|
protected resolveOS() {
|
||||||
let os = "";
|
let os = "";
|
||||||
|
|
||||||
if (App.isMac) {
|
if (App.isMac) {
|
||||||
os = "MacOS";
|
os = "MacOS";
|
||||||
} else if(App.isWindows) {
|
} else if(App.isWindows) {
|
||||||
os = "Windows";
|
os = "Windows";
|
||||||
} else if (App.isLinux) {
|
} else if (App.isLinux) {
|
||||||
os = "Linux";
|
os = "Linux";
|
||||||
|
|
||||||
if (App.isSnap) {
|
if (App.isSnap) {
|
||||||
os += "; Snap";
|
os += "; Snap";
|
||||||
} else {
|
} else {
|
||||||
@ -139,12 +147,14 @@ export class Tracker extends Util.Singleton {
|
|||||||
} else {
|
} else {
|
||||||
os = "Unknown";
|
os = "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
return os;
|
return os;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
||||||
try {
|
try {
|
||||||
const allowed = await this.isTelemetryAllowed();
|
const allowed = await this.isTelemetryAllowed();
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,9 +14,7 @@ jest.setTimeout(60000);
|
|||||||
describe("Lens integration tests", () => {
|
describe("Lens integration tests", () => {
|
||||||
const TEST_NAMESPACE = "integration-tests";
|
const TEST_NAMESPACE = "integration-tests";
|
||||||
const BACKSPACE = "\uE003";
|
const BACKSPACE = "\uE003";
|
||||||
|
|
||||||
let app: Application;
|
let app: Application;
|
||||||
|
|
||||||
const appStart = async () => {
|
const appStart = async () => {
|
||||||
app = util.setup();
|
app = util.setup();
|
||||||
await app.start();
|
await app.start();
|
||||||
@ -25,19 +23,19 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.windowByIndex(0);
|
await app.client.windowByIndex(0);
|
||||||
await app.client.waitUntilWindowLoaded();
|
await app.client.waitUntilWindowLoaded();
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickWhatsNew = async (app: Application) => {
|
const clickWhatsNew = async (app: Application) => {
|
||||||
await app.client.waitUntilTextExists("h1", "What's new?");
|
await app.client.waitUntilTextExists("h1", "What's new?");
|
||||||
await app.client.click("button.primary");
|
await app.client.click("button.primary");
|
||||||
await app.client.waitUntilTextExists("h1", "Welcome");
|
await app.client.waitUntilTextExists("h1", "Welcome");
|
||||||
};
|
};
|
||||||
|
|
||||||
const minikubeReady = (): boolean => {
|
const minikubeReady = (): boolean => {
|
||||||
// determine if minikube is running
|
// determine if minikube is running
|
||||||
{
|
{
|
||||||
const { status } = spawnSync("minikube status", { shell: true });
|
const { status } = spawnSync("minikube status", { shell: true });
|
||||||
|
|
||||||
if (status !== 0) {
|
if (status !== 0) {
|
||||||
console.warn("minikube not running");
|
console.warn("minikube not running");
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,6 +43,7 @@ describe("Lens integration tests", () => {
|
|||||||
// Remove TEST_NAMESPACE if it already exists
|
// Remove TEST_NAMESPACE if it already exists
|
||||||
{
|
{
|
||||||
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
const { status } = spawnSync(`minikube kubectl -- get namespace ${TEST_NAMESPACE}`, { shell: true });
|
||||||
|
|
||||||
if (status === 0) {
|
if (status === 0) {
|
||||||
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
console.warn(`Removing existing ${TEST_NAMESPACE} namespace`);
|
||||||
|
|
||||||
@ -52,8 +51,10 @@ describe("Lens integration tests", () => {
|
|||||||
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
|
`minikube kubectl -- delete namespace ${TEST_NAMESPACE}`,
|
||||||
{ shell: true },
|
{ shell: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status !== 0) {
|
if (status !== 0) {
|
||||||
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
|
console.warn(`Error removing ${TEST_NAMESPACE} namespace: ${stderr.toString()}`);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +87,7 @@ describe("Lens integration tests", () => {
|
|||||||
describe("preferences page", () => {
|
describe("preferences page", () => {
|
||||||
it('shows "preferences"', async () => {
|
it('shows "preferences"', async () => {
|
||||||
const appName: string = process.platform === "darwin" ? "Lens" : "File";
|
const appName: string = process.platform === "darwin" ? "Lens" : "File";
|
||||||
|
|
||||||
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
|
await app.electron.ipcRenderer.send("test-menu-item-click", appName, "Preferences");
|
||||||
await app.client.waitUntilTextExists("h2", "Preferences");
|
await app.client.waitUntilTextExists("h2", "Preferences");
|
||||||
});
|
});
|
||||||
@ -153,13 +155,13 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
await app.client.waitUntilTextExists("div", "Select kubeconfig file");
|
||||||
await app.client.click("div.Select__control"); // show the context drop-down list
|
await app.client.click("div.Select__control"); // show the context drop-down list
|
||||||
await app.client.waitUntilTextExists("div", "minikube");
|
await app.client.waitUntilTextExists("div", "minikube");
|
||||||
|
|
||||||
if (!await app.client.$("button.primary").isEnabled()) {
|
if (!await app.client.$("button.primary").isEnabled()) {
|
||||||
await app.client.click("div.minikube"); // select minikube context
|
await app.client.click("div.minikube"); // select minikube context
|
||||||
} // else the only context, which must be 'minikube', is automatically selected
|
} // else the only context, which must be 'minikube', is automatically selected
|
||||||
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
await app.client.click("div.Select__control"); // hide the context drop-down list (it might be obscuring the Add cluster(s) button)
|
||||||
await app.client.click("button.primary"); // add minikube cluster
|
await app.client.click("button.primary"); // add minikube cluster
|
||||||
};
|
};
|
||||||
|
|
||||||
const waitForMinikubeDashboard = async (app: Application) => {
|
const waitForMinikubeDashboard = async (app: Application) => {
|
||||||
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
await app.client.waitUntilTextExists("pre.kube-auth-out", "Authentication proxy started");
|
||||||
await app.client.waitForExist(`iframe[name="minikube"]`);
|
await app.client.waitForExist(`iframe[name="minikube"]`);
|
||||||
@ -169,7 +171,6 @@ describe("Lens integration tests", () => {
|
|||||||
|
|
||||||
util.describeIf(ready)("cluster tests", () => {
|
util.describeIf(ready)("cluster tests", () => {
|
||||||
let clusterAdded = false;
|
let clusterAdded = false;
|
||||||
|
|
||||||
const addCluster = async () => {
|
const addCluster = async () => {
|
||||||
await clickWhatsNew(app);
|
await clickWhatsNew(app);
|
||||||
await addMinikubeCluster(app);
|
await addMinikubeCluster(app);
|
||||||
@ -443,6 +444,7 @@ describe("Lens integration tests", () => {
|
|||||||
expectedText: "Custom Resources"
|
expectedText: "Custom Resources"
|
||||||
}]
|
}]
|
||||||
}];
|
}];
|
||||||
|
|
||||||
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
tests.forEach(({ drawer = "", drawerId = "", pages }) => {
|
||||||
if (drawer !== "") {
|
if (drawer !== "") {
|
||||||
it(`shows ${drawer} drawer`, async () => {
|
it(`shows ${drawer} drawer`, async () => {
|
||||||
@ -458,6 +460,7 @@ describe("Lens integration tests", () => {
|
|||||||
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
await app.client.waitUntilTextExists(expectedSelector, expectedText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (drawer !== "") {
|
if (drawer !== "") {
|
||||||
// hide the drawer
|
// hide the drawer
|
||||||
it(`hides ${drawer} drawer`, async () => {
|
it(`hides ${drawer} drawer`, async () => {
|
||||||
|
|||||||
@ -30,7 +30,9 @@ type AsyncPidGetter = () => Promise<number>;
|
|||||||
|
|
||||||
export async function tearDown(app: Application) {
|
export async function tearDown(app: Application) {
|
||||||
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
const pid = await (app.mainProcess.pid as any as AsyncPidGetter)();
|
||||||
|
|
||||||
await app.stop();
|
await app.stop();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
process.kill(pid, "SIGKILL");
|
process.kill(pid, "SIGKILL");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -1594,6 +1594,7 @@ msgstr "Names"
|
|||||||
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
||||||
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
||||||
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
||||||
|
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:144
|
||||||
msgid "Namespace"
|
msgid "Namespace"
|
||||||
msgstr "Namespace"
|
msgstr "Namespace"
|
||||||
|
|
||||||
@ -2003,6 +2004,10 @@ msgstr "Read-only Root Filesystem"
|
|||||||
msgid "Readiness"
|
msgid "Readiness"
|
||||||
msgstr "Readiness"
|
msgstr "Readiness"
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:145
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr "Ready"
|
||||||
|
|
||||||
#: src/renderer/components/+events/event-details.tsx:32
|
#: src/renderer/components/+events/event-details.tsx:32
|
||||||
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
||||||
msgid "Reason"
|
msgid "Reason"
|
||||||
|
|||||||
@ -1585,6 +1585,7 @@ msgstr ""
|
|||||||
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
||||||
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
||||||
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
||||||
|
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:144
|
||||||
msgid "Namespace"
|
msgid "Namespace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1986,6 +1987,10 @@ msgstr ""
|
|||||||
msgid "Readiness"
|
msgid "Readiness"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:145
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/renderer/components/+events/event-details.tsx:32
|
#: src/renderer/components/+events/event-details.tsx:32
|
||||||
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
||||||
msgid "Reason"
|
msgid "Reason"
|
||||||
|
|||||||
@ -1595,6 +1595,7 @@ msgstr ""
|
|||||||
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
#: src/renderer/components/dock/upgrade-chart.tsx:98
|
||||||
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
#: src/renderer/components/item-object-list/page-filters-select.tsx:57
|
||||||
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
#: src/renderer/components/kube-object/kube-object-meta.tsx:23
|
||||||
|
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:144
|
||||||
msgid "Namespace"
|
msgid "Namespace"
|
||||||
msgstr "Namespace"
|
msgstr "Namespace"
|
||||||
|
|
||||||
@ -2004,6 +2005,10 @@ msgstr ""
|
|||||||
msgid "Readiness"
|
msgid "Readiness"
|
||||||
msgstr "Готовность"
|
msgstr "Готовность"
|
||||||
|
|
||||||
|
#: src/renderer/components/+workloads-pods/pod-details-list.tsx:145
|
||||||
|
msgid "Ready"
|
||||||
|
msgstr "Готовы"
|
||||||
|
|
||||||
#: src/renderer/components/+events/event-details.tsx:32
|
#: src/renderer/components/+events/event-details.tsx:32
|
||||||
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
#: src/renderer/components/+workloads-pods/pod-details-container.tsx:25
|
||||||
msgid "Reason"
|
msgid "Reason"
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "kontena-lens",
|
"name": "kontena-lens",
|
||||||
"productName": "Lens",
|
"productName": "Lens",
|
||||||
"description": "Lens - The Kubernetes IDE",
|
"description": "Lens - The Kubernetes IDE",
|
||||||
"version": "4.0.0-rc.1",
|
"version": "4.0.0-rc.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -37,7 +37,7 @@
|
|||||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||||
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
||||||
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 src/ integration/ __mocks__/ build/ extensions/",
|
"lint": "yarn run eslint $@ --ext js,ts,tsx --max-warnings=0 .",
|
||||||
"lint:fix": "yarn run lint --fix",
|
"lint:fix": "yarn run lint --fix",
|
||||||
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
"mkdocs-serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
|
||||||
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
"verify-docs": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
|
||||||
|
|||||||
@ -7,6 +7,20 @@ import { workspaceStore } from "../workspace-store";
|
|||||||
|
|
||||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
||||||
|
|
||||||
|
jest.mock("electron", () => {
|
||||||
|
return {
|
||||||
|
app: {
|
||||||
|
getVersion: () => "99.99.99",
|
||||||
|
getPath: () => "tmp",
|
||||||
|
getLocale: () => "en"
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
handle: jest.fn(),
|
||||||
|
on: jest.fn()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
let clusterStore: ClusterStore;
|
let clusterStore: ClusterStore;
|
||||||
|
|
||||||
describe("empty config", () => {
|
describe("empty config", () => {
|
||||||
@ -17,8 +31,10 @@ describe("empty config", () => {
|
|||||||
"lens-cluster-store.json": JSON.stringify({})
|
"lens-cluster-store.json": JSON.stringify({})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,13 +61,16 @@ describe("empty config", () => {
|
|||||||
|
|
||||||
it("adds new cluster to store", async () => {
|
it("adds new cluster to store", async () => {
|
||||||
const storedCluster = clusterStore.getById("foo");
|
const storedCluster = clusterStore.getById("foo");
|
||||||
|
|
||||||
expect(storedCluster.id).toBe("foo");
|
expect(storedCluster.id).toBe("foo");
|
||||||
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
|
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
|
||||||
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
|
expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5");
|
||||||
|
expect(storedCluster.enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds cluster to default workspace", () => {
|
it("adds cluster to default workspace", () => {
|
||||||
const storedCluster = clusterStore.getById("foo");
|
const storedCluster = clusterStore.getById("foo");
|
||||||
|
|
||||||
expect(storedCluster.workspace).toBe("default");
|
expect(storedCluster.workspace).toBe("default");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,6 +118,7 @@ describe("empty config", () => {
|
|||||||
it("gets clusters by workspaces", () => {
|
it("gets clusters by workspaces", () => {
|
||||||
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
const wsClusters = clusterStore.getByWorkspaceId("workstation");
|
||||||
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
const defaultClusters = clusterStore.getByWorkspaceId("default");
|
||||||
|
|
||||||
expect(defaultClusters.length).toBe(0);
|
expect(defaultClusters.length).toBe(0);
|
||||||
expect(wsClusters.length).toBe(2);
|
expect(wsClusters.length).toBe(2);
|
||||||
expect(wsClusters[0].id).toBe("prod");
|
expect(wsClusters[0].id).toBe("prod");
|
||||||
@ -107,6 +127,7 @@ describe("empty config", () => {
|
|||||||
|
|
||||||
it("check if cluster's kubeconfig file saved", () => {
|
it("check if cluster's kubeconfig file saved", () => {
|
||||||
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig");
|
||||||
|
|
||||||
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,6 +135,7 @@ describe("empty config", () => {
|
|||||||
clusterStore.swapIconOrders("workstation", 1, 1);
|
clusterStore.swapIconOrders("workstation", 1, 1);
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
const clusters = clusterStore.getByWorkspaceId("workstation");
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("prod");
|
expect(clusters[0].id).toBe("prod");
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
expect(clusters[0].preferences.iconOrder).toBe(0);
|
||||||
expect(clusters[1].id).toBe("dev");
|
expect(clusters[1].id).toBe("dev");
|
||||||
@ -124,6 +146,7 @@ describe("empty config", () => {
|
|||||||
clusterStore.swapIconOrders("workstation", 0, 1);
|
clusterStore.swapIconOrders("workstation", 0, 1);
|
||||||
|
|
||||||
const clusters = clusterStore.getByWorkspaceId("workstation");
|
const clusters = clusterStore.getByWorkspaceId("workstation");
|
||||||
|
|
||||||
expect(clusters[0].id).toBe("dev");
|
expect(clusters[0].id).toBe("dev");
|
||||||
expect(clusters[0].preferences.iconOrder).toBe(0);
|
expect(clusters[0].preferences.iconOrder).toBe(0);
|
||||||
expect(clusters[1].id).toBe("prod");
|
expect(clusters[1].id).toBe("prod");
|
||||||
@ -170,14 +193,17 @@ describe("config with existing clusters", () => {
|
|||||||
kubeConfig: "foo",
|
kubeConfig: "foo",
|
||||||
contextName: "foo",
|
contextName: "foo",
|
||||||
preferences: { terminalCWD: "/foo" },
|
preferences: { terminalCWD: "/foo" },
|
||||||
workspace: "foo"
|
workspace: "foo",
|
||||||
|
ownerRef: "foo"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -187,6 +213,7 @@ describe("config with existing clusters", () => {
|
|||||||
|
|
||||||
it("allows to retrieve a cluster", () => {
|
it("allows to retrieve a cluster", () => {
|
||||||
const storedCluster = clusterStore.getById("cluster1");
|
const storedCluster = clusterStore.getById("cluster1");
|
||||||
|
|
||||||
expect(storedCluster.id).toBe("cluster1");
|
expect(storedCluster.id).toBe("cluster1");
|
||||||
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
||||||
});
|
});
|
||||||
@ -194,13 +221,16 @@ describe("config with existing clusters", () => {
|
|||||||
it("allows to delete a cluster", () => {
|
it("allows to delete a cluster", () => {
|
||||||
clusterStore.removeById("cluster2");
|
clusterStore.removeById("cluster2");
|
||||||
const storedCluster = clusterStore.getById("cluster1");
|
const storedCluster = clusterStore.getById("cluster1");
|
||||||
|
|
||||||
expect(storedCluster).toBeTruthy();
|
expect(storedCluster).toBeTruthy();
|
||||||
const storedCluster2 = clusterStore.getById("cluster2");
|
const storedCluster2 = clusterStore.getById("cluster2");
|
||||||
|
|
||||||
expect(storedCluster2).toBeUndefined();
|
expect(storedCluster2).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows getting all of the clusters", async () => {
|
it("allows getting all of the clusters", async () => {
|
||||||
const storedClusters = clusterStore.clustersList;
|
const storedClusters = clusterStore.clustersList;
|
||||||
|
|
||||||
expect(storedClusters.length).toBe(3);
|
expect(storedClusters.length).toBe(3);
|
||||||
expect(storedClusters[0].id).toBe("cluster1");
|
expect(storedClusters[0].id).toBe("cluster1");
|
||||||
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo");
|
expect(storedClusters[0].preferences.terminalCWD).toBe("/foo");
|
||||||
@ -208,6 +238,13 @@ describe("config with existing clusters", () => {
|
|||||||
expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2");
|
expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2");
|
||||||
expect(storedClusters[2].id).toBe("cluster3");
|
expect(storedClusters[2].id).toBe("cluster3");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("marks owned cluster disabled by default", () => {
|
||||||
|
const storedClusters = clusterStore.clustersList;
|
||||||
|
|
||||||
|
expect(storedClusters[0].enabled).toBe(true);
|
||||||
|
expect(storedClusters[2].enabled).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("pre 2.0 config with an existing cluster", () => {
|
describe("pre 2.0 config with an existing cluster", () => {
|
||||||
@ -225,8 +262,10 @@ describe("pre 2.0 config with an existing cluster", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -236,6 +275,7 @@ describe("pre 2.0 config with an existing cluster", () => {
|
|||||||
|
|
||||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||||
|
|
||||||
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -257,8 +297,10 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -270,6 +312,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
|||||||
const file = clusterStore.clustersList[0].kubeConfigPath;
|
const file = clusterStore.clustersList[0].kubeConfigPath;
|
||||||
const config = fs.readFileSync(file, "utf8");
|
const config = fs.readFileSync(file, "utf8");
|
||||||
const kc = yaml.safeLoad(config);
|
const kc = yaml.safeLoad(config);
|
||||||
|
|
||||||
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
|
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
|
||||||
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
|
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
|
||||||
});
|
});
|
||||||
@ -297,8 +340,10 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
|||||||
"icon_path": testDataIcon,
|
"icon_path": testDataIcon,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -308,6 +353,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
|||||||
|
|
||||||
it("moves the icon into preferences", async () => {
|
it("moves the icon into preferences", async () => {
|
||||||
const storedClusterData = clusterStore.clustersList[0];
|
const storedClusterData = clusterStore.clustersList[0];
|
||||||
|
|
||||||
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
|
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
|
||||||
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
|
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
|
||||||
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
|
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
|
||||||
@ -334,8 +380,10 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -345,6 +393,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
|
|
||||||
it("adds cluster to default workspace", async () => {
|
it("adds cluster to default workspace", async () => {
|
||||||
const storedClusterData = clusterStore.clustersList[0];
|
const storedClusterData = clusterStore.clustersList[0];
|
||||||
|
|
||||||
expect(storedClusterData.workspace).toBe("default");
|
expect(storedClusterData.workspace).toBe("default");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -374,8 +423,10 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
|||||||
"icon_path": testDataIcon,
|
"icon_path": testDataIcon,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||||
|
|
||||||
return clusterStore.load();
|
return clusterStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -385,11 +436,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
|||||||
|
|
||||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||||
|
|
||||||
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("migrates to modern format with icon not in file", async () => {
|
it("migrates to modern format with icon not in file", async () => {
|
||||||
const { icon } = clusterStore.clustersList[0].preferences;
|
const { icon } = clusterStore.clustersList[0].preferences;
|
||||||
|
|
||||||
expect(icon.startsWith("data:;base64,")).toBe(true);
|
expect(icon.startsWith("data:;base64,")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ describe("event bus tests", () => {
|
|||||||
describe("emit", () => {
|
describe("emit", () => {
|
||||||
it("emits an event", () => {
|
it("emits an event", () => {
|
||||||
let event: AppEvent = null;
|
let event: AppEvent = null;
|
||||||
|
|
||||||
appEventBus.addListener((data) => {
|
appEventBus.addListener((data) => {
|
||||||
event = data;
|
event = data;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
import { SearchStore } from "../search-store";
|
import { SearchStore } from "../search-store";
|
||||||
|
|
||||||
let searchStore: SearchStore = null;
|
let searchStore: SearchStore = null;
|
||||||
|
|
||||||
const logs = [
|
const logs = [
|
||||||
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
|
"1:M 30 Oct 2020 16:17:41.553 # Connection with replica 172.17.0.12:6379 lost",
|
||||||
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
|
"1:M 30 Oct 2020 16:17:41.623 * Replica 172.17.0.12:6379 asks for synchronization",
|
||||||
@ -64,6 +63,7 @@ describe("search store tests", () => {
|
|||||||
|
|
||||||
it("escapes string for using in regex", () => {
|
it("escapes string for using in regex", () => {
|
||||||
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
|
const regex = searchStore.escapeRegex("some.interesting-query\\#?()[]");
|
||||||
|
|
||||||
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
|
expect(regex).toBe("some\\.interesting\\-query\\\\\\#\\?\\(\\)\\[\\]");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,7 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
it("correctly resets theme to default value", async () => {
|
it("correctly resets theme to default value", async () => {
|
||||||
const us = UserStore.getInstance<UserStore>();
|
const us = UserStore.getInstance<UserStore>();
|
||||||
|
|
||||||
us.isLoaded = true;
|
us.isLoaded = true;
|
||||||
|
|
||||||
us.preferences.colorTheme = "some other theme";
|
us.preferences.colorTheme = "some other theme";
|
||||||
|
|||||||
@ -6,6 +6,10 @@ jest.mock("electron", () => {
|
|||||||
getVersion: () => "99.99.99",
|
getVersion: () => "99.99.99",
|
||||||
getPath: () => "tmp",
|
getPath: () => "tmp",
|
||||||
getLocale: () => "en"
|
getLocale: () => "en"
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
handle: jest.fn(),
|
||||||
|
on: jest.fn()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -40,7 +44,6 @@ describe("workspace store tests", () => {
|
|||||||
|
|
||||||
it("can update workspace description", () => {
|
it("can update workspace description", () => {
|
||||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||||
|
|
||||||
const workspace = ws.addWorkspace(new Workspace({
|
const workspace = ws.addWorkspace(new Workspace({
|
||||||
id: "foobar",
|
id: "foobar",
|
||||||
name: "foobar",
|
name: "foobar",
|
||||||
@ -60,7 +63,10 @@ describe("workspace store tests", () => {
|
|||||||
name: "foobar",
|
name: "foobar",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
expect(ws.getById("123").name).toBe("foobar");
|
const workspace = ws.getById("123");
|
||||||
|
|
||||||
|
expect(workspace.name).toBe("foobar");
|
||||||
|
expect(workspace.enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("cannot set a non-existent workspace to be active", () => {
|
it("cannot set a non-existent workspace to be active", () => {
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
if (this.params.autoLoad) {
|
if (this.params.autoLoad) {
|
||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.params.syncEnabled) {
|
if (this.params.syncEnabled) {
|
||||||
await this.whenLoaded;
|
await this.whenLoaded;
|
||||||
this.enableSync();
|
this.enableSync();
|
||||||
@ -63,6 +64,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const { autoLoad, syncEnabled, ...confOptions } = this.params;
|
const { autoLoad, syncEnabled, ...confOptions } = this.params;
|
||||||
|
|
||||||
this.storeConfig = new Config({
|
this.storeConfig = new Config({
|
||||||
...confOptions,
|
...confOptions,
|
||||||
projectName: "lens",
|
projectName: "lens",
|
||||||
@ -90,19 +92,23 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
this.syncDisposers.push(
|
this.syncDisposers.push(
|
||||||
reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
|
reaction(() => this.toJSON(), model => this.onModelChange(model), this.params.syncOptions),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
const callback = (event: IpcMainEvent, model: T) => {
|
const callback = (event: IpcMainEvent, model: T) => {
|
||||||
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
||||||
this.onSync(model);
|
this.onSync(model);
|
||||||
};
|
};
|
||||||
|
|
||||||
subscribeToBroadcast(this.syncMainChannel, callback);
|
subscribeToBroadcast(this.syncMainChannel, callback);
|
||||||
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback));
|
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
const callback = (event: IpcRendererEvent, model: T) => {
|
const callback = (event: IpcRendererEvent, model: T) => {
|
||||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||||
this.onSyncFromMain(model);
|
this.onSyncFromMain(model);
|
||||||
};
|
};
|
||||||
|
|
||||||
subscribeToBroadcast(this.syncRendererChannel, callback);
|
subscribeToBroadcast(this.syncRendererChannel, callback);
|
||||||
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback));
|
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback));
|
||||||
}
|
}
|
||||||
@ -127,6 +133,7 @@ export abstract class BaseStore<T = any> extends Singleton {
|
|||||||
protected applyWithoutSync(callback: () => void) {
|
protected applyWithoutSync(callback: () => void) {
|
||||||
this.disableSync();
|
this.disableSync();
|
||||||
runInAction(callback);
|
runInAction(callback);
|
||||||
|
|
||||||
if (this.params.syncEnabled) {
|
if (this.params.syncEnabled) {
|
||||||
this.enableSync();
|
this.enableSync();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
|||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
return cluster.activate(force);
|
return cluster.activate(force);
|
||||||
}
|
}
|
||||||
@ -22,20 +23,24 @@ if (ipcMain) {
|
|||||||
|
|
||||||
handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => {
|
handleRequest(clusterSetFrameIdHandler, (event, clusterId: ClusterId, frameId: number) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
clusterFrameMap.set(cluster.id, frameId);
|
clusterFrameMap.set(cluster.id, frameId);
|
||||||
|
|
||||||
return cluster.pushState();
|
return cluster.pushState();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) return cluster.refresh({ refreshMetadata: true });
|
if (cluster) return cluster.refresh({ refreshMetadata: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
||||||
appEventBus.emit({name: "cluster", action: "stop"});
|
appEventBus.emit({name: "cluster", action: "stop"});
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.disconnect();
|
cluster.disconnect();
|
||||||
clusterFrameMap.delete(cluster.id);
|
clusterFrameMap.delete(cluster.id);
|
||||||
@ -45,8 +50,10 @@ if (ipcMain) {
|
|||||||
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
||||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
|
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
|
||||||
const cluster = clusterStore.getById(clusterId);
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
const applier = new ResourceApplier(cluster);
|
const applier = new ResourceApplier(cluster);
|
||||||
|
|
||||||
applier.kubectlApplyAll(resources);
|
applier.kubectlApplyAll(resources);
|
||||||
} else {
|
} else {
|
||||||
throw `${clusterId} is not a valid cluster id`;
|
throw `${clusterId} is not a valid cluster id`;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { appEventBus } from "./event-bus";
|
|||||||
import { dumpConfigYaml } from "./kube-helpers";
|
import { dumpConfigYaml } from "./kube-helpers";
|
||||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import move from "array-move";
|
import move from "array-move";
|
||||||
import type { WorkspaceId } from "./workspace-store";
|
import type { WorkspaceId } from "./workspace-store";
|
||||||
@ -40,13 +40,30 @@ export interface ClusterStoreModel {
|
|||||||
export type ClusterId = string;
|
export type ClusterId = string;
|
||||||
|
|
||||||
export interface ClusterModel {
|
export interface ClusterModel {
|
||||||
|
/** Unique id for a cluster */
|
||||||
id: ClusterId;
|
id: ClusterId;
|
||||||
|
|
||||||
|
/** Path to cluster kubeconfig */
|
||||||
kubeConfigPath: string;
|
kubeConfigPath: string;
|
||||||
|
|
||||||
|
/** Workspace id */
|
||||||
workspace?: WorkspaceId;
|
workspace?: WorkspaceId;
|
||||||
|
|
||||||
|
/** User context in kubeconfig */
|
||||||
contextName?: string;
|
contextName?: string;
|
||||||
|
|
||||||
|
/** Preferences */
|
||||||
preferences?: ClusterPreferences;
|
preferences?: ClusterPreferences;
|
||||||
|
|
||||||
|
/** Metadata */
|
||||||
metadata?: ClusterMetadata;
|
metadata?: ClusterMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If extension sets ownerRef it has to explicitly mark a cluster as enabled during onActive (or when cluster is saved)
|
||||||
|
*/
|
||||||
ownerRef?: string;
|
ownerRef?: string;
|
||||||
|
|
||||||
|
/** List of accessible namespaces */
|
||||||
accessibleNamespaces?: string[];
|
accessibleNamespaces?: string[];
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
@ -81,7 +98,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
static embedCustomKubeConfig(clusterId: ClusterId, kubeConfig: KubeConfig | string): string {
|
||||||
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
|
const filePath = ClusterStore.getCustomKubeConfigPath(clusterId);
|
||||||
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
|
const fileContents = typeof kubeConfig == "string" ? kubeConfig : dumpConfigYaml(kubeConfig);
|
||||||
|
|
||||||
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
|
saveToAppFiles(filePath, fileContents, { mode: 0o600 });
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +108,8 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||||
|
|
||||||
|
private static stateRequestChannel = "cluster:states";
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super({
|
super({
|
||||||
configName: "lens-cluster-store",
|
configName: "lens-cluster-store",
|
||||||
@ -102,8 +123,45 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
this.pushStateToViewsAutomatically();
|
this.pushStateToViewsAutomatically();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
await super.load();
|
||||||
|
type clusterStateSync = {
|
||||||
|
id: string;
|
||||||
|
state: ClusterState;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ipcRenderer) {
|
||||||
|
logger.info("[CLUSTER-STORE] requesting initial state sync");
|
||||||
|
const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel);
|
||||||
|
|
||||||
|
clusterStates.forEach((clusterState) => {
|
||||||
|
const cluster = this.getById(clusterState.id);
|
||||||
|
|
||||||
|
if (cluster) {
|
||||||
|
cluster.setState(clusterState.state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => {
|
||||||
|
const states: clusterStateSync[] = [];
|
||||||
|
|
||||||
|
this.clustersList.forEach((cluster) => {
|
||||||
|
states.push({
|
||||||
|
state: cluster.getState(),
|
||||||
|
id: cluster.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return states;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected pushStateToViewsAutomatically() {
|
protected pushStateToViewsAutomatically() {
|
||||||
if (!ipcRenderer) {
|
if (!ipcRenderer) {
|
||||||
|
reaction(() => this.enabledClustersList, () => {
|
||||||
|
this.pushState();
|
||||||
|
});
|
||||||
reaction(() => this.connectedClustersList, () => {
|
reaction(() => this.connectedClustersList, () => {
|
||||||
this.pushState();
|
this.pushState();
|
||||||
});
|
});
|
||||||
@ -156,6 +214,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@action
|
@action
|
||||||
setActive(id: ClusterId) {
|
setActive(id: ClusterId) {
|
||||||
const clusterId = this.clusters.has(id) ? id : null;
|
const clusterId = this.clusters.has(id) ? id : null;
|
||||||
|
|
||||||
this.activeCluster = clusterId;
|
this.activeCluster = clusterId;
|
||||||
workspaceStore.setLastActiveClusterId(clusterId);
|
workspaceStore.setLastActiveClusterId(clusterId);
|
||||||
}
|
}
|
||||||
@ -163,11 +222,13 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@action
|
@action
|
||||||
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
swapIconOrders(workspace: WorkspaceId, from: number, to: number) {
|
||||||
const clusters = this.getByWorkspaceId(workspace);
|
const clusters = this.getByWorkspaceId(workspace);
|
||||||
|
|
||||||
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) {
|
||||||
throw new Error(`invalid from<->to arguments`);
|
throw new Error(`invalid from<->to arguments`);
|
||||||
}
|
}
|
||||||
|
|
||||||
move.mutate(clusters, from, to);
|
move.mutate(clusters, from, to);
|
||||||
|
|
||||||
for (const i in clusters) {
|
for (const i in clusters) {
|
||||||
// This resets the iconOrder to the current display order
|
// This resets the iconOrder to the current display order
|
||||||
clusters[i].preferences.iconOrder = +i;
|
clusters[i].preferences.iconOrder = +i;
|
||||||
@ -185,12 +246,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
getByWorkspaceId(workspaceId: string): Cluster[] {
|
||||||
const clusters = Array.from(this.clusters.values())
|
const clusters = Array.from(this.clusters.values())
|
||||||
.filter(cluster => cluster.workspace === workspaceId);
|
.filter(cluster => cluster.workspace === workspaceId);
|
||||||
|
|
||||||
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
addClusters(...models: ClusterModel[]): Cluster[] {
|
||||||
const clusters: Cluster[] = [];
|
const clusters: Cluster[] = [];
|
||||||
|
|
||||||
models.forEach(model => {
|
models.forEach(model => {
|
||||||
clusters.push(this.addCluster(model));
|
clusters.push(this.addCluster(model));
|
||||||
});
|
});
|
||||||
@ -202,10 +265,16 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
addCluster(model: ClusterModel | Cluster): Cluster {
|
addCluster(model: ClusterModel | Cluster): Cluster {
|
||||||
appEventBus.emit({ name: "cluster", action: "add" });
|
appEventBus.emit({ name: "cluster", action: "add" });
|
||||||
let cluster = model as Cluster;
|
let cluster = model as Cluster;
|
||||||
|
|
||||||
if (!(model instanceof Cluster)) {
|
if (!(model instanceof Cluster)) {
|
||||||
cluster = new Cluster(model);
|
cluster = new Cluster(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cluster.isManaged) {
|
||||||
|
cluster.enabled = true;
|
||||||
|
}
|
||||||
this.clusters.set(model.id, cluster);
|
this.clusters.set(model.id, cluster);
|
||||||
|
|
||||||
return cluster;
|
return cluster;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,11 +286,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
async removeById(clusterId: ClusterId) {
|
async removeById(clusterId: ClusterId) {
|
||||||
appEventBus.emit({ name: "cluster", action: "remove" });
|
appEventBus.emit({ name: "cluster", action: "remove" });
|
||||||
const cluster = this.getById(clusterId);
|
const cluster = this.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
this.clusters.delete(clusterId);
|
this.clusters.delete(clusterId);
|
||||||
|
|
||||||
if (this.activeCluster === clusterId) {
|
if (this.activeCluster === clusterId) {
|
||||||
this.setActive(null);
|
this.setActive(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove only custom kubeconfigs (pasted as text)
|
// remove only custom kubeconfigs (pasted as text)
|
||||||
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
||||||
unlink(cluster.kubeConfigPath).catch(() => null);
|
unlink(cluster.kubeConfigPath).catch(() => null);
|
||||||
@ -245,10 +317,12 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
// update new clusters
|
// update new clusters
|
||||||
for (const clusterModel of clusters) {
|
for (const clusterModel of clusters) {
|
||||||
let cluster = currentClusters.get(clusterModel.id);
|
let cluster = currentClusters.get(clusterModel.id);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.updateModel(clusterModel);
|
cluster.updateModel(clusterModel);
|
||||||
} else {
|
} else {
|
||||||
cluster = new Cluster(clusterModel);
|
cluster = new Cluster(clusterModel);
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
if (!cluster.isManaged) {
|
||||||
cluster.enabled = true;
|
cluster.enabled = true;
|
||||||
}
|
}
|
||||||
@ -282,6 +356,7 @@ export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
|||||||
|
|
||||||
export function getClusterIdFromHost(hostname: string): ClusterId {
|
export function getClusterIdFromHost(hostname: string): ClusterId {
|
||||||
const subDomains = hostname.split(":")[0].split(".");
|
const subDomains = hostname.split(":")[0].split(".");
|
||||||
|
|
||||||
return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345"
|
return subDomains.slice(-2)[0]; // e.g host == "%clusterId.localhost:45345"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export class ExecValidationNotFoundError extends Error {
|
|||||||
constructor(execPath: string, isAbsolute: boolean) {
|
constructor(execPath: string, isAbsolute: boolean) {
|
||||||
super(`User Exec command "${execPath}" not found on host.`);
|
super(`User Exec command "${execPath}" not found on host.`);
|
||||||
let message = `User Exec command "${execPath}" not found on host.`;
|
let message = `User Exec command "${execPath}" not found on host.`;
|
||||||
|
|
||||||
if (!isAbsolute) {
|
if (!isAbsolute) {
|
||||||
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export class EventEmitter<D extends [...any[]]> {
|
|||||||
addListener(callback: Callback<D>, options: Options = {}) {
|
addListener(callback: Callback<D>, options: Options = {}) {
|
||||||
if (options.prepend) {
|
if (options.prepend) {
|
||||||
const listeners = [...this.listeners];
|
const listeners = [...this.listeners];
|
||||||
|
|
||||||
listeners.unshift([callback, options]);
|
listeners.unshift([callback, options]);
|
||||||
this.listeners = new Map(listeners);
|
this.listeners = new Map(listeners);
|
||||||
}
|
}
|
||||||
@ -33,7 +34,9 @@ export class EventEmitter<D extends [...any[]]> {
|
|||||||
[...this.listeners].every(([callback, options]) => {
|
[...this.listeners].every(([callback, options]) => {
|
||||||
if (options.once) this.removeListener(callback);
|
if (options.once) this.removeListener(callback);
|
||||||
const result = callback(...data);
|
const result = callback(...data);
|
||||||
|
|
||||||
if (result === false) return; // break cycle
|
if (result === false) return; // break cycle
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,18 +16,22 @@ export async function requestMain(channel: string, ...args: any[]) {
|
|||||||
|
|
||||||
async function getSubFrames(): Promise<number[]> {
|
async function getSubFrames(): Promise<number[]> {
|
||||||
const subFrames: number[] = [];
|
const subFrames: number[] = [];
|
||||||
|
|
||||||
clusterFrameMap.forEach(frameId => {
|
clusterFrameMap.forEach(frameId => {
|
||||||
subFrames.push(frameId);
|
subFrames.push(frameId);
|
||||||
});
|
});
|
||||||
|
|
||||||
return subFrames;
|
return subFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function broadcastMessage(channel: string, ...args: any[]) {
|
export function broadcastMessage(channel: string, ...args: any[]) {
|
||||||
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
||||||
|
|
||||||
if (!views) return;
|
if (!views) return;
|
||||||
|
|
||||||
views.forEach(webContent => {
|
views.forEach(webContent => {
|
||||||
const type = webContent.getType();
|
const type = webContent.getType();
|
||||||
|
|
||||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
||||||
webContent.send(channel, ...args);
|
webContent.send(channel, ...args);
|
||||||
getSubFrames().then((frames) => {
|
getSubFrames().then((frames) => {
|
||||||
@ -36,6 +40,7 @@ export function broadcastMessage(channel: string, ...args: any[]) {
|
|||||||
});
|
});
|
||||||
}).catch((e) => e);
|
}).catch((e) => e);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
ipcRenderer.send(channel, ...args);
|
ipcRenderer.send(channel, ...args);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ function resolveTilde(filePath: string) {
|
|||||||
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
|
if (filePath[0] === "~" && (filePath[1] === "/" || filePath.length === 1)) {
|
||||||
return filePath.replace("~", os.homedir());
|
return filePath.replace("~", os.homedir());
|
||||||
}
|
}
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,12 +41,15 @@ export function validateConfig(config: KubeConfig | string): KubeConfig {
|
|||||||
config = loadConfig(config);
|
config = loadConfig(config);
|
||||||
}
|
}
|
||||||
logger.debug(`validating kube config: ${JSON.stringify(config)}`);
|
logger.debug(`validating kube config: ${JSON.stringify(config)}`);
|
||||||
|
|
||||||
if (!config.users || config.users.length == 0) {
|
if (!config.users || config.users.length == 0) {
|
||||||
throw new Error("No users provided in config");
|
throw new Error("No users provided in config");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.clusters || config.clusters.length == 0) {
|
if (!config.clusters || config.clusters.length == 0) {
|
||||||
throw new Error("No clusters provided in config");
|
throw new Error("No clusters provided in config");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.contexts || config.contexts.length == 0) {
|
if (!config.contexts || config.contexts.length == 0) {
|
||||||
throw new Error("No contexts provided in config");
|
throw new Error("No contexts provided in config");
|
||||||
}
|
}
|
||||||
@ -58,11 +62,13 @@ export function validateConfig(config: KubeConfig | string): KubeConfig {
|
|||||||
*/
|
*/
|
||||||
export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
||||||
const configs: KubeConfig[] = [];
|
const configs: KubeConfig[] = [];
|
||||||
|
|
||||||
if (!kubeConfig.contexts) {
|
if (!kubeConfig.contexts) {
|
||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
kubeConfig.contexts.forEach(ctx => {
|
kubeConfig.contexts.forEach(ctx => {
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
|
||||||
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
|
kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n);
|
||||||
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n);
|
kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n);
|
||||||
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n);
|
kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n);
|
||||||
@ -70,6 +76,7 @@ export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] {
|
|||||||
|
|
||||||
configs.push(kc);
|
configs.push(kc);
|
||||||
});
|
});
|
||||||
|
|
||||||
return configs;
|
return configs;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,10 +161,12 @@ export function validateKubeConfig (config: KubeConfig) {
|
|||||||
|
|
||||||
// Validate the User Object
|
// Validate the User Object
|
||||||
const user = config.getCurrentUser();
|
const user = config.getCurrentUser();
|
||||||
|
|
||||||
if (user.exec) {
|
if (user.exec) {
|
||||||
const execCommand = user.exec["command"];
|
const execCommand = user.exec["command"];
|
||||||
// check if the command is absolute or not
|
// check if the command is absolute or not
|
||||||
const isAbsolute = path.isAbsolute(execCommand);
|
const isAbsolute = path.isAbsolute(execCommand);
|
||||||
|
|
||||||
// validate the exec struct in the user object, start with the command field
|
// validate the exec struct in the user object, start with the command field
|
||||||
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
|
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry
|
|||||||
|
|
||||||
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
|
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
|
||||||
const provider = new providerClass();
|
const provider = new providerClass();
|
||||||
|
|
||||||
PrometheusProviderRegistry.registerProvider(provider.id, provider);
|
PrometheusProviderRegistry.registerProvider(provider.id, provider);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -42,10 +42,12 @@ export function isAllowedResource(resources: KubeResource | KubeResource[]) {
|
|||||||
resources = [resources];
|
resources = [resources];
|
||||||
}
|
}
|
||||||
const { allowedResources = [] } = getHostedCluster() || {};
|
const { allowedResources = [] } = getHostedCluster() || {};
|
||||||
|
|
||||||
for (const resource of resources) {
|
for (const resource of resources) {
|
||||||
if (!allowedResources.includes(resource)) {
|
if (!allowedResources.includes(resource)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export function registerFileProtocol(name: string, basePath: string) {
|
|||||||
protocol.registerFileProtocol(name, (request, callback) => {
|
protocol.registerFileProtocol(name, (request, callback) => {
|
||||||
const filePath = request.url.replace(`${name}://`, "");
|
const filePath = request.url.replace(`${name}://`, "");
|
||||||
const absPath = path.resolve(basePath, filePath);
|
const absPath = path.resolve(basePath, filePath);
|
||||||
|
|
||||||
callback({ path: absPath });
|
callback({ path: absPath });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { userStore } from "./user-store";
|
|||||||
|
|
||||||
function getDefaultRequestOpts(): Partial<request.Options> {
|
function getDefaultRequestOpts(): Partial<request.Options> {
|
||||||
const { httpsProxy, allowUntrustedCAs } = userStore.preferences;
|
const { httpsProxy, allowUntrustedCAs } = userStore.preferences;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
proxy: httpsProxy || undefined,
|
proxy: httpsProxy || undefined,
|
||||||
rejectUnauthorized: !allowUntrustedCAs,
|
rejectUnauthorized: !allowUntrustedCAs,
|
||||||
|
|||||||
@ -14,8 +14,10 @@ export class SearchStore {
|
|||||||
@action
|
@action
|
||||||
onSearch(text: string[], query = this.searchQuery) {
|
onSearch(text: string[], query = this.searchQuery) {
|
||||||
this.searchQuery = query;
|
this.searchQuery = query;
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
this.reset();
|
this.reset();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.occurrences = this.findOccurences(text, query);
|
this.occurrences = this.findOccurences(text, query);
|
||||||
@ -36,11 +38,14 @@ export class SearchStore {
|
|||||||
findOccurences(text: string[], query: string) {
|
findOccurences(text: string[], query: string) {
|
||||||
if (!text) return [];
|
if (!text) return [];
|
||||||
const occurences: number[] = [];
|
const occurences: number[] = [];
|
||||||
|
|
||||||
text.forEach((line, index) => {
|
text.forEach((line, index) => {
|
||||||
const regex = new RegExp(this.escapeRegex(query), "gi");
|
const regex = new RegExp(this.escapeRegex(query), "gi");
|
||||||
const matches = [...line.matchAll(regex)];
|
const matches = [...line.matchAll(regex)];
|
||||||
|
|
||||||
matches.forEach(() => occurences.push(index));
|
matches.forEach(() => occurences.push(index));
|
||||||
});
|
});
|
||||||
|
|
||||||
return occurences;
|
return occurences;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,9 +56,11 @@ export class SearchStore {
|
|||||||
*/
|
*/
|
||||||
getNextOverlay(loopOver = false) {
|
getNextOverlay(loopOver = false) {
|
||||||
const next = this.activeOverlayIndex + 1;
|
const next = this.activeOverlayIndex + 1;
|
||||||
|
|
||||||
if (next > this.occurrences.length - 1) {
|
if (next > this.occurrences.length - 1) {
|
||||||
return loopOver ? 0 : this.activeOverlayIndex;
|
return loopOver ? 0 : this.activeOverlayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,9 +71,11 @@ export class SearchStore {
|
|||||||
*/
|
*/
|
||||||
getPrevOverlay(loopOver = false) {
|
getPrevOverlay(loopOver = false) {
|
||||||
const prev = this.activeOverlayIndex - 1;
|
const prev = this.activeOverlayIndex - 1;
|
||||||
|
|
||||||
if (prev < 0) {
|
if (prev < 0) {
|
||||||
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
|
return loopOver ? this.occurrences.length - 1 : this.activeOverlayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +113,7 @@ export class SearchStore {
|
|||||||
@autobind()
|
@autobind()
|
||||||
isActiveOverlay(line: number, occurence: number) {
|
isActiveOverlay(line: number, occurence: number) {
|
||||||
const firstLineIndex = this.occurrences.findIndex(item => item === line);
|
const firstLineIndex = this.occurrences.findIndex(item => item === line);
|
||||||
|
|
||||||
return firstLineIndex + occurence === this.activeOverlayIndex;
|
return firstLineIndex + occurence === this.activeOverlayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import logger from "../main/logger";
|
|||||||
if (isMac) {
|
if (isMac) {
|
||||||
for (const crt of macca.all()) {
|
for (const crt of macca.all()) {
|
||||||
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`);
|
const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`);
|
||||||
|
|
||||||
logger.debug(`Using host CA: ${attributes.join(",")}`);
|
logger.debug(`Using host CA: ${attributes.join(",")}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
protected refreshNewContexts = async () => {
|
protected refreshNewContexts = async () => {
|
||||||
try {
|
try {
|
||||||
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
||||||
|
|
||||||
if (kubeConfig) {
|
if (kubeConfig) {
|
||||||
this.newContexts.clear();
|
this.newContexts.clear();
|
||||||
loadConfig(kubeConfig).getContexts()
|
loadConfig(kubeConfig).getContexts()
|
||||||
@ -118,6 +119,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
@action
|
@action
|
||||||
markNewContextsAsSeen() {
|
markNewContextsAsSeen() {
|
||||||
const { seenContexts, newContexts } = this;
|
const { seenContexts, newContexts } = this;
|
||||||
|
|
||||||
this.seenContexts.replace([...seenContexts, ...newContexts]);
|
this.seenContexts.replace([...seenContexts, ...newContexts]);
|
||||||
this.newContexts.clear();
|
this.newContexts.clear();
|
||||||
}
|
}
|
||||||
@ -133,9 +135,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
@action
|
@action
|
||||||
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
||||||
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
|
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
|
||||||
|
|
||||||
if (lastSeenAppVersion) {
|
if (lastSeenAppVersion) {
|
||||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kubeConfigPath) {
|
if (kubeConfigPath) {
|
||||||
this.kubeConfigPath = kubeConfigPath;
|
this.kubeConfigPath = kubeConfigPath;
|
||||||
}
|
}
|
||||||
@ -150,6 +154,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
seenContexts: Array.from(this.seenContexts),
|
seenContexts: Array.from(this.seenContexts),
|
||||||
preferences: this.preferences,
|
preferences: this.preferences,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(model, {
|
return toJS(model, {
|
||||||
recurseEverything: true,
|
recurseEverything: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,7 +12,6 @@ export function autobind() {
|
|||||||
function bindClass<T extends Constructor>(constructor: T) {
|
function bindClass<T extends Constructor>(constructor: T) {
|
||||||
const proto = constructor.prototype;
|
const proto = constructor.prototype;
|
||||||
const descriptors = Object.getOwnPropertyDescriptors(proto);
|
const descriptors = Object.getOwnPropertyDescriptors(proto);
|
||||||
|
|
||||||
const skipMethod = (methodName: string) => {
|
const skipMethod = (methodName: string) => {
|
||||||
return methodName === "constructor"
|
return methodName === "constructor"
|
||||||
|| typeof descriptors[methodName].value !== "function";
|
|| typeof descriptors[methodName].value !== "function";
|
||||||
@ -21,6 +20,7 @@ function bindClass<T extends Constructor>(constructor: T) {
|
|||||||
Object.keys(descriptors).forEach(prop => {
|
Object.keys(descriptors).forEach(prop => {
|
||||||
if (skipMethod(prop)) return;
|
if (skipMethod(prop)) return;
|
||||||
const boundDescriptor = bindMethod(proto, prop, descriptors[prop]);
|
const boundDescriptor = bindMethod(proto, prop, descriptors[prop]);
|
||||||
|
|
||||||
Object.defineProperty(proto, prop, boundDescriptor);
|
Object.defineProperty(proto, prop, boundDescriptor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -38,6 +38,7 @@ function bindMethod(target: object, prop?: string, descriptor?: PropertyDescript
|
|||||||
get() {
|
get() {
|
||||||
if (this === target) return func; // direct access from prototype
|
if (this === target) return func; // direct access from prototype
|
||||||
if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this));
|
if (!boundFunc.has(this)) boundFunc.set(this, func.bind(this));
|
||||||
|
|
||||||
return boundFunc.get(this);
|
return boundFunc.get(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,8 +7,10 @@ export interface IURLParams<P extends object = {}, Q extends object = {}> {
|
|||||||
|
|
||||||
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
|
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
|
||||||
const pathBuilder = compile(String(path));
|
const pathBuilder = compile(String(path));
|
||||||
|
|
||||||
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
||||||
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
|
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
|
||||||
|
|
||||||
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "");
|
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,9 @@ export function toCamelCase(obj: Record<string, any>): any {
|
|||||||
else if (isPlainObject(obj)) {
|
else if (isPlainObject(obj)) {
|
||||||
return Object.keys(obj).reduce((result, key) => {
|
return Object.keys(obj).reduce((result, key) => {
|
||||||
const value = obj[key];
|
const value = obj[key];
|
||||||
|
|
||||||
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
|
result[camelCase(key)] = typeof value === "object" ? toCamelCase(value) : value;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, {} as any);
|
}, {} as any);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
|
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
return (...params: any[]) => new Promise(resolve => {
|
return (...params: any[]) => new Promise(resolve => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
|
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export function downloadFile({ url, timeout, gzip = true }: DownloadFileOptions)
|
|||||||
resolve(Buffer.concat(fileChunks));
|
resolve(Buffer.concat(fileChunks));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
promise,
|
promise,
|
||||||
|
|||||||
@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
|
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
|
||||||
const randId = () => Math.random().toString(16).substr(2);
|
const randId = () => Math.random().toString(16).substr(2);
|
||||||
|
|
||||||
return [prefix, randId(), suffix].filter(s => s).join(sep);
|
return [prefix, randId(), suffix].filter(s => s).join(sep);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import { WriteFileOptions } from "fs";
|
|||||||
|
|
||||||
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
|
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
|
||||||
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
|
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);
|
||||||
|
|
||||||
ensureDirSync(path.dirname(absPath));
|
ensureDirSync(path.dirname(absPath));
|
||||||
writeFileSync(absPath, contents, options);
|
writeFileSync(absPath, contents, options);
|
||||||
|
|
||||||
return absPath;
|
return absPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ class Singleton {
|
|||||||
if (!Singleton.instances.has(this)) {
|
if (!Singleton.instances.has(this)) {
|
||||||
Singleton.instances.set(this, Reflect.construct(this, args));
|
Singleton.instances.set(this, Reflect.construct(this, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Singleton.instances.get(this) as T;
|
return Singleton.instances.get(this) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,8 +12,10 @@
|
|||||||
*/
|
*/
|
||||||
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
|
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
|
||||||
const index = array.indexOf(element);
|
const index = array.indexOf(element);
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return [array, [], false];
|
return [array, [], false];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [array.slice(0, index), array.slice(index + 1, array.length), true];
|
return [array.slice(0, index), array.slice(index + 1, array.length), true];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: Re
|
|||||||
|
|
||||||
await tar.list({
|
await tar.list({
|
||||||
file: tarPath,
|
file: tarPath,
|
||||||
filter: path => path === filePath,
|
filter: entryPath => path.normalize(entryPath) === filePath,
|
||||||
onentry(entry: FileStat) {
|
onentry(entry: FileStat) {
|
||||||
entry.on("data", chunk => {
|
entry.on("data", chunk => {
|
||||||
fileChunks.push(chunk);
|
fileChunks.push(chunk);
|
||||||
@ -26,6 +26,7 @@ export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: Re
|
|||||||
entry.once("end", () => {
|
entry.once("end", () => {
|
||||||
const data = Buffer.concat(fileChunks);
|
const data = Buffer.concat(fileChunks);
|
||||||
const result = parseJson ? JSON.parse(data.toString("utf8")) : data;
|
const result = parseJson ? JSON.parse(data.toString("utf8")) : data;
|
||||||
|
|
||||||
resolve(result);
|
resolve(result);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -39,10 +40,14 @@ export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: Re
|
|||||||
|
|
||||||
export async function listTarEntries(filePath: string): Promise<string[]> {
|
export async function listTarEntries(filePath: string): Promise<string[]> {
|
||||||
const entries: string[] = [];
|
const entries: string[] = [];
|
||||||
|
|
||||||
await tar.list({
|
await tar.list({
|
||||||
file: filePath,
|
file: filePath,
|
||||||
onentry: (entry: FileStat) => entries.push(entry.path as any as string),
|
onentry: (entry: FileStat) => {
|
||||||
|
entries.push(path.normalize(entry.path as any as string));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ defineGlobal("__static", {
|
|||||||
if (isDevelopment) {
|
if (isDevelopment) {
|
||||||
return path.resolve(contextDir, "static");
|
return path.resolve(contextDir, "static");
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.resolve(process.resourcesPath, "static");
|
return path.resolve(process.resourcesPath, "static");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { action, computed, observable, toJS, reaction } from "mobx";
|
|||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import { clusterStore } from "./cluster-store";
|
import { clusterStore } from "./cluster-store";
|
||||||
import { appEventBus } from "./event-bus";
|
import { appEventBus } from "./event-bus";
|
||||||
import { broadcastMessage } from "../common/ipc";
|
import { broadcastMessage, handleRequest, requestMain } from "../common/ipc";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import type { ClusterId } from "./cluster-store";
|
import type { ClusterId } from "./cluster-store";
|
||||||
|
|
||||||
@ -33,32 +33,44 @@ export interface WorkspaceState {
|
|||||||
*/
|
*/
|
||||||
export class Workspace implements WorkspaceModel, WorkspaceState {
|
export class Workspace implements WorkspaceModel, WorkspaceState {
|
||||||
/**
|
/**
|
||||||
* Unique id
|
* Unique id for workspace
|
||||||
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable id: WorkspaceId;
|
@observable id: WorkspaceId;
|
||||||
/**
|
/**
|
||||||
* Workspace name
|
* Workspace name
|
||||||
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable name: string;
|
@observable name: string;
|
||||||
/**
|
/**
|
||||||
* Description
|
* Workspace description
|
||||||
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable description?: string;
|
@observable description?: string;
|
||||||
/**
|
/**
|
||||||
* Owner reference
|
* Workspace owner reference
|
||||||
|
*
|
||||||
|
* If extension sets ownerRef then it needs to explicitly mark workspace as enabled onActivate (or when workspace is saved)
|
||||||
*
|
*
|
||||||
* If an extension sets this then extension also needs explicitly to set workspace as enabled
|
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable ownerRef?: string;
|
@observable ownerRef?: string;
|
||||||
/**
|
/**
|
||||||
* Is workspace enabled (disabled workspaces are currently hidden)
|
* Is workspace enabled
|
||||||
|
*
|
||||||
|
* Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace.
|
||||||
|
*
|
||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable enabled: boolean;
|
@observable enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Last active cluster id
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable lastActiveClusterId?: ClusterId;
|
@observable lastActiveClusterId?: ClusterId;
|
||||||
|
|
||||||
constructor(data: WorkspaceModel) {
|
constructor(data: WorkspaceModel) {
|
||||||
@ -83,9 +95,9 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
getState(): WorkspaceState {
|
getState(): WorkspaceState {
|
||||||
return {
|
return toJS({
|
||||||
enabled: this.enabled
|
enabled: this.enabled
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,16 +132,45 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
|||||||
|
|
||||||
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||||
static readonly defaultId: WorkspaceId = "default";
|
static readonly defaultId: WorkspaceId = "default";
|
||||||
|
private static stateRequestChannel = "workspace:states";
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super({
|
super({
|
||||||
configName: "lens-workspace-store",
|
configName: "lens-workspace-store",
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!ipcRenderer) {
|
async load() {
|
||||||
setInterval(() => {
|
await super.load();
|
||||||
this.pushState();
|
type workspaceStateSync = {
|
||||||
}, 5000);
|
id: string;
|
||||||
|
state: WorkspaceState;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ipcRenderer) {
|
||||||
|
logger.info("[WORKSPACE-STORE] requesting initial state sync");
|
||||||
|
const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel);
|
||||||
|
|
||||||
|
workspaceStates.forEach((workspaceState) => {
|
||||||
|
const workspace = this.getById(workspaceState.id);
|
||||||
|
|
||||||
|
if (workspace) {
|
||||||
|
workspace.setState(workspaceState.state);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => {
|
||||||
|
const states: workspaceStateSync[] = [];
|
||||||
|
|
||||||
|
this.workspacesList.forEach((workspace) => {
|
||||||
|
states.push({
|
||||||
|
state: workspace.getState(),
|
||||||
|
id: workspace.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return states;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +228,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
@action
|
@action
|
||||||
setActive(id = WorkspaceStore.defaultId) {
|
setActive(id = WorkspaceStore.defaultId) {
|
||||||
if (id === this.currentWorkspaceId) return;
|
if (id === this.currentWorkspaceId) return;
|
||||||
|
|
||||||
if (!this.getById(id)) {
|
if (!this.getById(id)) {
|
||||||
throw new Error(`workspace ${id} doesn't exist`);
|
throw new Error(`workspace ${id} doesn't exist`);
|
||||||
}
|
}
|
||||||
@ -196,11 +238,18 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
@action
|
@action
|
||||||
addWorkspace(workspace: Workspace) {
|
addWorkspace(workspace: Workspace) {
|
||||||
const { id, name } = workspace;
|
const { id, name } = workspace;
|
||||||
|
|
||||||
if (!name.trim() || this.getByName(name.trim())) {
|
if (!name.trim() || this.getByName(name.trim())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.workspaces.set(id, workspace);
|
this.workspaces.set(id, workspace);
|
||||||
|
|
||||||
|
if (!workspace.isManaged) {
|
||||||
|
workspace.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
appEventBus.emit({name: "workspace", action: "add"});
|
appEventBus.emit({name: "workspace", action: "add"});
|
||||||
|
|
||||||
return workspace;
|
return workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,10 +267,13 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
@action
|
@action
|
||||||
removeWorkspaceById(id: WorkspaceId) {
|
removeWorkspaceById(id: WorkspaceId) {
|
||||||
const workspace = this.getById(id);
|
const workspace = this.getById(id);
|
||||||
|
|
||||||
if (!workspace) return;
|
if (!workspace) return;
|
||||||
|
|
||||||
if (this.isDefault(id)) {
|
if (this.isDefault(id)) {
|
||||||
throw new Error("Cannot remove default workspace");
|
throw new Error("Cannot remove default workspace");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentWorkspaceId === id) {
|
if (this.currentWorkspaceId === id) {
|
||||||
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
||||||
}
|
}
|
||||||
@ -240,10 +292,12 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
|||||||
if (currentWorkspace) {
|
if (currentWorkspace) {
|
||||||
this.currentWorkspaceId = currentWorkspace;
|
this.currentWorkspaceId = currentWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workspaces.length) {
|
if (workspaces.length) {
|
||||||
this.workspaces.clear();
|
this.workspaces.clear();
|
||||||
workspaces.forEach(ws => {
|
workspaces.forEach(ws => {
|
||||||
const workspace = new Workspace(ws);
|
const workspace = new Workspace(ws);
|
||||||
|
|
||||||
if (!workspace.isManaged) {
|
if (!workspace.isManaged) {
|
||||||
workspace.enabled = true;
|
workspace.enabled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,24 @@
|
|||||||
import { ExtensionLoader } from "../extension-loader";
|
import { ExtensionLoader } from "../extension-loader";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import { extensionsStore } from "../extensions-store";
|
||||||
|
|
||||||
const manifestPath = "manifest/path";
|
const manifestPath = "manifest/path";
|
||||||
const manifestPath2 = "manifest/path2";
|
const manifestPath2 = "manifest/path2";
|
||||||
const manifestPath3 = "manifest/path3";
|
const manifestPath3 = "manifest/path3";
|
||||||
|
|
||||||
|
jest.mock("../extensions-store", () => ({
|
||||||
|
extensionsStore: {
|
||||||
|
whenLoaded: Promise.resolve(true),
|
||||||
|
mergeState: jest.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
"electron",
|
"electron",
|
||||||
() => ({
|
() => ({
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
invoke: jest.fn(async (channel: string) => {
|
invoke: jest.fn(async (channel: string) => {
|
||||||
if (channel === "extensions:loaded") {
|
if (channel === "extensions:main") {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
manifestPath,
|
manifestPath,
|
||||||
@ -44,7 +53,7 @@ jest.mock(
|
|||||||
}),
|
}),
|
||||||
on: jest.fn(
|
on: jest.fn(
|
||||||
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||||
if (channel === "extensions:loaded") {
|
if (channel === "extensions:main") {
|
||||||
// First initialize with extensions 1 and 2
|
// First initialize with extensions 1 and 2
|
||||||
// and then broadcast event to remove extensioin 2 and add extension number 3
|
// and then broadcast event to remove extensioin 2 and add extension number 3
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -129,4 +138,29 @@ describe("ExtensionLoader", () => {
|
|||||||
done();
|
done();
|
||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates ExtensionsStore after isEnabled is changed", async () => {
|
||||||
|
(extensionsStore.mergeState as any).mockClear();
|
||||||
|
|
||||||
|
// Disable sending events in this test
|
||||||
|
(ipcRenderer.on as any).mockImplementation();
|
||||||
|
|
||||||
|
const extensionLoader = new ExtensionLoader();
|
||||||
|
|
||||||
|
await extensionLoader.init();
|
||||||
|
|
||||||
|
expect(extensionsStore.mergeState).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
|
||||||
|
|
||||||
|
expect(extensionsStore.mergeState).toHaveBeenCalledWith({
|
||||||
|
"manifest/path": {
|
||||||
|
enabled: false,
|
||||||
|
name: "TestExtension"
|
||||||
|
},
|
||||||
|
"manifest/path2": {
|
||||||
|
enabled: true,
|
||||||
|
name: "TestExtension2"
|
||||||
|
}});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -108,12 +108,15 @@ export abstract class ClusterFeature {
|
|||||||
*/
|
*/
|
||||||
protected renderTemplates(folderPath: string): string[] {
|
protected renderTemplates(folderPath: string): string[] {
|
||||||
const resources: string[] = [];
|
const resources: string[] = [];
|
||||||
|
|
||||||
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
||||||
fs.readdirSync(folderPath).forEach(filename => {
|
fs.readdirSync(folderPath).forEach(filename => {
|
||||||
const file = path.join(folderPath, filename);
|
const file = path.join(folderPath, filename);
|
||||||
const raw = fs.readFileSync(file);
|
const raw = fs.readFileSync(file);
|
||||||
|
|
||||||
if (filename.endsWith(".hb")) {
|
if (filename.endsWith(".hb")) {
|
||||||
const template = hb.compile(raw.toString());
|
const template = hb.compile(raw.toString());
|
||||||
|
|
||||||
resources.push(template(this.templateContext));
|
resources.push(template(this.templateContext));
|
||||||
} else {
|
} else {
|
||||||
resources.push(raw.toString());
|
resources.push(raw.toString());
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { extensionsStore } from "../extensions-store";
|
|||||||
|
|
||||||
export const version = getAppVersion();
|
export const version = getAppVersion();
|
||||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||||
|
|
||||||
export function getEnabledExtensions(): string[] {
|
export function getEnabledExtensions(): string[] {
|
||||||
return extensionsStore.enabledExtensions;
|
return extensionsStore.enabledExtensions;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export interface InstalledExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logModule = "[EXTENSION-DISCOVERY]";
|
const logModule = "[EXTENSION-DISCOVERY]";
|
||||||
|
|
||||||
export const manifestFilename = "package.json";
|
export const manifestFilename = "package.json";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -113,8 +114,8 @@ export class ExtensionDiscovery {
|
|||||||
|
|
||||||
// chokidar works better than fs.watch
|
// chokidar works better than fs.watch
|
||||||
chokidar.watch(this.localFolderPath, {
|
chokidar.watch(this.localFolderPath, {
|
||||||
// Dont watch recursively into subdirectories
|
// For adding and removing symlinks to work, the depth has to be 1.
|
||||||
depth: 0,
|
depth: 1,
|
||||||
// Try to wait until the file has been completely copied.
|
// Try to wait until the file has been completely copied.
|
||||||
// The OS might emit an event for added file even it's not completely written to the filesysten.
|
// The OS might emit an event for added file even it's not completely written to the filesysten.
|
||||||
awaitWriteFinish: {
|
awaitWriteFinish: {
|
||||||
@ -123,7 +124,7 @@ export class ExtensionDiscovery {
|
|||||||
stabilityThreshold: 300
|
stabilityThreshold: 300
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Extension add is detected by watching "<extensionDir>package.json" add
|
// Extension add is detected by watching "<extensionDir>/package.json" add
|
||||||
.on("add", this.handleWatchFileAdd)
|
.on("add", this.handleWatchFileAdd)
|
||||||
// Extension remove is detected by watching <extensionDir>" unlink
|
// Extension remove is detected by watching <extensionDir>" unlink
|
||||||
.on("unlinkDir", this.handleWatchUnlinkDir);
|
.on("unlinkDir", this.handleWatchUnlinkDir);
|
||||||
@ -133,7 +134,6 @@ export class ExtensionDiscovery {
|
|||||||
if (path.basename(filePath) === manifestFilename) {
|
if (path.basename(filePath) === manifestFilename) {
|
||||||
try {
|
try {
|
||||||
const absPath = path.dirname(filePath);
|
const absPath = path.dirname(filePath);
|
||||||
|
|
||||||
// this.loadExtensionFromPath updates this.packagesJson
|
// this.loadExtensionFromPath updates this.packagesJson
|
||||||
const extension = await this.loadExtensionFromPath(absPath);
|
const extension = await this.loadExtensionFromPath(absPath);
|
||||||
|
|
||||||
@ -251,6 +251,7 @@ export class ExtensionDiscovery {
|
|||||||
|
|
||||||
manifestJson = __non_webpack_require__(manifestPath);
|
manifestJson = __non_webpack_require__(manifestPath);
|
||||||
const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
|
const installedManifestPath = path.join(this.nodeModulesPath, manifestJson.name, "package.json");
|
||||||
|
|
||||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath);
|
||||||
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||||
|
|
||||||
@ -272,6 +273,7 @@ export class ExtensionDiscovery {
|
|||||||
async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
async loadExtensions(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||||
const bundledExtensions = await this.loadBundledExtensions();
|
const bundledExtensions = await this.loadBundledExtensions();
|
||||||
const localExtensions = await this.loadFromFolder(this.localFolderPath);
|
const localExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||||
|
|
||||||
await this.installPackages();
|
await this.installPackages();
|
||||||
const extensions = bundledExtensions.concat(localExtensions);
|
const extensions = bundledExtensions.concat(localExtensions);
|
||||||
|
|
||||||
@ -333,12 +335,14 @@ export class ExtensionDiscovery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const extension = await this.loadExtensionFromPath(absPath);
|
const extension = await this.loadExtensionFromPath(absPath);
|
||||||
|
|
||||||
if (extension) {
|
if (extension) {
|
||||||
extensions.push(extension);
|
extensions.push(extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
logger.debug(`${logModule}: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||||
|
|
||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export class ExtensionInstaller {
|
|||||||
cwd: extensionPackagesRoot(),
|
cwd: extensionPackagesRoot(),
|
||||||
silent: true
|
silent: true
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", () => {
|
child.on("close", () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { app, ipcRenderer, remote } from "electron";
|
import { app, ipcRenderer, remote } from "electron";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
import { isEqual } from "lodash";
|
||||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getHostedCluster } from "../common/cluster-store";
|
import { getHostedCluster } from "../common/cluster-store";
|
||||||
@ -25,7 +26,12 @@ const logModule = "[EXTENSIONS-LOADER]";
|
|||||||
export class ExtensionLoader {
|
export class ExtensionLoader {
|
||||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||||
protected readonly requestExtensionsChannel = "extensions:loaded";
|
|
||||||
|
// IPC channel to broadcast changes to extensions from main
|
||||||
|
protected static readonly extensionsMainChannel = "extensions:main";
|
||||||
|
|
||||||
|
// IPC channel to broadcast changes to extensions from renderer
|
||||||
|
protected static readonly extensionsRendererChannel = "extensions:renderer";
|
||||||
|
|
||||||
// emits event "remove" of type LensExtension when the extension is removed
|
// emits event "remove" of type LensExtension when the extension is removed
|
||||||
private events = new EventEmitter();
|
private events = new EventEmitter();
|
||||||
@ -45,6 +51,17 @@ export class ExtensionLoader {
|
|||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transform userExtensions to a state object for storing into ExtensionsStore
|
||||||
|
@computed get storeState() {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Array.from(this.userExtensions)
|
||||||
|
.map(([extId, extension]) => [extId, {
|
||||||
|
enabled: extension.isEnabled,
|
||||||
|
name: extension.manifest.name,
|
||||||
|
}])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async init() {
|
async init() {
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
@ -53,7 +70,12 @@ export class ExtensionLoader {
|
|||||||
await this.initMain();
|
await this.initMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionsStore.manageState(this);
|
await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]);
|
||||||
|
|
||||||
|
// save state on change `extension.isEnabled`
|
||||||
|
reaction(() => this.storeState, extensionsState => {
|
||||||
|
extensionsStore.mergeState(extensionsState);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initExtensions(extensions?: Map<LensExtensionId, InstalledExtension>) {
|
initExtensions(extensions?: Map<LensExtensionId, InstalledExtension>) {
|
||||||
@ -95,28 +117,27 @@ export class ExtensionLoader {
|
|||||||
this.loadOnMain();
|
this.loadOnMain();
|
||||||
this.broadcastExtensions();
|
this.broadcastExtensions();
|
||||||
|
|
||||||
reaction(() => this.extensions.toJS(), () => {
|
reaction(() => this.toJSON(), () => {
|
||||||
this.broadcastExtensions();
|
this.broadcastExtensions();
|
||||||
});
|
});
|
||||||
|
|
||||||
handleRequest(this.requestExtensionsChannel, () => {
|
handleRequest(ExtensionLoader.extensionsMainChannel, () => {
|
||||||
return Array.from(this.toJSON());
|
return Array.from(this.toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
subscribeToBroadcast(ExtensionLoader.extensionsRendererChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||||
|
this.syncExtensions(extensions);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async initRenderer() {
|
protected async initRenderer() {
|
||||||
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
|
const extensionListHandler = (extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||||
this.isLoaded = true;
|
this.isLoaded = true;
|
||||||
|
this.syncExtensions(extensions);
|
||||||
|
|
||||||
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
const receivedExtensionIds = extensions.map(([lensExtensionId]) => lensExtensionId);
|
||||||
|
|
||||||
// Add new extensions
|
// Remove deleted extensions in renderer side only
|
||||||
extensions.forEach(([extId, ext]) => {
|
|
||||||
if (!this.extensions.has(extId)) {
|
|
||||||
this.extensions.set(extId, ext);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove deleted extensions
|
|
||||||
this.extensions.forEach((_, lensExtensionId) => {
|
this.extensions.forEach((_, lensExtensionId) => {
|
||||||
if (!receivedExtensionIds.includes(lensExtensionId)) {
|
if (!receivedExtensionIds.includes(lensExtensionId)) {
|
||||||
this.removeExtension(lensExtensionId);
|
this.removeExtension(lensExtensionId);
|
||||||
@ -124,14 +145,26 @@ export class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
requestMain(this.requestExtensionsChannel).then(extensionListHandler);
|
reaction(() => this.toJSON(), () => {
|
||||||
subscribeToBroadcast(this.requestExtensionsChannel, (event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
this.broadcastExtensions(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
requestMain(ExtensionLoader.extensionsMainChannel).then(extensionListHandler);
|
||||||
|
subscribeToBroadcast(ExtensionLoader.extensionsMainChannel, (_event, extensions: [LensExtensionId, InstalledExtension][]) => {
|
||||||
extensionListHandler(extensions);
|
extensionListHandler(extensions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncExtensions(extensions: [LensExtensionId, InstalledExtension][]) {
|
||||||
|
extensions.forEach(([lensExtensionId, extension]) => {
|
||||||
|
if (!isEqual(this.extensions.get(lensExtensionId), extension)) {
|
||||||
|
this.extensions.set(lensExtensionId, extension);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
loadOnMain() {
|
loadOnMain() {
|
||||||
logger.info(`${logModule}: load on main`);
|
logger.debug(`${logModule}: load on main`);
|
||||||
this.autoInitExtensions(async (extension: LensMainExtension) => {
|
this.autoInitExtensions(async (extension: LensMainExtension) => {
|
||||||
// Each .add returns a function to remove the item
|
// Each .add returns a function to remove the item
|
||||||
const removeItems = [
|
const removeItems = [
|
||||||
@ -151,7 +184,7 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterManagerRenderer() {
|
loadOnClusterManagerRenderer() {
|
||||||
logger.info(`${logModule}: load on main renderer (cluster manager)`);
|
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
||||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||||
const removeItems = [
|
const removeItems = [
|
||||||
registries.globalPageRegistry.add(extension.globalPages, extension),
|
registries.globalPageRegistry.add(extension.globalPages, extension),
|
||||||
@ -174,8 +207,9 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterRenderer() {
|
loadOnClusterRenderer() {
|
||||||
logger.info(`${logModule}: load on cluster renderer (dashboard)`);
|
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
||||||
const cluster = getHostedCluster();
|
const cluster = getHostedCluster();
|
||||||
|
|
||||||
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
this.autoInitExtensions(async (extension: LensRendererExtension) => {
|
||||||
if (await extension.isEnabledForCluster(cluster) === false) {
|
if (await extension.isEnabledForCluster(cluster) === false) {
|
||||||
return [];
|
return [];
|
||||||
@ -203,24 +237,26 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) {
|
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Function[]>) {
|
||||||
return reaction(() => this.toJSON(), installedExtensions => {
|
return reaction(() => this.toJSON(), installedExtensions => {
|
||||||
for (const [extId, ext] of installedExtensions) {
|
for (const [extId, extension] of installedExtensions) {
|
||||||
const alreadyInit = this.instances.has(extId);
|
const alreadyInit = this.instances.has(extId);
|
||||||
|
|
||||||
if (ext.isEnabled && !alreadyInit) {
|
if (extension.isEnabled && !alreadyInit) {
|
||||||
try {
|
try {
|
||||||
const LensExtensionClass = this.requireExtension(ext);
|
const LensExtensionClass = this.requireExtension(extension);
|
||||||
|
|
||||||
if (!LensExtensionClass) {
|
if (!LensExtensionClass) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new LensExtensionClass(ext);
|
const instance = new LensExtensionClass(extension);
|
||||||
|
|
||||||
instance.whenEnabled(() => register(instance));
|
instance.whenEnabled(() => register(instance));
|
||||||
instance.enable();
|
instance.enable();
|
||||||
this.instances.set(extId, instance);
|
this.instances.set(extId, instance);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`${logModule}: activation extension error`, { ext, err });
|
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
|
||||||
}
|
}
|
||||||
} else if (!ext.isEnabled && alreadyInit) {
|
} else if (!extension.isEnabled && alreadyInit) {
|
||||||
this.removeInstance(extId);
|
this.removeInstance(extId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,12 +267,14 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
|
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor {
|
||||||
let extEntrypoint = "";
|
let extEntrypoint = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (ipcRenderer && extension.manifest.renderer) {
|
if (ipcRenderer && extension.manifest.renderer) {
|
||||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer));
|
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer));
|
||||||
} else if (!ipcRenderer && extension.manifest.main) {
|
} else if (!ipcRenderer && extension.manifest.main) {
|
||||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main));
|
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extEntrypoint !== "") {
|
if (extEntrypoint !== "") {
|
||||||
return __non_webpack_require__(extEntrypoint).default;
|
return __non_webpack_require__(extEntrypoint).default;
|
||||||
}
|
}
|
||||||
@ -257,8 +295,8 @@ export class ExtensionLoader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastExtensions() {
|
broadcastExtensions(main = true) {
|
||||||
broadcastMessage(this.requestExtensionsChannel, Array.from(this.toJSON()));
|
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,13 @@ export abstract class ExtensionStore<T> extends BaseStore<T> {
|
|||||||
|
|
||||||
async loadExtension(extension: LensExtension) {
|
async loadExtension(extension: LensExtension) {
|
||||||
this.extension = extension;
|
this.extension = extension;
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
if (!this.extension) { return; }
|
if (!this.extension) { return; }
|
||||||
|
|
||||||
return super.load();
|
return super.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import type { LensExtensionId } from "./lens-extension";
|
import type { LensExtensionId } from "./lens-extension";
|
||||||
import type { ExtensionLoader } from "./extension-loader";
|
|
||||||
import { BaseStore } from "../common/base-store";
|
import { BaseStore } from "../common/base-store";
|
||||||
import { action, computed, observable, reaction, toJS } from "mobx";
|
import { action, computed, observable, toJS } from "mobx";
|
||||||
|
|
||||||
export interface LensExtensionsStoreModel {
|
export interface LensExtensionsStoreModel {
|
||||||
extensions: Record<LensExtensionId, LensExtensionState>;
|
extensions: Record<LensExtensionId, LensExtensionState>;
|
||||||
@ -28,40 +27,17 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
|||||||
|
|
||||||
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
protected state = observable.map<LensExtensionId, LensExtensionState>();
|
||||||
|
|
||||||
protected getState(extensionLoader: ExtensionLoader) {
|
|
||||||
const state: Record<LensExtensionId, LensExtensionState> = {};
|
|
||||||
return Array.from(extensionLoader.userExtensions).reduce((state, [extId, ext]) => {
|
|
||||||
state[extId] = {
|
|
||||||
enabled: ext.isEnabled,
|
|
||||||
name: ext.manifest.name,
|
|
||||||
};
|
|
||||||
return state;
|
|
||||||
}, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
async manageState(extensionLoader: ExtensionLoader) {
|
|
||||||
await extensionLoader.whenLoaded;
|
|
||||||
await this.whenLoaded;
|
|
||||||
|
|
||||||
// apply state on changes from store
|
|
||||||
reaction(() => this.state.toJS(), extensionsState => {
|
|
||||||
extensionsState.forEach((state, extId) => {
|
|
||||||
const ext = extensionLoader.getExtension(extId);
|
|
||||||
if (ext && !ext.isBundled) {
|
|
||||||
ext.isEnabled = state.enabled;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// save state on change `extension.isEnabled`
|
|
||||||
reaction(() => this.getState(extensionLoader), extensionsState => {
|
|
||||||
this.state.merge(extensionsState);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled(extId: LensExtensionId) {
|
isEnabled(extId: LensExtensionId) {
|
||||||
const state = this.state.get(extId);
|
const state = this.state.get(extId);
|
||||||
return state && state.enabled; // by default false
|
|
||||||
|
// By default false, so that copied extensions are disabled by default.
|
||||||
|
// If user installs the extension from the UI, the Extensions component will specifically enable it.
|
||||||
|
return Boolean(state?.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
mergeState(extensionsState: Record<LensExtensionId, LensExtensionState>) {
|
||||||
|
this.state.merge(extensionsState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -86,6 +86,7 @@ export class LensExtension {
|
|||||||
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
|
const cancelReaction = reaction(() => this.isEnabled, async (isEnabled) => {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
const handlerDisposers = await handlers();
|
const handlerDisposers = await handlers();
|
||||||
|
|
||||||
disposers.push(...handlerDisposers);
|
disposers.push(...handlerDisposers);
|
||||||
} else {
|
} else {
|
||||||
unregisterHandlers();
|
unregisterHandlers();
|
||||||
@ -93,6 +94,7 @@ export class LensExtension {
|
|||||||
}, {
|
}, {
|
||||||
fireImmediately: true
|
fireImmediately: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unregisterHandlers();
|
unregisterHandlers();
|
||||||
cancelReaction();
|
cancelReaction();
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export class LensMainExtension extends LensExtension {
|
|||||||
pageId,
|
pageId,
|
||||||
params: params ?? {}, // compile to url with params
|
params: params ?? {}, // compile to url with params
|
||||||
});
|
});
|
||||||
|
|
||||||
await windowManager.navigate(pageUrl, frameId);
|
await windowManager.navigate(pageUrl, frameId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
pageId,
|
pageId,
|
||||||
params: params ?? {}, // compile to url with params
|
params: params ?? {}, // compile to url with params
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(pageUrl);
|
navigate(pageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,7 @@ describe("globalPageRegistry", () => {
|
|||||||
describe("getByPageMenuTarget", () => {
|
describe("getByPageMenuTarget", () => {
|
||||||
it("matching to first registered page without id", () => {
|
it("matching to first registered page without id", () => {
|
||||||
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
|
const page = globalPageRegistry.getByPageMenuTarget({ extensionId: ext.name });
|
||||||
|
|
||||||
expect(page.id).toEqual(undefined);
|
expect(page.id).toEqual(undefined);
|
||||||
expect(page.extensionId).toEqual(ext.name);
|
expect(page.extensionId).toEqual(ext.name);
|
||||||
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
expect(page.routePath).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||||
@ -83,6 +84,7 @@ describe("globalPageRegistry", () => {
|
|||||||
pageId: "test-page",
|
pageId: "test-page",
|
||||||
extensionId: ext.name
|
extensionId: ext.name
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page.id).toEqual("test-page");
|
expect(page.id).toEqual("test-page");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,6 +93,7 @@ describe("globalPageRegistry", () => {
|
|||||||
pageId: "wrong-page",
|
pageId: "wrong-page",
|
||||||
extensionId: ext.name
|
extensionId: ext.name
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(page).toBeNull();
|
expect(page).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,7 +14,9 @@ export class BaseRegistry<T> {
|
|||||||
@action
|
@action
|
||||||
add(items: T | T[]) {
|
add(items: T | T[]) {
|
||||||
const itemArray = rectify(items);
|
const itemArray = rectify(items);
|
||||||
|
|
||||||
this.items.push(...itemArray);
|
this.items.push(...itemArray);
|
||||||
|
|
||||||
return () => this.remove(...itemArray);
|
return () => this.remove(...itemArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,8 +20,10 @@ export class KubeObjectDetailRegistry extends BaseRegistry<KubeObjectDetailRegis
|
|||||||
if (item.priority === null) {
|
if (item.priority === null) {
|
||||||
item.priority = 50;
|
item.priority = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
return items.sort((a, b) => b.priority - a.priority);
|
return items.sort((a, b) => b.priority - a.priority);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,8 +35,10 @@ export class GlobalPageMenuRegistry extends BaseRegistry<PageMenuRegistration> {
|
|||||||
extensionId: ext.name,
|
extensionId: ext.name,
|
||||||
...(menuItem.target || {}),
|
...(menuItem.target || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return menuItem;
|
return menuItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
return super.add(normalizedItems);
|
return super.add(normalizedItems);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,8 +51,10 @@ export class ClusterPageMenuRegistry extends BaseRegistry<ClusterPageMenuRegistr
|
|||||||
extensionId: ext.name,
|
extensionId: ext.name,
|
||||||
...(menuItem.target || {}),
|
...(menuItem.target || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
return menuItem;
|
return menuItem;
|
||||||
});
|
});
|
||||||
|
|
||||||
return super.add(normalizedItems);
|
return super.add(normalizedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,10 +44,12 @@ export function getExtensionPageUrl<P extends object>({ extensionId, pageId = ""
|
|||||||
const extensionBaseUrl = compile(`/extension/:name`)({
|
const extensionBaseUrl = compile(`/extension/:name`)({
|
||||||
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
|
name: sanitizeExtensionName(extensionId), // compile only with extension-id first and define base path
|
||||||
});
|
});
|
||||||
const extPageRoutePath = path.join(extensionBaseUrl, pageId); // page-id might contain route :param-s, so don't compile yet
|
const extPageRoutePath = path.posix.join(extensionBaseUrl, pageId);
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
return compile(extPageRoutePath)(params); // might throw error when required params not passed
|
return compile(extPageRoutePath)(params); // might throw error when required params not passed
|
||||||
}
|
}
|
||||||
|
|
||||||
return extPageRoutePath;
|
return extPageRoutePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +58,7 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
|||||||
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
|
add(items: PageRegistration | PageRegistration[], ext: LensExtension) {
|
||||||
const itemArray = rectify(items);
|
const itemArray = rectify(items);
|
||||||
let registeredPages: RegisteredPage[] = [];
|
let registeredPages: RegisteredPage[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
registeredPages = itemArray.map(page => ({
|
registeredPages = itemArray.map(page => ({
|
||||||
...page,
|
...page,
|
||||||
@ -69,6 +72,7 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
|||||||
error: String(err),
|
error: String(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.add(registeredPages);
|
return super.add(registeredPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,8 +82,10 @@ export class PageRegistry extends BaseRegistry<RegisteredPage> {
|
|||||||
|
|
||||||
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
|
getByPageMenuTarget(target: PageMenuTarget = {}): RegisteredPage | null {
|
||||||
const targetUrl = getExtensionPageUrl(target);
|
const targetUrl = getExtensionPageUrl(target);
|
||||||
|
|
||||||
return this.getItems().find(({ id: pageId, extensionId }) => {
|
return this.getItems().find(({ id: pageId, extensionId }) => {
|
||||||
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
|
const pageUrl = getExtensionPageUrl({ extensionId, pageId, params: target.params }); // compiled with provided params
|
||||||
|
|
||||||
return targetUrl === pageUrl;
|
return targetUrl === pageUrl;
|
||||||
}) || null;
|
}) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export class ClusterStore extends Singleton {
|
|||||||
if (!this.activeClusterId) {
|
if (!this.activeClusterId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.getById(this.activeClusterId);
|
return this.getById(this.activeClusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -75,6 +75,7 @@ describe("create clusters", () => {
|
|||||||
preferences: {},
|
preferences: {},
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
|
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true));
|
||||||
c = new Cluster({
|
c = new Cluster({
|
||||||
@ -112,6 +113,7 @@ describe("create clusters", () => {
|
|||||||
|
|
||||||
it("activating cluster should try to connect to cluster and do a refresh", async () => {
|
it("activating cluster should try to connect to cluster and do a refresh", async () => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
|
|
||||||
jest.spyOn(ContextHandler.prototype, "ensureServer");
|
jest.spyOn(ContextHandler.prototype, "ensureServer");
|
||||||
|
|
||||||
const mockListNSs = jest.fn();
|
const mockListNSs = jest.fn();
|
||||||
@ -122,17 +124,13 @@ describe("create clusters", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true));
|
||||||
jest.spyOn(Cluster.prototype, "canI")
|
jest.spyOn(Cluster.prototype, "canI")
|
||||||
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
|
|
||||||
expect(attr.namespace).toBe("default");
|
|
||||||
expect(attr.resource).toBe("pods");
|
|
||||||
expect(attr.verb).toBe("list");
|
|
||||||
return Promise.resolve(true);
|
|
||||||
})
|
|
||||||
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
.mockImplementation((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||||
expect(attr.namespace).toBe("default");
|
expect(attr.namespace).toBe("default");
|
||||||
expect(attr.verb).toBe("list");
|
expect(attr.verb).toBe("list");
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
});
|
});
|
||||||
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any);
|
jest.spyOn(Cluster.prototype, "getProxyKubeconfig").mockReturnValue(mockKC as any);
|
||||||
@ -148,6 +146,7 @@ describe("create clusters", () => {
|
|||||||
|
|
||||||
mockedRequest.mockImplementationOnce(((uri: any) => {
|
mockedRequest.mockImplementationOnce(((uri: any) => {
|
||||||
expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
|
expect(uri).toBe(`http://localhost:${port}/api-kube/version`);
|
||||||
|
|
||||||
return Promise.resolve({ gitVersion: "1.2.3" });
|
return Promise.resolve({ gitVersion: "1.2.3" });
|
||||||
}) as any);
|
}) as any);
|
||||||
|
|
||||||
@ -165,6 +164,7 @@ describe("create clusters", () => {
|
|||||||
kubeConfigPath: "minikube-config.yml",
|
kubeConfigPath: "minikube-config.yml",
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
workspace: workspaceStore.currentWorkspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
await c.init(port);
|
await c.init(port);
|
||||||
await c.activate();
|
await c.activate();
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,7 @@ describe("kube auth proxy tests", () => {
|
|||||||
it("calling exit multiple times shouldn't throw", async () => {
|
it("calling exit multiple times shouldn't throw", async () => {
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {});
|
const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" }), port, {});
|
||||||
|
|
||||||
kap.exit();
|
kap.exit();
|
||||||
kap.exit();
|
kap.exit();
|
||||||
kap.exit();
|
kap.exit();
|
||||||
@ -69,24 +70,29 @@ describe("kube auth proxy tests", () => {
|
|||||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
|
||||||
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
|
||||||
listeners[event] = listener;
|
listeners[event] = listener;
|
||||||
|
|
||||||
return mockedCP;
|
return mockedCP;
|
||||||
});
|
});
|
||||||
mockedCP.stderr = mock<Readable>();
|
mockedCP.stderr = mock<Readable>();
|
||||||
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
mockedCP.stderr.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||||
listeners[`stderr/${event}`] = listener;
|
listeners[`stderr/${event}`] = listener;
|
||||||
|
|
||||||
return mockedCP.stderr;
|
return mockedCP.stderr;
|
||||||
});
|
});
|
||||||
mockedCP.stdout = mock<Readable>();
|
mockedCP.stdout = mock<Readable>();
|
||||||
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
mockedCP.stdout.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): Readable => {
|
||||||
listeners[`stdout/${event}`] = listener;
|
listeners[`stdout/${event}`] = listener;
|
||||||
|
|
||||||
return mockedCP.stdout;
|
return mockedCP.stdout;
|
||||||
});
|
});
|
||||||
mockSpawn.mockImplementationOnce((command: string): ChildProcess => {
|
mockSpawn.mockImplementationOnce((command: string): ChildProcess => {
|
||||||
expect(command).toBe(bundledKubectlPath());
|
expect(command).toBe(bundledKubectlPath());
|
||||||
|
|
||||||
return mockedCP;
|
return mockedCP;
|
||||||
});
|
});
|
||||||
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
|
mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve());
|
||||||
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" });
|
const cluster = new Cluster({ id: "foobar", kubeConfigPath: "fake-path.yml" });
|
||||||
|
|
||||||
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal");
|
jest.spyOn(cluster, "apiUrl", "get").mockReturnValue("https://fake.k8s.internal");
|
||||||
proxy = new KubeAuthProxy(cluster, port, {});
|
proxy = new KubeAuthProxy(cluster, port, {});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { getFreePort } from "../port";
|
|||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { loadYaml } from "@kubernetes/client-node";
|
import { loadYaml } from "@kubernetes/client-node";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
|
|
||||||
@ -64,6 +65,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
preferences: {},
|
preferences: {},
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
mockFs(mockOpts);
|
mockFs(mockOpts);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -83,9 +85,10 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
||||||
|
|
||||||
expect(logger.error).not.toBeCalled();
|
expect(logger.error).not.toBeCalled();
|
||||||
expect(kubeConfManager.getPath()).toBe("tmp/kubeconfig-foo");
|
expect(kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`);
|
||||||
const file = await fse.readFile(kubeConfManager.getPath());
|
const file = await fse.readFile(kubeConfManager.getPath());
|
||||||
const yml = loadYaml<any>(file.toString());
|
const yml = loadYaml<any>(file.toString());
|
||||||
|
|
||||||
expect(yml["current-context"]).toBe("minikube");
|
expect(yml["current-context"]).toBe("minikube");
|
||||||
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`);
|
expect(yml["clusters"][0]["cluster"]["server"]).toBe(`http://127.0.0.1:${port}/foo`);
|
||||||
expect(yml["users"][0]["name"]).toBe("proxy");
|
expect(yml["users"][0]["name"]).toBe("proxy");
|
||||||
@ -101,8 +104,8 @@ describe("kubeconfig manager tests", () => {
|
|||||||
const contextHandler = new ContextHandler(cluster);
|
const contextHandler = new ContextHandler(cluster);
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
||||||
|
|
||||||
const configPath = kubeConfManager.getPath();
|
const configPath = kubeConfManager.getPath();
|
||||||
|
|
||||||
expect(await fse.pathExists(configPath)).toBe(true);
|
expect(await fse.pathExists(configPath)).toBe(true);
|
||||||
await kubeConfManager.unlink();
|
await kubeConfManager.unlink();
|
||||||
expect(await fse.pathExists(configPath)).toBe(false);
|
expect(await fse.pathExists(configPath)).toBe(false);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export class AppUpdater {
|
|||||||
|
|
||||||
public start() {
|
public start() {
|
||||||
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
|
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
|
||||||
|
|
||||||
return AppUpdater.checkForUpdates();
|
return AppUpdater.checkForUpdates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export class BaseClusterDetector {
|
|||||||
|
|
||||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||||
const apiUrl = this.cluster.kubeProxyUrl + path;
|
const apiUrl = this.cluster.kubeProxyUrl + path;
|
||||||
|
|
||||||
return request(apiUrl, {
|
return request(apiUrl, {
|
||||||
json: true,
|
json: true,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
|
|||||||
@ -7,17 +7,20 @@ export class ClusterIdDetector extends BaseClusterDetector {
|
|||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
let id: string;
|
let id: string;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
id = await this.getDefaultNamespaceId();
|
id = await this.getDefaultNamespaceId();
|
||||||
} catch(_) {
|
} catch(_) {
|
||||||
id = this.cluster.apiUrl;
|
id = this.cluster.apiUrl;
|
||||||
}
|
}
|
||||||
const value = createHash("sha256").update(id).digest("hex");
|
const value = createHash("sha256").update(id).digest("hex");
|
||||||
|
|
||||||
return { value, accuracy: 100 };
|
return { value, accuracy: 100 };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getDefaultNamespaceId() {
|
protected async getDefaultNamespaceId() {
|
||||||
const response = await this.k8sRequest("/api/v1/namespaces/default");
|
const response = await this.k8sRequest("/api/v1/namespaces/default");
|
||||||
|
|
||||||
return response.metadata.uid;
|
return response.metadata.uid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,12 +17,16 @@ export class DetectorRegistry {
|
|||||||
|
|
||||||
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
|
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
|
||||||
const results: {[key: string]: ClusterDetectionResult } = {};
|
const results: {[key: string]: ClusterDetectionResult } = {};
|
||||||
|
|
||||||
for (const detectorClass of this.registry) {
|
for (const detectorClass of this.registry) {
|
||||||
const detector = new detectorClass(cluster);
|
const detector = new detectorClass(cluster);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await detector.detect();
|
const data = await detector.detect();
|
||||||
|
|
||||||
if (!data) continue;
|
if (!data) continue;
|
||||||
const existingValue = results[detector.key];
|
const existingValue = results[detector.key];
|
||||||
|
|
||||||
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
|
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
|
||||||
results[detector.key] = data;
|
results[detector.key] = data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -30,9 +34,11 @@ export class DetectorRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const metadata: ClusterMetadata = {};
|
const metadata: ClusterMetadata = {};
|
||||||
|
|
||||||
for (const [key, result] of Object.entries(results)) {
|
for (const [key, result] of Object.entries(results)) {
|
||||||
metadata[key] = result.value;
|
metadata[key] = result.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,30 +7,59 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
this.version = await this.getKubernetesVersion();
|
this.version = await this.getKubernetesVersion();
|
||||||
if (await this.isRancher()) {
|
|
||||||
return { value: "rancher", accuracy: 80};
|
if (this.isRke()) {
|
||||||
|
return { value: "rke", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isK3s()) {
|
||||||
|
return { value: "k3s", accuracy: 80};
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isGKE()) {
|
if (this.isGKE()) {
|
||||||
return { value: "gke", accuracy: 80};
|
return { value: "gke", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isEKS()) {
|
if (this.isEKS()) {
|
||||||
return { value: "eks", accuracy: 80};
|
return { value: "eks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isIKS()) {
|
if (this.isIKS()) {
|
||||||
return { value: "iks", accuracy: 80};
|
return { value: "iks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isAKS()) {
|
if (this.isAKS()) {
|
||||||
return { value: "aks", accuracy: 80};
|
return { value: "aks", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isDigitalOcean()) {
|
if (this.isDigitalOcean()) {
|
||||||
return { value: "digitalocean", accuracy: 90};
|
return { value: "digitalocean", accuracy: 90};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isMirantis()) {
|
||||||
|
return { value: "mirantis", accuracy: 90};
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isMinikube()) {
|
if (this.isMinikube()) {
|
||||||
return { value: "minikube", accuracy: 80};
|
return { value: "minikube", accuracy: 80};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isMicrok8s()) {
|
||||||
|
return { value: "microk8s", accuracy: 80};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isKind()) {
|
||||||
|
return { value: "kind", accuracy: 70};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDockerDesktop()) {
|
||||||
|
return { value: "docker-desktop", accuracy: 80};
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isCustom()) {
|
if (this.isCustom()) {
|
||||||
return { value: "custom", accuracy: 10};
|
return { value: "custom", accuracy: 10};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { value: "unknown", accuracy: 10};
|
return { value: "unknown", accuracy: 10};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +67,7 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
if (this.cluster.version) return this.cluster.version;
|
if (this.cluster.version) return this.cluster.version;
|
||||||
|
|
||||||
const response = await this.k8sRequest("/version");
|
const response = await this.k8sRequest("/version");
|
||||||
|
|
||||||
return response.gitVersion;
|
return response.gitVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +87,10 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return this.cluster.apiUrl.endsWith("azmk8s.io");
|
return this.cluster.apiUrl.endsWith("azmk8s.io");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isMirantis() {
|
||||||
|
return this.version.includes("-mirantis-") || this.version.includes("-docker-");
|
||||||
|
}
|
||||||
|
|
||||||
protected isDigitalOcean() {
|
protected isDigitalOcean() {
|
||||||
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
|
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com");
|
||||||
}
|
}
|
||||||
@ -65,16 +99,27 @@ export class DistributionDetector extends BaseClusterDetector {
|
|||||||
return this.cluster.contextName.startsWith("minikube");
|
return this.cluster.contextName.startsWith("minikube");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected isMicrok8s() {
|
||||||
|
return this.cluster.contextName.startsWith("microk8s");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isKind() {
|
||||||
|
return this.cluster.contextName.startsWith("kubernetes-admin@kind-");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isDockerDesktop() {
|
||||||
|
return this.cluster.contextName === "docker-desktop";
|
||||||
|
}
|
||||||
|
|
||||||
protected isCustom() {
|
protected isCustom() {
|
||||||
return this.version.includes("+");
|
return this.version.includes("+");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async isRancher() {
|
protected isRke() {
|
||||||
try {
|
return this.version.includes("-rancher");
|
||||||
const response = await this.k8sRequest("");
|
}
|
||||||
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined;
|
|
||||||
} catch (e) {
|
protected isK3s() {
|
||||||
return false;
|
return this.version.includes("+k3s");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,6 +8,7 @@ export class LastSeenDetector extends BaseClusterDetector {
|
|||||||
if (!this.cluster.accessible) return null;
|
if (!this.cluster.accessible) return null;
|
||||||
|
|
||||||
await this.k8sRequest("/version");
|
await this.k8sRequest("/version");
|
||||||
|
|
||||||
return { value: new Date().toJSON(), accuracy: 100 };
|
return { value: new Date().toJSON(), accuracy: 100 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,11 +7,13 @@ export class NodesCountDetector extends BaseClusterDetector {
|
|||||||
public async detect() {
|
public async detect() {
|
||||||
if (!this.cluster.accessible) return null;
|
if (!this.cluster.accessible) return null;
|
||||||
const nodeCount = await this.getNodeCount();
|
const nodeCount = await this.getNodeCount();
|
||||||
|
|
||||||
return { value: nodeCount, accuracy: 100};
|
return { value: nodeCount, accuracy: 100};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getNodeCount(): Promise<number> {
|
protected async getNodeCount(): Promise<number> {
|
||||||
const response = await this.k8sRequest("/api/v1/nodes");
|
const response = await this.k8sRequest("/api/v1/nodes");
|
||||||
|
|
||||||
return response.items.length;
|
return response.items.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7,11 +7,13 @@ export class VersionDetector extends BaseClusterDetector {
|
|||||||
|
|
||||||
public async detect() {
|
public async detect() {
|
||||||
const version = await this.getKubernetesVersion();
|
const version = await this.getKubernetesVersion();
|
||||||
|
|
||||||
return { value: version, accuracy: 100};
|
return { value: version, accuracy: 100};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getKubernetesVersion() {
|
public async getKubernetesVersion() {
|
||||||
const response = await this.k8sRequest("/version");
|
const response = await this.k8sRequest("/version");
|
||||||
|
|
||||||
return response.gitVersion;
|
return response.gitVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -24,8 +24,10 @@ export class ClusterManager extends Singleton {
|
|||||||
// auto-stop removed clusters
|
// auto-stop removed clusters
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
||||||
|
|
||||||
if (removedClusters.length > 0) {
|
if (removedClusters.length > 0) {
|
||||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||||
|
|
||||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
||||||
removedClusters.forEach(cluster => cluster.disconnect());
|
removedClusters.forEach(cluster => cluster.disconnect());
|
||||||
clusterStore.removedClusters.clear();
|
clusterStore.removedClusters.clear();
|
||||||
@ -70,7 +72,9 @@ export class ClusterManager extends Singleton {
|
|||||||
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
// lens-server is connecting to 127.0.0.1:<port>/<uid>
|
||||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||||
const clusterId = req.url.split("/")[1];
|
const clusterId = req.url.split("/")[1];
|
||||||
|
|
||||||
cluster = clusterStore.getById(clusterId);
|
cluster = clusterStore.getById(clusterId);
|
||||||
|
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
// we need to swap path prefix so that request is proxied to kube api
|
// we need to swap path prefix so that request is proxied to kube api
|
||||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
||||||
@ -79,6 +83,7 @@ export class ClusterManager extends Singleton {
|
|||||||
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
|
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
|
||||||
} else {
|
} else {
|
||||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||||
|
|
||||||
cluster = clusterStore.getById(clusterId);
|
cluster = clusterStore.getById(clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export type ClusterRefreshOptions = {
|
|||||||
|
|
||||||
export interface ClusterState {
|
export interface ClusterState {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
|
enabled: boolean;
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
disconnected: boolean;
|
disconnected: boolean;
|
||||||
@ -224,6 +225,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
||||||
const { prometheus, prometheusProvider } = this.preferences;
|
const { prometheus, prometheusProvider } = this.preferences;
|
||||||
|
|
||||||
return toJS({ prometheus, prometheusProvider }, {
|
return toJS({ prometheus, prometheusProvider }, {
|
||||||
recurseEverything: true,
|
recurseEverything: true,
|
||||||
});
|
});
|
||||||
@ -239,6 +241,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
const kubeconfig = this.getKubeconfig();
|
const kubeconfig = this.getKubeconfig();
|
||||||
|
|
||||||
if (kubeconfig.getContextObject(this.contextName)) {
|
if (kubeconfig.getContextObject(this.contextName)) {
|
||||||
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
||||||
}
|
}
|
||||||
@ -324,13 +327,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
logger.info(`[CLUSTER]: activate`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
|
|
||||||
if (!this.eventDisposers.length) {
|
if (!this.eventDisposers.length) {
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.disconnected || !this.accessible) {
|
if (this.disconnected || !this.accessible) {
|
||||||
await this.reconnect();
|
await this.reconnect();
|
||||||
}
|
}
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
await this.refreshAllowedResources();
|
await this.refreshAllowedResources();
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
@ -338,6 +344,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.ensureKubectl();
|
this.ensureKubectl();
|
||||||
}
|
}
|
||||||
this.activated = true;
|
this.activated = true;
|
||||||
|
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,6 +353,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
protected async ensureKubectl() {
|
protected async ensureKubectl() {
|
||||||
this.kubeCtl = new Kubectl(this.version);
|
this.kubeCtl = new Kubectl(this.version);
|
||||||
|
|
||||||
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,9 +390,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
|
|
||||||
if (this.accessible) {
|
if (this.accessible) {
|
||||||
this.isAdmin = await this.isClusterAdmin();
|
this.isAdmin = await this.isClusterAdmin();
|
||||||
await this.refreshAllowedResources();
|
await this.refreshAllowedResources();
|
||||||
|
|
||||||
if (opts.refreshMetadata) {
|
if (opts.refreshMetadata) {
|
||||||
this.refreshMetadata();
|
this.refreshMetadata();
|
||||||
}
|
}
|
||||||
@ -400,6 +410,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||||
const metadata = await detectorRegistry.detectForCluster(this);
|
const metadata = await detectorRegistry.detectForCluster(this);
|
||||||
const existingMetadata = this.metadata;
|
const existingMetadata = this.metadata;
|
||||||
|
|
||||||
this.metadata = Object.assign(existingMetadata, metadata);
|
this.metadata = Object.assign(existingMetadata, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,6 +419,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
@action async refreshConnectionStatus() {
|
@action async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
|
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||||
}
|
}
|
||||||
@ -456,6 +468,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
|
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
|
||||||
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
||||||
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
||||||
|
|
||||||
return this.k8sRequest(metricsPath, {
|
return this.k8sRequest(metricsPath, {
|
||||||
timeout: 0,
|
timeout: 0,
|
||||||
resolveWithFullResponse: false,
|
resolveWithFullResponse: false,
|
||||||
@ -468,28 +481,36 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
try {
|
try {
|
||||||
const versionDetector = new VersionDetector(this);
|
const versionDetector = new VersionDetector(this);
|
||||||
const versionData = await versionDetector.detect();
|
const versionData = await versionDetector.detect();
|
||||||
|
|
||||||
this.metadata.version = versionData.value;
|
this.metadata.version = versionData.value;
|
||||||
|
|
||||||
return ClusterStatus.AccessGranted;
|
return ClusterStatus.AccessGranted;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
|
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`);
|
||||||
|
|
||||||
if (error.statusCode) {
|
if (error.statusCode) {
|
||||||
if (error.statusCode >= 400 && error.statusCode < 500) {
|
if (error.statusCode >= 400 && error.statusCode < 500) {
|
||||||
this.failureReason = "Invalid credentials";
|
this.failureReason = "Invalid credentials";
|
||||||
|
|
||||||
return ClusterStatus.AccessDenied;
|
return ClusterStatus.AccessDenied;
|
||||||
} else {
|
} else {
|
||||||
this.failureReason = error.error || error.message;
|
this.failureReason = error.error || error.message;
|
||||||
|
|
||||||
return ClusterStatus.Offline;
|
return ClusterStatus.Offline;
|
||||||
}
|
}
|
||||||
} else if (error.failed === true) {
|
} else if (error.failed === true) {
|
||||||
if (error.timedOut === true) {
|
if (error.timedOut === true) {
|
||||||
this.failureReason = "Connection timed out";
|
this.failureReason = "Connection timed out";
|
||||||
|
|
||||||
return ClusterStatus.Offline;
|
return ClusterStatus.Offline;
|
||||||
} else {
|
} else {
|
||||||
this.failureReason = "Failed to fetch credentials";
|
this.failureReason = "Failed to fetch credentials";
|
||||||
|
|
||||||
return ClusterStatus.AccessDenied;
|
return ClusterStatus.AccessDenied;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.failureReason = error.message;
|
this.failureReason = error.message;
|
||||||
|
|
||||||
return ClusterStatus.Offline;
|
return ClusterStatus.Offline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -500,15 +521,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
*/
|
*/
|
||||||
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||||
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
|
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accessReview = await authApi.createSelfSubjectAccessReview({
|
const accessReview = await authApi.createSelfSubjectAccessReview({
|
||||||
apiVersion: "authorization.k8s.io/v1",
|
apiVersion: "authorization.k8s.io/v1",
|
||||||
kind: "SelfSubjectAccessReview",
|
kind: "SelfSubjectAccessReview",
|
||||||
spec: { resourceAttributes }
|
spec: { resourceAttributes }
|
||||||
});
|
});
|
||||||
|
|
||||||
return accessReview.body.status.allowed;
|
return accessReview.body.status.allowed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -535,6 +559,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
ownerRef: this.ownerRef,
|
ownerRef: this.ownerRef,
|
||||||
accessibleNamespaces: this.accessibleNamespaces,
|
accessibleNamespaces: this.accessibleNamespaces,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(model, {
|
return toJS(model, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
});
|
});
|
||||||
@ -546,6 +571,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
getState(): ClusterState {
|
getState(): ClusterState {
|
||||||
const state: ClusterState = {
|
const state: ClusterState = {
|
||||||
initialized: this.initialized,
|
initialized: this.initialized,
|
||||||
|
enabled: this.enabled,
|
||||||
apiUrl: this.apiUrl,
|
apiUrl: this.apiUrl,
|
||||||
online: this.online,
|
online: this.online,
|
||||||
ready: this.ready,
|
ready: this.ready,
|
||||||
@ -556,6 +582,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
allowedNamespaces: this.allowedNamespaces,
|
allowedNamespaces: this.allowedNamespaces,
|
||||||
allowedResources: this.allowedResources,
|
allowedResources: this.allowedResources,
|
||||||
};
|
};
|
||||||
|
|
||||||
return toJS(state, {
|
return toJS(state, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
});
|
});
|
||||||
@ -597,21 +624,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
const api = this.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const namespaceList = await api.listNamespace();
|
const namespaceList = await api.listNamespace();
|
||||||
const nsAccessStatuses = await Promise.all(
|
|
||||||
namespaceList.body.items.map(ns => this.canI({
|
return namespaceList.body.items.map(ns => ns.metadata.name);
|
||||||
namespace: ns.metadata.name,
|
|
||||||
resource: "pods",
|
|
||||||
verb: "list",
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
return namespaceList.body.items
|
|
||||||
.filter((ns, i) => nsAccessStatuses[i])
|
|
||||||
.map(ns => ns.metadata.name);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
|
const ctx = this.getProxyKubeconfig().getContextObject(this.contextName);
|
||||||
|
|
||||||
if (ctx.namespace) return [ctx.namespace];
|
if (ctx.namespace) return [ctx.namespace];
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -629,6 +651,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
namespace: this.allowedNamespaces[0]
|
namespace: this.allowedNamespaces[0]
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
return apiResources
|
return apiResources
|
||||||
.filter((resource, i) => resourceAccessStatuses[i])
|
.filter((resource, i) => resourceAccessStatuses[i])
|
||||||
.map(apiResource => apiResource.resource);
|
.map(apiResource => apiResource.resource);
|
||||||
|
|||||||
@ -25,28 +25,34 @@ export class ContextHandler {
|
|||||||
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) {
|
||||||
this.prometheusProvider = preferences.prometheusProvider?.type;
|
this.prometheusProvider = preferences.prometheusProvider?.type;
|
||||||
this.prometheusPath = null;
|
this.prometheusPath = null;
|
||||||
|
|
||||||
if (preferences.prometheus) {
|
if (preferences.prometheus) {
|
||||||
const { namespace, service, port } = preferences.prometheus;
|
const { namespace, service, port } = preferences.prometheus;
|
||||||
|
|
||||||
this.prometheusPath = `${namespace}/services/${service}:${port}`;
|
this.prometheusPath = `${namespace}/services/${service}:${port}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async resolvePrometheusPath(): Promise<string> {
|
protected async resolvePrometheusPath(): Promise<string> {
|
||||||
const prometheusService = await this.getPrometheusService();
|
const prometheusService = await this.getPrometheusService();
|
||||||
|
|
||||||
if (!prometheusService) return null;
|
if (!prometheusService) return null;
|
||||||
const { service, namespace, port } = prometheusService;
|
const { service, namespace, port } = prometheusService;
|
||||||
|
|
||||||
return `${namespace}/services/${service}:${port}`;
|
return `${namespace}/services/${service}:${port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPrometheusProvider() {
|
async getPrometheusProvider() {
|
||||||
if (!this.prometheusProvider) {
|
if (!this.prometheusProvider) {
|
||||||
const service = await this.getPrometheusService();
|
const service = await this.getPrometheusService();
|
||||||
|
|
||||||
if (!service) {
|
if (!service) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
logger.info(`using ${service.id} as prometheus provider`);
|
logger.info(`using ${service.id} as prometheus provider`);
|
||||||
this.prometheusProvider = service.id;
|
this.prometheusProvider = service.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return prometheusProviders.find(p => p.id === this.prometheusProvider);
|
return prometheusProviders.find(p => p.id === this.prometheusProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,9 +60,11 @@ export class ContextHandler {
|
|||||||
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
||||||
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
const prometheusPromises: Promise<PrometheusService>[] = providers.map(async (provider: PrometheusProvider): Promise<PrometheusService> => {
|
||||||
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
const apiClient = this.cluster.getProxyKubeconfig().makeApiClient(CoreV1Api);
|
||||||
|
|
||||||
return await provider.getPrometheusService(apiClient);
|
return await provider.getPrometheusService(apiClient);
|
||||||
});
|
});
|
||||||
const resolvedPrometheusServices = await Promise.all(prometheusPromises);
|
const resolvedPrometheusServices = await Promise.all(prometheusPromises);
|
||||||
|
|
||||||
return resolvedPrometheusServices.filter(n => n)[0];
|
return resolvedPrometheusServices.filter(n => n)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,12 +72,14 @@ export class ContextHandler {
|
|||||||
if (!this.prometheusPath) {
|
if (!this.prometheusPath) {
|
||||||
this.prometheusPath = await this.resolvePrometheusPath();
|
this.prometheusPath = await this.resolvePrometheusPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prometheusPath;
|
return this.prometheusPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveAuthProxyUrl() {
|
async resolveAuthProxyUrl() {
|
||||||
const proxyPort = await this.ensurePort();
|
const proxyPort = await this.ensurePort();
|
||||||
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
const path = this.clusterUrl.path !== "/" ? this.clusterUrl.path : "";
|
||||||
|
|
||||||
return `http://127.0.0.1:${proxyPort}${path}`;
|
return `http://127.0.0.1:${proxyPort}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,14 +89,17 @@ export class ContextHandler {
|
|||||||
}
|
}
|
||||||
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
|
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
|
||||||
const apiTarget = await this.newApiTarget(timeout);
|
const apiTarget = await this.newApiTarget(timeout);
|
||||||
|
|
||||||
if (!isWatchRequest) {
|
if (!isWatchRequest) {
|
||||||
this.apiTarget = apiTarget;
|
this.apiTarget = apiTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiTarget;
|
return apiTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
protected async newApiTarget(timeout: number): Promise<httpProxy.ServerOptions> {
|
||||||
const proxyUrl = await this.resolveAuthProxyUrl();
|
const proxyUrl = await this.resolveAuthProxyUrl();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
target: proxyUrl,
|
target: proxyUrl,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
@ -101,6 +114,7 @@ export class ContextHandler {
|
|||||||
if (!this.proxyPort) {
|
if (!this.proxyPort) {
|
||||||
this.proxyPort = await getFreePort();
|
this.proxyPort = await getFreePort();
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.proxyPort;
|
return this.proxyPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +122,7 @@ export class ContextHandler {
|
|||||||
if (!this.kubeAuthProxy) {
|
if (!this.kubeAuthProxy) {
|
||||||
await this.ensurePort();
|
await this.ensurePort();
|
||||||
const proxyEnv = Object.assign({}, process.env);
|
const proxyEnv = Object.assign({}, process.env);
|
||||||
|
|
||||||
if (this.cluster.preferences.httpsProxy) {
|
if (this.cluster.preferences.httpsProxy) {
|
||||||
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,10 @@ import logger from "./logger";
|
|||||||
export function exitApp() {
|
export function exitApp() {
|
||||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||||
const clusterManager = ClusterManager.getInstance<ClusterManager>();
|
const clusterManager = ClusterManager.getInstance<ClusterManager>();
|
||||||
|
|
||||||
appEventBus.emit({ name: "service", action: "close" });
|
appEventBus.emit({ name: "service", action: "close" });
|
||||||
windowManager.hide();
|
windowManager?.hide();
|
||||||
clusterManager.stop();
|
clusterManager?.stop();
|
||||||
logger.info("SERVICE:QUIT");
|
logger.info("SERVICE:QUIT");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
app.exit();
|
app.exit();
|
||||||
|
|||||||
@ -32,11 +32,14 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
|||||||
const salt = randomBytes(32).toString("hex");
|
const salt = randomBytes(32).toString("hex");
|
||||||
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
|
||||||
const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
|
const dirPath = path.resolve(app.getPath("userData"), "extension_data", hashedName);
|
||||||
|
|
||||||
this.registeredExtensions.set(extensionName, dirPath);
|
this.registeredExtensions.set(extensionName, dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirPath = this.registeredExtensions.get(extensionName);
|
const dirPath = this.registeredExtensions.get(extensionName);
|
||||||
|
|
||||||
await fse.ensureDir(dirPath);
|
await fse.ensureDir(dirPath);
|
||||||
|
|
||||||
return dirPath;
|
return dirPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,32 +20,39 @@ export class HelmChartManager {
|
|||||||
|
|
||||||
public async chart(name: string) {
|
public async chart(name: string) {
|
||||||
const charts = await this.charts();
|
const charts = await this.charts();
|
||||||
|
|
||||||
return charts[name];
|
return charts[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async charts(): Promise<any> {
|
public async charts(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const cachedYaml = await this.cachedYaml();
|
const cachedYaml = await this.cachedYaml();
|
||||||
|
|
||||||
return cachedYaml["entries"];
|
return cachedYaml["entries"];
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReadme(name: string, version = "") {
|
public async getReadme(name: string, version = "") {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
if(version && version != "") {
|
if(version && version != "") {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} else {
|
} else {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" show readme ${this.repo.name}/${name}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getValues(name: string, version = "") {
|
public async getValues(name: string, version = "") {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
if(version && version != "") {
|
if(version && version != "") {
|
||||||
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" show values ${this.repo.name}/${name} --version ${version}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
@ -61,6 +68,7 @@ export class HelmChartManager {
|
|||||||
if (!(this.repo.name in this.cache)) {
|
if (!(this.repo.name in this.cache)) {
|
||||||
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
|
const cacheFile = await fs.promises.readFile(this.repo.cacheFilePath, "utf-8");
|
||||||
const data = yaml.safeLoad(cacheFile);
|
const data = yaml.safeLoad(cacheFile);
|
||||||
|
|
||||||
for(const key in data["entries"]) {
|
for(const key in data["entries"]) {
|
||||||
data["entries"][key].forEach((version: any) => {
|
data["entries"][key].forEach((version: any) => {
|
||||||
version["repo"] = this.repo.name;
|
version["repo"] = this.repo.name;
|
||||||
@ -69,6 +77,7 @@ export class HelmChartManager {
|
|||||||
}
|
}
|
||||||
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
|
this.cache[this.repo.name] = Buffer.from(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(this.cache[this.repo.name].toString());
|
return JSON.parse(this.cache[this.repo.name].toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export class HelmCli extends LensBinary {
|
|||||||
originalBinaryName: "helm",
|
originalBinaryName: "helm",
|
||||||
newBinaryName: "helm3"
|
newBinaryName: "helm3"
|
||||||
};
|
};
|
||||||
|
|
||||||
super(opts);
|
super(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,14 +12,15 @@ export class HelmReleaseManager {
|
|||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
|
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
|
||||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
const output = JSON.parse(stdout);
|
const output = JSON.parse(stdout);
|
||||||
|
|
||||||
if (output.length == 0) {
|
if (output.length == 0) {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
output.forEach((release: any, index: number) => {
|
output.forEach((release: any, index: number) => {
|
||||||
output[index] = toCamelCase(release);
|
output[index] = toCamelCase(release);
|
||||||
});
|
});
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,15 +28,19 @@ export class HelmReleaseManager {
|
|||||||
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
|
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({name: "values.yaml"});
|
||||||
|
|
||||||
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let generateName = "";
|
let generateName = "";
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
generateName = "--generate-name";
|
generateName = "--generate-name";
|
||||||
name = "";
|
name = "";
|
||||||
}
|
}
|
||||||
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
|
||||||
const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
|
const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: stdout,
|
log: stdout,
|
||||||
release: {
|
release: {
|
||||||
@ -51,10 +56,12 @@ export class HelmReleaseManager {
|
|||||||
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
|
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const fileName = tempy.file({name: "values.yaml"});
|
const fileName = tempy.file({name: "values.yaml"});
|
||||||
|
|
||||||
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log: stdout,
|
log: stdout,
|
||||||
release: this.getRelease(name, namespace, cluster)
|
release: this.getRelease(name, namespace, cluster)
|
||||||
@ -68,7 +75,9 @@ export class HelmReleaseManager {
|
|||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${cluster.getProxyKubeconfigPath()}`).catch((error) => { throw(error.stderr);});
|
||||||
const release = JSON.parse(stdout);
|
const release = JSON.parse(stdout);
|
||||||
|
|
||||||
release.resources = await this.getResources(name, namespace, cluster);
|
release.resources = await this.getResources(name, namespace, cluster);
|
||||||
|
|
||||||
return release;
|
return release;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,18 +91,21 @@ export class HelmReleaseManager {
|
|||||||
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
|
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return JSON.parse(stdout);
|
return JSON.parse(stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
|
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +116,7 @@ export class HelmReleaseManager {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
|
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
|
||||||
return { stdout: JSON.stringify({items: []})};
|
return { stdout: JSON.stringify({items: []})};
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,12 +42,14 @@ export class HelmRepoManager extends Singleton {
|
|||||||
resolveWithFullResponse: true,
|
resolveWithFullResponse: true,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return orderBy<HelmRepo>(res.body, repo => repo.name);
|
return orderBy<HelmRepo>(res.body, repo => repo.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
helmCli.setLogger(logger);
|
helmCli.setLogger(logger);
|
||||||
await helmCli.ensureBinary();
|
await helmCli.ensureBinary();
|
||||||
|
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.helmEnv = await this.parseHelmEnv();
|
this.helmEnv = await this.parseHelmEnv();
|
||||||
await this.update();
|
await this.update();
|
||||||
@ -62,12 +64,15 @@ export class HelmRepoManager extends Singleton {
|
|||||||
});
|
});
|
||||||
const lines = stdout.split(/\r?\n/); // split by new line feed
|
const lines = stdout.split(/\r?\n/); // split by new line feed
|
||||||
const env: HelmEnv = {};
|
const env: HelmEnv = {};
|
||||||
|
|
||||||
lines.forEach((line: string) => {
|
lines.forEach((line: string) => {
|
||||||
const [key, value] = line.split("=");
|
const [key, value] = line.split("=");
|
||||||
|
|
||||||
if (key && value) {
|
if (key && value) {
|
||||||
env[key] = value.replace(/"/g, ""); // strip quotas
|
env[key] = value.replace(/"/g, ""); // strip quotas
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +80,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
|
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
|
||||||
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
|
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
|
||||||
@ -82,22 +88,27 @@ export class HelmRepoManager extends Singleton {
|
|||||||
.catch(() => ({
|
.catch(() => ({
|
||||||
repositories: []
|
repositories: []
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!repositories.length) {
|
if (!repositories.length) {
|
||||||
await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
|
await this.addRepo({ name: "bitnami", url: "https://charts.bitnami.com/bitnami" });
|
||||||
|
|
||||||
return await this.repositories();
|
return await this.repositories();
|
||||||
}
|
}
|
||||||
|
|
||||||
return repositories.map(repo => ({
|
return repositories.map(repo => ({
|
||||||
...repo,
|
...repo,
|
||||||
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
|
cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[HELM]: repositories listing error "${error}"`);
|
logger.error(`[HELM]: repositories listing error "${error}"`);
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async repository(name: string) {
|
public async repository(name: string) {
|
||||||
const repositories = await this.repositories();
|
const repositories = await this.repositories();
|
||||||
|
|
||||||
return repositories.find(repo => repo.name == name);
|
return repositories.find(repo => repo.name == name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +117,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
|
const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => {
|
||||||
return { stdout: error.stdout };
|
return { stdout: error.stdout };
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,6 +127,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
|
const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => {
|
||||||
throw(error.stderr);
|
throw(error.stderr);
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +137,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
|
const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => {
|
||||||
throw(error.stderr);
|
throw(error.stderr);
|
||||||
});
|
});
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,18 +11,23 @@ class HelmService {
|
|||||||
|
|
||||||
public async listCharts() {
|
public async listCharts() {
|
||||||
const charts: any = {};
|
const charts: any = {};
|
||||||
|
|
||||||
await repoManager.init();
|
await repoManager.init();
|
||||||
const repositories = await repoManager.repositories();
|
const repositories = await repoManager.repositories();
|
||||||
|
|
||||||
for (const repo of repositories) {
|
for (const repo of repositories) {
|
||||||
charts[repo.name] = {};
|
charts[repo.name] = {};
|
||||||
const manager = new HelmChartManager(repo);
|
const manager = new HelmChartManager(repo);
|
||||||
let entries = await manager.charts();
|
let entries = await manager.charts();
|
||||||
|
|
||||||
entries = this.excludeDeprecated(entries);
|
entries = this.excludeDeprecated(entries);
|
||||||
|
|
||||||
for (const key in entries) {
|
for (const key in entries) {
|
||||||
entries[key] = entries[key][0];
|
entries[key] = entries[key][0];
|
||||||
}
|
}
|
||||||
charts[repo.name] = entries;
|
charts[repo.name] = entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
return charts;
|
return charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,50 +39,60 @@ class HelmService {
|
|||||||
const repo = await repoManager.repository(repoName);
|
const repo = await repoManager.repository(repoName);
|
||||||
const chartManager = new HelmChartManager(repo);
|
const chartManager = new HelmChartManager(repo);
|
||||||
const chart = await chartManager.chart(chartName);
|
const chart = await chartManager.chart(chartName);
|
||||||
|
|
||||||
result.readme = await chartManager.getReadme(chartName, version);
|
result.readme = await chartManager.getReadme(chartName, version);
|
||||||
result.versions = chart;
|
result.versions = chart;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChartValues(repoName: string, chartName: string, version = "") {
|
public async getChartValues(repoName: string, chartName: string, version = "") {
|
||||||
const repo = await repoManager.repository(repoName);
|
const repo = await repoManager.repository(repoName);
|
||||||
const chartManager = new HelmChartManager(repo);
|
const chartManager = new HelmChartManager(repo);
|
||||||
|
|
||||||
return chartManager.getValues(chartName, version);
|
return chartManager.getValues(chartName, version);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||||
await repoManager.init();
|
await repoManager.init();
|
||||||
|
|
||||||
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
|
return await releaseManager.listReleases(cluster.getProxyKubeconfigPath(), namespace);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release");
|
logger.debug("Fetch release");
|
||||||
|
|
||||||
return await releaseManager.getRelease(releaseName, namespace, cluster);
|
return await releaseManager.getRelease(releaseName, namespace, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release values");
|
logger.debug("Fetch release values");
|
||||||
|
|
||||||
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
return await releaseManager.getValues(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Fetch release history");
|
logger.debug("Fetch release history");
|
||||||
|
|
||||||
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
return await releaseManager.getHistory(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||||
logger.debug("Delete release");
|
logger.debug("Delete release");
|
||||||
|
|
||||||
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
return await releaseManager.deleteRelease(releaseName, namespace, cluster.getProxyKubeconfigPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
|
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
|
||||||
logger.debug("Upgrade release");
|
logger.debug("Upgrade release");
|
||||||
|
|
||||||
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
||||||
logger.debug("Rollback release");
|
logger.debug("Rollback release");
|
||||||
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
|
const output = await releaseManager.rollback(releaseName, namespace, revision, cluster.getProxyKubeconfigPath());
|
||||||
|
|
||||||
return { message: output };
|
return { message: output };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,9 +102,11 @@ class HelmService {
|
|||||||
if (Array.isArray(entry)) {
|
if (Array.isArray(entry)) {
|
||||||
return entry[0]["deprecated"] != true;
|
return entry[0]["deprecated"] != true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry["deprecated"] != true;
|
return entry["deprecated"] != true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,13 @@ let clusterManager: ClusterManager;
|
|||||||
let windowManager: WindowManager;
|
let windowManager: WindowManager;
|
||||||
|
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
|
|
||||||
if (!process.env.CICD) {
|
if (!process.env.CICD) {
|
||||||
app.setPath("userData", workingDir);
|
app.setPath("userData", workingDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
mangleProxyEnv();
|
mangleProxyEnv();
|
||||||
|
|
||||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
|
||||||
}
|
}
|
||||||
@ -48,6 +50,7 @@ app.on("ready", async () => {
|
|||||||
await shellSync();
|
await shellSync();
|
||||||
|
|
||||||
const updater = new AppUpdater();
|
const updater = new AppUpdater();
|
||||||
|
|
||||||
updater.start();
|
updater.start();
|
||||||
|
|
||||||
registerFileProtocol("static", __static);
|
registerFileProtocol("static", __static);
|
||||||
@ -110,6 +113,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
app.on("activate", (event, hasVisibleWindows) => {
|
app.on("activate", (event, hasVisibleWindows) => {
|
||||||
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
||||||
|
|
||||||
if (!hasVisibleWindows) {
|
if (!hasVisibleWindows) {
|
||||||
windowManager.initMainWindow();
|
windowManager.initMainWindow();
|
||||||
}
|
}
|
||||||
@ -121,6 +125,7 @@ app.on("will-quit", (event) => {
|
|||||||
appEventBus.emit({name: "app", action: "close"});
|
appEventBus.emit({name: "app", action: "close"});
|
||||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||||
clusterManager?.stop(); // close cluster connections
|
clusterManager?.stop(); // close cluster connections
|
||||||
|
|
||||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export class KubeAuthProxy {
|
|||||||
"--accept-hosts", this.acceptHosts,
|
"--accept-hosts", this.acceptHosts,
|
||||||
"--reject-paths", "^[^/]"
|
"--reject-paths", "^[^/]"
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.DEBUG_PROXY === "true") {
|
if (process.env.DEBUG_PROXY === "true") {
|
||||||
args.push("-v", "9");
|
args.push("-v", "9");
|
||||||
}
|
}
|
||||||
@ -62,6 +63,7 @@ export class KubeAuthProxy {
|
|||||||
|
|
||||||
this.proxyProcess.stdout.on("data", (data) => {
|
this.proxyProcess.stdout.on("data", (data) => {
|
||||||
let logItem = data.toString();
|
let logItem = data.toString();
|
||||||
|
|
||||||
if (logItem.startsWith("Starting to serve on")) {
|
if (logItem.startsWith("Starting to serve on")) {
|
||||||
logItem = "Authentication proxy started\n";
|
logItem = "Authentication proxy started\n";
|
||||||
}
|
}
|
||||||
@ -80,19 +82,23 @@ export class KubeAuthProxy {
|
|||||||
const error = data.split("http: proxy error:").slice(1).join("").trim();
|
const error = data.split("http: proxy error:").slice(1).join("").trim();
|
||||||
let errorMsg = error;
|
let errorMsg = error;
|
||||||
const jsonError = error.split("Response: ")[1];
|
const jsonError = error.split("Response: ")[1];
|
||||||
|
|
||||||
if (jsonError) {
|
if (jsonError) {
|
||||||
try {
|
try {
|
||||||
const parsedError = JSON.parse(jsonError);
|
const parsedError = JSON.parse(jsonError);
|
||||||
|
|
||||||
errorMsg = parsedError.error_description || parsedError.error || jsonError;
|
errorMsg = parsedError.error_description || parsedError.error || jsonError;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
errorMsg = jsonError.trim();
|
errorMsg = jsonError.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorMsg;
|
return errorMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
|
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
|
||||||
const channel = `kube-auth:${this.cluster.id}`;
|
const channel = `kube-auth:${this.cluster.id}`;
|
||||||
|
|
||||||
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
|
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
|
||||||
broadcastMessage(channel, res);
|
broadcastMessage(channel, res);
|
||||||
}
|
}
|
||||||
|
|||||||