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

Merge branch 'master' into fix/namespace-selector-gradients

This commit is contained in:
Alex Andreev 2021-05-20 10:19:32 +03:00
commit 550c678dc8
379 changed files with 4772 additions and 2105 deletions

View File

@ -1,3 +1,24 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const packageJson = require("./package.json");
module.exports = {
@ -27,9 +48,11 @@ module.exports = {
sourceType: "module",
},
plugins: [
"unused-imports"
"header",
"unused-imports",
],
rules: {
"header/header": [2, "./license-header"],
"indent": ["error", 2, {
"SwitchCase": 1,
}],
@ -72,6 +95,7 @@ module.exports = {
"plugin:@typescript-eslint/recommended",
],
plugins: [
"header",
"unused-imports"
],
parserOptions: {
@ -79,6 +103,7 @@ module.exports = {
sourceType: "module",
},
rules: {
"header/header": [2, "./license-header"],
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
@ -127,6 +152,7 @@ module.exports = {
],
parser: "@typescript-eslint/parser",
plugins: [
"header",
"unused-imports"
],
extends: [
@ -139,6 +165,7 @@ module.exports = {
jsx: true,
},
rules: {
"header/header": [2, "./license-header"],
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",

View File

@ -7,7 +7,7 @@ on:
branches:
- master
jobs:
test:
css:
runs-on: ubuntu-latest
steps:
@ -23,16 +23,4 @@ jobs:
set -e
export PATH=${PATH}:`go env GOPATH`/bin
addlicense -check -l mit -c "OpenLens Authors" *.ts*
addlicense -check -l mit -c "OpenLens Authors" src/common/**/*.ts*
addlicense -check -l mit -c "OpenLens Authors" src/common/**/*.?css
addlicense -check -l mit -c "OpenLens Authors" src/main/**/*.ts*
addlicense -check -l mit -c "OpenLens Authors" src/renderer/**/*.ts*
addlicense -check -l mit -c "OpenLens Authors" src/renderer/**/*.?css
addlicense -check -l mit -c "OpenLens Authors" src/extensions/**/*.ts*
addlicense -check -l mit -c "OpenLens Authors" extensions/**/*.ts*
addlicense -check -l mit -c "OpenLens Authors" src/**/*.?css

View File

@ -125,6 +125,7 @@ docs:
clean-extensions:
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/dist)
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/node_modules)
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm $(dir)/*.tgz || true)
.PHONY: clean-npm
clean-npm:

View File

@ -1,4 +1,24 @@
module.exports = {
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export default {
Trans: ({ children }: { children: React.ReactNode }) => children,
t: (message: string) => message
};

View File

@ -1,4 +1,24 @@
module.exports = {
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export default {
require: jest.fn(),
match: jest.fn(),
app: {

View File

@ -1 +1,22 @@
module.exports = {};
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export default {};

View File

@ -1 +1,21 @@
module.exports = {};
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export default {};

View File

@ -1,5 +1,23 @@
// Generate tray icons from SVG to PNG + different sizes and colors (B&W)
// Command: `yarn build:tray-icons`
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import path from "path";
import sharp from "sharp";
import jsdom from "jsdom";

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { helmCli } from "../src/main/helm/helm-cli";
helmCli.ensureBinary();

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import packageInfo from "../package.json";
import fs from "fs";
import request from "request";

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const { notarize } = require("electron-notarize");
exports.default = async function notarizing(context) {

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import * as fs from "fs";
import * as path from "path";
import appInfo from "../package.json";

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import * as fs from "fs";
import * as path from "path";
import packageInfo from "../src/extensions/npm/extensions/package.json";

View File

@ -1,12 +1 @@
{% extends "base.html" %}
{% block analytics %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-90E1JB4HW4"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-90E1JB4HW4');
</script>
{% endblock %}

View File

@ -24,6 +24,7 @@ Each guide or code sample includes the following:
| [KubeObjectListLayout](kube-object-list-layout.md) | |
| [Working with mobx](working-with-mobx.md) | |
| [Protocol Handlers](protocol-handlers.md) | |
| [Sending Data between main and renderer](ipc.md) | |
## Samples

View File

@ -0,0 +1,131 @@
# Inter Process Communication
A Lens Extension can utilize IPC to send information between its `LensRendererExtension` and its `LensMainExtension`.
This is useful when wanting to communicate directly within your extension.
For example, if a user logs into a service that your extension is a facade for and `main` needs to know some information so that you can start syncing items to the `Catalog`, this would be a good way to send that information along.
IPC channels are blocked off per extension.
Meaning that each extension can only communicate with itself.
## Types of IPC
There are two flavours of IPC that are provided:
- Event based
- Request based
### Event Based IPC
This is the same as an [Event Emitter](https://nodejs.org/api/events.html#events_class_eventemitter) but is not limited to just one Javascript process.
This is a good option when you need to report that something has happened but you don't need a response.
This is a fully two-way form of communication.
Both `LensMainExtension` and `LensRendererExtension` can do this sort of IPC.
### Request Based IPC
This is more like a Remote Procedure Call (RPC).
With this sort of IPC the caller waits for the result from the other side.
This is accomplished by returning a `Promise<T>` which needs to be `await`-ed.
This is a unidirectional form of communication.
Only `LensRendererExtension` can initiate this kind of request, and only `LensMainExtension` can and respond this this kind of request.
## Registering IPC Handlers and Listeners
The general terminology is as follows:
- A "handler" is the function that responds to a "Request Based IPC" event.
- A "listener" is the function that is called when a "Event Based IPC" event is emitted.
To register either a handler or a listener, you should do something like the following:
`main.ts`:
```typescript
import { LensMainExtension, Interface, Types, Store } from "@k8slens/extensions";
import { registerListeners, IpcMain } from "./helpers/main";
export class ExampleExtensionMain extends LensMainExtension {
onActivate() {
IpcMain.createInstance(this);
}
}
```
This file shows that you need to create an instance of the store to be able to use IPC.
Lens will automatically clean up that store and all the handlers on deactivation and uninstall.
---
`helpers/main.ts`:
```typescript
import { Store } from "@k8slens/extensions";
export class IpcMain extends Store.MainIpcStore {
constructor(extension: LensMainExtension) {
super(extension);
this.listenIpc("initialize", onInitialize);
}
}
function onInitialize(event: Types.IpcMainEvent, id: string) {
console.log(`starting to initialize: ${id}`);
}
```
In other files, it is not necessary to pass around any instances.
It should be able to just call `getInstance()` everywhere in your extension as needed.
---
`renderer.ts`:
```typescript
import { LensRendererExtension, Interface, Types } from "@k8slens/extensions";
import { IpcRenderer } from "./helpers/renderer";
export class ExampleExtensionRenderer extends LensRendererExtension {
onActivate() {
const ipc = IpcRenderer.createInstance(this);
setTimeout(() => ipc.broadcastIpc("initialize", "an-id"), 5000);
}
}
```
It is also needed to create an instance to broadcast messages too.
---
`helpers/renderer.ts`:
```typescript
import { Store } from "@k8slens/extensions";
export class IpcMain extends Store.RendererIpcStore {}
```
It is necessary to create child classes of these `abstract class`'s in your extension before you can use them.
---
As this example shows: the channel names *must* be the same.
It should also be noted that "listeners" and "handlers" are specific to either `LensRendererExtension` and `LensMainExtension`.
There is no behind the scenes transfer of these functions.
If you want to register a "handler" you would call `Store.MainIpcStore.handleIpc(...)` instead.
The cleanup of these handlers is handled by Lens itself.
`Store.RendererIpcStore.broadcastIpc(...)` and `Store.MainIpcStore.broadcastIpc(...)` sends an event to all renderer frames and to main.
Because of this, no matter where you broadcast from, all listeners in `main` and `renderer` will be notified.
### Allowed Values
This IPC mechanism utilizes the [Structured Clone Algorithm](developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) for serialization.
This means that more types than what are JSON serializable can be used, but not all the information will be passed through.
## Using IPC
Calling IPC is very simple.
If you are meaning to do an event based call, merely call `broadcastIpc(<channel>, ...<args>)` from within your extension.
If you are meaning to do a request based call from `renderer`, you should do `const res = await Store.RendererIpcStore.invokeIpc(<channel>, ...<args>));` instead.

View File

@ -10,7 +10,6 @@ The custom Lens UI elements that you can add include:
* [Cluster page menus](#clusterpagemenus)
* [Global pages](#globalpages)
* [Global page menus](#globalpagemenus)
* [Cluster features](#clusterfeatures)
* [App preferences](#apppreferences)
* [Status bar items](#statusbaritems)
* [KubeObject menu items](#kubeobjectmenuitems)

View File

@ -3,7 +3,6 @@
## Modules
* [App](modules/_core_api_app_.md)
* [ClusterFeature](modules/_core_api_cluster_feature_.md)
* [EventBus](modules/_core_api_event_bus_.md)
* [Store](modules/_core_api_stores_.md)
* [Util](modules/_core_api_utils_.md)

View File

@ -5,13 +5,38 @@
:root > * {
/* Footer */
--md-footer-bg-color: #3d90ce;
--md-footer-bg-color: #e8e8e8;
--md-footer-bg-color--dark: #cccdcf;
--md-footer-fg-color: #3d90ce;
--md-footer-fg-color--light: #fff;
/* Background */
--md-default-bg-color: #f1f1f1;
}
[data-md-color-scheme="slate"] {
/* Footer */
--md-footer-bg-color: #2e3136;
--md-footer-bg-color--dark: #262b2f;
--md-footer-fg-color: #3d90ce;
--md-footer-fg-color--light: #fff;
/* Background */
--md-default-bg-color: #1e2124;
}
.md-version__list {
overflow: auto;
}
.md-header-nav__title {
margin-left: 0;
}
.md-header__title {
margin-left: 0;
}
ul.video-list {
counter-reset: section;
list-style: none;

1
extensions/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*/*.tgz

View File

@ -1661,12 +1661,6 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@ -1721,15 +1715,6 @@
"path-exists": "^3.0.0"
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -1896,12 +1881,6 @@
"minimist": "^1.2.5"
}
},
"mobx": {
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.7.tgz",
"integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==",
"dev": true
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -2213,17 +2192,6 @@
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
"dev": true
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
}
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -2322,23 +2290,6 @@
"safe-buffer": "^5.1.0"
}
},
"react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",

View File

@ -8,17 +8,18 @@
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch",
"build": "webpack && npm pack",
"dev": "webpack --watch",
"test": "echo NO TESTS"
},
"files": [
"dist/**/*"
],
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"mobx": "^5.15.5",
"react": "^16.13.1"
"webpack": "^4.44.2"
}
}

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const path = require("path");
module.exports = [
@ -22,7 +42,8 @@ module.exports = [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact"
}
],
resolve: {

File diff suppressed because it is too large Load Diff

View File

@ -8,18 +8,21 @@
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
"build": "webpack && npm pack",
"dev": "webpack --watch",
"test": "jest --passWithNoTests --env=jsdom src $@",
"clean": "rm -rf dist/ && rm *.tgz"
},
"files": [
"dist/**/*"
],
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "^26.6.3",
"mobx": "^5.15.5",
"react": "^16.13.1",
"semver": "^7.3.2",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2"
}
},
"dependencies": {}
}

View File

@ -19,58 +19,24 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { LensRendererExtension, Interface, Component, Catalog} from "@k8slens/extensions";
import { MetricsFeature } from "./src/metrics-feature";
import React from "react";
import { LensRendererExtension, Catalog } from "@k8slens/extensions";
import { MetricsSettings } from "./src/metrics-settings";
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
onActivate() {
const category = Catalog.catalogCategories.getForGroupKind<Catalog.KubernetesClusterCategory>("entity.k8slens.dev", "KubernetesCluster");
if (!category) {
return;
}
category.on("contextMenuOpen", this.clusterContextMenuOpen.bind(this));
}
async clusterContextMenuOpen(cluster: Catalog.KubernetesCluster, ctx: Interface.CatalogEntityContextMenuContext) {
if (!cluster.status.active) {
return;
}
const metricsFeature = new MetricsFeature();
await metricsFeature.updateStatus(cluster);
if (metricsFeature.status.installed) {
if (metricsFeature.status.canUpgrade) {
ctx.menuItems.unshift({
icon: "refresh",
title: "Upgrade Lens Metrics stack",
onClick: async () => {
metricsFeature.upgrade(cluster);
}
});
entitySettings = [
{
apiVersions: ["entity.k8slens.dev/v1alpha1"],
kind: "KubernetesCluster",
title: "Lens Metrics",
priority: 5,
components: {
View: ({ entity = null }: { entity: Catalog.KubernetesCluster}) => {
return (
<MetricsSettings cluster={entity} />
);
}
}
ctx.menuItems.unshift({
icon: "toggle_off",
title: "Uninstall Lens Metrics stack",
onClick: async () => {
await metricsFeature.uninstall(cluster);
Component.Notifications.info(`Lens Metrics has been removed from ${cluster.metadata.name}`, { timeout: 10_000 });
}
});
} else {
ctx.menuItems.unshift({
icon: "toggle_on",
title: "Install Lens Metrics stack",
onClick: async () => {
metricsFeature.install(cluster);
Component.Notifications.info(`Lens Metrics is now installed to ${cluster.metadata.name}`, { timeout: 10_000 });
}
});
}
}
];
}

View File

@ -2,3 +2,5 @@ apiVersion: v1
kind: Namespace
metadata:
name: lens-metrics
annotations:
extensionVersion: "{{ version }}"

View File

@ -1,3 +1,4 @@
{{#if prometheus.enabled}}
apiVersion: v1
kind: Service
metadata:
@ -14,3 +15,4 @@ spec:
protocol: TCP
port: 80
targetPort: 9090
{{/if}}

View File

@ -1,3 +1,4 @@
{{#if prometheus.enabled}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
@ -46,14 +47,14 @@ spec:
serviceAccountName: prometheus
initContainers:
- name: chown
image: docker.io/alpine:3.9
image: docker.io/alpine:3.12
command: ["chown", "-R", "65534:65534", "/var/lib/prometheus"]
volumeMounts:
- name: data
mountPath: /var/lib/prometheus
containers:
- name: prometheus
image: quay.io/prometheus/prometheus:v2.19.3
image: quay.io/prometheus/prometheus:v2.26.0
args:
- --web.listen-address=0.0.0.0:9090
- --config.file=/etc/prometheus/prometheus.yaml
@ -114,3 +115,4 @@ spec:
requests:
storage: {{persistence.size}}
{{/if}}
{{/if}}

View File

@ -41,7 +41,7 @@ spec:
hostPID: true
containers:
- name: node-exporter
image: quay.io/prometheus/node-exporter:v1.0.1
image: quay.io/prometheus/node-exporter:v1.1.2
args:
- --path.procfs=/host/proc
- --path.sysfs=/host/sys

View File

@ -39,7 +39,7 @@ spec:
serviceAccountName: kube-state-metrics
containers:
- name: kube-state-metrics
image: quay.io/coreos/kube-state-metrics:v1.9.7
image: quay.io/coreos/kube-state-metrics:v1.9.8
ports:
- name: metrics
containerPort: 8080
@ -52,7 +52,7 @@ spec:
resources:
requests:
cpu: 10m
memory: 150Mi
memory: 32Mi
limits:
cpu: 200m
memory: 150Mi

View File

@ -19,12 +19,15 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ClusterFeature, Catalog, K8sApi } from "@k8slens/extensions";
import { Catalog, K8sApi } from "@k8slens/extensions";
import semver from "semver";
import * as path from "path";
export interface MetricsConfiguration {
// Placeholder for Metrics config structure
prometheus: {
enabled: boolean;
};
persistence: {
enabled: boolean;
storageClass: string;
@ -43,78 +46,72 @@ export interface MetricsConfiguration {
alertManagers: string[];
replicas: number;
storageClass: string;
version?: string;
}
export class MetricsFeature extends ClusterFeature.Feature {
name = "metrics";
latestVersion = "v2.19.3-lens1";
export interface MetricsStatus {
installed: boolean;
canUpgrade: boolean;
}
templateContext: MetricsConfiguration = {
persistence: {
enabled: false,
storageClass: null,
size: "20G",
},
nodeExporter: {
enabled: true,
},
retention: {
time: "2d",
size: "5GB",
},
kubeStateMetrics: {
enabled: true,
},
alertManagers: null,
replicas: 1,
storageClass: null,
};
export class MetricsFeature {
name = "lens-metrics";
latestVersion = "v2.26.0-lens1";
async install(cluster: Catalog.KubernetesCluster): Promise<void> {
protected stack: K8sApi.ResourceStack;
constructor(protected cluster: Catalog.KubernetesCluster) {
this.stack = new K8sApi.ResourceStack(cluster, this.name);
}
get resourceFolder() {
return path.join(__dirname, "../resources/");
}
async install(config: MetricsConfiguration): Promise<string> {
// Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass);
const storageClassApi = K8sApi.forCluster(this.cluster, K8sApi.StorageClass);
const scs = await storageClassApi.list();
this.templateContext.persistence.enabled = scs.some(sc => (
config.persistence.enabled = scs.some(sc => (
sc.metadata?.annotations?.["storageclass.kubernetes.io/is-default-class"] === "true" ||
sc.metadata?.annotations?.["storageclass.beta.kubernetes.io/is-default-class"] === "true"
));
super.applyResources(cluster, path.join(__dirname, "../resources/"));
config.version = this.latestVersion;
return this.stack.kubectlApplyFolder(this.resourceFolder, config, ["--prune"]);
}
async upgrade(cluster: Catalog.KubernetesCluster): Promise<void> {
return this.install(cluster);
async upgrade(config: MetricsConfiguration): Promise<string> {
return this.install(config);
}
async updateStatus(cluster: Catalog.KubernetesCluster): Promise<ClusterFeature.FeatureStatus> {
async getStatus(): Promise<MetricsStatus> {
const status: MetricsStatus = { installed: false, canUpgrade: false};
try {
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet);
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
const namespaceApi = K8sApi.forCluster(this.cluster, K8sApi.Namespace);
const namespace = await namespaceApi.get({name: "lens-metrics"});
if (prometheus?.kind) {
this.status.installed = true;
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true);
if (namespace?.kind) {
const currentVersion = namespace.metadata.annotations?.extensionVersion || "0.0.0";
status.installed = true;
status.canUpgrade = semver.lt(currentVersion, this.latestVersion, true);
} else {
this.status.installed = false;
status.installed = false;
}
} catch(e) {
if (e?.error?.code === 404) {
this.status.installed = false;
status.installed = false;
}
}
return this.status;
return status;
}
async uninstall(cluster: Catalog.KubernetesCluster): Promise<void> {
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace);
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding);
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole);
await namespaceApi.delete({name: "lens-metrics"});
await clusterRoleBindingApi.delete({name: "lens-prometheus"});
await clusterRoleApi.delete({name: "lens-prometheus"});
async uninstall(config: MetricsConfiguration): Promise<string> {
return this.stack.kubectlDeleteFolder(this.resourceFolder, config);
}
}

View File

@ -0,0 +1,279 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import { Component, Catalog, K8sApi } from "@k8slens/extensions";
import { observer } from "mobx-react";
import { computed, observable } from "mobx";
import { MetricsFeature, MetricsConfiguration } from "./metrics-feature";
interface Props {
cluster: Catalog.KubernetesCluster;
}
@observer
export class MetricsSettings extends React.Component<Props> {
@observable featureStates = {
prometheus: false,
kubeStateMetrics: false,
nodeExporter: false
};
@observable canUpgrade = false;
@observable upgrading = false;
@observable changed = false;
@observable inProgress = false;
config: MetricsConfiguration = {
prometheus: {
enabled: false
},
persistence: {
enabled: false,
storageClass: null,
size: "20G",
},
nodeExporter: {
enabled: false,
},
retention: {
time: "2d",
size: "5GB",
},
kubeStateMetrics: {
enabled: false,
},
alertManagers: null,
replicas: 1,
storageClass: null,
};
feature: MetricsFeature;
@computed get isTogglable() {
if (this.inProgress) return false;
if (!this.props.cluster.status.active) return false;
if (this.canUpgrade) return false;
if (!this.isActiveMetricsProvider) return false;
return true;
}
get metricsProvider() {
return this.props.cluster.spec?.metrics?.prometheus?.type || "";
}
get isActiveMetricsProvider() {
return (!this.metricsProvider || this.metricsProvider === "lens");
}
async componentDidMount() {
this.feature = new MetricsFeature(this.props.cluster);
await this.updateFeatureStates();
}
async updateFeatureStates() {
const status = await this.feature.getStatus();
this.canUpgrade = status.canUpgrade;
if (this.canUpgrade) {
this.changed = true;
}
const statefulSet = K8sApi.forCluster(this.props.cluster, K8sApi.StatefulSet);
try {
await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
this.featureStates.prometheus = true;
} catch(e) {
if (e?.error?.code === 404) {
this.featureStates.prometheus = false;
} else {
this.featureStates.prometheus = undefined;
}
}
const deployment = K8sApi.forCluster(this.props.cluster, K8sApi.Deployment);
try {
await deployment.get({name: "kube-state-metrics", namespace: "lens-metrics"});
this.featureStates.kubeStateMetrics = true;
} catch(e) {
if (e?.error?.code === 404) {
this.featureStates.kubeStateMetrics = false;
} else {
this.featureStates.kubeStateMetrics = undefined;
}
}
const daemonSet = K8sApi.forCluster(this.props.cluster, K8sApi.DaemonSet);
try {
await daemonSet.get({name: "node-exporter", namespace: "lens-metrics"});
this.featureStates.nodeExporter = true;
} catch(e) {
if (e?.error?.code === 404) {
this.featureStates.nodeExporter = false;
} else {
this.featureStates.nodeExporter = undefined;
}
}
}
async save() {
this.config.prometheus.enabled = !!this.featureStates.prometheus;
this.config.kubeStateMetrics.enabled = !!this.featureStates.kubeStateMetrics;
this.config.nodeExporter.enabled = !!this.featureStates.nodeExporter;
this.inProgress = true;
try {
if (!this.config.prometheus.enabled && !this.config.kubeStateMetrics.enabled && !this.config.nodeExporter.enabled) {
await this.feature.uninstall(this.config);
} else {
await this.feature.install(this.config);
}
} finally {
this.inProgress = false;
this.changed = false;
await this.updateFeatureStates();
}
}
async togglePrometheus(enabled: boolean) {
this.featureStates.prometheus = enabled;
this.changed = true;
}
async toggleKubeStateMetrics(enabled: boolean) {
this.featureStates.kubeStateMetrics = enabled;
this.changed = true;
}
async toggleNodeExporter(enabled: boolean) {
this.featureStates.nodeExporter = enabled;
this.changed = true;
}
@computed get buttonLabel() {
const allDisabled = !this.featureStates.kubeStateMetrics && !this.featureStates.nodeExporter && !this.featureStates.prometheus;
if (this.inProgress && this.canUpgrade) return "Upgrading ...";
if (this.inProgress && allDisabled) return "Uninstalling ...";
if (this.inProgress) return "Applying ...";
if (this.canUpgrade) return "Upgrade";
if (this.changed && allDisabled) {
return "Uninstall";
}
return "Apply";
}
render() {
return (
<>
{ !this.props.cluster.status.active && (
<section>
<p style={ {color: "var(--colorError)"} }>
Lens Metrics settings requires established connection to the cluster.
</p>
</section>
)}
{ !this.isActiveMetricsProvider && (
<section>
<p style={ {color: "var(--colorError)"} }>
Other metrics provider is currently active. See &quot;Metrics&quot; tab for details.
</p>
</section>
)}
<section>
<Component.SubTitle title="Prometheus" />
<Component.FormSwitch
control={
<Component.Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.prometheus && this.props.cluster.status.active}
onChange={v => this.togglePrometheus(v.target.checked)}
name="prometheus"
/>
}
label="Enable bundled Prometheus metrics stack"
/>
<small className="hint">
Enable timeseries data visualization (Prometheus stack) for your cluster.
</small>
</section>
<section>
<Component.SubTitle title="Kube State Metrics" />
<Component.FormSwitch
control={
<Component.Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.active}
onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
name="node-exporter"
/>
}
label="Enable bundled kube-state-metrics stack"
/>
<small className="hint">
Enable Kubernetes API object metrics for your cluster.
Enable this only if you don&apos;t have existing kube-state-metrics stack installed.
</small>
</section>
<section>
<Component.SubTitle title="Node Exporter" />
<Component.FormSwitch
control={
<Component.Switcher
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.active}
onChange={v => this.toggleNodeExporter(v.target.checked)}
name="node-exporter"
/>
}
label="Enable bundled node-exporter stack"
/>
<small className="hint">
Enable node level metrics for your cluster.
Enable this only if you don&apos;t have existing node-exporter stack installed.
</small>
</section>
<section>
<Component.Button
label={this.buttonLabel}
waiting={this.inProgress}
onClick={() => this.save()}
primary
disabled={!this.changed} />
{this.canUpgrade && (<small className="hint">
An update is available for enabled metrics components.
</small>)}
</section>
</>
);
}
}

View File

@ -16,8 +16,8 @@
"jsx": "react"
},
"include": [
"./*.ts",
"./*.tsx"
"./**/*.ts",
"./**/*.tsx"
],
"exclude": [
"node_modules",

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const path = require("path");
module.exports = [
@ -22,7 +42,8 @@ module.exports = [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact"
}
],
resolve: {

View File

@ -4037,15 +4037,6 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -4247,12 +4238,6 @@
"minimist": "^1.2.5"
}
},
"mobx": {
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.7.tgz",
"integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==",
"dev": true
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -4793,25 +4778,6 @@
"sisteransi": "^1.0.5"
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
},
"dependencies": {
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
}
}
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -4922,17 +4888,6 @@
"safe-buffer": "^5.1.0"
}
},
"react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
}
},
"react-is": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",

View File

@ -8,16 +8,17 @@
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch",
"build": "webpack && npm pack",
"dev": "webpack --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
},
"files": [
"dist/**/*"
],
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "^26.6.3",
"mobx": "^5.15.5",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2"

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const path = require("path");
module.exports = [
@ -22,7 +42,8 @@ module.exports = [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact"
}
],
resolve: {

View File

@ -4674,15 +4674,6 @@
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -4884,12 +4875,6 @@
"minimist": "^1.2.5"
}
},
"mobx": {
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/mobx/-/mobx-5.15.7.tgz",
"integrity": "sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==",
"dev": true
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -5430,25 +5415,6 @@
"sisteransi": "^1.0.5"
}
},
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.8.1"
},
"dependencies": {
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
}
}
},
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@ -5559,17 +5525,6 @@
"safe-buffer": "^5.1.0"
}
},
"react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"dev": true,
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
}
},
"react-is": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz",

View File

@ -8,16 +8,17 @@
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch",
"build": "webpack && npm pack",
"dev": "webpack --watch",
"test": "jest --passWithNoTests --env=jsdom src $@"
},
"files": [
"dist/**/*"
],
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"jest": "^26.6.3",
"mobx": "^5.15.5",
"react": "^16.13.1",
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2"

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const path = require("path");
module.exports = [
@ -22,7 +42,8 @@ module.exports = [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
"mobx": "var global.Mobx",
"mobx-react": "var global.MobxReact"
}
],
resolve: {

View File

@ -1,5 +1,22 @@
/**
* @jest-environment node
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
@ -8,7 +25,7 @@
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa.
*/
import { Application } from "spectron";
import type { Application } from "spectron";
import * as utils from "../helpers/utils";
import { listHelmRepositories } from "../helpers/utils";
import { fail } from "assert";

View File

@ -1,10 +1,31 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa.
*/
import { Application } from "spectron";
import type { Application } from "spectron";
import * as utils from "../helpers/utils";
import { minikubeReady, waitForMinikubeDashboard } from "../helpers/minikube";
import { exec } from "child_process";

View File

@ -1,4 +1,25 @@
import { Application } from "spectron";
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Application } from "spectron";
import * as utils from "../helpers/utils";
jest.setTimeout(60000);

View File

@ -1,5 +1,25 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { spawnSync } from "child_process";
import { Application } from "spectron";
import type { Application } from "spectron";
export function minikubeReady(testNamespace: string): boolean {
// determine if minikube is running

View File

@ -1,3 +1,23 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Application } from "spectron";
import * as util from "util";
import { exec } from "child_process";

View File

@ -19,5 +19,4 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export { ClusterFeature as Feature } from "../cluster-feature";
export type { ClusterFeatureStatus as FeatureStatus } from "../cluster-feature";

View File

@ -7,6 +7,9 @@ repo_name: GitHub
repo_url: https://github.com/lensapp/lens
copyright: Copyright &copy; 2021 <a href="https://mirantis.com/">Mirantis Inc.</a> - All rights reserved.
edit_uri: ""
google_analytics:
- UA-159377374-2
- auto
nav:
- Overview: README.md
- Getting Started: getting-started/README.md
@ -35,6 +38,7 @@ nav:
- Stores: extensions/guides/stores.md
- Working with MobX: extensions/guides/working-with-mobx.md
- Protocol Handlers: extensions/guides/protocol-handlers.md
- IPC: extensions/guides/ipc.md
- Testing and Publishing:
- Testing Extensions: extensions/testing-and-publishing/testing.md
- Publishing Extensions: extensions/testing-and-publishing/publishing.md
@ -55,14 +59,14 @@ theme:
favicon: img/favicon.ico
logo: img/lens-logo-icon.svg
palette:
- scheme: default
toggle:
icon: material/toggle-switch
name: Switch to light mode
- scheme: slate
toggle:
icon: material/toggle-switch-off-outline
name: Switch to dark mode
- scheme: default
toggle:
icon: material/toggle-switch
name: Switch to light mode
features:
- toc.autohide
- search.suggest

View File

@ -2,7 +2,7 @@
"name": "open-lens",
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"version": "5.0.0-beta.4",
"version": "5.0.0-beta.5",
"main": "static/build/main.js",
"copyright": "© 2021 OpenLens Authors",
"license": "MIT",
@ -98,8 +98,8 @@
"from": "extensions/",
"to": "./extensions/",
"filter": [
"**/*.js*",
"**/*.yml*",
"**/*.tgz",
"**/package.json",
"!**/node_modules"
]
},
@ -291,7 +291,6 @@
"@types/tar": "^4.0.4",
"@types/tcp-port-used": "^1.0.0",
"@types/tempy": "^0.3.0",
"@types/universal-analytics": "^0.4.4",
"@types/url-parse": "^1.4.3",
"@types/uuid": "^8.3.0",
"@types/webdriverio": "^4.13.0",
@ -315,6 +314,7 @@
"electron-builder": "^22.10.5",
"electron-notarize": "^0.3.0",
"eslint": "^7.7.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-unused-imports": "^1.0.1",
"file-loader": "^6.0.0",

View File

@ -20,10 +20,30 @@
*/
import mockFs from "mock-fs";
import { CatalogEntityItem } from "../../renderer/components/+catalog/catalog-entity.store";
import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
jest.mock("../../renderer/api/catalog-entity-registry", () => ({
catalogEntityRegistry: {
items: [
{
metadata: {
uid: "1dfa26e2ebab15780a3547e9c7fa785c",
name: "mycluster",
source: "local"
}
},
{
metadata: {
uid: "55b42c3c7ba3b04193416cda405269a5",
name: "my_shiny_cluster",
source: "remote"
}
}
]
}
}));
const testCluster = {
uid: "test",
name: "test",
@ -87,6 +107,17 @@ const awsCluster = {
}
};
jest.mock("electron", () => {
return {
app: {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: (): void => void 0,
}
};
});
describe("HotbarStore", () => {
beforeEach(() => {
ClusterStore.resetInstance();
@ -127,10 +158,9 @@ describe("HotbarStore", () => {
it("adds items", () => {
const hotbarStore = HotbarStore.createInstance();
const entity = new CatalogEntityItem(testCluster);
hotbarStore.load();
hotbarStore.addToHotbar(entity);
hotbarStore.addToHotbar(testCluster);
const items = hotbarStore.getActive().items.filter(Boolean);
expect(items.length).toEqual(1);
@ -138,10 +168,9 @@ describe("HotbarStore", () => {
it("removes items", () => {
const hotbarStore = HotbarStore.createInstance();
const entity = new CatalogEntityItem(testCluster);
hotbarStore.load();
hotbarStore.addToHotbar(entity);
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("test");
const items = hotbarStore.getActive().items.filter(Boolean);
@ -150,10 +179,9 @@ describe("HotbarStore", () => {
it("does nothing if removing with invalid uid", () => {
const hotbarStore = HotbarStore.createInstance();
const entity = new CatalogEntityItem(testCluster);
hotbarStore.load();
hotbarStore.addToHotbar(entity);
hotbarStore.addToHotbar(testCluster);
hotbarStore.removeFromHotbar("invalid uid");
const items = hotbarStore.getActive().items.filter(Boolean);
@ -162,14 +190,11 @@ describe("HotbarStore", () => {
it("moves item to empty cell", () => {
const hotbarStore = HotbarStore.createInstance();
const test = new CatalogEntityItem(testCluster);
const minikube = new CatalogEntityItem(minikubeCluster);
const aws = new CatalogEntityItem(awsCluster);
hotbarStore.load();
hotbarStore.addToHotbar(test);
hotbarStore.addToHotbar(minikube);
hotbarStore.addToHotbar(aws);
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
expect(hotbarStore.getActive().items[5]).toBeNull();
@ -181,14 +206,11 @@ describe("HotbarStore", () => {
it("moves items down", () => {
const hotbarStore = HotbarStore.createInstance();
const test = new CatalogEntityItem(testCluster);
const minikube = new CatalogEntityItem(minikubeCluster);
const aws = new CatalogEntityItem(awsCluster);
hotbarStore.load();
hotbarStore.addToHotbar(test);
hotbarStore.addToHotbar(minikube);
hotbarStore.addToHotbar(aws);
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
// aws -> test
hotbarStore.restackItems(2, 0);
@ -200,14 +222,11 @@ describe("HotbarStore", () => {
it("moves items up", () => {
const hotbarStore = HotbarStore.createInstance();
const test = new CatalogEntityItem(testCluster);
const minikube = new CatalogEntityItem(minikubeCluster);
const aws = new CatalogEntityItem(awsCluster);
hotbarStore.load();
hotbarStore.addToHotbar(test);
hotbarStore.addToHotbar(minikube);
hotbarStore.addToHotbar(aws);
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(minikubeCluster);
hotbarStore.addToHotbar(awsCluster);
// test -> aws
hotbarStore.restackItems(0, 2);
@ -219,10 +238,9 @@ describe("HotbarStore", () => {
it("does nothing when item moved to same cell", () => {
const hotbarStore = HotbarStore.createInstance();
const test = new CatalogEntityItem(testCluster);
hotbarStore.load();
hotbarStore.addToHotbar(test);
hotbarStore.addToHotbar(testCluster);
hotbarStore.restackItems(0, 0);
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("test");
@ -230,15 +248,12 @@ describe("HotbarStore", () => {
it("new items takes first empty cell", () => {
const hotbarStore = HotbarStore.createInstance();
const test = new CatalogEntityItem(testCluster);
const minikube = new CatalogEntityItem(minikubeCluster);
const aws = new CatalogEntityItem(awsCluster);
hotbarStore.load();
hotbarStore.addToHotbar(test);
hotbarStore.addToHotbar(aws);
hotbarStore.addToHotbar(testCluster);
hotbarStore.addToHotbar(awsCluster);
hotbarStore.restackItems(0, 3);
hotbarStore.addToHotbar(minikube);
hotbarStore.addToHotbar(minikubeCluster);
expect(hotbarStore.getActive().items[0].entity.uid).toEqual("minikube");
});
@ -250,10 +265,9 @@ describe("HotbarStore", () => {
console.error = jest.fn();
const hotbarStore = HotbarStore.createInstance();
const test = new CatalogEntityItem(testCluster);
hotbarStore.load();
hotbarStore.addToHotbar(test);
hotbarStore.addToHotbar(testCluster);
expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
expect(() => hotbarStore.restackItems(2, -1)).toThrow();
@ -264,4 +278,111 @@ describe("HotbarStore", () => {
console.error = err;
});
});
describe("pre beta-5 migrations", () => {
beforeEach(() => {
HotbarStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-hotbar-store.json": JSON.stringify({
__internal__: {
migrations: {
version: "5.0.0-beta.3"
}
},
"hotbars": [
{
"id": "3caac17f-aec2-4723-9694-ad204465d935",
"name": "myhotbar",
"items": [
{
"entity": {
"uid": "1dfa26e2ebab15780a3547e9c7fa785c"
}
},
{
"entity": {
"uid": "55b42c3c7ba3b04193416cda405269a5"
}
},
{
"entity": {
"uid": "176fd331968660832f62283219d7eb6e"
}
},
{
"entity": {
"uid": "61c4fb45528840ebad1badc25da41d14",
"name": "user1-context",
"source": "local"
}
},
{
"entity": {
"uid": "27d6f99fe9e7548a6e306760bfe19969",
"name": "foo2",
"source": "local"
}
},
null,
{
"entity": {
"uid": "c0b20040646849bb4dcf773e43a0bf27",
"name": "multinode-demo",
"source": "local"
}
},
null,
null,
null,
null,
null
]
}
],
})
}
};
mockFs(mockOpts);
return HotbarStore.createInstance().load();
});
afterEach(() => {
mockFs.restore();
});
it("allows to retrieve a hotbar", () => {
const hotbar = HotbarStore.getInstance().getById("3caac17f-aec2-4723-9694-ad204465d935");
expect(hotbar.id).toBe("3caac17f-aec2-4723-9694-ad204465d935");
});
it("clears cells without entity", () => {
const items = HotbarStore.getInstance().hotbars[0].items;
expect(items[2]).toBeNull();
});
it("adds extra data to cells with according entity", () => {
const items = HotbarStore.getInstance().hotbars[0].items;
expect(items[0]).toEqual({
entity: {
name: "mycluster",
source: "local",
uid: "1dfa26e2ebab15780a3547e9c7fa785c"
}
});
expect(items[1]).toEqual({
entity: {
name: "my_shiny_cluster",
source: "remote",
uid: "55b42c3c7ba3b04193416cda405269a5"
}
});
});
});
});

View File

@ -23,6 +23,12 @@ import { SearchStore } from "../search-store";
import { Console } from "console";
import { stdout, stderr } from "process";
jest.mock("electron", () => ({
app: {
getPath: () => "/foo",
},
}));
console = new Console(stdout, stderr);
let searchStore: SearchStore = null;

View File

@ -21,7 +21,7 @@
import path from "path";
import Config from "conf";
import { Options as ConfOptions } from "conf/dist/source/types";
import type { Options as ConfOptions } from "conf/dist/source/types";
import { app, ipcMain, IpcMainEvent, ipcRenderer, IpcRendererEvent, remote } from "electron";
import { IReactionOptions, observable, reaction, runInAction, when } from "mobx";
import Singleton from "./utils/singleton";

View File

@ -21,15 +21,31 @@
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { clusterDisconnectHandler } from "../cluster-ipc";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
import { productName } from "../vars";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { app } from "electron";
export type KubernetesClusterPrometheusMetrics = {
address?: {
namespace: string;
service: string;
port: number;
prefix: string;
};
type?: string;
};
export type KubernetesClusterSpec = {
kubeconfigPath: string;
kubeconfigContext: string;
metrics?: {
source: string;
prometheus?: KubernetesClusterPrometheusMetrics;
}
};
export interface KubernetesClusterStatus extends CatalogEntityStatus {
@ -40,6 +56,38 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "KubernetesCluster";
async connect(): Promise<void> {
if (app) {
const cluster = ClusterStore.getInstance().getById(this.metadata.uid);
if (!cluster) return;
await cluster.activate();
return;
}
await requestMain(clusterActivateHandler, this.metadata.uid, false);
return;
}
async disconnect(): Promise<void> {
if (app) {
const cluster = ClusterStore.getInstance().getById(this.metadata.uid);
if (!cluster) return;
cluster.disconnect();
return;
}
await requestMain(clusterDisconnectHandler, this.metadata.uid, false);
return;
}
async onRun(context: CatalogEntityActionContext) {
context.navigate(`/cluster/${this.metadata.uid}`);
}
@ -55,7 +103,6 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
async onContextMenuOpen(context: CatalogEntityContextMenuContext) {
context.menuItems = [
{
icon: "settings",
title: "Settings",
onlyVisibleForSource: "local",
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
@ -64,7 +111,6 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
if (this.metadata.labels["file"]?.startsWith(ClusterStore.storedKubeConfigFolder)) {
context.menuItems.push({
icon: "delete",
title: "Delete",
onlyVisibleForSource: "local",
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
@ -75,14 +121,20 @@ export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, Kube
}
if (this.status.phase == "connected") {
context.menuItems.unshift({
icon: "link_off",
context.menuItems.push({
title: "Disconnect",
onClick: async () => {
ClusterStore.getInstance().deactivate(this.metadata.uid);
requestMain(clusterDisconnectHandler, this.metadata.uid);
}
});
} else {
context.menuItems.push({
title: "Connect",
onClick: async () => {
context.navigate(`/cluster/${this.metadata.uid}`);
}
});
}
const category = catalogCategoryRegistry.getCategoryForEntity<KubernetesClusterCategory>(this);

View File

@ -32,7 +32,7 @@ export type WebLinkSpec = {
export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
public readonly kind = "KubernetesCluster";
public readonly kind = "WebLink";
async onRun() {
window.open(this.spec.url, "_blank");

View File

@ -19,26 +19,38 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { action, computed, observable, toJS } from "mobx";
import { action, computed, observable } from "mobx";
import { Disposer, ExtendedMap } from "../utils";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
export class CatalogCategoryRegistry {
@observable protected categories: CatalogCategory[] = [];
protected categories = observable.set<CatalogCategory>();
@action add(category: CatalogCategory) {
this.categories.push(category);
@action add(category: CatalogCategory): Disposer {
this.categories.add(category);
return () => this.categories.delete(category);
}
@action remove(category: CatalogCategory) {
this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind);
@computed private get groupKindLookup(): Map<string, Map<string, CatalogCategory>> {
// ExtendedMap has the convenience methods `getOrInsert` and `strictSet`
const res = new ExtendedMap<string, ExtendedMap<string, CatalogCategory>>();
for (const category of this.categories) {
res
.getOrInsert(category.spec.group, ExtendedMap.new)
.strictSet(category.spec.names.kind, category);
}
return res;
}
@computed get items() {
return toJS(this.categories);
return Array.from(this.categories);
}
getForGroupKind<T extends CatalogCategory>(group: string, kind: string) {
return this.categories.find((c) => c.spec.group === group && c.spec.names.kind === kind) as T;
getForGroupKind<T extends CatalogCategory>(group: string, kind: string): T | undefined {
return this.groupKindLookup.get(group)?.get(kind) as T;
}
getEntityForData(data: CatalogEntityData & CatalogEntityKindData) {
@ -60,17 +72,11 @@ export class CatalogCategoryRegistry {
return new specVersion.entityClass(data);
}
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData) {
getCategoryForEntity<T extends CatalogCategory>(data: CatalogEntityData & CatalogEntityKindData): T | undefined {
const splitApiVersion = data.apiVersion.split("/");
const group = splitApiVersion[0];
const category = this.categories.find((category) => {
return category.spec.group === group && category.spec.names.kind === data.kind;
});
if (!category) return null;
return category as T;
return this.getForGroupKind(group, data.kind);
}
}

View File

@ -20,7 +20,7 @@
*/
import { action, computed, observable, IComputedValue, IObservableArray } from "mobx";
import { CatalogEntity } from "./catalog-entity";
import type { CatalogEntity } from "./catalog-entity";
import { iter } from "../utils";
export class CatalogEntityRegistry {

View File

@ -83,7 +83,6 @@ export interface CatalogEntityActionContext {
}
export interface CatalogEntityContextMenu {
icon: string;
title: string;
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
onClick: () => void | Promise<void>;
@ -92,6 +91,10 @@ export interface CatalogEntityContextMenu {
}
}
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
icon: string;
}
export interface CatalogEntitySettingsMenu {
group?: string;
title: string;
@ -111,7 +114,7 @@ export interface CatalogEntitySettingsContext {
export interface CatalogEntityAddMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityContextMenu[];
menuItems: CatalogEntityAddMenu[];
}
export type CatalogEntitySpec = Record<string, any>;

View File

@ -31,14 +31,13 @@ export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
if (ipcMain) {
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
return cluster.activate(force);
}
return ClusterStore.getInstance()
.getById(clusterId)
?.activate(force);
});
handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
@ -46,15 +45,14 @@ if (ipcMain) {
if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
return cluster.pushState();
cluster.pushState();
}
});
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) return cluster.refresh({ refreshMetadata: true });
return ClusterStore.getInstance()
.getById(clusterId)
?.refresh({ refreshMetadata: true });
});
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
@ -67,14 +65,39 @@ if (ipcMain) {
}
});
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
handleRequest(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
applier.kubectlApplyAll(resources);
try {
const stdout = await applier.kubectlApplyAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}
});
handleRequest(clusterKubectlDeleteAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-delete-all"});
const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) {
const applier = new ResourceApplier(cluster);
try {
const stdout = await applier.kubectlDeleteAll(resources, extraArgs);
return { stdout };
} catch (error: any) {
return { stderr: error };
}
} else {
throw `${clusterId} is not a valid cluster id`;
}

View File

@ -30,9 +30,9 @@ import logger from "../main/logger";
import { appEventBus } from "./event-bus";
import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node";
import type { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
import type { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
import { disposer, noop } from "./utils";
export interface ClusterIconUpload {

View File

@ -53,12 +53,11 @@ export class EventEmitter<D extends [...any[]]> {
emit(...data: D) {
[...this.listeners].every(([callback, options]) => {
if (options.once) this.removeListener(callback);
const result = callback(...data);
if (options.once) {
this.removeListener(callback);
}
if (result === false) return; // break cycle
return true;
return callback(...data) !== false;
});
}
}

View File

@ -23,12 +23,14 @@ import { action, comparer, observable, toJS } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/hotbar-store";
import * as uuid from "uuid";
import { CatalogEntityItem } from "../renderer/components/+catalog/catalog-entity.store";
import isNull from "lodash/isNull";
import { CatalogEntity } from "./catalog";
export interface HotbarItem {
entity: {
uid: string;
name?: string;
source?: string;
};
params?: {
[key: string]: string;
@ -79,8 +81,12 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
}
hotbarIndex(id: string) {
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
}
get activeHotbarIndex() {
return this.hotbars.findIndex((hotbar) => hotbar.id === this.activeHotbarId);
return this.hotbarIndex(this.activeHotbarId);
}
get initialItems() {
@ -144,11 +150,16 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
}
addToHotbar(item: CatalogEntityItem, cellIndex = -1) {
@action
addToHotbar(item: CatalogEntity, cellIndex = -1) {
const hotbar = this.getActive();
const newItem = { entity: { uid: item.id }};
const newItem = { entity: {
uid: item.metadata.uid,
name: item.metadata.name,
source: item.metadata.source
}};
if (hotbar.items.find(i => i?.entity.uid === item.id)) {
if (hotbar.items.find(i => i?.entity.uid === item.metadata.uid)) {
return;
}
@ -167,6 +178,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
}
@action
removeFromHotbar(uid: string) {
const hotbar = this.getActive();
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);

View File

@ -42,35 +42,33 @@ function getSubFrames(): ClusterFrameInfo[] {
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
}
export async function broadcastMessage(channel: string, ...args: any[]) {
export function broadcastMessage(channel: string, ...args: any[]) {
const views = (webContents || remote?.webContents)?.getAllWebContents();
if (!views) return;
if (ipcRenderer) {
ipcRenderer.send(channel, ...args);
} else if (ipcMain) {
ipcMain.emit(channel, ...args);
}
ipcRenderer?.send(channel, ...args);
ipcMain?.emit(channel, ...args);
for (const view of views) {
const type = view.getType();
const subFramesP = ipcRenderer
? requestMain(subFramesChannel)
: Promise.resolve(getSubFrames());
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args });
view.send(channel, ...args);
subFramesP
.then(subFrames => {
for (const view of views) {
try {
logger.silly(`[IPC]: broadcasting "${channel}" to ${view.getType()}=${view.id}`, { args });
view.send(channel, ...args);
try {
const subFrames: ClusterFrameInfo[] = ipcRenderer
? await requestMain(subFramesChannel)
: getSubFrames();
for (const frameInfo of subFrames) {
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
for (const frameInfo of subFrames) {
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
}
} catch (error) {
logger.error("[IPC]: failed to send IPC message", { error: String(error) });
}
}
} catch (error) {
logger.error("[IPC]: failed to send IPC message", { error: String(error) });
}
}
});
}
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {

View File

@ -19,10 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { EventEmitter } from "events";
import type { EventEmitter } from "events";
import { ipcMain } from "electron";
import logger from "../../main/logger";
import type { Disposer } from "../utils";
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
export type ListenerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
export type Rest<T> = T extends [any, ...infer R] ? R : [];
@ -34,22 +36,22 @@ export type Rest<T> = T extends [any, ...infer R] ? R : [];
* @param verifier The function to be called to verify that the args are the correct type
*/
export function onceCorrect<
EM extends EventEmitter,
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
IPC extends EventEmitter,
Listener extends (event: ListenerEvent<IPC>, ...args: any[]) => any
>({
source,
channel,
listener,
verifier,
}: {
source: EM,
channel: string | symbol,
listener: L,
verifier: ListVerifier<Rest<Parameters<L>>>,
source: IPC,
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
}): void {
function handler(event: HandlerEvent<EM>, ...args: unknown[]): void {
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]): void {
if (verifier(args)) {
source.removeListener(channel, handler); // remove immediately
source.removeListener(channel, wrappedListener); // remove immediately
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
.catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error }));
@ -58,7 +60,7 @@ export function onceCorrect<
}
}
source.on(channel, handler);
source.on(channel, wrappedListener);
}
/**
@ -68,25 +70,53 @@ export function onceCorrect<
* @param verifier The function to be called to verify that the args are the correct type
*/
export function onCorrect<
EM extends EventEmitter,
L extends (event: HandlerEvent<EM>, ...args: any[]) => any
IPC extends EventEmitter,
Listener extends (event: ListenerEvent<IPC>, ...args: any[]) => any
>({
source,
channel,
listener,
verifier,
}: {
source: EM,
channel: string | symbol,
listener: L,
verifier: ListVerifier<Rest<Parameters<L>>>,
}): void {
source.on(channel, (event, ...args: unknown[]) => {
source: IPC,
channel: string,
listener: Listener,
verifier: ListVerifier<Rest<Parameters<Listener>>>,
}): Disposer {
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]) {
if (verifier(args)) {
(async () => (listener(event, ...args)))() // might return a promise, or throw, or reject
.catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error }));
} else {
logger.error("[IPC]: channel was emitted with invalid data", { channel, args });
}
});
}
source.on(channel, wrappedListener);
return () => source.off(channel, wrappedListener);
}
export function handleCorrect<
Handler extends (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any,
>({
channel,
handler,
verifier,
}: {
channel: string,
handler: Handler,
verifier: ListVerifier<Rest<Parameters<Handler>>>,
}): Disposer {
function wrappedHandler(event: Electron.IpcMainInvokeEvent, ...args: unknown[]): ReturnType<Handler> {
if (verifier(args)) {
return handler(event, ...args);
}
throw new TypeError(`Invalid args for invoke on channel: ${channel}`);
}
ipcMain.handle(channel, wrappedHandler);
return () => ipcMain.removeHandler(channel);
}

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { UpdateInfo } from "electron-updater";
import type { UpdateInfo } from "electron-updater";
export const UpdateAvailableChannel = "update-available";
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";

View File

@ -0,0 +1,152 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import fse from "fs-extra";
import path from "path";
import hb from "handlebars";
import { ResourceApplier } from "../../main/resource-applier";
import type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger";
import { app } from "electron";
import { requestMain } from "../ipc";
import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store";
import yaml from "js-yaml";
import { productName } from "../vars";
export class ResourceStack {
constructor(protected cluster: KubernetesCluster, protected name: string) {}
/**
*
* @param folderPath folder path that is searched for files defining kubernetes resources.
* @param templateContext sets the template parameters that are to be applied to any templated kubernetes resources that are to be applied.
*/
async kubectlApplyFolder(folderPath: string, templateContext?: any, extraArgs?: string[]): Promise<string> {
const resources = await this.renderTemplates(folderPath, templateContext);
return this.applyResources(resources, extraArgs);
}
/**
*
* @param folderPath folder path that is searched for files defining kubernetes resources.
* @param templateContext sets the template parameters that are to be applied to any templated kubernetes resources that are to be applied.
*/
async kubectlDeleteFolder(folderPath: string, templateContext?: any, extraArgs?: string[]): Promise<string> {
const resources = await this.renderTemplates(folderPath, templateContext);
return this.deleteResources(resources, extraArgs);
}
protected async applyResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);
}
let kubectlArgs = extraArgs || [];
kubectlArgs = this.appendKubectlArgs(kubectlArgs);
if (app) {
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
} else {
const response = await requestMain(clusterKubectlApplyAllHandler, this.cluster.metadata.uid, resources, kubectlArgs);
if (response.stderr) {
throw new Error(response.stderr);
}
return response.stdout;
}
}
protected async deleteResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);
}
let kubectlArgs = extraArgs || [];
kubectlArgs = this.appendKubectlArgs(kubectlArgs);
if (app) {
return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
} else {
const response = await requestMain(clusterKubectlDeleteAllHandler, this.cluster.metadata.uid, resources, kubectlArgs);
if (response.stderr) {
throw new Error(response.stderr);
}
return response.stdout;
}
}
protected appendKubectlArgs(kubectlArgs: string[]) {
if (!kubectlArgs.includes("-l") && !kubectlArgs.includes("--label")) {
return kubectlArgs.concat(["-l", `app.kubernetes.io/name=${this.name}`]);
}
return kubectlArgs;
}
protected async renderTemplates(folderPath: string, templateContext: any): Promise<string[]> {
const resources: string[] = [];
logger.info(`[RESOURCE-STACK]: render templates from ${folderPath}`);
const files = await fse.readdir(folderPath);
for(const filename of files) {
const file = path.join(folderPath, filename);
const raw = await fse.readFile(file);
let resourceData: string;
if (filename.endsWith(".hb")) {
const template = hb.compile(raw.toString());
resourceData = template(templateContext);
} else {
resourceData = raw.toString();
}
if (!resourceData.trim()) continue;
const resourceArray = yaml.safeLoadAll(resourceData.toString());
resourceArray.forEach((resource) => {
if (resource?.metadata) {
resource.metadata.labels ||= {};
resource.metadata.labels["app.kubernetes.io/name"] = this.name;
resource.metadata.labels["app.kubernetes.io/managed-by"] = productName;
resource.metadata.labels["app.kubernetes.io/created-by"] = "resource-stack";
}
resources.push(yaml.safeDump(resource));
});
}
return resources;
}
}

View File

@ -230,7 +230,7 @@ export function getNodeWarningConditions(node: V1Node) {
*
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
*/
export function validateKubeConfig(config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}): Error | undefined {
export function validateKubeConfig(config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}): Error | void {
try {
// we only receive a single context, cluster & user object here so lets validate them as this
// will be called when we add a new cluster to Lens

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Url from "url-parse";
import type Url from "url-parse";
export enum RoutingErrorType {
INVALID_PROTOCOL = "invalid-protocol",
@ -52,6 +52,8 @@ export class RoutingError extends Error {
return "no extension ID";
case RoutingErrorType.MISSING_EXTENSION:
return "extension not found";
default:
return `unknown error: ${this.type}`;
}
}
}

View File

@ -24,12 +24,12 @@ import { countBy } from "lodash";
import { Singleton } from "../utils";
import { pathToRegexp } from "path-to-regexp";
import logger from "../../main/logger";
import Url from "url-parse";
import type Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error";
import { ExtensionsStore } from "../../extensions/extensions-store";
import { ExtensionLoader } from "../../extensions/extension-loader";
import { LensExtension } from "../../extensions/lens-extension";
import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry";
import type { LensExtension } from "../../extensions/lens-extension";
import type { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry";
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
export const ProtocolHandlerIpcPrefix = "protocol-handler";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { AbortController } from "abort-controller";
import type { AbortController } from "abort-controller";
/**
* Return a promise that will be resolved after at least `timeout` ms have

View File

@ -22,19 +22,17 @@
import { action, IEnhancer, IObservableMapInitialValues, ObservableMap } from "mobx";
export class ExtendedMap<K, V> extends Map<K, V> {
constructor(protected getDefault: () => V, entries?: readonly (readonly [K, V])[] | null) {
super(entries);
static new<K, V>(entries?: readonly (readonly [K, V])[] | null): ExtendedMap<K, V> {
return new ExtendedMap<K, V>(entries);
}
getOrInsert(key: K, val: V): V {
if (this.has(key)) {
return this.get(key);
}
return this.set(key, val).get(key);
}
getOrInsertWith(key: K, getVal: () => V): V {
/**
* Get the value behind `key`. If it was not pressent, first insert the value returned by `getVal`
* @param key The key to insert into the map with
* @param getVal A function that returns a new instance of `V`.
* @returns The value in the map
*/
getOrInsert(key: K, getVal: () => V): V {
if (this.has(key)) {
return this.get(key);
}
@ -42,12 +40,29 @@ export class ExtendedMap<K, V> extends Map<K, V> {
return this.set(key, getVal()).get(key);
}
getOrDefault(key: K): V {
/**
* Set the value associated with `key` iff there was not a previous value
* @throws if `key` already in map
* @returns `this` so that `strictSet` can be chained
*/
strictSet(key: K, val: V): this {
if (this.has(key)) {
return this.get(key);
throw new TypeError("Duplicate key in map");
}
return this.set(key, this.getDefault()).get(key);
return this.set(key, val);
}
/**
* Get the value associated with `key`
* @throws if `key` did not a value associated with it
*/
strictGet(key: K): V {
if (!this.has(key)) {
throw new TypeError("key not in map");
}
return this.get(key);
}
}

View File

@ -19,6 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export type Falsey = false | 0 | "" | null | undefined;
/**
* Create a new type safe empty Iterable
* @returns An `Iterable` that yields 0 items
@ -57,6 +59,36 @@ export function* map<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
}
}
/**
* The single layer flattening of an iterator, discarding `Falsey` values.
* @param src A type that can be iterated over
* @param fn The function that returns either an iterable over items that should be filtered out or a `Falsey` value indicating that it should be ignored
*/
export function* filterFlatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U | Falsey> | Falsey): Iterable<U> {
for (const from of src) {
if (!from) {
continue;
}
const mapping = fn(from);
if (!mapping) {
continue;
}
for (const mapped of mapping) {
if (mapped) {
yield mapped;
}
}
}
}
/**
* Returns a new iterator that yields the items that each call to `fn` would produce
* @param src A type that can be iterated over
* @param fn A function that returns an iterator
*/
export function* flatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U>): Iterable<U> {
for (const from of src) {
yield* fn(from);
@ -83,7 +115,7 @@ export function* filter<T>(src: Iterable<T>, fn: (from: T) => any): Iterable<T>
* @param src A type that can be iterated over
* @param fn The function that is called for each value
*/
export function* filterMap<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
export function* filterMap<T, U>(src: Iterable<T>, fn: (from: T) => U | Falsey): Iterable<U> {
for (const from of src) {
const res = fn(from);
@ -99,7 +131,7 @@ export function* filterMap<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable
* @param src A type that can be iterated over
* @param fn The function that is called for each value
*/
export function* filterMapStrict<T, U>(src: Iterable<T>, fn: (from: T) => U): Iterable<U> {
export function* filterMapStrict<T, U>(src: Iterable<T>, fn: (from: T) => U | null | undefined): Iterable<U> {
for (const from of src) {
const res = fn(from);

View File

@ -23,7 +23,7 @@
import path from "path";
import { app, remote } from "electron";
import { ensureDirSync, writeFileSync } from "fs-extra";
import { WriteFileOptions } from "fs";
import type { WriteFileOptions } from "fs";
export function saveToAppFiles(filePath: string, contents: any, options?: WriteFileOptions): string {
const absPath = path.resolve((app || remote.app).getPath("userData"), filePath);

View File

@ -77,6 +77,8 @@ jest.mock(
],
];
}
return [];
}),
on: jest.fn(
(channel: string, listener: (event: any, ...args: any[]) => void) => {

View File

@ -1,156 +0,0 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import fs from "fs";
import path from "path";
import hb from "handlebars";
import { observable } from "mobx";
import { ResourceApplier } from "../main/resource-applier";
import { KubernetesCluster } from "./core-api/catalog";
import logger from "../main/logger";
import { app } from "electron";
import { requestMain } from "../common/ipc";
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
import { ClusterStore } from "../common/cluster-store";
export interface ClusterFeatureStatus {
/** feature's current version, as set by the implementation */
currentVersion: string;
/** feature's latest version, as set by the implementation */
latestVersion: string;
/** whether the feature is installed or not, as set by the implementation */
installed: boolean;
/** whether the feature can be upgraded or not, as set by the implementation */
canUpgrade: boolean;
}
export abstract class ClusterFeature {
/**
* this field sets the template parameters that are to be applied to any templated kubernetes resources that are to be installed for the feature.
* See the renderTemplates() method for more details
*/
templateContext: any;
/**
* this field holds the current feature status, is accessed directly by Lens
*/
@observable status: ClusterFeatureStatus = {
currentVersion: null,
installed: false,
latestVersion: null,
canUpgrade: false
};
/**
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be installed. The implementation
* of this method should install kubernetes resources using the applyResources() method, or by directly accessing the kubernetes api (K8sApi)
*
* @param cluster the cluster that the feature is to be installed on
*/
abstract install(cluster: KubernetesCluster): Promise<void>;
/**
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be upgraded. The implementation
* of this method should upgrade the kubernetes resources already installed, if relevant to the feature
*
* @param cluster the cluster that the feature is to be upgraded on
*/
abstract upgrade(cluster: KubernetesCluster): Promise<void>;
/**
* to be implemented in the derived class, this method is typically called by Lens when a user has indicated that this feature is to be uninstalled. The implementation
* of this method should uninstall kubernetes resources using the kubernetes api (K8sApi)
*
* @param cluster the cluster that the feature is to be uninstalled from
*/
abstract uninstall(cluster: KubernetesCluster): Promise<void>;
/**
* to be implemented in the derived class, this method is called periodically by Lens to determine details about the feature's current status. The implementation
* of this method should provide the current status information. The currentVersion and latestVersion fields may be displayed by Lens in describing the feature.
* The installed field should be set to true if the feature has been installed, otherwise false. Also, Lens relies on the canUpgrade field to determine if the feature
* can be upgraded so the implementation should set the canUpgrade field according to specific rules for the feature, if relevant.
*
* @param cluster the cluster that the feature may be installed on
*
* @return a promise, resolved with the updated ClusterFeatureStatus
*/
abstract updateStatus(cluster: KubernetesCluster): Promise<ClusterFeatureStatus>;
/**
* this is a helper method that conveniently applies kubernetes resources to the cluster.
*
* @param cluster the cluster that the resources are to be applied to
* @param resourceSpec as a string type this is a folder path that is searched for files specifying kubernetes resources. The files are read and if any of the resource
* files are templated, the template parameters are filled using the templateContext field (See renderTemplate() method). Finally the resources are applied to the
* cluster. As a string[] type resourceSpec is treated as an array of fully formed (not templated) kubernetes resources that are applied to the cluster
*/
protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
let resources: string[];
const clusterModel = ClusterStore.getInstance().getById(cluster.metadata.uid);
if (!clusterModel) {
throw new Error(`cluster not found`);
}
if ( typeof resourceSpec === "string" ) {
resources = this.renderTemplates(resourceSpec);
} else {
resources = resourceSpec;
}
if (app) {
await new ResourceApplier(clusterModel).kubectlApplyAll(resources);
} else {
await requestMain(clusterKubectlApplyAllHandler, cluster.metadata.uid, resources);
}
}
/**
* this is a helper method that conveniently reads kubernetes resource files into a string array. It also fills templated resource files with the template parameter values
* specified by the templateContext field. Templated files must end with the extension '.hb' and the template syntax must be compatible with handlebars.js
*
* @param folderPath this is a folder path that is searched for files defining kubernetes resources.
*
* @return an array of strings, each string being the contents of a resource file found in the folder path. This can be passed directly to applyResources()
*/
protected renderTemplates(folderPath: string): string[] {
const resources: string[] = [];
logger.info(`[FEATURE]: render templates from ${folderPath}`);
fs.readdirSync(folderPath).forEach(filename => {
const file = path.join(folderPath, filename);
const raw = fs.readFileSync(file);
if (filename.endsWith(".hb")) {
const template = hb.compile(raw.toString());
resources.push(template(this.templateContext));
} else {
resources.push(raw.toString());
}
});
return resources;
}
}

View File

@ -28,16 +28,16 @@ import * as App from "./app";
import * as EventBus from "./event-bus";
import * as Store from "./stores";
import * as Util from "./utils";
import * as ClusterFeature from "./cluster-feature";
import * as Interface from "../interfaces";
import * as Catalog from "./catalog";
import * as Types from "./types";
export {
App,
EventBus,
Catalog,
ClusterFeature,
Interface,
Store,
Types,
Util,
};

View File

@ -20,3 +20,5 @@
*/
export { ExtensionStore } from "../extension-store";
export { MainIpcStore } from "../main-ipc-store";
export { RendererIpcStore } from "../renderer-ipc-store";

View File

@ -19,15 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
.MetricsSelect {
$spacing: $padding;
--flex-gap: #{$spacing};
.Badge {
margin-top: $spacing;
}
.Button {
margin-top: $spacing;
}
}
export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
export type IpcRendererEvent = Electron.IpcRendererEvent;
export type IpcMainEvent = Electron.IpcMainEvent;

View File

@ -342,10 +342,13 @@ export class ExtensionDiscovery extends Singleton {
const manifest = await fse.readJson(manifestPath);
const installedManifestPath = this.getInstalledManifestPath(manifest.name);
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
const extensionDir = path.dirname(manifestPath);
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
const absolutePath = (await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
return {
id: installedManifestPath,
absolutePath: path.dirname(manifestPath),
absolutePath,
manifestPath: installedManifestPath,
manifest,
isBundled,

View File

@ -325,6 +325,8 @@ export class ExtensionLoader extends Singleton {
} catch (error) {
logger.error(`${logModule}: can't load extension main at ${extAbsolutePath}: ${error}`, { extension, error });
}
return null;
}
getExtension(extId: LensExtensionId): InstalledExtension {

View File

@ -21,7 +21,7 @@
import { BaseStore } from "../common/base-store";
import * as path from "path";
import { LensExtension } from "./lens-extension";
import type { LensExtension } from "./lens-extension";
export abstract class ExtensionStore<T> extends BaseStore<T> {
protected extension: LensExtension;

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Singleton } from "../common/utils";
import type { LensExtension } from "./lens-extension";
import { createHash } from "crypto";
import { broadcastMessage } from "../common/ipc";
export const IpcPrefix = Symbol();
export abstract class IpcStore extends Singleton {
readonly [IpcPrefix]: string;
constructor(protected extension: LensExtension) {
super();
this[IpcPrefix] = createHash("sha256").update(extension.id).digest("hex");
}
broadcastIpc(channel: string, ...args: any[]): void {
broadcastMessage(`extensions@${this[IpcPrefix]}:${channel}`, ...args);
}
}

View File

@ -23,7 +23,8 @@ import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry";
import type { ProtocolHandlerRegistration } from "./registries";
import { disposer } from "../common/utils";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@ -37,6 +38,8 @@ export interface LensExtensionManifest {
lens?: object; // fixme: add more required fields for validation
}
export const Disposers = Symbol();
export class LensExtension {
readonly id: LensExtensionId;
readonly manifest: LensExtensionManifest;
@ -46,6 +49,7 @@ export class LensExtension {
protocolHandlers: ProtocolHandlerRegistration[] = [];
@observable private isEnabled = false;
[Disposers] = disposer();
constructor({ id, manifest, manifestPath, isBundled }: InstalledExtension) {
this.id = id;
@ -62,6 +66,10 @@ export class LensExtension {
return this.manifest.version;
}
get description() {
return this.manifest.description;
}
/**
* getExtensionFileFolder returns the path to an already created folder. This
* folder is for the sole use of this extension.
@ -73,15 +81,11 @@ export class LensExtension {
return FilesystemProvisionerStore.getInstance().requestDirectory(this.id);
}
get description() {
return this.manifest.description;
}
@action
async enable() {
if (this.isEnabled) return;
this.isEnabled = true;
this.onActivate();
this.onActivate?.();
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
}
@ -89,7 +93,8 @@ export class LensExtension {
async disable() {
if (!this.isEnabled) return;
this.isEnabled = false;
this.onDeactivate();
this.onDeactivate?.();
this[Disposers]();
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
}
@ -125,12 +130,12 @@ export class LensExtension {
};
}
protected onActivate() {
// mock
protected onActivate(): void {
return;
}
protected onDeactivate() {
// mock
protected onDeactivate(): void {
return;
}
}

View File

@ -19,12 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { MenuRegistration } from "./registries/menu-registry";
import { LensExtension } from "./lens-extension";
import { WindowManager } from "../main/window-manager";
import { getExtensionPageUrl } from "./registries/page-registry";
import { CatalogEntity, catalogEntityRegistry } from "../common/catalog";
import { IObservableArray } from "mobx";
import type { IObservableArray } from "mobx";
import type { MenuRegistration } from "./registries";
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = [];

View File

@ -26,8 +26,8 @@ import type {
import type { Cluster } from "../main/cluster";
import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry";
import { CommandRegistration } from "./registries/command-registry";
import { EntitySettingRegistration } from "./registries/entity-setting-registry";
import type { CommandRegistration } from "./registries/command-registry";
import type { EntitySettingRegistration } from "./registries/entity-setting-registry";
export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = [];

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ipcMain } from "electron";
import { IpcPrefix, IpcStore } from "./ipc-store";
import { Disposers } from "./lens-extension";
import type { LensMainExtension } from "./lens-main-extension";
export abstract class MainIpcStore extends IpcStore {
constructor(extension: LensMainExtension) {
super(extension);
extension[Disposers].push(() => MainIpcStore.resetInstance());
}
handleIpc(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
ipcMain.handle(prefixedChannel, handler);
this.extension[Disposers].push(() => ipcMain.removeHandler(prefixedChannel));
}
listenIpc(channel: string, listener: (event: Electron.IpcMainEvent, ...args: any[]) => any): void {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
ipcMain.addListener(prefixedChannel, listener);
this.extension[Disposers].push(() => ipcMain.removeListener(prefixedChannel, listener));
}
}

View File

@ -20,7 +20,7 @@
*/
import type React from "react";
import { CatalogEntity } from "../../common/catalog";
import type { CatalogEntity } from "../../common/catalog";
import { BaseRegistry } from "./base-registry";
export interface EntitySettingViewProps {
@ -32,13 +32,14 @@ export interface EntitySettingComponents {
}
export interface EntitySettingRegistration {
title: string;
kind: string;
apiVersions: string[];
source?: string;
kind: string;
title: string;
components: EntitySettingComponents;
source?: string;
id?: string;
priority?: number;
group?: string;
}
export interface RegisteredEntitySetting extends EntitySettingRegistration {

View File

@ -32,3 +32,4 @@ export * from "./kube-object-status-registry";
export * from "./command-registry";
export * from "./entity-setting-registry";
export * from "./welcome-menu-registry";
export * from "./protocol-handler-registry";

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import type React from "react";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectDetailComponents {

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import React from "react";
import type React from "react";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectMenuComponents {

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { KubeObject, KubeObjectStatus } from "../renderer-api/k8s-api";
import type { KubeObject, KubeObjectStatus } from "../renderer-api/k8s-api";
import { BaseRegistry } from "./base-registry";
export interface KubeObjectStatusRegistration {

View File

@ -25,7 +25,7 @@ import React from "react";
import { observer } from "mobx-react";
import { BaseRegistry } from "./base-registry";
import { LensExtension, sanitizeExtensionName } from "../lens-extension";
import { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
import type { PageParam, PageParamInit } from "../../renderer/navigation/page-param";
import { createPageParam } from "../../renderer/navigation/helpers";
export interface PageRegistration {
@ -119,9 +119,9 @@ export class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage>
return components;
}
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> {
protected normalizeParams(params?: PageParams<string | ExtensionPageParamInit>): PageParams<PageParam> | undefined {
if (!params) {
return;
return undefined;
}
Object.entries(params).forEach(([name, value]) => {
const paramInit: PageParamInit = typeof value === "object"

View File

@ -21,7 +21,7 @@
// Extensions API -> Status bar customizations
import React from "react";
import type React from "react";
import { BaseRegistry } from "./base-registry";
interface StatusBarComponents {

View File

@ -30,6 +30,7 @@ export * from "../../renderer/components/checkbox";
export * from "../../renderer/components/radio";
export * from "../../renderer/components/select";
export * from "../../renderer/components/slider";
export * from "../../renderer/components/switch";
export * from "../../renderer/components/input/input";
// command-overlay

View File

@ -20,11 +20,12 @@
*/
export { isAllowedResource } from "../../common/rbac";
export { ResourceStack } from "../../common/k8s/resource-stack";
export { apiManager } from "../../renderer/api/api-manager";
export { KubeObjectStore } from "../../renderer/kube-object.store";
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
export { KubeApi, forCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object";
export { Pod, podsApi, PodsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
export { Pod, podsApi, PodsApi } from "../../renderer/api/endpoints";
export { Node, nodesApi, NodesApi } from "../../renderer/api/endpoints";
export { Deployment, deploymentApi, DeploymentApi } from "../../renderer/api/endpoints";
export { DaemonSet, daemonSetApi } from "../../renderer/api/endpoints";
@ -32,7 +33,7 @@ export { StatefulSet, statefulSetApi } from "../../renderer/api/endpoints";
export { Job, jobApi } from "../../renderer/api/endpoints";
export { CronJob, cronJobApi } from "../../renderer/api/endpoints";
export { ConfigMap, configMapApi } from "../../renderer/api/endpoints";
export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints";
export { Secret, secretsApi } from "../../renderer/api/endpoints";
export { ReplicaSet, replicaSetApi } from "../../renderer/api/endpoints";
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
export { LimitRange, limitRangeApi } from "../../renderer/api/endpoints";
@ -53,7 +54,13 @@ export { RoleBinding, roleBindingApi } from "../../renderer/api/endpoints";
export { ClusterRole, clusterRoleApi } from "../../renderer/api/endpoints";
export { ClusterRoleBinding, clusterRoleBindingApi } from "../../renderer/api/endpoints";
export { CustomResourceDefinition, crdApi } from "../../renderer/api/endpoints";
export { KubeObjectStatus, KubeObjectStatusLevel } from "./kube-object-status";
export { KubeObjectStatusLevel } from "./kube-object-status";
// types
export type { IKubeApiCluster } from "../../renderer/api/kube-api";
export type { IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
export type { ISecretRef } from "../../renderer/api/endpoints";
export type { KubeObjectStatus } from "./kube-object-status";
// stores
export type { EventStore } from "../../renderer/components/+events/event.store";

View File

@ -25,7 +25,7 @@ import { navigation } from "../../renderer/navigation";
export type { PageParamInit, PageParam } from "../../renderer/navigation/page-param";
export { navigate, isActiveRoute } from "../../renderer/navigation/helpers";
export { hideDetails, showDetails, getDetailsUrl } from "../../renderer/components/kube-object/kube-object-details";
export { IURLParams } from "../../common/utils/buildUrl";
export type { IURLParams } from "../../common/utils/buildUrl";
// exporting to extensions-api version of helper without `isSystem` flag
export function createPageParam<V = string>(init: PageParamInit<V>) {

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ipcRenderer } from "electron";
import { IpcPrefix, IpcStore } from "./ipc-store";
import { Disposers } from "./lens-extension";
import type { LensRendererExtension } from "./lens-renderer-extension";
export abstract class RendererIpcStore extends IpcStore {
constructor(extension: LensRendererExtension) {
super(extension);
extension[Disposers].push(() => RendererIpcStore.resetInstance());
}
listenIpc(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): void {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
ipcRenderer.addListener(prefixedChannel, listener);
this.extension[Disposers].push(() => ipcRenderer.removeListener(prefixedChannel, listener));
}
invokeIpc(channel: string, ...args: any[]): Promise<any> {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
return ipcRenderer.invoke(prefixedChannel, ...args);
}
}

View File

@ -1,3 +1,24 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import fetchMock from "jest-fetch-mock";
// rewire global.fetch to call 'fetchMock'
fetchMock.enableMocks();

View File

@ -44,6 +44,12 @@ jest.mock("winston", () => ({
}
}));
jest.mock("electron", () => ({
app: {
getPath: () => "/foo",
},
}));
jest.mock("../../common/ipc");
jest.mock("child_process");
jest.mock("tcp-port-used");
@ -56,7 +62,7 @@ import { ChildProcess, spawn } from "child_process";
import { bundledKubectlPath, Kubectl } from "../kubectl";
import { mock, MockProxy } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used";
import { Readable } from "stream";
import type { Readable } from "stream";
import { UserStore } from "../../common/user-store";
import { Console } from "console";
import { stdout, stderr } from "process";

View File

@ -28,6 +28,12 @@ const logger = {
crit: jest.fn(),
};
jest.mock("electron", () => ({
app: {
getPath: () => `/tmp`,
},
}));
jest.mock("winston", () => ({
format: {
colorize: jest.fn(),
@ -47,7 +53,7 @@ jest.mock("winston", () => ({
import { KubeconfigManager } from "../kubeconfig-manager";
import mockFs from "mock-fs";
import { Cluster } from "../cluster";
import { ContextHandler } from "../context-handler";
import type { ContextHandler } from "../context-handler";
import fse from "fs-extra";
import { loadYaml } from "@kubernetes/client-node";
import { Console } from "console";
@ -91,7 +97,9 @@ describe("kubeconfig manager tests", () => {
contextName: "minikube",
kubeConfigPath: "minikube-config.yml",
});
contextHandler = jest.fn() as any;
contextHandler = {
ensureServer: () => Promise.resolve(),
} as any;
jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo");
});
@ -103,7 +111,7 @@ describe("kubeconfig manager tests", () => {
const kubeConfManager = new KubeconfigManager(cluster, contextHandler);
expect(logger.error).not.toBeCalled();
expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`);
expect(await kubeConfManager.getPath()).toBe(`${path.sep}tmp${path.sep}kubeconfig-foo`);
// this causes an intermittent "ENXIO: no such device or address, read" error
// const file = await fse.readFile(await kubeConfManager.getPath());
const file = fse.readFileSync(await kubeConfManager.getPath());

View File

@ -21,9 +21,9 @@
import { reaction, toJS } from "mobx";
import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "../common/ipc";
import { CatalogEntityRegistry} from "../common/catalog";
import type { CatalogEntityRegistry} from "../common/catalog";
import "../common/catalog-entities/kubernetes-cluster";
import { Disposer } from "../common/utils";
import type { Disposer } from "../common/utils";
export class CatalogPusher {
static init(catalog: CatalogEntityRegistry) {

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