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

Merge remote-tracking branch 'origin/master' into fix/cluster-view-commands

# Conflicts:
#	src/renderer/components/+nodes/node-details.tsx
This commit is contained in:
Roman 2021-05-28 13:37:12 +03:00
commit 5ec8bfa28f
84 changed files with 1190 additions and 1305 deletions

View File

@ -1 +1 @@
docs.k8slens.dev api-docs.k8slens.dev

View File

@ -1,21 +1,46 @@
# Overview # Lens Extension API
Lens is the most powerful Kubernetes IDE on the market. It is a standalone application, and it is available on macOS, Windows, and Linux. Some of the benefits of using Lens include: Customize and enhance the Lens experience with the Lens Extension API.
Use the extension API to create menus or page content.
The same extension API was used to create many of Lens's core features.
To install your first extension you should goto the [extension page](lens://app/extensions) in lens.
* Confidence that your clusters are properly setup and configured. This documentation describes:
* Increased visibility, real time statistics, log streams, and hands-on troubleshooting capabilities.
* The ability to work with your clusters quickly and easily, radically improving productivity and the speed of business.
Watch this introductory video to see Lens in action: * How to build, run, test, and publish an extension.
* How to take full advantage of the Lens Extension API.
* Where to find [guides](extensions/guides/README.md) and [code samples](https://github.com/lensapp/lens-extension-samples) to help get you started.
[![Screenshot](img/lens-intro-video-screenshot.png)](https://www.youtube.com/watch?v=eeDwdVXattc) ## What Extensions Can Do
**Note:** Use CTRL+click (on Windows and Linux) or CMD+click (on MacOS) to open the above in a new tab Here are some examples of what you can achieve with the Extension API:
* Add custom components & views in the UI - Extending the Lens Workbench
For an overview of the Lens Extension API, refer to the [Common Capabilities](extensions/capabilities/common-capabilities.md) page. [Extension Guides Overview](extensions/guides/README.md) also includes a list of code samples and guides that illustrate various ways of using the Lens Extension API.
## How to Build Extensions
Here is what each section of the Lens Extension API docs can help you with:
* **Getting Started** teaches fundamental concepts for building extensions with the Hello World sample.
* **Extension Capabilities** dissects Lens's Extension API into smaller categories and points you to more detailed topics.
* **Extension Guides** includes guides and code samples that explain specific usages of Lens Extension API.
* **Testing and Publishing** includes in-depth guides on various extension development topics, such as testing and publishing extensions.
* **API Reference** contains exhaustive references for the Lens Extension API, Contribution Points, and many other topics.
## What's New
Just like Lens itself, the extension API updates on a monthly cadence, rolling out new features with every release.
Keep up with Lens and the Lens Extension API by reviewing the [release notes](https://github.com/lensapp/lens/releases).
## Looking for Help
If you have questions for extension development, try asking on the [Lens Dev Slack](http://k8slens.slack.com/). It's a public chatroom for Lens developers, where Lens team members chime in from time to time.
To provide feedback on the documentation or issues with the Lens Extension API, create new issues at [lensapp/lens](https://github.com/lensapp/lens/issues). Please use the labels `area/documentation` and/or `area/extension`.
## Downloading Lens ## Downloading Lens
[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux. [Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux.
## Quick Start
Get up and running quickly by learning to [add clusters](clusters/adding-clusters.md).

View File

@ -1,22 +0,0 @@
# Adding Clusters
Add clusters by clicking the **Add Cluster** button in the left-side menu.
1. Click the **Add Cluster** button (indicated with a '+' icon). Or [click here](lens://app/cluster).
2. Enter the path to your kubeconfig file. You'll need to have a kubeconfig file for the cluster you want to add. You can either browse for the path from the file system or or enter it directly.
Selected [cluster contexts](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#context) are added as a separate item in the left-side cluster menu to allow you to operate easily on multiple clusters and/or contexts.
**NOTE**: Any cluster that you added manually will not be merged into your kubeconfig file.
![Add Cluster](images/add-cluster.png)
For more information on kubeconfig see [Kubernetes docs](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/).
To see your currently-enabled config with `kubectl`, enter `kubectl config view --minify --raw` in your terminal.
When connecting to a cluster, make sure you have a valid and working kubeconfig for the cluster. Following lists known "gotchas" in some authentication types used in kubeconfig with Lens app.
## Exec auth plugins
When using [exec auth](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration) plugins make sure the paths that are used to call any binaries are full paths as Lens app might not be able to call binaries with relative paths. Make also sure that you pass all needed information either as arguments or env variables in the config, Lens app might not have all login shell env variables set automatically.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

@ -1,12 +0,0 @@
# Removing Clusters
Remove Lens clusters using the context menu that appears when you right-click the cluster in the left-side menu that you want to remove.
To remove a cluster from your cluster list:
1. Right-click the name of the cluster in the left-side menu that you want to remove.
2. Click **Remove**.
**NOTE**: This will only remove the cluster from your Lens cluster list. It will not affect your actual Kubernetes cluster or its configuration.
![Remove Cluster](images/remove-cluster.png)

View File

@ -1,75 +0,0 @@
# Settings
It is easy to configure Lens Clusters to your liking through its various settings.
1. Right-click the name of the cluster in the left-side menu that you want to open the settings for.
2. Click **Settings**.
![Cluster settings](images/cluster-context-menu.png)
## Status
Overview of the cluster status
### Cluster Status
Cluster status information including the detected distribution, kernel version, API endpoint, and online status
![Cluster settings status](images/cluster-settings-status.png)
## General
General cluster settings
### Cluster Name
The cluster name is inheritated by default from the kubeconfig file. Change the cluster name to another value by updating it here. Note that doing so does not update your kubeconfig file.
### Workspace
This is the Lens workspace that the cluster is associated with. Change workspaces by selecting a different workspace from the dropdown menu. Create a new workspace by clicking **workspace** in "Define cluster workspace" above the dropdown menu. This option will take you the workspaces editor. Create a new workspace and then navigate back to cluster settings.
### Cluster Icon
Lens randomly generates an icon to associate with each newly-created cluster. Use this setting to choose your own icon.
### HTTP Proxy
Some users will need to define an HTTP proxy for communicating with the Kubernetes API. Use this setting to do so.
### Prometheus
Lens can be configured to query a Prometheus server installed in the cluster. Select a query format by choosing either to auto-detect or from the following configurations:
* Lens
* Helm Operator
* Prometheus Operator
* Stacklight
To learn more about custom Prometheus configurations, please refer to this [guide](https://github.com/lensapp/lens/blob/master/troubleshooting/custom-prometheus.md).
### Working Directory
Use this field to set the terminal working directory. The default is `$HOME`.
![Cluster settings general](images/cluster-settings-general.png)
## Features
Additional Lens features that can be installed by the user
### Metrics
Enable timeseries data visualization (Prometheus stack) for your cluster. Install this only if you don't have existing Prometheus stack installed.
### User Mode
User Mode feature enables non-admin users to see namespaces they have access to. This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
![Cluster settings features](images/cluster-settings-features.png)
## Removal
Use this setting to remove the current cluster.
![Cluster settings removal](images/cluster-settings-removal.png)

View File

@ -1,17 +0,0 @@
# Contributing
There are multiple ways you can contribute to Lens. Even if you are not a developer, you can still contribute. We are always looking for assistance with creating or updating documentation, testing the application, reporting, and troubleshooting issues.
Here are some ways you can contribute!
* [Development](./development.md) Help make Lens better.
* [Maintaining the Project](./maintainers.md) Become a community maintainer and help us maintain the project.
* [Extension Development](../extensions) Add integrations via Lens Extensions.
* [Documentation](./documentation.md) Help improve Lens documentation.
* [Promotion](./promotion.md) Show your support, be an ambassador to Lens, write blogs, and make videos!
If you are an influencer, blogger, or journalist, feel free to [spread the word](./promotion.md)!
## Code of Conduct
This project adheres to the [Contributor Covenant](https://www.contributor-covenant.org/) code of conduct. By participating and contributing to Lens, you are expected to uphold this code. Please report unacceptable behaviour to info@k8slens.dev.

View File

@ -1,43 +0,0 @@
# Development
Thank you for taking the time to make a contribution to Lens. The following document is a set of guidelines and instructions for contributing to Lens.
When contributing to this repository, please consider first discussing the change you wish to make by opening an issue.
## Recommended Reading:
- [TypeScript](https://www.typescriptlang.org/docs/home.html) (front-end/back-end)
- [ReactJS](https://reactjs.org/docs/getting-started.html) (front-end, ui)
- [MobX](https://mobx.js.org/) (app-state-management, back-end/front-end)
- [ElectronJS](https://www.electronjs.org/docs) (chrome/node)
- [NodeJS](https://nodejs.org/dist/latest-v12.x/docs/api/) (api docs)
## Local Development Environment
> Prerequisites: Nodejs v12, make, yarn
* `make dev` - builds and starts the app
* `make clean` - cleanup local environment build artifacts
### Developing on Windows
On Windows we only support [Git Bash](https://gitforwindows.org/) (or similar shell) for running commands.
## Github Workflow
We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), so all code changes are tracked via Pull Requests.
A detailed guide on the recommended workflow can be found below:
* [Github Workflow](./github_workflow.md)
## Code Testing
All submitted PRs go through a set of tests and reviews. You can run most of these tests *before* a PR is submitted.
In fact, we recommend it, because it will save on many possible review iterations and automated tests.
The testing guidelines can be found here:
* [Contributor's Guide to Testing](./testing.md)
## License
By contributing, you agree that your contributions will be licensed as described in [LICENSE](https://github.com/lensapp/lens/blob/master/LICENSE).

View File

@ -1,33 +0,0 @@
# Documentation
We are glad to see you're interested in contributing to the Lens documentation. If this is the first Open Source project you've contributed to, we strongly suggest reading GitHub's excellent guide: [How to Contribute to Open Source](https://opensource.guide/how-to-contribute).
## Finding Documentation Issues to Work On
You can find a list of open documentation-related issues [here](https://github.com/lensapp/lens/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Fdocumentation). When you find something you would like to work on:
1. Express your interest to start working on an issue via comments.
2. One of the maintainers will assign the issue for you.
3. You can start working on the issue. When you're done, simply submit a pull request.
## Requirements for Documentation Pull Requests
When you create a new pull request, we expect some requirements to be met.
* Follow this naming convention for Pull Requests:
* When adding new documentation, add `New Documentation:` before the title. E.g. `New Documentation: Getting Started`
* When fixing documentation, add `Fix Documentation:` before the title. E.g. `Fix Documentation: Getting Started`
* When updating documentation, add `Update Documentation:` before the title. E.g. `Update Documentation: Getting Started`
* If your Pull Request closes an issue, you must write `Closes #ISSUE_NUMBER` where the ISSUE_NUMBER is the number in the end of the link url or the relevent issue. This will link your pull request to the issue, and when it is merged, the issue will close.
* For each pull request made, we run tests to check if there are any broken links, the markdown formatting is valid, and the linter is passing.
## Testing Documentation Site Locally
Run a local instance of `mkdocs` in a docker container for developing the Lens Documentation.
> Prerequisites: docker, yarn
* `make docs` - local build and serve of mkdocs with auto update enabled
Go to [localhost:8000](http://127.0.0.1:8000).

View File

@ -1,148 +0,0 @@
# Github Workflow
<!-- TOC -->
- [Fork The Project](#fork-the-project)
- [Adding the Forked Remote](#adding-the-forked-remote)
- [Create & Rebase Your Feature Branch](#create--rebase-your-feature-branch)
- [Commit & Push](#commit--push)
- [Open a Pull Request](#open-a-pull-request)
- [Get a code review](#get-a-code-review)
- [Squash commits](#squash-commits)
- [Push Your Final Changes](#push-your-final-changes)
<!-- /TOC -->
This guide assumes you have already cloned the upstream repo to your system via git clone.
## Fork The Project
1. Go to [http://github.com/lensapp/lens](http://github.com/lensapp/lens)
2. On the top, right-hand side, click on "fork" and select your username for the fork destination.
## Adding the Forked Remote
```
export GITHUB_USER={ your github's username }
cd $WORKDIR/lens
git remote add $GITHUB_USER git@github.com:${GITHUB_USER}/lens.git
# Prevent push to Upstream
git remote set-url --push origin no_push
# Set your fork remote as a default push target
git push --set-upstream $GITHUB_USER master
```
Your remotes should look something like this:
```
➜ git remote -v
origin https://github.com/lensapp/lens (fetch)
origin no_push (push)
my_fork git@github.com:{ github_username }/lens.git (fetch)
my_fork git@github.com:{ github_username }/lens.git (push)
```
## Create & Rebase Your Feature Branch
Create a feature branch:
```
git branch -b my_feature_branch
```
Rebase your branch:
```
git fetch origin
git rebase origin/master
Current branch my_feature_branch is up to date.
```
Please don't use `git pull` instead of the above `fetch / rebase`. `git pull` does a merge, which leaves merge commits. These make the commit history messy and violate the principle that commits ought to be individually understandable and useful.
## Commit & Push
Commit and sign your changes:
```
git commit -m "my commit title" --signoff
```
You can go back and edit/build/test some more, then `commit --amend` in a few cycles.
When ready, push your changes to your fork's repository:
```
git push --set-upstream my_fork my_feature_branch
```
## Open a Pull Request
See [Github Docs](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork).
### Get a code review
Once your pull request has been opened it will be assigned to one or more reviewers, and will go through a series of smoke tests.
Commit changes made in response to review comments should be added to the same branch on your fork.
Very small PRs are easy to review. Very large PRs are very difficult to review.
### Squashing Commits
Commits on your branch should represent meaningful milestones or units of work.
Small commits that contain typo fixes, rebases, review feedbacks, etc should be squashed.
To do that, it's best to perform an [interactive rebase](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History):
#### Example
If you PR has 3 commits, count backwards from your last commit using `HEAD~3`:
```
git rebase -i HEAD~3
```
Output would be similar to this:
```
pick f7f3f6d Changed some code
pick 310154e fixed some typos
pick a5f4a0d made some review changes
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
```
Use a command line text editor to change the word `pick` to `fixup` for the commits you want to squash, then save your changes and continue the rebase:
Per the output above, you can see that:
```
fixup <commit> = like "squash", but discard this commit's log message
```
Which means that when rebased, the commit message "fixed some typos" will be removed, and squashed with the parent commit.
### Push Your Final Changes
Once done, you can push the final commits to your branch:
```
git push --force
```
You can run multiple iteration of `rebase`/`push -f`, if needed.

View File

@ -1,17 +0,0 @@
# Maintainers
We are looking for community maintainers for the Lens project. Maintainers will be added to a special team with write permissions. These permissions consist of opening, closing, tagging, and editing issues and pull requests, as well as creating and deleting non-protected branches.
The responsibilities of a community maintainer are listed below.
## Issues Triage
* **Labeling Issues:** Label issues accordingly.
* **Finding Duplicates:** Finding and closing duplicate issues.
* **Doing First Level Contact:** Getting more information on the issues (like version number or asking for clarification) if needed.
* **Closing Irrelevant Issues:** Closing issues that are determined irrelevant, no longer needed, not relevant to the project and/or don't follow the issues guidelines.
## Help with Contributions
* **Help Manage Pull Requests:** Help the author of the pull request with any problems.
* **Contributing:** Create pull requests to help maintain and drive the project forward.

View File

@ -1,27 +0,0 @@
# Promotion
Help promote Lens! If you are not a developer (or even if you are), you can still contribute to the project a lot by helping us to promote it. As we are a free and open source project, the community is our most important asset. Here are some ways that you can help the project continue to grow.
## Follow, Like, Recommend, Favorite, Vote and Star Us
There are many sites where you can vote, recommend, favorite, and star us.
* [Twitter](https://twitter.com/k8slens) - Like, comment and retweet our posts, and follow us on Twitter.
* [Medium](https://medium.com/k8slens) - Give claps to our articles and follow us on Medium.
* [GitHub](https://github.com/lensapp/lens) - Become a stargazer on GitHub.
* [StackShare](https://stackshare.io/lens) - Indicate you are using Lens and follow us on StackShare.
* [Reddit](https://www.reddit.com/search/?q=lens%20kubernetes&sort=new) - Upvote and be a Lens ambassador by participating in relevant discussions on Reddit.
* [Hacker News](https://hn.algolia.com/?dateRange=all&page=0&prefix=false&query=lens%20kubernetes&sort=byDate&type=story) - Upvote and be a Lens ambassador by participating in relevant discussions on Hacker News.
## Write Blogs or Make Videos About Us
Here are some nice blog posts and videos about our project for you to get some inspiration:
* [Onboard AWS EKS Cluster on Lens(Kubernetes IDE)](https://dev.to/himwad05/onboard-aws-eks-cluster-on-lens-kubernetes-ide-492l)
* [Using Lens to Manage All Your Kubernetes Cluster](https://medium.com/@magicmagnate/using-lens-to-manage-all-your-kubernetes-cluster-c1ef88fdb476)
* [Kontena Lens - Beautiful Kubernetes UI](https://www.youtube.com/watch?v=YGgaiGdYfdI)
* [Gerenciando Kubernetes com Lens e Octant](https://www.youtube.com/watch?v=h9ZqDelJLQQ)
* [Walkthrough of Kubernetes IDE - Lens](https://www.youtube.com/watch?v=602aHZSdEfY)
* [LENS - Interfaz Gráfica para Kubernetes en 1 PASO.](https://www.youtube.com/watch?v=DFMKcR4BqwM)
Psst... If you have created some content around Lens, let us know!

View File

@ -1,45 +0,0 @@
## Testing Your Code
Lens uses github actions to run automated tests on any PR, before merging.
However, a PR will not be reviewed before all tests are green, so to save time and prevent your PR from going stale, it is best to test it before submitting the PR.
### Run Local Verifications
Please run the following style and formatting commands and fix/check-in any changes:
#### 1. Linting
We use [ESLing](https://eslint.org/) for style verification.
In the repository's root directory, simply run:
```
make lint
```
#### 3. Pre-submit Flight Checks
In the repository root directory, make sure that:
* `make build` runs successfully.
* `make test` runs successfully.
* `make integration` runs successfully (some tests require minikube running).
Please note that this last test is prone to "flakiness", so it might fail on occasion. If it fails constantly, take a deeper look at your code to find the source of the problem.
If you find that all tests passed, you may open a pull request upstream.
### Opening A Pull Request
#### Draft Mode
You may open a pull request in [draft mode](https://github.blog/2019-02-14-introducing-draft-pull-requests).
All automated tests will still run against the PR, but the PR will not be assigned for review.
Once a PR is ready for review, transition it from Draft mode, and code owners will be notified.
#### Pre-Requisites for PR Merge
In order for a PR to be merged, the following conditions should exist:
1. The PR has passed all the automated tests (style, build & conformance tests).
2. PR commits have been signed with the `--signoff` option.
3. PR was reviewed and approved by a code owner.
4. PR is rebased against upstream's master branch.

View File

@ -1,46 +0,0 @@
# Lens Extension API
Customize and enhance the Lens experience with the Lens Extension API.
Use the extension API to create menus or page content.
The same extension API was used to create many of Lens's core features.
To install your first extension you should goto the [extension page](lens://app/extensions) in lens.
This documentation describes:
* How to build, run, test, and publish an extension.
* How to take full advantage of the Lens Extension API.
* Where to find [guides](guides/README.md) and [code samples](https://github.com/lensapp/lens-extension-samples) to help get you started.
## What Extensions Can Do
Here are some examples of what you can achieve with the Extension API:
* Add custom components & views in the UI - Extending the Lens Workbench
For an overview of the Lens Extension API, refer to the [Common Capabilities](capabilities/common-capabilities.md) page. [Extension Guides Overview](guides/README.md) also includes a list of code samples and guides that illustrate various ways of using the Lens Extension API.
## How to Build Extensions
Here is what each section of the Lens Extension API docs can help you with:
* **Getting Started** teaches fundamental concepts for building extensions with the Hello World sample.
* **Extension Capabilities** dissects Lens's Extension API into smaller categories and points you to more detailed topics.
* **Extension Guides** includes guides and code samples that explain specific usages of Lens Extension API.
* **Testing and Publishing** includes in-depth guides on various extension development topics, such as testing and publishing extensions.
* **API Reference** contains exhaustive references for the Lens Extension API, Contribution Points, and many other topics.
## What's New
Just like Lens itself, the extension API updates on a monthly cadence, rolling out new features with every release.
Keep up with Lens and the Lens Extension API by reviewing the [release notes](https://github.com/lensapp/lens/releases).
## Looking for Help
If you have questions for extension development, try asking on the [Lens Dev Slack](http://k8slens.slack.com/). It's a public chatroom for Lens developers, where Lens team members chime in from time to time.
To provide feedback on the documentation or issues with the Lens Extension API, create new issues at [lensapp/lens](https://github.com/lensapp/lens/issues). Please use the labels `area/documentation` and/or `area/extension`.
## Downloading Lens
[Download Lens](https://github.com/lensapp/lens/releases) for macOS, Windows, or Linux.

View File

@ -46,7 +46,7 @@ cmd /c mklink /D helloworld-sample lens-extension-samples\helloworld-sample
Without administrator rights, you need to copy the extensions sample directory into `C:\Users\<user>\.k8slens\extensions`: Without administrator rights, you need to copy the extensions sample directory into `C:\Users\<user>\.k8slens\extensions`:
``` ```sh
Copy-Item 'lens-extension-samples\helloworld-sample' 'C:\Users\<user>\.k8slens\extensions\helloworld-sample' Copy-Item 'lens-extension-samples\helloworld-sample' 'C:\Users\<user>\.k8slens\extensions\helloworld-sample'
``` ```
@ -78,7 +78,7 @@ npm run dev
You must restart Lens for the extension to load. You must restart Lens for the extension to load.
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds. After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
With Lens running, either connect to an existing cluster or [create a new one](../../clusters/adding-clusters.md). With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/latest/clusters/adding-clusters) for details on how to add a cluster in Lens IDE.
You will see the "Hello World" page in the left-side cluster menu. You will see the "Hello World" page in the left-side cluster menu.
## Develop the Extension ## Develop the Extension

View File

@ -70,6 +70,6 @@ To debug your extension, please see our instructions on [Testing Extensions](../
To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md). To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md).
If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues). If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues).
You can find the Lens contribution guidelines [here](../../contributing/README.md). You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing).
The Generator source code is hosted at [Github](https://github.com/lensapp/generator-lens-ext). The Generator source code is hosted at [Github](https://github.com/lensapp/generator-lens-ext).

View File

@ -30,7 +30,7 @@ With this sort of communication the caller needs to wait for the result from the
This is accomplished by `await`-ing the returned `Promise<any>`. This is accomplished by `await`-ing the returned `Promise<any>`.
This is a unidirectional form of communication. This is a unidirectional form of communication.
Only `renderer` can initiate this kind of request, and only `main` can and respond to this kind of request. Only `renderer` can initiate this kind of request, and only `main` can receive and respond to this kind of request.
## Registering IPC Handlers and Listeners ## Registering IPC Handlers and Listeners

View File

@ -914,4 +914,4 @@ Construct the table using the `Component.Table` and related elements.
For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods. For each pod the name, age, and status are obtained using the `K8sApi.Pod` methods.
The table is constructed using the `Component.Table` and related elements. The table is constructed using the `Component.Table` and related elements.
See [`Component` documentation](https://docs.k8slens.dev/master/extensions/api/modules/_renderer_api_components_/) for further details. See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details.

View File

@ -1,66 +0,0 @@
# FAQ
### What operating systems does Lens support?
Lens supports MacOS, Windows and Linux operating systems. For Linux there are Snap and AppImage versions. For MacOS there are DMG and Homebrew options.
### Lens application is not opening, what might be wrong?
When Lens is started, it will start HTTP proxy server on the background and requires that operating system allows to start listening to some free port. You can see the port allocated for Lens from application logs. Lens expects also that `localhost` DNS points to `127.0.0.1` address.
### Why can't I add any clusters?
When adding new clusters, a valid Kubeconfig file is required. Please check that all contexts present in Kubeconfig file are valid.
### Why Cluster dashboard is not opening?
To see Cluster dashboard properly, Kubernetes cluster must be reachable either directly from your computer or via HTTP proxy. You can configure HTTP proxy in Cluster Settigns. Also, provided credentials in Kubeconfig must be valid. If Kubeconfig uses `exec` command, the binary must be available in global PATH or absolute path must be used. Lens application can't see PATH modifications made by any shell init scripts. There might be also some issues on the Snap version if the exec binary is installed also from Snap and requires additional symlinking, please see [#699](https://github.com/lensapp/lens/issues/699).
### Why I don't see anything on Cluster dashboard?
Users will see on Cluster dashboard only those resources that they are allowed to see (RBAC). Lens requires that user has access at least to one namespace. Lens tries first fetch namespaces from Kubernetes API. If user is not allowed to list namespaces, allowed namespaces can be configured in Cluster settings or in Kubeconfig.
### Why I don't see any metrics or some of the metrics are not working?
In order to display cluster metrics, Lens requires that Prometheus is running in the cluster. You can install Prometheus in Cluster settings if needed.
Lens tries to detect Prometheus installation automatically. If it fails to detect the installation properly, you can configure Prometheus service address in Cluster settings. If some of the metrics are not displayed correctly, you can see queries that Lens is using [here](https://github.com/lensapp/lens/tree/master/src/main/prometheus) and adapt your prometheus configuration to support those queries. Please refer [Prometheus documentation](https://prometheus.io/docs/prometheus/latest/configuration/configuration/) or your Prometheus installer documentation how to do this.
### Kubectl is not working in Lens terminal, what should I do?
Lens tries to download correct Kubectl version for the cluster and use that in Lens terminal. Some operating systems (namely Windows) might have restrictions set that prevent downloading and executing binaries from the default location that Lens is using. You can change the directory where Lens downloads the binaries in App Preferences. It's also possible to change the Download mirror to use Azure if default Google is not reachable from your network. If downloading Kubectl is not option for you, you can define path to pre-installed Kubectl on your machine and Lens will use that binary instead.
### How can I configure Helm repositories?
Lens comes with bundled Helm 3 binary and Lens will add by default `bitnami` repository if no other repositories are configured. You can add more repositories from Artifact HUB in App preferences. At this moment it is not possible to add private repositories. Those and other public repositories can be added manually via command line.
### Where can I find application logs?
Lens will store application logs to following locations depending on your operating system:
- MacOS: ~/Library/Logs/Lens/
- Windows: %USERPROFILE%\AppData\Roaming\Lens\logs\
- Linux: ~/.config/Lens/logs/
### How can I see more verbose logs?
You can start Lens application on debug mode from the command line to see more verbose logs. To start application on debug mode, please provide `DEBUG=true` environment variable and before starting the application, for example: `DEBUG=TRUE /Applications/Lens.app/Contents/MacOS/Lens`
### Why Lens window rendering is broken?
MacOS users can encouter visual bug with fuzzy lines appeared while [connected to external 4K display](https://www.forbes.com/sites/gordonkelly/2020/06/11/apple-macos-macbook-pro-google-chrome-display-problem/?sh=331ac27967b4). Same thing can happen with any of Electron applications or Chrome itself.
![fuzzy lines](https://user-images.githubusercontent.com/4453/78537270-80cc8e80-77ef-11ea-8a6e-0bc69cc28abe.png "Fuzzy lines on MacOS")
As a temporary workaround there is a possibility to disable Chromium GPU acceleration. To do this for Lens, you need to provide `LENS_DISABLE_GPU=true` env variable and relaunch app.
First, open `.bash_profile` file from your terminal
```
open -a TextEdit.app ~/.bash_profile
```
Then, add this line
```
export LENS_DISABLE_GPU=true
```

View File

@ -1,77 +0,0 @@
# Getting Started
Lens is lightweight and simple to install. You'll be up and running in just a few minutes.
## System Requirements
Review the [System Requirements](../supporting/requirements.md) to check if your computer configuration is supported.
## macOS
1. [Download Lens](https://github.com/lensapp/lens/releases) for macOS.
2. Open the browser's download list and locate the downloaded archive.
3. Select the 'magnifying glass' icon to open the archive in Finder.
4. Double-click `Lens-{version}.dmg` and drag `Lens.app` to the `Applications` folder, making it available in the macOS Launchpad.
5. Add Lens to your Dock by right-clicking on the icon to bring up the context menu and choosing **Options**, **Keep in Dock**.
## Windows
1. Download the [Lens installer](https://github.com/lensapp/lens/releases) for Windows.
2. Once it is downloaded, run the installer `Lens-Setup-{version}.exe`. This will only take a minute.
3. By default, Lens is installed under `C:\users\{username}\AppData\Local\Programs\Lens`.
## Linux
See the [Download Lens](https://github.com/lensapp/lens/releases) page for a complete list of available installation options.
After installing Lens manually (not using a package manager file such as `.deb` or `.rpm`) the following will need to be done to allow protocol handling.
This assumes that your linux distribution uses `xdg-open` and the `xdg-*` suite of programs for determining which application can handle custom URIs.
1. Create a file called `lens.desktop` in either `~/.local/share/applications/` or `/usr/share/applications` (if you have permissions and are installing Lens for all users).
1. That file should have the following contents, with `<path/to/executable>` being the absolute path to where you have installed the unpacked `Lens` executable:
```
[Desktop Entry]
Name=Lens
Exec=<path/to/executable> %U
Terminal=false
Type=Application
Icon=lens
StartupWMClass=Lens
Comment=Lens - The Kubernetes IDE
MimeType=x-scheme-handler/lens;
Categories=Network;
```
1. Then run the following command:
```
xdg-settings set default-url-scheme-handler lens lens.desktop
```
1. If that succeeds (exits with code `0`) then your Lens install should be set up to handle `lens://` URIs.
### Snap
Lens is officially distributed as a Snap package in the [Snap Store](https://snapcraft.io/store):
[![Get it from the Snap Store](images/snap-store.png)](https://snapcraft.io/kontena-lens)
You can install it by running:
```bash
sudo snap install kontena-lens --classic
```
## Update Cadence
Lens releases a new version each month with new features and important bug fixes. Lens supports auto updating and you will be prompted to install the new release when it becomes available!
To stay current with the Lens features, you can review the [release notes](https://github.com/lensapp/lens/releases).
## Next Steps
- [Launch Lens](lens://app/landing)
- [Add clusters](../clusters/adding-clusters.md)
- [Watch introductory videos](./introductory-videos.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,36 +0,0 @@
# Introductory Videos
Continue your Lens journey with this set of introductory videos! These videos are meant to quickly familiarize you with Lens' various powerful features.
<ul class="video-list">
<li class="video">
<a target="_blank" href="https://youtu.be/mc-BzPDKfkQ">
<img src="https://img.youtube.com/vi/mc-BzPDKfkQ/mqdefault.jpg" alt aria-hidden="true" class="thumb"/>
<div class="info">
<h3 class="title">Getting started</h3>
<p class="description">Get Lens Kubernetes IDE Running in 5 Minutes</p>
<span class="duration"><span class="sr-only">Duration </span>35<span class="sr-only"> minutes</span></span>
</div>
</a>
</li>
<li class="video">
<a target="_blank" href="https://youtu.be/epw_MjxjMYI">
<img src="https://img.youtube.com/vi/epw_MjxjMYI/mqdefault.jpg" alt aria-hidden="true" class="thumb"/>
<div class="info">
<h3 class="title">Introducing Lens</h3>
<p class="description">Lens Kubernetes IDE overview</p>
<span class="duration"><span class="sr-only">Duration </span>2<span class="sr-only"> minutes</span></span>
</div>
</a>
</li>
<li class="video">
<a target="_blank" href="https://youtu.be/fqneoWCgJdw">
<img src="https://img.youtube.com/vi/fqneoWCgJdw/mqdefault.jpg" alt aria-hidden="true" class="thumb"/>
<div class="info">
<h3 class="title">Demo of Mirantis Lens</h3>
<p class="description">The Best IDE For Kubernetes</p>
<span class="duration"><span class="sr-only">Duration </span>10<span class="sr-only"> minutes</span></span>
</div>
</a>
</li>
</ul>

View File

@ -1,26 +0,0 @@
# Preferences
## Color Themes
The Color Themes option in Lens preferences lets you set the colors in the Lens user interface to suit your liking.
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). Or follow [this link](lens://app/preferences?highlight=appearance).
2. Select your preferred theme from the **Color Theme** dropdown.
![Color Theme](images/color-theme.png)
## Telemetry & Usage Tracking
Lens collects telemetry data, which is used to help us understand how to improve the product. For example, this usage data helps us to debug issues and to prioritize new features. While we appreciate the insights this data provides, we also know that not everyone wants to send usage data. Please see our [privacy statement](https://www.mirantis.com/company/privacy-policy/) to learn more.
### Disable Telemetry Reporting
If you don't wish to send usage data to Mirantis, you can disable the "Telemetry & Usage Tracking" in the Lens preferences.
1. Go to **File** > **Preferences** (**Lens** > **Preferences** on Mac). Or follow [this link](lens://app/preferences?highlight=telemetry-tracking).
2. Scroll down to **Telemetry & Usage Tracking**
3. Uncheck **Allow Telemetry & Usage Tracking**.
This will silence all telemetry events from Lens going forward. Telemetry information may have been collected and sent up until the point when you disable this setting.
![Disable Telemetry & Usage Tracking](images/disabled-telemetry-usage-tracking.png)

View File

@ -1,21 +0,0 @@
# Using Helm Charts
Lens has integration to Helm making it easy to install and manage Helm charts and releases in Apps section.
![Helm Charts](images/helm-charts.png)
## Managing Helm Repositories
Used Helm repositories are possible to configure in the [Preferences](/getting-started/preferences). Lens app will fetch available Helm repositories from the [Artifact HUB](https://artifacthub.io/) and automatically add `bitnami` repository by default if no other repositories are already configured. If any other repositories are needed to add, those can be added manually via command line. **Note!** Configured Helm repositories are added globally to user's computer, so other processes can see those as well.
## Installing a Helm Chart
Lens will list all charts from configured Helm repositries on Apps section. To install a chart, you need to select a chart and click "Install" button. Lens will open the chart in the editor where you can select a chart version, target namespace and give optionally a name for the release and configure values for the release. Finally, by clicking "Install" button Lens will deploy the chart into the cluster.
## Updating a Helm Release
To update a Helm release, you can open the release details and modify the release values and click "Save" button. To upgrade or downgrade the release, click "Upgrade" button in the release details. In the release editor you can select a new chart version and edit the release values if needed and then click "Upgrade" or "Upgrade and Close" button.
## Deleting a Helm Release
To delete existing Helm release open the release details and click trash can icon on the top of the panel. Deletion removes all Kubernetes resources created by the Helm release. **Note!** If the release included any persistent volumes, those are required to remove manually!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,16 +0,0 @@
# Support
Here you will find different ways of getting support for Lens IDE.
## Community Support
* [Community Slack](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI) - Request for support and help from the Lens community via Slack.
* [Github Issues](https://github.com/lensapp/lens/issues) - Submit your issues and feature requests to Lens IDE via Github.
## Commercial Support & Services
If you are interested in paid support options, professional services or training, please see the offerings from the following vendors:
* [Mirantis](https://www.mirantis.com/software/lens/) offers commercial support for officially released versions of Lens IDE on MacOS, Windows and Linux operating systems. In addition, Mirantis offers professional services to create proprietary / custom Lens IDE extensions and custom `msi` packaging to meet enterprise IT policies for software distribution and configuration. Training is also available.
If you'd like to get your business listed in here, please contact us via email [info@k8slens.dev](mailto:info@k8slens.dev)

View File

@ -1,26 +0,0 @@
# Requirements for Lens
## Hardware
Lens is a small download (< 300 MB) and has a disk footprint of 600 MB. Lens is lightweight and should easily run on today's hardware.
We recommend:
* 2 GHz or faster processor
* 1 GB of RAM
## Platforms
Lens has been tested on the following platforms:
* macOS
* Windows
* Linux
### Additional Windows requirements
...
### Additional Linux Requirements
...

View File

@ -0,0 +1,25 @@
{
"name": "kube-object-event-status",
"version": "0.1.0",
"description": "Adds kube object status from events",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"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"
}
}

View File

@ -19,45 +19,45 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { LensRendererExtension, K8sApi } from "@k8slens/extensions"; import { Renderer } from "@k8slens/extensions";
import { resolveStatus, resolveStatusForCronJobs, resolveStatusForPods } from "./src/resolver"; import { resolveStatus, resolveStatusForCronJobs, resolveStatusForPods } from "./src/resolver";
export default class EventResourceStatusRendererExtension extends LensRendererExtension { export default class EventResourceStatusRendererExtension extends Renderer.LensExtension {
kubeObjectStatusTexts = [ kubeObjectStatusTexts = [
{ {
kind: "Pod", kind: "Pod",
apiVersions: ["v1"], apiVersions: ["v1"],
resolve: (pod: K8sApi.Pod) => resolveStatusForPods(pod) resolve: (pod: Renderer.K8sApi.Pod) => resolveStatusForPods(pod)
}, },
{ {
kind: "ReplicaSet", kind: "ReplicaSet",
apiVersions: ["v1"], apiVersions: ["v1"],
resolve: (replicaSet: K8sApi.ReplicaSet) => resolveStatus(replicaSet) resolve: (replicaSet: Renderer.K8sApi.ReplicaSet) => resolveStatus(replicaSet)
}, },
{ {
kind: "Deployment", kind: "Deployment",
apiVersions: ["apps/v1"], apiVersions: ["apps/v1"],
resolve: (deployment: K8sApi.Deployment) => resolveStatus(deployment) resolve: (deployment: Renderer.K8sApi.Deployment) => resolveStatus(deployment)
}, },
{ {
kind: "StatefulSet", kind: "StatefulSet",
apiVersions: ["apps/v1"], apiVersions: ["apps/v1"],
resolve: (statefulSet: K8sApi.StatefulSet) => resolveStatus(statefulSet) resolve: (statefulSet: Renderer.K8sApi.StatefulSet) => resolveStatus(statefulSet)
}, },
{ {
kind: "DaemonSet", kind: "DaemonSet",
apiVersions: ["apps/v1"], apiVersions: ["apps/v1"],
resolve: (daemonSet: K8sApi.DaemonSet) => resolveStatus(daemonSet) resolve: (daemonSet: Renderer.K8sApi.DaemonSet) => resolveStatus(daemonSet)
}, },
{ {
kind: "Job", kind: "Job",
apiVersions: ["batch/v1"], apiVersions: ["batch/v1"],
resolve: (job: K8sApi.Job) => resolveStatus(job) resolve: (job: Renderer.K8sApi.Job) => resolveStatus(job)
}, },
{ {
kind: "CronJob", kind: "CronJob",
apiVersions: ["batch/v1"], apiVersions: ["batch/v1"],
resolve: (cronJob: K8sApi.CronJob) => resolveStatusForCronJobs(cronJob) resolve: (cronJob: Renderer.K8sApi.CronJob) => resolveStatusForCronJobs(cronJob)
}, },
]; ];
} }

View File

@ -19,11 +19,19 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { K8sApi } from "@k8slens/extensions"; import { Renderer } from "@k8slens/extensions";
export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatus { const { apiManager, eventApi, KubeObjectStatusLevel } = Renderer.K8sApi;
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(object); type KubeObject = Renderer.K8sApi.KubeObject;
type Pod = Renderer.K8sApi.Pod;
type CronJob = Renderer.K8sApi.CronJob;
type KubeObjectStatus = Renderer.K8sApi.KubeObjectStatus;
type EventStore = Renderer.K8sApi.EventStore;
export function resolveStatus(object: KubeObject): KubeObjectStatus {
const eventStore = apiManager.getStore(eventApi);
const events = (eventStore as EventStore).getEventsByObject(object);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (!events.length || !warnings.length) { if (!events.length || !warnings.length) {
@ -32,18 +40,18 @@ export function resolveStatus(object: K8sApi.KubeObject): K8sApi.KubeObjectStatu
const event = [...warnings, ...events][0]; // get latest event const event = [...warnings, ...events][0]; // get latest event
return { return {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
}; };
} }
export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus { export function resolveStatusForPods(pod: Pod): KubeObjectStatus {
if (!pod.hasIssues()) { if (!pod.hasIssues()) {
return null; return null;
} }
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = apiManager.getStore(eventApi);
const events = (eventStore as K8sApi.EventStore).getEventsByObject(pod); const events = (eventStore as EventStore).getEventsByObject(pod);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (!events.length || !warnings.length) { if (!events.length || !warnings.length) {
@ -52,15 +60,15 @@ export function resolveStatusForPods(pod: K8sApi.Pod): K8sApi.KubeObjectStatus {
const event = [...warnings, ...events][0]; // get latest event const event = [...warnings, ...events][0]; // get latest event
return { return {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
}; };
} }
export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeObjectStatus { export function resolveStatusForCronJobs(cronJob: CronJob): KubeObjectStatus {
const eventStore = K8sApi.apiManager.getStore(K8sApi.eventApi); const eventStore = apiManager.getStore(eventApi);
let events = (eventStore as K8sApi.EventStore).getEventsByObject(cronJob); let events = (eventStore as EventStore).getEventsByObject(cronJob);
const warnings = events.filter(evt => evt.isWarning()); const warnings = events.filter(evt => evt.isWarning());
if (cronJob.isNeverRun()) { if (cronJob.isNeverRun()) {
@ -73,7 +81,7 @@ export function resolveStatusForCronJobs(cronJob: K8sApi.CronJob): K8sApi.KubeOb
const event = [...warnings, ...events][0]; // get latest event const event = [...warnings, ...events][0]; // get latest event
return { return {
level: K8sApi.KubeObjectStatusLevel.WARNING, level: KubeObjectStatusLevel.WARNING,
text: `${event.message}`, text: `${event.message}`,
timestamp: event.metadata.creationTimestamp timestamp: event.metadata.creationTimestamp
}; };

View File

@ -20,10 +20,10 @@
*/ */
import React from "react"; import React from "react";
import { LensRendererExtension, Catalog } from "@k8slens/extensions"; import { Common, Renderer } from "@k8slens/extensions";
import { MetricsSettings } from "./src/metrics-settings"; import { MetricsSettings } from "./src/metrics-settings";
export default class ClusterMetricsFeatureExtension extends LensRendererExtension { export default class ClusterMetricsFeatureExtension extends Renderer.LensExtension {
entitySettings = [ entitySettings = [
{ {
apiVersions: ["entity.k8slens.dev/v1alpha1"], apiVersions: ["entity.k8slens.dev/v1alpha1"],
@ -31,7 +31,7 @@ export default class ClusterMetricsFeatureExtension extends LensRendererExtensio
title: "Lens Metrics", title: "Lens Metrics",
priority: 5, priority: 5,
components: { components: {
View: ({ entity = null }: { entity: Catalog.KubernetesCluster}) => { View: ({ entity = null }: { entity: Common.Catalog.KubernetesCluster}) => {
return ( return (
<MetricsSettings cluster={entity} /> <MetricsSettings cluster={entity} />
); );

View File

@ -19,10 +19,14 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { Catalog, K8sApi } from "@k8slens/extensions"; import { Renderer, Common } from "@k8slens/extensions";
import semver from "semver"; import semver from "semver";
import * as path from "path"; import * as path from "path";
const { ResourceStack, forCluster, StorageClass, Namespace } = Renderer.K8sApi;
type ResourceStack = Renderer.K8sApi.ResourceStack;
export interface MetricsConfiguration { export interface MetricsConfiguration {
// Placeholder for Metrics config structure // Placeholder for Metrics config structure
prometheus: { prometheus: {
@ -58,10 +62,10 @@ export class MetricsFeature {
name = "lens-metrics"; name = "lens-metrics";
latestVersion = "v2.26.0-lens1"; latestVersion = "v2.26.0-lens1";
protected stack: K8sApi.ResourceStack; protected stack: ResourceStack;
constructor(protected cluster: Catalog.KubernetesCluster) { constructor(protected cluster: Common.Catalog.KubernetesCluster) {
this.stack = new K8sApi.ResourceStack(cluster, this.name); this.stack = new ResourceStack(cluster, this.name);
} }
get resourceFolder() { get resourceFolder() {
@ -70,7 +74,7 @@ export class MetricsFeature {
async install(config: MetricsConfiguration): Promise<string> { async install(config: MetricsConfiguration): Promise<string> {
// Check if there are storageclasses // Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(this.cluster, K8sApi.StorageClass); const storageClassApi = forCluster(this.cluster, StorageClass);
const scs = await storageClassApi.list(); const scs = await storageClassApi.list();
config.persistence.enabled = scs.some(sc => ( config.persistence.enabled = scs.some(sc => (
@ -91,7 +95,7 @@ export class MetricsFeature {
const status: MetricsStatus = { installed: false, canUpgrade: false}; const status: MetricsStatus = { installed: false, canUpgrade: false};
try { try {
const namespaceApi = K8sApi.forCluster(this.cluster, K8sApi.Namespace); const namespaceApi = forCluster(this.cluster, Namespace);
const namespace = await namespaceApi.get({name: "lens-metrics"}); const namespace = await namespaceApi.get({name: "lens-metrics"});
if (namespace?.kind) { if (namespace?.kind) {

View File

@ -19,13 +19,22 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import React from "react"; import React from "react";
import { Component, Catalog, K8sApi } from "@k8slens/extensions"; import { Common, Renderer } from "@k8slens/extensions";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { computed, observable, makeObservable } from "mobx"; import { computed, observable, makeObservable } from "mobx";
import { MetricsFeature, MetricsConfiguration } from "./metrics-feature"; import { MetricsFeature, MetricsConfiguration } from "./metrics-feature";
const {
K8sApi: {
forCluster, StatefulSet, DaemonSet, Deployment,
},
Component: {
SubTitle, FormSwitch, Switcher, Button,
}
} = Renderer;
interface Props { interface Props {
cluster: Catalog.KubernetesCluster; cluster: Common.Catalog.KubernetesCluster;
} }
@observer @observer
@ -102,7 +111,7 @@ export class MetricsSettings extends React.Component<Props> {
this.changed = true; this.changed = true;
} }
const statefulSet = K8sApi.forCluster(this.props.cluster, K8sApi.StatefulSet); const statefulSet = forCluster(this.props.cluster, StatefulSet);
try { try {
await statefulSet.get({name: "prometheus", namespace: "lens-metrics"}); await statefulSet.get({name: "prometheus", namespace: "lens-metrics"});
@ -115,7 +124,7 @@ export class MetricsSettings extends React.Component<Props> {
} }
} }
const deployment = K8sApi.forCluster(this.props.cluster, K8sApi.Deployment); const deployment = forCluster(this.props.cluster, Deployment);
try { try {
await deployment.get({name: "kube-state-metrics", namespace: "lens-metrics"}); await deployment.get({name: "kube-state-metrics", namespace: "lens-metrics"});
@ -128,7 +137,7 @@ export class MetricsSettings extends React.Component<Props> {
} }
} }
const daemonSet = K8sApi.forCluster(this.props.cluster, K8sApi.DaemonSet); const daemonSet = forCluster(this.props.cluster, DaemonSet);
try { try {
await daemonSet.get({name: "node-exporter", namespace: "lens-metrics"}); await daemonSet.get({name: "node-exporter", namespace: "lens-metrics"});
@ -211,10 +220,10 @@ export class MetricsSettings extends React.Component<Props> {
</section> </section>
)} )}
<section> <section>
<Component.SubTitle title="Prometheus" /> <SubTitle title="Prometheus" />
<Component.FormSwitch <FormSwitch
control={ control={
<Component.Switcher <Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable} disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.prometheus && this.props.cluster.status.active} checked={!!this.featureStates.prometheus && this.props.cluster.status.active}
onChange={v => this.togglePrometheus(v.target.checked)} onChange={v => this.togglePrometheus(v.target.checked)}
@ -229,10 +238,10 @@ export class MetricsSettings extends React.Component<Props> {
</section> </section>
<section> <section>
<Component.SubTitle title="Kube State Metrics" /> <SubTitle title="Kube State Metrics" />
<Component.FormSwitch <FormSwitch
control={ control={
<Component.Switcher <Switcher
disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable} disabled={this.featureStates.kubeStateMetrics === undefined || !this.isTogglable}
checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.active} checked={!!this.featureStates.kubeStateMetrics && this.props.cluster.status.active}
onChange={v => this.toggleKubeStateMetrics(v.target.checked)} onChange={v => this.toggleKubeStateMetrics(v.target.checked)}
@ -248,10 +257,10 @@ export class MetricsSettings extends React.Component<Props> {
</section> </section>
<section> <section>
<Component.SubTitle title="Node Exporter" /> <SubTitle title="Node Exporter" />
<Component.FormSwitch <FormSwitch
control={ control={
<Component.Switcher <Switcher
disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable} disabled={this.featureStates.nodeExporter === undefined || !this.isTogglable}
checked={!!this.featureStates.nodeExporter && this.props.cluster.status.active} checked={!!this.featureStates.nodeExporter && this.props.cluster.status.active}
onChange={v => this.toggleNodeExporter(v.target.checked)} onChange={v => this.toggleNodeExporter(v.target.checked)}
@ -267,7 +276,7 @@ export class MetricsSettings extends React.Component<Props> {
</section> </section>
<section> <section>
<Component.Button <Button
label={this.buttonLabel} label={this.buttonLabel}
waiting={this.inProgress} waiting={this.inProgress}
onClick={() => this.save()} onClick={() => this.save()}

View File

@ -19,11 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { LensRendererExtension } from "@k8slens/extensions"; import { Renderer } from "@k8slens/extensions";
import React from "react"; import React from "react";
import { NodeMenu, NodeMenuProps } from "./src/node-menu"; import { NodeMenu, NodeMenuProps } from "./src/node-menu";
export default class NodeMenuRendererExtension extends LensRendererExtension { export default class NodeMenuRendererExtension extends Renderer.LensExtension {
kubeObjectMenuItems = [ kubeObjectMenuItems = [
{ {
kind: "Node", kind: "Node",

View File

@ -20,9 +20,23 @@
*/ */
import React from "react"; import React from "react";
import { Component, K8sApi, Navigation} from "@k8slens/extensions"; import { Renderer } from "@k8slens/extensions";
export interface NodeMenuProps extends Component.KubeObjectMenuProps<K8sApi.Node> { type Node = Renderer.K8sApi.Node;
const {
Component: {
terminalStore,
createTerminalTab,
ConfirmDialog,
MenuItem,
Icon,
},
Navigation
} = Renderer;
export interface NodeMenuProps extends Renderer.Component.KubeObjectMenuProps<Node> {
} }
export function NodeMenu(props: NodeMenuProps) { export function NodeMenu(props: NodeMenuProps) {
@ -32,7 +46,7 @@ export function NodeMenu(props: NodeMenuProps) {
const nodeName = node.getName(); const nodeName = node.getName();
const sendToTerminal = (command: string) => { const sendToTerminal = (command: string) => {
Component.terminalStore.sendCommand(command, { terminalStore.sendCommand(command, {
enter: true, enter: true,
newTab: true, newTab: true,
}); });
@ -40,7 +54,7 @@ export function NodeMenu(props: NodeMenuProps) {
}; };
const shell = () => { const shell = () => {
Component.createTerminalTab({ createTerminalTab({
title: `Node: ${nodeName}`, title: `Node: ${nodeName}`,
node: nodeName, node: nodeName,
}); });
@ -58,7 +72,7 @@ export function NodeMenu(props: NodeMenuProps) {
const drain = () => { const drain = () => {
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`; const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
Component.ConfirmDialog.open({ ConfirmDialog.open({
ok: () => sendToTerminal(command), ok: () => sendToTerminal(command),
labelOk: `Drain Node`, labelOk: `Drain Node`,
message: ( message: (
@ -71,26 +85,26 @@ export function NodeMenu(props: NodeMenuProps) {
return ( return (
<> <>
<Component.MenuItem onClick={shell}> <MenuItem onClick={shell}>
<Component.Icon svg="ssh" interactive={toolbar} title="Node shell"/> <Icon svg="ssh" interactive={toolbar} title="Node shell"/>
<span className="title">Shell</span> <span className="title">Shell</span>
</Component.MenuItem> </MenuItem>
{!node.isUnschedulable() && ( {!node.isUnschedulable() && (
<Component.MenuItem onClick={cordon}> <MenuItem onClick={cordon}>
<Component.Icon material="pause_circle_filled" title="Cordon" interactive={toolbar}/> <Icon material="pause_circle_filled" title="Cordon" interactive={toolbar}/>
<span className="title">Cordon</span> <span className="title">Cordon</span>
</Component.MenuItem> </MenuItem>
)} )}
{node.isUnschedulable() && ( {node.isUnschedulable() && (
<Component.MenuItem onClick={unCordon}> <MenuItem onClick={unCordon}>
<Component.Icon material="play_circle_filled" title="Uncordon" interactive={toolbar}/> <Icon material="play_circle_filled" title="Uncordon" interactive={toolbar}/>
<span className="title">Uncordon</span> <span className="title">Uncordon</span>
</Component.MenuItem> </MenuItem>
)} )}
<Component.MenuItem onClick={drain}> <MenuItem onClick={drain}>
<Component.Icon material="delete_sweep" title="Drain" interactive={toolbar}/> <Icon material="delete_sweep" title="Drain" interactive={toolbar}/>
<span className="title">Drain</span> <span className="title">Drain</span>
</Component.MenuItem> </MenuItem>
</> </>
); );
} }

View File

@ -19,12 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { LensRendererExtension } from "@k8slens/extensions"; import { Renderer } from "@k8slens/extensions";
import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu"; import { PodShellMenu, PodShellMenuProps } from "./src/shell-menu";
import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu"; import { PodLogsMenu, PodLogsMenuProps } from "./src/logs-menu";
import React from "react"; import React from "react";
export default class PodMenuRendererExtension extends LensRendererExtension { export default class PodMenuRendererExtension extends Renderer.LensExtension {
kubeObjectMenuItems = [ kubeObjectMenuItems = [
{ {
kind: "Pod", kind: "Pod",

View File

@ -20,17 +20,34 @@
*/ */
import React from "react"; import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions"; import { Renderer, Common } from "@k8slens/extensions";
export interface PodLogsMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> { type Pod = Renderer.K8sApi.Pod;
type IPodContainer = Renderer.K8sApi.IPodContainer;
const {
Component: {
logTabStore,
MenuItem,
Icon,
SubMenu,
StatusBrick,
},
Navigation,
} = Renderer;
const {
Util,
} = Common;
export interface PodLogsMenuProps extends Renderer.Component.KubeObjectMenuProps<Pod> {
} }
export class PodLogsMenu extends React.Component<PodLogsMenuProps> { export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
showLogs(container: K8sApi.IPodContainer) { showLogs(container: IPodContainer) {
Navigation.hideDetails(); Navigation.hideDetails();
const pod = this.props.object; const pod = this.props.object;
Component.logTabStore.createPodTab({ logTabStore.createPodTab({
selectedPod: pod, selectedPod: pod,
selectedContainer: container, selectedContainer: container,
}); });
@ -44,35 +61,35 @@ export class PodLogsMenu extends React.Component<PodLogsMenuProps> {
if (!containers.length) return null; if (!containers.length) return null;
return ( return (
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}> <MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
<Component.Icon material="subject" title="Logs" interactive={toolbar}/> <Icon material="subject" title="Logs" interactive={toolbar}/>
<span className="title">Logs</span> <span className="title">Logs</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
<Component.Icon className="arrow" material="keyboard_arrow_right"/> <Icon className="arrow" material="keyboard_arrow_right"/>
<Component.SubMenu> <SubMenu>
{ {
containers.map(container => { containers.map(container => {
const { name } = container; const { name } = container;
const status = statuses.find(status => status.name === name); const status = statuses.find(status => status.name === name);
const brick = status ? ( const brick = status ? (
<Component.StatusBrick <StatusBrick
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })} className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
/> />
) : null; ) : null;
return ( return (
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center"> <MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
{brick} {brick}
<span>{name}</span> <span>{name}</span>
</Component.MenuItem> </MenuItem>
); );
}) })
} }
</Component.SubMenu> </SubMenu>
</> </>
)} )}
</Component.MenuItem> </MenuItem>
); );
} }
} }

View File

@ -22,9 +22,26 @@
import React from "react"; import React from "react";
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions"; import { Renderer, Common } from "@k8slens/extensions";
export interface PodShellMenuProps extends Component.KubeObjectMenuProps<K8sApi.Pod> { type Pod = Renderer.K8sApi.Pod;
const {
Component: {
createTerminalTab,
terminalStore,
MenuItem,
Icon,
SubMenu,
StatusBrick,
},
Navigation,
} = Renderer;
const {
Util,
} = Common;
export interface PodShellMenuProps extends Renderer.Component.KubeObjectMenuProps<Pod> {
} }
export class PodShellMenu extends React.Component<PodShellMenuProps> { export class PodShellMenu extends React.Component<PodShellMenuProps> {
@ -44,11 +61,11 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
command = `${command} sh -c "clear; (bash || ash || sh)"`; command = `${command} sh -c "clear; (bash || ash || sh)"`;
} }
const shell = Component.createTerminalTab({ const shell = createTerminalTab({
title: `Pod: ${pod.getName()} (namespace: ${pod.getNs()})` title: `Pod: ${pod.getName()} (namespace: ${pod.getNs()})`
}); });
Component.terminalStore.sendCommand(command, { terminalStore.sendCommand(command, {
enter: true, enter: true,
tabId: shell.id tabId: shell.id
}); });
@ -61,29 +78,29 @@ export class PodShellMenu extends React.Component<PodShellMenuProps> {
if (!containers.length) return null; if (!containers.length) return null;
return ( return (
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}> <MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/> <Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
<span className="title">Shell</span> <span className="title">Shell</span>
{containers.length > 1 && ( {containers.length > 1 && (
<> <>
<Component.Icon className="arrow" material="keyboard_arrow_right"/> <Icon className="arrow" material="keyboard_arrow_right"/>
<Component.SubMenu> <SubMenu>
{ {
containers.map(container => { containers.map(container => {
const { name } = container; const { name } = container;
return ( return (
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center"> <MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
<Component.StatusBrick/> <StatusBrick/>
<span>{name}</span> <span>{name}</span>
</Component.MenuItem> </MenuItem>
); );
}) })
} }
</Component.SubMenu> </SubMenu>
</> </>
)} )}
</Component.MenuItem> </MenuItem>
); );
} }
} }

View File

@ -1,7 +1,7 @@
site_name: Lens Documentation site_name: Lens Extension API
site_description: Documentation for Lens IDE. The only system youll ever need to take control of your Kubernetes clusters. It's open source and free. Download it today! site_description: Documentation for Lens Extension API.
site_author: Mirantis, Inc. site_author: Mirantis, Inc.
site_url: https://docs.k8slens.dev site_url: https://api-docs.k8slens.dev
docs_dir: docs/ docs_dir: docs/
repo_name: GitHub repo_name: GitHub
repo_url: https://github.com/lensapp/lens repo_url: https://github.com/lensapp/lens
@ -12,16 +12,7 @@ google_analytics:
- auto - auto
nav: nav:
- Overview: README.md - Overview: README.md
- Getting Started: getting-started/README.md
- Managing Clusters:
- Adding Clusters: clusters/adding-clusters.md
- Removing Clusters: clusters/removing-clusters.md
- Settings: clusters/settings.md
- Using Helm Charts: helm/README.md
- Using Extensions: extensions/usage/README.md
- Preferences: getting-started/preferences.md
- Extension Development: - Extension Development:
- Overview: extensions/README.md
- Getting Started: - Getting Started:
- Overview: extensions/get-started/overview.md - Overview: extensions/get-started/overview.md
- Your First Extension: extensions/get-started/your-first-extension.md - Your First Extension: extensions/get-started/your-first-extension.md
@ -43,14 +34,6 @@ nav:
- Testing Extensions: extensions/testing-and-publishing/testing.md - Testing Extensions: extensions/testing-and-publishing/testing.md
- Publishing Extensions: extensions/testing-and-publishing/publishing.md - Publishing Extensions: extensions/testing-and-publishing/publishing.md
- API Reference: extensions/api/README.md - API Reference: extensions/api/README.md
- Contributing:
- Overview: contributing/README.md
- Development: contributing/development.md
- Documentation: contributing/documentation.md
- Maintainers: contributing/maintainers.md
- Promotion: contributing/promotion.md
- Support: support/README.md
- FAQ: faq/README.md
theme: theme:
name: 'material' name: 'material'
highlightjs: true highlightjs: true

View File

@ -19,4 +19,5 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export * from "../../common/catalog-entities";
export * from "../../common/catalog/catalog-entity"; export * from "../../common/catalog/catalog-entity";

View File

@ -19,27 +19,19 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
// Lens-extensions api developer's kit
export { LensMainExtension } from "../lens-main-extension";
export { LensRendererExtension } from "../lens-renderer-extension";
// APIs // APIs
import * as App from "./app"; import * as App from "./app";
import * as EventBus from "./event-bus"; import * as EventBus from "./event-bus";
import * as Store from "./stores"; import * as Store from "./stores";
import * as Util from "./utils"; import * as Util from "./utils";
import * as Interface from "../interfaces";
import * as Catalog from "./catalog"; import * as Catalog from "./catalog";
import * as Types from "./types"; import * as Types from "./types";
import * as Ipc from "./ipc";
export { export {
App, App,
EventBus, EventBus,
Catalog, Catalog,
Interface,
Store, Store,
Types, Types,
Util, Util,
Ipc,
}; };

View File

@ -22,3 +22,5 @@
export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent; export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
export type IpcRendererEvent = Electron.IpcRendererEvent; export type IpcRendererEvent = Electron.IpcRendererEvent;
export type IpcMainEvent = Electron.IpcMainEvent; export type IpcMainEvent = Electron.IpcMainEvent;
export * from "./registrations";

View File

@ -22,5 +22,12 @@
// Extensions-api types bundle (main + renderer) // Extensions-api types bundle (main + renderer)
// Available for lens-extensions via NPM-package "@k8slens/extensions" // Available for lens-extensions via NPM-package "@k8slens/extensions"
export * from "./core-api"; import * as Common from "./common-api";
export * from "./renderer-api"; import * as Renderer from "./renderer-api";
import * as Main from "./main-api";
export {
Common,
Renderer,
Main,
};

View File

@ -337,7 +337,11 @@ export class ExtensionLoader extends Singleton {
try { try {
return __non_webpack_require__(extAbsolutePath).default; return __non_webpack_require__(extAbsolutePath).default;
} catch (error) { } catch (error) {
logger.error(`${logModule}: can't load extension main at ${extAbsolutePath}: ${error}`, { extension, error }); if (ipcRenderer) {
console.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${error.stack || error}`, extension);
} else {
logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${error}`, { extension });
}
} }
return null; return null;

View File

@ -37,7 +37,7 @@ export abstract class IpcRenderer extends IpcRegistrar {
* a component then putting the returned value in a `disposeOnUnmount` call will suffice. * a component then putting the returned value in a `disposeOnUnmount` call will suffice.
* @param channel The channel to listen for broadcasts on * @param channel The channel to listen for broadcasts on
* @param listener The function that will be called with the arguments of the broadcast * @param listener The function that will be called with the arguments of the broadcast
* @returns An optional disopser, Lens will cleanup even if this is not called * @returns An optional disposer, Lens will cleanup even if this is not called
*/ */
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer { listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`; const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;

View File

@ -19,12 +19,10 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { CatalogEntity } from "../../common/catalog"; import type { CatalogEntity } from "../../common/catalog";
import { catalogEntityRegistry as registry } from "../../main/catalog"; import { catalogEntityRegistry as registry } from "../../main/catalog";
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry"; export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
export * from "../../common/catalog-entities";
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] { getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {

View File

@ -19,5 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export * from "./registrations"; import * as Catalog from "./catalog";
export * from "./catalog"; import { IpcMain as Ipc } from "../ipc/ipc-main";
import { LensMainExtension as LensExtension } from "../lens-main-extension";
export {
Catalog,
Ipc,
LensExtension,
};

View File

@ -19,16 +19,35 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { PrometheusLens } from "../main/prometheus/lens";
import { PrometheusHelm } from "../main/prometheus/helm";
import { PrometheusOperator } from "../main/prometheus/operator";
import { PrometheusStacklight } from "../main/prometheus/stacklight";
import { PrometheusProviderRegistry } from "../main/prometheus/provider-registry";
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => { import type { CatalogCategory, CatalogEntity } from "../../common/catalog";
const provider = new providerClass(); import { catalogEntityRegistry as registry } from "../../renderer/api/catalog-entity-registry";
PrometheusProviderRegistry.registerProvider(provider.id, provider); export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
});
export const prometheusProviders = PrometheusProviderRegistry.getProviders(); export class CatalogEntityRegistry {
/**
* Currently active/visible entity
*/
get activeEntity() {
return registry.activeEntity;
}
get entities(): Map<string, CatalogEntity> {
return registry.entities;
}
getById(id: string) {
return this.entities.get(id);
}
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return registry.getItemsForApiKind<T>(apiVersion, kind);
}
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
return registry.getItemsForCategory(category);
}
}
export const catalogEntities = new CatalogEntityRegistry();

View File

@ -22,14 +22,20 @@
// Lens-extensions apis, required in renderer process runtime // Lens-extensions apis, required in renderer process runtime
// APIs // APIs
import * as Catalog from "./catalog";
import * as Component from "./components"; import * as Component from "./components";
import * as K8sApi from "./k8s-api"; import * as K8sApi from "./k8s-api";
import * as Navigation from "./navigation"; import * as Navigation from "./navigation";
import * as Theme from "./theming"; import * as Theme from "./theming";
import { IpcRenderer as Ipc } from "../ipc/ipc-renderer";
import { LensRendererExtension as LensExtension } from "../lens-renderer-extension";
export { export {
Catalog,
Component, Component,
K8sApi, K8sApi,
Navigation, Navigation,
Theme, Theme,
Ipc,
LensExtension,
}; };

View File

@ -0,0 +1,210 @@
/**
* 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 { UserStore } from "../../common/user-store";
import { ContextHandler } from "../context-handler";
import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus";
enum ServiceResult {
Success,
Failure,
Undefined,
}
class TestProvider extends PrometheusProvider {
name = "TestProvider1";
rateAccuracy = "1h";
isConfigurable = false;
constructor(public id: string, public alwaysFail: ServiceResult) {
super();
}
getQuery(): string {
throw new Error("getQuery is not implemented.");
}
async getPrometheusService(): Promise<PrometheusService> {
switch (this.alwaysFail) {
case ServiceResult.Success:
return {
id: this.id,
namespace: "default",
port: 7000,
service: "",
};
case ServiceResult.Failure:
throw new Error("does fail");
case ServiceResult.Undefined:
return undefined;
}
}
}
function getHandler() {
return new ContextHandler(({
getProxyKubeconfig: (): any => ({
makeApiClient: (): any => undefined,
}),
apiUrl: "http://localhost:81",
}) as any);
}
describe("ContextHandler", () => {
beforeEach(() => {
PrometheusProviderRegistry.createInstance();
UserStore.createInstance();
});
afterEach(() => {
PrometheusProviderRegistry.resetInstance();
UserStore.resetInstance();
});
describe("getPrometheusService", () => {
it.each([
[0, 0],
[0, 1],
[0, 2],
[0, 3],
])("should return undefined from %d success(es) after %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
for (let i = 0; i < successes; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
const service = await getHandler().getPrometheusService();
expect(service).toBeUndefined();
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
for (let i = 0; i < successes; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
const service = await getHandler().getPrometheusService();
expect(service.id === `id_${failures}`);
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
for (let i = 0; i < successes; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
const service = await getHandler().getPrometheusService();
expect(service.id === "id_0");
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
const beforeSuccesses = Math.floor(successes / 2);
const afterSuccesses = successes - beforeSuccesses;
for (let i = 0; i < beforeSuccesses; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
for (let i = 0; i < afterSuccesses; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
const service = await getHandler().getPrometheusService();
expect(service.id === "id_0");
});
it("shouldn't pick the second provider of 2 succcess(es) after 1 failure(s)", async () => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Failure));
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
const service = await getHandler().getPrometheusService();
expect(service.id).not.toBe("id_2");
});
});
});

View File

@ -19,13 +19,13 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { PrometheusService } from "./prometheus/provider-registry"; import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry";
import { PrometheusProviderRegistry } from "./prometheus/provider-registry";
import type { ClusterPrometheusPreferences } from "../common/cluster-store"; import type { ClusterPrometheusPreferences } from "../common/cluster-store";
import type { Cluster } from "./cluster"; import type { Cluster } from "./cluster";
import type httpProxy from "http-proxy"; import type httpProxy from "http-proxy";
import url, { UrlWithStringQuery } from "url"; import url, { UrlWithStringQuery } from "url";
import { CoreV1Api } from "@kubernetes/client-node"; import { CoreV1Api } from "@kubernetes/client-node";
import { prometheusProviders } from "../common/prometheus-providers";
import logger from "./logger"; import logger from "./logger";
import { KubeAuthProxy } from "./kube-auth-proxy"; import { KubeAuthProxy } from "./kube-auth-proxy";
@ -33,7 +33,7 @@ export class ContextHandler {
public clusterUrl: UrlWithStringQuery; public clusterUrl: UrlWithStringQuery;
protected kubeAuthProxy?: KubeAuthProxy; protected kubeAuthProxy?: KubeAuthProxy;
protected apiTarget?: httpProxy.ServerOptions; protected apiTarget?: httpProxy.ServerOptions;
protected prometheusProvider: string; protected prometheusProvider?: string;
protected prometheusPath: string | null; protected prometheusPath: string | null;
constructor(protected cluster: Cluster) { constructor(protected cluster: Cluster) {
@ -72,18 +72,34 @@ export class ContextHandler {
this.prometheusProvider = service.id; this.prometheusProvider = service.id;
} }
return prometheusProviders.find(p => p.id === this.prometheusProvider); return PrometheusProviderRegistry.getInstance().getByKind(this.prometheusProvider);
} }
async getPrometheusService(): Promise<PrometheusService | void> { protected listPotentialProviders(): PrometheusProvider[] {
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; const registry = PrometheusProviderRegistry.getInstance();
const prometheusPromises: Promise<PrometheusService | void>[] = providers.map(async provider => {
const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
return provider.getPrometheusService(apiClient); if (typeof this.prometheusProvider === "string") {
}); return [registry.getByKind(this.prometheusProvider)];
}
return (await Promise.all(prometheusPromises)).find(Boolean); return Array.from(registry.providers.values());
}
async getPrometheusService(): Promise<PrometheusService | undefined> {
const providers = this.listPotentialProviders();
const proxyConfig = await this.cluster.getProxyKubeconfig();
const apiClient = proxyConfig.makeApiClient(CoreV1Api);
const potentialServices = await Promise.allSettled(
providers.map(provider => provider.getPrometheusService(apiClient))
);
for (const result of potentialServices) {
if (result.status === "fulfilled" && result.value) {
return result.value;
}
}
return undefined;
} }
async getPrometheusPath(): Promise<string> { async getPrometheusPath(): Promise<string> {

View File

@ -22,9 +22,9 @@
// Main process // Main process
import "../common/system-ca"; import "../common/system-ca";
import "../common/prometheus-providers";
import * as Mobx from "mobx"; import * as Mobx from "mobx";
import * as LensExtensionsCoreApi from "../extensions/core-api"; import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-api";
import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron"; import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron";
import { appName, isMac, productName } from "../common/vars"; import { appName, isMac, productName } from "../common/vars";
import path from "path"; import path from "path";
@ -56,6 +56,7 @@ import { HelmRepoManager } from "./helm/helm-repo-manager";
import { KubeconfigSyncManager } from "./catalog-sources"; import { KubeconfigSyncManager } from "./catalog-sources";
import { handleWsUpgrade } from "./proxy/ws-upgrade"; import { handleWsUpgrade } from "./proxy/ws-upgrade";
import configurePackages from "../common/configure-packages"; import configurePackages from "../common/configure-packages";
import { PrometheusProviderRegistry, registerDefaultPrometheusProviders } from "./prometheus";
const workingDir = path.join(app.getPath("appData"), appName); const workingDir = path.join(app.getPath("appData"), appName);
const cleanup = disposer(); const cleanup = disposer();
@ -124,6 +125,9 @@ app.on("ready", async () => {
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
PrometheusProviderRegistry.createInstance();
registerDefaultPrometheusProviders();
const userStore = UserStore.createInstance(); const userStore = UserStore.createInstance();
const clusterStore = ClusterStore.createInstance(); const clusterStore = ClusterStore.createInstance();
const hotbarStore = HotbarStore.createInstance(); const hotbarStore = HotbarStore.createInstance();
@ -276,7 +280,8 @@ app.on("open-url", (event, rawUrl) => {
* e.g. global.Mobx, global.LensExtensions * e.g. global.Mobx, global.LensExtensions
*/ */
const LensExtensions = { const LensExtensions = {
...LensExtensionsCoreApi, Common: LensExtensionsCommonApi,
Main: LensExtensionsMainApi,
}; };
export { export {

View File

@ -22,32 +22,14 @@
import { PrometheusLens } from "./lens"; import { PrometheusLens } from "./lens";
import type { CoreV1Api } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import type { PrometheusService } from "./provider-registry"; import type { PrometheusService } from "./provider-registry";
import logger from "../logger";
export class PrometheusHelm extends PrometheusLens { export class PrometheusHelm extends PrometheusLens {
id = "helm"; readonly id: string = "helm";
name = "Helm"; readonly name: string = "Helm";
rateAccuracy = "5m"; readonly rateAccuracy: string = "5m";
readonly isConfigurable: boolean = false;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
const labelSelector = "app=prometheus,component=server,heritage=Helm"; return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
try {
const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector);
const service = serviceList.body.items[0];
if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
};
} catch(error) {
logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`);
return;
}
} }
} }

View File

@ -0,0 +1,37 @@
/**
* 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 { PrometheusHelm } from "./helm";
import { PrometheusLens } from "./lens";
import { PrometheusOperator } from "./operator";
import { PrometheusProviderRegistry } from "./provider-registry";
import { PrometheusStacklight } from "./stacklight";
export * from "./provider-registry";
export function registerDefaultPrometheusProviders() {
PrometheusProviderRegistry
.getInstance()
.registerProvider(new PrometheusLens())
.registerProvider(new PrometheusHelm())
.registerProvider(new PrometheusOperator())
.registerProvider(new PrometheusStacklight());
}

View File

@ -19,16 +19,18 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry"; import { PrometheusProvider, PrometheusService } from "./provider-registry";
import type { CoreV1Api } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"; import logger from "../logger";
import { inspect } from "util";
export class PrometheusLens implements PrometheusProvider { export class PrometheusLens extends PrometheusProvider {
id = "lens"; readonly id: string = "lens";
name = "Lens"; readonly name: string = "Lens";
rateAccuracy = "1m"; readonly rateAccuracy: string = "1m";
readonly isConfigurable: boolean = true;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try { try {
const resp = await client.readNamespacedService("prometheus", "lens-metrics"); const resp = await client.readNamespacedService("prometheus", "lens-metrics");
const service = resp.body; const service = resp.body;
@ -41,66 +43,101 @@ export class PrometheusLens implements PrometheusProvider {
}; };
} catch(error) { } catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
return undefined;
} }
} }
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { public getQuery(opts: Record<string, string>, queryName: string): string {
switch(opts.category) { switch(opts.category) {
case "cluster": case "cluster":
return { switch (queryName) {
memoryUsage: ` case "memoryUsage":
sum( return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`);
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) case "memoryRequests":
) by (kubernetes_name) return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
`.replace(/_bytes/g, `_bytes{kubernetes_node=~"${opts.nodes}"}`), case "memoryLimits":
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`;
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`, case "memoryCapacity":
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`, case "cpuUsage":
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`, case "cpuRequests":
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`, case "cpuLimits":
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`, case "cpuCapacity":
fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)` return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
}; case "podUsage":
return `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`;
case "podCapacity":
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`;
case "fsSize":
return `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`;
case "fsUsage":
return `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`;
}
break;
case "nodes": case "nodes":
return { switch (queryName) {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`, case "memoryUsage":
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`, case "memoryCapacity":
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`, case "cpuUsage":
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)` return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`;
}; case "cpuCapacity":
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
case "fsSize":
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`;
case "fsUsage":
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)`;
}
break;
case "pods": case "pods":
return { switch (queryName) {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, case "cpuUsage":
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuRequests":
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuLimits":
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, case "memoryUsage":
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})` case "memoryRequests":
}; return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "memoryLimits":
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "fsUsage":
return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "networkReceive":
return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
case "networkTransmit":
return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
}
break;
case "pvc": case "pvc":
return { switch (queryName) {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, case "diskUsage":
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
}; case "diskCapacity":
return `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
}
break;
case "ingress": case "ingress":
const bytesSent = (ingress: string, namespace: string, statuses: string) => switch (queryName) {
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; case "bytesSentSuccess":
return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*");
case "bytesSentFailure":
return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*");
case "requestDurationSeconds":
return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
case "responseDurationSeconds":
return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
}
break;
}
return { throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`);
bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`
};
}
} }
} }

View File

@ -19,98 +19,110 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry"; import { PrometheusProvider, PrometheusService } from "./provider-registry";
import type { CoreV1Api, V1Service } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"; import { inspect } from "util";
export class PrometheusOperator implements PrometheusProvider { export class PrometheusOperator extends PrometheusProvider {
rateAccuracy = "1m"; readonly rateAccuracy: string = "1m";
id = "operator"; readonly id: string = "operator";
name = "Prometheus Operator"; readonly name: string = "Prometheus Operator";
readonly isConfigurable: boolean = false;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try { return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
let service: V1Service;
for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) {
if (!service) {
const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector);
service = serviceList.body.items[0];
}
}
if (!service) return;
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
};
} catch(error) {
logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`);
return;
}
} }
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { public getQuery(opts: Record<string, string>, queryName: string): string {
switch(opts.category) { switch(opts.category) {
case "cluster": case "cluster":
return { switch (queryName) {
memoryUsage: ` case "memoryUsage":
sum( return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) case "memoryRequests":
) return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`), case "memoryLimits":
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`;
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`, case "memoryCapacity":
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`, return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`;
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`, case "cpuUsage":
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`, return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`, case "cpuRequests":
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`, return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", node=~"${opts.nodes}"})`, case "cpuLimits":
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`, case "cpuCapacity":
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})` return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`;
}; case "podUsage":
return `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", node=~"${opts.nodes}"})`;
case "podCapacity":
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`;
case "fsSize":
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
case "fsUsage":
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
}
break;
case "nodes": case "nodes":
return { switch (queryName) {
memoryUsage: `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, case "memoryUsage":
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, case "memoryCapacity":
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`, case "cpuUsage":
fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)` return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
}; case "cpuCapacity":
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
case "fsSize":
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
case "fsUsage":
return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
}
break;
case "pods": case "pods":
return { switch (queryName) {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, case "cpuUsage":
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuRequests":
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuLimits":
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, case "memoryUsage":
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, return `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})` case "memoryRequests":
}; return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "memoryLimits":
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "fsUsage":
return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "networkReceive":
return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
case "networkTransmit":
return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
}
break;
case "pvc": case "pvc":
return { switch (queryName) {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, case "diskUsage":
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
}; case "diskCapacity":
return `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
}
break;
case "ingress": case "ingress":
const bytesSent = (ingress: string, namespace: string, statuses: string) => switch (queryName) {
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; case "bytesSentSuccess":
return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*");
case "bytesSentFailure":
return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*");
case "requestDurationSeconds":
return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
case "responseDurationSeconds":
return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
}
break;
}
return { throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`);
bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`
};
}
} }
} }

View File

@ -20,58 +20,8 @@
*/ */
import type { CoreV1Api } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import { Singleton } from "../../common/utils";
export type PrometheusClusterQuery = { import logger from "../logger";
memoryUsage: string;
memoryRequests: string;
memoryLimits: string;
memoryCapacity: string;
cpuUsage: string;
cpuRequests: string;
cpuLimits: string;
cpuCapacity: string;
podUsage: string;
podCapacity: string;
};
export type PrometheusNodeQuery = {
memoryUsage: string;
memoryCapacity: string;
cpuUsage: string;
cpuCapacity: string;
fsSize: string;
fsUsage: string;
};
export type PrometheusPodQuery = {
memoryUsage: string;
memoryRequests: string;
memoryLimits: string;
cpuUsage: string;
cpuRequests: string;
cpuLimits: string;
fsUsage: string;
networkReceive: string;
networkTransmit: string;
};
export type PrometheusPvcQuery = {
diskUsage: string;
diskCapacity: string;
};
export type PrometheusIngressQuery = {
bytesSentSuccess: string;
bytesSentFailure: string;
requestDurationSeconds: string;
responseDurationSeconds: string;
};
export type PrometheusQueryOpts = {
[key: string]: string | any;
};
export type PrometheusQuery = PrometheusNodeQuery | PrometheusClusterQuery | PrometheusPodQuery | PrometheusPvcQuery | PrometheusIngressQuery;
export type PrometheusService = { export type PrometheusService = {
id: string; id: string;
@ -80,33 +30,61 @@ export type PrometheusService = {
port: number; port: number;
}; };
export interface PrometheusProvider { export abstract class PrometheusProvider {
id: string; abstract readonly id: string;
name: string; abstract readonly name: string;
getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void; abstract readonly rateAccuracy: string;
getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void>; abstract readonly isConfigurable: boolean;
abstract getQuery(opts: Record<string, string>, queryName: string): string;
abstract getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined>;
protected bytesSent(ingress: string, namespace: string, statuses: string): string {
return `sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
} }
export type PrometheusProviderList = { protected async getFirstNamespacedServer(client: CoreV1Api, ...selectors: string[]): Promise<PrometheusService | undefined> {
[key: string]: PrometheusProvider; try {
for (const selector of selectors) {
const { body: { items: [service] } } = await client.listServiceForAllNamespaces(null, null, null, selector);
if (service) {
return {
id: this.id,
namespace: service.metadata.namespace,
service: service.metadata.name,
port: service.spec.ports[0].port
}; };
}
export class PrometheusProviderRegistry { }
private static prometheusProviders: PrometheusProviderList = {}; } catch (error) {
logger.warn(`${this.name}: failed to list services: ${error.toString()}`);
static getProvider(type: string): PrometheusProvider { }
if (!this.prometheusProviders[type]) {
throw "Unknown Prometheus provider"; return undefined;
} }
}
return this.prometheusProviders[type];
} export class PrometheusProviderRegistry extends Singleton {
public providers = new Map<string, PrometheusProvider>();
static registerProvider(key: string, provider: PrometheusProvider) {
this.prometheusProviders[key] = provider; getByKind(kind: string): PrometheusProvider {
} const provider = this.providers.get(kind);
static getProviders(): PrometheusProvider[] { if (!provider) {
return Object.values(this.prometheusProviders); throw new Error("Unknown Prometheus provider");
}
return provider;
}
registerProvider(provider: PrometheusProvider): this {
if (this.providers.has(provider.id)) {
throw new Error("Provider already registered under that kind");
}
this.providers.set(provider.id, provider);
return this;
} }
} }

View File

@ -19,16 +19,18 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry"; import { PrometheusProvider, PrometheusService } from "./provider-registry";
import type { CoreV1Api } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import logger from "../logger"; import logger from "../logger";
import { inspect } from "util";
export class PrometheusStacklight implements PrometheusProvider { export class PrometheusStacklight extends PrometheusProvider {
id = "stacklight"; readonly id: string = "stacklight";
name = "Stacklight"; readonly name: string = "Stacklight";
rateAccuracy = "1m"; readonly rateAccuracy: string = "1m";
readonly isConfigurable: boolean = false;
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> { public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
try { try {
const resp = await client.readNamespacedService("prometheus-server", "stacklight"); const resp = await client.readNamespacedService("prometheus-server", "stacklight");
const service = resp.body; const service = resp.body;
@ -41,66 +43,101 @@ export class PrometheusStacklight implements PrometheusProvider {
}; };
} catch(error) { } catch(error) {
logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`);
return undefined;
} }
} }
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void { public getQuery(opts: Record<string, string>, queryName: string): string {
switch(opts.category) { switch(opts.category) {
case "cluster": case "cluster":
return { switch (queryName) {
memoryUsage: ` case "memoryUsage":
sum( return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_name)`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes) case "memoryRequests":
) by (kubernetes_name) return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`), case "memoryLimits":
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`;
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`, case "memoryCapacity":
memoryCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`, return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`, case "cpuUsage":
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`, case "cpuRequests":
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`, return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`, case "cpuLimits":
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`, return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`, case "cpuCapacity":
fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)` return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
}; case "podUsage":
return `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`;
case "podCapacity":
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`;
case "fsSize":
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
case "fsUsage":
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
}
break;
case "nodes": case "nodes":
return { switch (queryName) {
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`, case "memoryUsage":
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`, return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`, case "memoryCapacity":
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`, return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`, case "cpuUsage":
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)` return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`;
}; case "cpuCapacity":
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
case "fsSize":
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`;
case "fsUsage":
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`;
}
break;
case "pods": case "pods":
return { switch (queryName) {
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, case "cpuUsage":
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuRequests":
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, case "cpuLimits":
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`, return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`, case "memoryUsage":
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`, return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})` case "memoryRequests":
}; return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "memoryLimits":
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "fsUsage":
return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
case "networkReceive":
return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
case "networkTransmit":
return `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
}
break;
case "pvc": case "pvc":
return { switch (queryName) {
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`, case "diskUsage":
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)` return `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
}; case "diskCapacity":
return `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`;
}
break;
case "ingress": case "ingress":
const bytesSent = (ingress: string, namespace: string, statuses: string) => switch (queryName) {
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`; case "bytesSentSuccess":
return this.bytesSent(opts.ingress, opts.namespace, "^2\\\\d*");
case "bytesSentFailure":
return this.bytesSent(opts.ingress, opts.namespace, "^5\\\\d*");
case "requestDurationSeconds":
return `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
case "responseDurationSeconds":
return `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
}
break;
}
return { throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`);
bytesSentSuccess: bytesSent(opts.ingress, opts.namespace, "^2\\\\d*"),
bytesSentFailure: bytesSent(opts.ingress, opts.namespace, "^5\\\\d*"),
requestDurationSeconds: `sum(rate(nginx_ingress_controller_request_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`,
responseDurationSeconds: `sum(rate(nginx_ingress_controller_response_duration_seconds_sum{ingress="${opts.ingress}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (ingress, namespace)`
};
}
} }
} }

View File

@ -24,7 +24,7 @@ import * as uuid from "uuid";
import { broadcastMessage } from "../../../common/ipc"; import { broadcastMessage } from "../../../common/ipc";
import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler"; import { ProtocolHandlerExtension, ProtocolHandlerInternal } from "../../../common/protocol-handler";
import { noop } from "../../../common/utils"; import { noop } from "../../../common/utils";
import { LensMainExtension } from "../../../extensions/core-api"; import { LensExtension } from "../../../extensions/main-api";
import { ExtensionLoader } from "../../../extensions/extension-loader"; import { ExtensionLoader } from "../../../extensions/extension-loader";
import { ExtensionsStore } from "../../../extensions/extensions-store"; import { ExtensionsStore } from "../../../extensions/extensions-store";
import { LensProtocolRouterMain } from "../router"; import { LensProtocolRouterMain } from "../router";
@ -78,7 +78,7 @@ describe("protocol router tests", () => {
it.only("should not throw when has valid host", async () => { it.only("should not throw when has valid host", async () => {
const extId = uuid.v4(); const extId = uuid.v4();
const ext = new LensMainExtension({ const ext = new LensExtension({
id: extId, id: extId,
manifestPath: "/foo/bar", manifestPath: "/foo/bar",
manifest: { manifest: {
@ -155,7 +155,7 @@ describe("protocol router tests", () => {
const lpr = LensProtocolRouterMain.getInstance(); const lpr = LensProtocolRouterMain.getInstance();
const extId = uuid.v4(); const extId = uuid.v4();
const ext = new LensMainExtension({ const ext = new LensExtension({
id: extId, id: extId,
manifestPath: "/foo/bar", manifestPath: "/foo/bar",
manifest: { manifest: {
@ -195,7 +195,7 @@ describe("protocol router tests", () => {
{ {
const extId = uuid.v4(); const extId = uuid.v4();
const ext = new LensMainExtension({ const ext = new LensExtension({
id: extId, id: extId,
manifestPath: "/foo/bar", manifestPath: "/foo/bar",
manifest: { manifest: {
@ -219,7 +219,7 @@ describe("protocol router tests", () => {
{ {
const extId = uuid.v4(); const extId = uuid.v4();
const ext = new LensMainExtension({ const ext = new LensExtension({
id: extId, id: extId,
manifestPath: "/foo/bar", manifestPath: "/foo/bar",
manifest: { manifest: {

View File

@ -177,6 +177,7 @@ export class Router {
// Metrics API // Metrics API
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics); this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
// Port-forward API // Port-forward API
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward); this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward);

View File

@ -26,6 +26,7 @@ import { Cluster, ClusterMetadataKey } from "../cluster";
import type { ClusterPrometheusMetadata } from "../../common/cluster-store"; import type { ClusterPrometheusMetadata } from "../../common/cluster-store";
import logger from "../logger"; import logger from "../logger";
import { getMetrics } from "../k8s-request"; import { getMetrics } from "../k8s-request";
import { PrometheusProviderRegistry } from "../prometheus";
export type IMetricsQuery = string | string[] | { export type IMetricsQuery = string | string[] | {
[metricName: string]: string; [metricName: string]: string;
@ -62,6 +63,12 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
return Promise.all(queries.map(loadMetric)); return Promise.all(queries.map(loadMetric));
} }
interface MetricProviderInfo {
name: string;
id: string;
isConfigurable: boolean;
}
export class MetricsRoute { export class MetricsRoute {
static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) { static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
@ -92,8 +99,9 @@ export class MetricsRoute {
respondJson(response, data); respondJson(response, data);
} else { } else {
const queries = Object.entries(payload).map(([queryName, queryOpts]) => ( const queries = Object.entries<Record<string, string>>(payload)
(prometheusProvider.getQueries(queryOpts) as Record<string, string>)[queryName] .map(([queryName, queryOpts]) => (
prometheusProvider.getQuery(queryOpts, queryName)
)); ));
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]]));
@ -108,4 +116,14 @@ export class MetricsRoute {
cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata;
} }
} }
static async routeMetricsProviders({ response }: LensApiRequest) {
const providers: MetricProviderInfo[] = [];
for (const { name, id, isConfigurable } of PrometheusProviderRegistry.getInstance().providers.values()) {
providers.push({ name, id, isConfigurable });
}
respondJson(response, providers);
}
} }

View File

@ -46,6 +46,12 @@ export interface IMetricsResult {
values: [number, string][]; values: [number, string][];
} }
export interface MetricProviderInfo {
name: string;
id: string;
isConfigurable: boolean;
}
export interface IMetricsReqParams { export interface IMetricsReqParams {
start?: number | string; // timestamp in seconds or valid date-string start?: number | string; // timestamp in seconds or valid date-string
end?: number | string; end?: number | string;
@ -75,6 +81,10 @@ export const metricsApi = {
} }
}); });
}, },
async getMetricProviders(): Promise<MetricProviderInfo[]> {
return apiBase.get("/metrics/providers");
}
}; };
export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics {

View File

@ -52,39 +52,56 @@ export interface INodeMetrics<T = IMetrics> {
export interface Node { export interface Node {
spec: { spec: {
podCIDR: string; podCIDR?: string;
externalID: string; podCIDRs?: string[];
providerID?: string;
/**
* @deprecated see https://issues.k8s.io/61966
*/
externalID?: string;
taints?: { taints?: {
key: string; key: string;
value: string; value: string;
effect: string; effect: string;
timeAdded: string;
}[]; }[];
unschedulable?: boolean; unschedulable?: boolean;
}; };
status: { status: {
capacity: { capacity?: {
cpu: string; cpu: string;
["ephemeral-storage"]: string;
["hugepages-1Gi"]: string;
["hugepages-2Mi"]: string;
memory: string; memory: string;
pods: string; pods: string;
}; };
allocatable: { allocatable?: {
cpu: string; cpu: string;
["ephemeral-storage"]: string;
["hugepages-1Gi"]: string;
["hugepages-2Mi"]: string;
memory: string; memory: string;
pods: string; pods: string;
}; };
conditions: { conditions?: {
type: string; type: string;
status?: string; status: string;
lastHeartbeatTime?: string; lastHeartbeatTime?: string;
lastTransitionTime?: string; lastTransitionTime?: string;
reason?: string; reason?: string;
message?: string; message?: string;
}[]; }[];
addresses: { addresses?: {
type: string; type: string;
address: string; address: string;
}[]; }[];
nodeInfo: { daemonEndpoints?: {
kubeletEndpoint: {
Port: number; //it must be uppercase for backwards compatibility
}
}
nodeInfo?: {
machineID: string; machineID: string;
systemUUID: string; systemUUID: string;
bootID: string; bootID: string;
@ -96,9 +113,14 @@ export interface Node {
operatingSystem: string; operatingSystem: string;
architecture: string; architecture: string;
}; };
images: { images?: {
names: string[]; names: string[];
sizeBytes: number; sizeBytes?: number;
}[];
volumesInUse?: string[];
volumesAttached?: {
name: string;
devicePath: string;
}[]; }[];
}; };
} }

View File

@ -26,7 +26,7 @@ import * as Mobx from "mobx";
import * as MobxReact from "mobx-react"; import * as MobxReact from "mobx-react";
import * as ReactRouter from "react-router"; import * as ReactRouter from "react-router";
import * as ReactRouterDom from "react-router-dom"; import * as ReactRouterDom from "react-router-dom";
import * as LensExtensionsCoreApi from "../extensions/core-api"; import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsRendererApi from "../extensions/renderer-api"; import * as LensExtensionsRendererApi from "../extensions/renderer-api";
import { render, unmountComponentAtNode } from "react-dom"; import { render, unmountComponentAtNode } from "react-dom";
import { delay } from "../common/utils"; import { delay } from "../common/utils";
@ -123,8 +123,8 @@ bootstrap(process.isMainFrame ? LensApp : App);
* e.g. Devtools -> Console -> window.LensExtensions (renderer) * e.g. Devtools -> Console -> window.LensExtensions (renderer)
*/ */
const LensExtensions = { const LensExtensions = {
...LensExtensionsCoreApi, Common: LensExtensionsCommonApi,
...LensExtensionsRendererApi, Renderer: LensExtensionsRendererApi,
}; };
export { export {

View File

@ -19,5 +19,30 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
export { IpcMain as Main } from "../ipc/ipc-main"; .NodeDetailsResources {
export { IpcRegistrar as Registrar } from "../ipc/ipc-registrar"; .Table {
margin: 0 (-$margin * 3);
}
.TableCell {
&:first-child {
margin-left: $margin;
}
&:last-child {
margin-right: $margin;
}
&.pods {
flex-grow: 0.5;
}
&.memory {
flex-grow: 0.5;
}
&.cpu {
flex-grow:0.5;
}
}
}

View File

@ -0,0 +1,78 @@
/**
* 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 "./node-details-resources.scss";
import { Table } from "../table/table";
import { TableHead } from "../table/table-head";
import { TableRow } from "../table/table-row";
import React from "react";
import type { Node } from "../../api/endpoints";
import { TableCell } from "../table/table-cell";
interface Props {
node: Node;
type: "allocatable" | "capacity";
}
export class NodeDetailsResources extends React.Component<Props> {
toMi(resource: string) {
if (resource.endsWith("Ki")) {
return `${(parseInt(resource) / 1024).toFixed(1)}Mi`;
}
return resource;
}
render() {
const status = this.props.node.status;
const type = this.props.type;
if (!status) return null;
return (
<div className="NodeDetailsResources flex column">
<Table
selectable
scrollable={false}
>
<TableHead>
<TableCell className="cpu">CPU</TableCell>
<TableCell className="memory">Memory</TableCell>
<TableCell className="ephemeral-storage">Ephemeral Storage</TableCell>
<TableCell className="hugepages-1Gi">Hugepages-1Gi</TableCell>
<TableCell className="hugepages-2Mi">Hugepages-2Mi</TableCell>
<TableCell className="pods">Pods</TableCell>
</TableHead>
<TableRow>
<TableCell className="cpu">{status[type].cpu}</TableCell>
<TableCell className="memory">{this.toMi(status[type].memory)}</TableCell>
<TableCell
className="ephemeral-storage">{this.toMi(status[type]["ephemeral-storage"])}</TableCell>
<TableCell className="hugepages-1Gi">{status[type]["hugepages-1Gi"]}</TableCell>
<TableCell className="hugepages-2Mi">{status[type]["hugepages-2Mi"]}</TableCell>
<TableCell className="pods">{status[type].pods}</TableCell>
</TableRow>
</Table>
</div>
);
}
}

View File

@ -40,6 +40,8 @@ import { KubeEventDetails } from "../+events/kube-event-details";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
import { ClusterMetricsResourceType } from "../../../main/cluster"; import { ClusterMetricsResourceType } from "../../../main/cluster";
import { NodeDetailsResources } from "./node-details-resources";
import { DrawerTitle } from "../drawer/drawer-title";
interface Props extends KubeObjectDetailsProps<Node> { interface Props extends KubeObjectDetailsProps<Node> {
} }
@ -64,7 +66,7 @@ export class NodeDetails extends React.Component<Props> {
if (!node) return null; if (!node) return null;
const { status } = node; const { status } = node;
const { nodeInfo, addresses, capacity, allocatable } = status; const { nodeInfo, addresses } = status;
const conditions = node.getActiveConditions(); const conditions = node.getActiveConditions();
const taints = node.getTaints(); const taints = node.getTaints();
const childPods = podsStore.getPodsByNode(node.getName()); const childPods = podsStore.getPodsByNode(node.getName());
@ -88,16 +90,6 @@ export class NodeDetails extends React.Component<Props> {
</ResourceMetrics> </ResourceMetrics>
)} )}
<KubeObjectMeta object={node} hideFields={["labels", "annotations", "uid", "resourceVersion", "selfLink"]}/> <KubeObjectMeta object={node} hideFields={["labels", "annotations", "uid", "resourceVersion", "selfLink"]}/>
<DrawerItem name="Capacity">
CPU: {capacity.cpu},{" "}
Memory: {Math.floor(parseInt(capacity.memory) / 1024)}Mi,{" "}
Pods: {capacity.pods}
</DrawerItem>
<DrawerItem name="Allocatable">
CPU: {allocatable.cpu},{" "}
Memory: {Math.floor(parseInt(allocatable.memory) / 1024)}Mi,{" "}
Pods: {allocatable.pods}
</DrawerItem>
{addresses && {addresses &&
<DrawerItem name="Addresses"> <DrawerItem name="Addresses">
{ {
@ -167,6 +159,10 @@ export class NodeDetails extends React.Component<Props> {
} }
</DrawerItem> </DrawerItem>
} }
<DrawerTitle title="Capacity"/>
<NodeDetailsResources node={node} type={"capacity"}/>
<DrawerTitle title="Allocatable"/>
<NodeDetailsResources node={node} type={"allocatable"}/>
<PodDetailsList <PodDetailsList
pods={childPods} pods={childPods}
owner={node} owner={node}

View File

@ -21,18 +21,14 @@
import React from "react"; import React from "react";
import { observer, disposeOnUnmount } from "mobx-react"; import { observer, disposeOnUnmount } from "mobx-react";
import { prometheusProviders } from "../../../../common/prometheus-providers";
import type { Cluster } from "../../../../main/cluster"; import type { Cluster } from "../../../../main/cluster";
import { SubTitle } from "../../layout/sub-title"; import { SubTitle } from "../../layout/sub-title";
import { Select, SelectOption } from "../../select"; import { Select, SelectOption } from "../../select";
import { Input } from "../../input"; import { Input } from "../../input";
import { observable, computed, autorun, makeObservable } from "mobx"; import { observable, computed, autorun, makeObservable } from "mobx";
import { productName } from "../../../../common/vars"; import { productName } from "../../../../common/vars";
import { MetricProviderInfo, metricsApi } from "../../../api/endpoints/metrics.api";
const options: SelectOption<string>[] = [ import { Spinner } from "../../spinner";
{ value: "", label: "Auto detect" },
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
];
interface Props { interface Props {
cluster: Cluster; cluster: Cluster;
@ -42,16 +38,27 @@ interface Props {
export class ClusterPrometheusSetting extends React.Component<Props> { export class ClusterPrometheusSetting extends React.Component<Props> {
@observable path = ""; @observable path = "";
@observable provider = ""; @observable provider = "";
@observable loading = true;
@observable loadedOptions: MetricProviderInfo[] = [];
@computed get options(): SelectOption<string>[] {
return [
{ value: "", label: "Auto detect" },
...this.loadedOptions.map(({ name, id }) => ({ value: id, label: name }))
];
}
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
makeObservable(this); makeObservable(this);
} }
@computed get canEditPrometheusPath() { @computed get canEditPrometheusPath(): boolean {
if (this.provider === "" || this.provider === "lens") return false; return Boolean(
this.loadedOptions
return true; .find(opt => opt.id === this.provider)
?.isConfigurable
);
} }
componentDidMount() { componentDidMount() {
@ -74,6 +81,16 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
} }
}) })
); );
metricsApi
.getMetricProviders()
.then(values => {
this.loading = false;
if (values) {
this.loadedOptions = values;
}
});
} }
parsePrometheusPath = () => { parsePrometheusPath = () => {
@ -110,15 +127,21 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
<> <>
<section> <section>
<SubTitle title="Prometheus"/> <SubTitle title="Prometheus"/>
{
this.loading
? <Spinner />
: <>
<Select <Select
value={this.provider} value={this.provider}
onChange={({ value }) => { onChange={({ value }) => {
this.provider = value; this.provider = value;
this.onSaveProvider(); this.onSaveProvider();
}} }}
options={options} options={this.options}
/> />
<small className="hint">What query format is used to fetch metrics from Prometheus</small> <small className="hint">What query format is used to fetch metrics from Prometheus</small>
</>
}
</section> </section>
{this.canEditPrometheusPath && ( {this.canEditPrometheusPath && (
<section> <section>

View File

@ -55,6 +55,20 @@ export class LogStore {
}, { delay: 500 }); }, { delay: 500 });
} }
handlerError(tabId: TabId, error: any): void {
if (error.error && !(error.message || error.reason || error.code)) {
error = error.error;
}
const message = [
`Failed to load logs: ${error.message}`,
`Reason: ${error.reason} (${error.code})`
];
this.refresher.stop();
this.podLogs.set(tabId, message);
}
/** /**
* Function prepares tailLines param for passing to API request * Function prepares tailLines param for passing to API request
* Each time it increasing it's number, caused to fetch more logs. * Each time it increasing it's number, caused to fetch more logs.
@ -70,14 +84,8 @@ export class LogStore {
this.refresher.start(); this.refresher.start();
this.podLogs.set(tabId, logs); this.podLogs.set(tabId, logs);
} catch ({error}) { } catch (error) {
const message = [ this.handlerError(tabId, error);
`Failed to load logs: ${error.message}`,
`Reason: ${error.reason} (${error.code})`
];
this.refresher.stop();
this.podLogs.set(tabId, message);
} }
}; };
@ -88,7 +96,11 @@ export class LogStore {
* @param tabId * @param tabId
*/ */
loadMore = async (tabId: TabId) => { loadMore = async (tabId: TabId) => {
if (!this.podLogs.get(tabId).length) return; if (!this.podLogs.get(tabId).length) {
return;
}
try {
const oldLogs = this.podLogs.get(tabId); const oldLogs = this.podLogs.get(tabId);
const logs = await this.loadLogs(tabId, { const logs = await this.loadLogs(tabId, {
sinceTime: this.getLastSinceTime(tabId) sinceTime: this.getLastSinceTime(tabId)
@ -96,6 +108,9 @@ export class LogStore {
// Add newly received logs to bottom // Add newly received logs to bottom
this.podLogs.set(tabId, [...oldLogs, ...logs]); this.podLogs.set(tabId, [...oldLogs, ...logs]);
} catch (error) {
this.handlerError(tabId, error);
}
}; };
/** /**
@ -103,45 +118,39 @@ export class LogStore {
* an API request * an API request
* @param tabId * @param tabId
* @param params request parameters described in IPodLogsQuery interface * @param params request parameters described in IPodLogsQuery interface
* @returns {Promise} A fetch request promise * @returns A fetch request promise
*/ */
loadLogs = async (tabId: TabId, params: Partial<IPodLogsQuery>) => { async loadLogs(tabId: TabId, params: Partial<IPodLogsQuery>): Promise<string[]> {
const data = logTabStore.getData(tabId); const data = logTabStore.getData(tabId);
const { selectedContainer, previous } = data; const { selectedContainer, previous } = data;
const pod = new Pod(data.selectedPod); const pod = new Pod(data.selectedPod);
const namespace = pod.getNs(); const namespace = pod.getNs();
const name = pod.getName(); const name = pod.getName();
return podsApi.getLogs({ namespace, name }, { const result = await podsApi.getLogs({ namespace, name }, {
...params, ...params,
timestamps: true, // Always setting timestamp to separate old logs from new ones timestamps: true, // Always setting timestamp to separate old logs from new ones
container: selectedContainer.name, container: selectedContainer.name,
previous previous
}).then(result => {
const logs = [...result.split("\n")]; // Transform them into array
logs.pop(); // Remove last empty element
return logs;
}); });
};
return result.trimEnd().split("\n");
}
/** /**
* Converts logs into a string array * Converts logs into a string array
* @returns {number} Length of log lines * @returns Length of log lines
*/ */
@computed @computed
get lines() { get lines(): number {
const id = dockStore.selectedTabId; return this.logs.length;
const logs = this.podLogs.get(id);
return logs ? logs.length : 0;
} }
/** /**
* Returns logs with timestamps for selected tab * Returns logs with timestamps for selected tab
*/ */
@computed
get logs() { get logs() {
return this.podLogs.get(dockStore.selectedTabId) ?? []; return this.podLogs.get(dockStore.selectedTabId) ?? [];
} }
@ -150,6 +159,7 @@ export class LogStore {
* Removes timestamps from each log line and returns changed logs * Removes timestamps from each log line and returns changed logs
* @returns Logs without timestamps * @returns Logs without timestamps
*/ */
@computed
get logsWithoutTimestamps() { get logsWithoutTimestamps() {
return this.logs.map(item => this.removeTimestamps(item)); return this.logs.map(item => this.removeTimestamps(item));
} }

View File

@ -20,17 +20,20 @@
*/ */
import type { LocationDescriptor } from "history"; import type { LocationDescriptor } from "history";
import { createPath } from "history";
import { matchPath, RouteProps } from "react-router"; import { matchPath, RouteProps } from "react-router";
import { PageParam, PageParamInit } from "./page-param"; import { PageParam, PageParamInit } from "./page-param";
import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route"; import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route";
import { navigation } from "./history"; import { navigation } from "./history";
export function navigate(location: LocationDescriptor) { export function navigate(location: LocationDescriptor) {
const currentLocation = navigation.location.pathname; const currentLocation = createPath(navigation.location);
navigation.push(location); navigation.push(location);
if (currentLocation === navigation.location.pathname) { const newLocation = createPath(navigation.location);
if (currentLocation === newLocation) {
navigation.goBack(); // prevent sequences of same url in history navigation.goBack(); // prevent sequences of same url in history
} }
} }