Merge branch 'master' into expose-more-catalog-apis-to-renderer
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
@ -1 +1 @@
|
||||
docs.k8slens.dev
|
||||
api-docs.k8slens.dev
|
||||
|
||||
@ -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.
|
||||
* 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.
|
||||
This documentation describes:
|
||||
|
||||
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.
|
||||
|
||||
[](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
|
||||
|
||||
[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).
|
||||
|
||||
@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
Before Width: | Height: | Size: 611 KiB |
|
Before Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 311 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 90 KiB |
@ -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.
|
||||
|
||||

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

|
||||
|
||||
## Status
|
||||
|
||||
Overview of the cluster status
|
||||
|
||||
### Cluster Status
|
||||
|
||||
Cluster status information including the detected distribution, kernel version, API endpoint, and online status
|
||||
|
||||

|
||||
|
||||
## 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`.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## Removal
|
||||
|
||||
Use this setting to remove the current cluster.
|
||||
|
||||

|
||||
@ -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.
|
||||
@ -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).
|
||||
@ -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).
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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!
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -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`:
|
||||
|
||||
```
|
||||
```sh
|
||||
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.
|
||||
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.
|
||||
|
||||
## Develop the Extension
|
||||
|
||||
@ -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).
|
||||
|
||||
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).
|
||||
|
||||
@ -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 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
|
||||
|
||||
|
||||
@ -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.
|
||||
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.
|
||||
|
||||
@ -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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
```
|
||||
@ -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):
|
||||
|
||||
[](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)
|
||||
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
@ -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>
|
||||
@ -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.
|
||||

|
||||
|
||||
|
||||
## 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.
|
||||

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

|
||||
|
||||
## 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!
|
||||
|
Before Width: | Height: | Size: 1.1 MiB |
@ -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)
|
||||
@ -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
|
||||
|
||||
...
|
||||
23
mkdocs.yml
@ -1,7 +1,7 @@
|
||||
site_name: Lens Documentation
|
||||
site_description: Documentation for Lens IDE. The only system you’ll ever need to take control of your Kubernetes clusters. It's open source and free. Download it today!
|
||||
site_name: Lens Extension API
|
||||
site_description: Documentation for Lens Extension API.
|
||||
site_author: Mirantis, Inc.
|
||||
site_url: https://docs.k8slens.dev
|
||||
site_url: https://api-docs.k8slens.dev
|
||||
docs_dir: docs/
|
||||
repo_name: GitHub
|
||||
repo_url: https://github.com/lensapp/lens
|
||||
@ -12,16 +12,7 @@ google_analytics:
|
||||
- auto
|
||||
nav:
|
||||
- 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:
|
||||
- Overview: extensions/README.md
|
||||
- Getting Started:
|
||||
- Overview: extensions/get-started/overview.md
|
||||
- Your First Extension: extensions/get-started/your-first-extension.md
|
||||
@ -43,14 +34,6 @@ nav:
|
||||
- Testing Extensions: extensions/testing-and-publishing/testing.md
|
||||
- Publishing Extensions: extensions/testing-and-publishing/publishing.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:
|
||||
name: 'material'
|
||||
highlightjs: true
|
||||
|
||||
@ -182,7 +182,7 @@
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.12.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"array-move": "^3.0.0",
|
||||
"array-move": "^3.0.1",
|
||||
"auto-bind": "^4.0.0",
|
||||
"autobind-decorator": "^2.4.0",
|
||||
"await-lock": "^2.1.0",
|
||||
|
||||
@ -19,5 +19,15 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export * from "../../common/catalog-entities";
|
||||
export * from "../../common/catalog/catalog-entity";
|
||||
import type { CatalogEntity } from "../../common/catalog";
|
||||
import { catalogEntityRegistry as registry } from "../../main/catalog";
|
||||
|
||||
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return registry.getItemsForApiKind<T>(apiVersion, kind);
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntities = new CatalogEntityRegistry();
|
||||
|
||||
@ -21,18 +21,16 @@
|
||||
|
||||
// APIs
|
||||
import * as App from "./app";
|
||||
import * as Catalog from "./catalog";
|
||||
import * as EventBus from "./event-bus";
|
||||
import * as Store from "./stores";
|
||||
import * as Util from "./utils";
|
||||
import * as Registrations from "./registrations";
|
||||
import * as Catalog from "./catalog";
|
||||
import * as Types from "./types";
|
||||
|
||||
export {
|
||||
App,
|
||||
Catalog,
|
||||
EventBus,
|
||||
Registrations,
|
||||
Catalog,
|
||||
Store,
|
||||
Types,
|
||||
Util,
|
||||
|
||||
@ -22,3 +22,5 @@
|
||||
export type IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
|
||||
export type IpcRendererEvent = Electron.IpcRendererEvent;
|
||||
export type IpcMainEvent = Electron.IpcMainEvent;
|
||||
|
||||
export * from "./registrations";
|
||||
|
||||
@ -37,7 +37,7 @@ export abstract class IpcRenderer extends IpcRegistrar {
|
||||
* a component then putting the returned value in a `disposeOnUnmount` call will suffice.
|
||||
* @param channel The channel to listen for broadcasts on
|
||||
* @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 {
|
||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||
|
||||
210
src/main/__test__/context-handler.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -19,13 +19,13 @@
|
||||
* 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 { Cluster } from "./cluster";
|
||||
import type httpProxy from "http-proxy";
|
||||
import url, { UrlWithStringQuery } from "url";
|
||||
import { CoreV1Api } from "@kubernetes/client-node";
|
||||
import { prometheusProviders } from "../common/prometheus-providers";
|
||||
import logger from "./logger";
|
||||
import { KubeAuthProxy } from "./kube-auth-proxy";
|
||||
|
||||
@ -33,7 +33,7 @@ export class ContextHandler {
|
||||
public clusterUrl: UrlWithStringQuery;
|
||||
protected kubeAuthProxy?: KubeAuthProxy;
|
||||
protected apiTarget?: httpProxy.ServerOptions;
|
||||
protected prometheusProvider: string;
|
||||
protected prometheusProvider?: string;
|
||||
protected prometheusPath: string | null;
|
||||
|
||||
constructor(protected cluster: Cluster) {
|
||||
@ -72,18 +72,34 @@ export class ContextHandler {
|
||||
this.prometheusProvider = service.id;
|
||||
}
|
||||
|
||||
return prometheusProviders.find(p => p.id === this.prometheusProvider);
|
||||
return PrometheusProviderRegistry.getInstance().getByKind(this.prometheusProvider);
|
||||
}
|
||||
|
||||
async getPrometheusService(): Promise<PrometheusService | void> {
|
||||
const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders;
|
||||
const prometheusPromises: Promise<PrometheusService | void>[] = providers.map(async provider => {
|
||||
const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
protected listPotentialProviders(): PrometheusProvider[] {
|
||||
const registry = PrometheusProviderRegistry.getInstance();
|
||||
|
||||
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> {
|
||||
|
||||
@ -22,7 +22,6 @@
|
||||
// Main process
|
||||
|
||||
import "../common/system-ca";
|
||||
import "../common/prometheus-providers";
|
||||
import * as Mobx from "mobx";
|
||||
import * as LensExtensionsCommonApi from "../extensions/common-api";
|
||||
import * as LensExtensionsMainApi from "../extensions/main-api";
|
||||
@ -57,6 +56,7 @@ import { HelmRepoManager } from "./helm/helm-repo-manager";
|
||||
import { KubeconfigSyncManager } from "./catalog-sources";
|
||||
import { handleWsUpgrade } from "./proxy/ws-upgrade";
|
||||
import configurePackages from "../common/configure-packages";
|
||||
import { PrometheusProviderRegistry, registerDefaultPrometheusProviders } from "./prometheus";
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
const cleanup = disposer();
|
||||
@ -125,6 +125,9 @@ app.on("ready", async () => {
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
PrometheusProviderRegistry.createInstance();
|
||||
registerDefaultPrometheusProviders();
|
||||
|
||||
const userStore = UserStore.createInstance();
|
||||
const clusterStore = ClusterStore.createInstance();
|
||||
const hotbarStore = HotbarStore.createInstance();
|
||||
|
||||
@ -22,32 +22,14 @@
|
||||
import { PrometheusLens } from "./lens";
|
||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||
import type { PrometheusService } from "./provider-registry";
|
||||
import logger from "../logger";
|
||||
|
||||
export class PrometheusHelm extends PrometheusLens {
|
||||
id = "helm";
|
||||
name = "Helm";
|
||||
rateAccuracy = "5m";
|
||||
readonly id: string = "helm";
|
||||
readonly name: string = "Helm";
|
||||
readonly rateAccuracy: string = "5m";
|
||||
readonly isConfigurable: boolean = false;
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> {
|
||||
const labelSelector = "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;
|
||||
}
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,16 +19,19 @@
|
||||
* 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";
|
||||
import { PrometheusHelm } from "./helm";
|
||||
import { PrometheusLens } from "./lens";
|
||||
import { PrometheusOperator } from "./operator";
|
||||
import { PrometheusProviderRegistry } from "./provider-registry";
|
||||
import { PrometheusStacklight } from "./stacklight";
|
||||
|
||||
[PrometheusLens, PrometheusHelm, PrometheusOperator, PrometheusStacklight].forEach(providerClass => {
|
||||
const provider = new providerClass();
|
||||
export * from "./provider-registry";
|
||||
|
||||
PrometheusProviderRegistry.registerProvider(provider.id, provider);
|
||||
});
|
||||
|
||||
export const prometheusProviders = PrometheusProviderRegistry.getProviders();
|
||||
export function registerDefaultPrometheusProviders() {
|
||||
PrometheusProviderRegistry
|
||||
.getInstance()
|
||||
.registerProvider(new PrometheusLens())
|
||||
.registerProvider(new PrometheusHelm())
|
||||
.registerProvider(new PrometheusOperator())
|
||||
.registerProvider(new PrometheusStacklight());
|
||||
}
|
||||
@ -19,16 +19,18 @@
|
||||
* 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 logger from "../logger";
|
||||
import { inspect } from "util";
|
||||
|
||||
export class PrometheusLens implements PrometheusProvider {
|
||||
id = "lens";
|
||||
name = "Lens";
|
||||
rateAccuracy = "1m";
|
||||
export class PrometheusLens extends PrometheusProvider {
|
||||
readonly id: string = "lens";
|
||||
readonly name: string = "Lens";
|
||||
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 {
|
||||
const resp = await client.readNamespacedService("prometheus", "lens-metrics");
|
||||
const service = resp.body;
|
||||
@ -41,66 +43,101 @@ export class PrometheusLens implements PrometheusProvider {
|
||||
};
|
||||
} catch(error) {
|
||||
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) {
|
||||
case "cluster":
|
||||
return {
|
||||
memoryUsage: `
|
||||
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}"}`),
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryCapacity: `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}]))`,
|
||||
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`,
|
||||
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{kubernetes_node=~"${opts.nodes}", mountpoint="/"}) by (kubernetes_node)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
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}"}`);
|
||||
case "memoryRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "memoryLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "cpuUsage":
|
||||
return `sum(rate(node_cpu_seconds_total{kubernetes_node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "cpuCapacity":
|
||||
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":
|
||||
return {
|
||||
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`,
|
||||
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
|
||||
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(kubernetes_node)`,
|
||||
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (kubernetes_node)`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (kubernetes_node)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (kubernetes_node)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||
case "cpuUsage":
|
||||
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":
|
||||
return {
|
||||
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "cpuUsage":
|
||||
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "memoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) 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":
|
||||
return {
|
||||
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`,
|
||||
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "diskUsage":
|
||||
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":
|
||||
const bytesSent = (ingress: string, namespace: string, statuses: string) =>
|
||||
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
|
||||
|
||||
return {
|
||||
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)`
|
||||
};
|
||||
switch (queryName) {
|
||||
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;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,98 +19,110 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import type { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry";
|
||||
import type { CoreV1Api, V1Service } from "@kubernetes/client-node";
|
||||
import logger from "../logger";
|
||||
import { PrometheusProvider, PrometheusService } from "./provider-registry";
|
||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||
import { inspect } from "util";
|
||||
|
||||
export class PrometheusOperator implements PrometheusProvider {
|
||||
rateAccuracy = "1m";
|
||||
id = "operator";
|
||||
name = "Prometheus Operator";
|
||||
export class PrometheusOperator extends PrometheusProvider {
|
||||
readonly rateAccuracy: string = "1m";
|
||||
readonly id: string = "operator";
|
||||
readonly name: string = "Prometheus Operator";
|
||||
readonly isConfigurable: boolean = false;
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void> {
|
||||
try {
|
||||
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 async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
||||
}
|
||||
|
||||
public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void {
|
||||
public getQuery(opts: Record<string, string>, queryName: string): string {
|
||||
switch(opts.category) {
|
||||
case "cluster":
|
||||
return {
|
||||
memoryUsage: `
|
||||
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}"}`),
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`,
|
||||
memoryCapacity: `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}"})`,
|
||||
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`,
|
||||
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"})`,
|
||||
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", node=~"${opts.nodes}"})`,
|
||||
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"})`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`,
|
||||
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}"})`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
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}"}`);
|
||||
case "memoryRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
|
||||
case "memoryLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"})`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"})`;
|
||||
case "cpuUsage":
|
||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"})`;
|
||||
case "cpuCapacity":
|
||||
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":
|
||||
return {
|
||||
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)`,
|
||||
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) 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)`,
|
||||
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`,
|
||||
fsUsage: `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
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)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||
case "cpuUsage":
|
||||
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":
|
||||
return {
|
||||
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "cpuUsage":
|
||||
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "memoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) 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":
|
||||
return {
|
||||
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`,
|
||||
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "diskUsage":
|
||||
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":
|
||||
const bytesSent = (ingress: string, namespace: string, statuses: string) =>
|
||||
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
|
||||
|
||||
return {
|
||||
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)`
|
||||
};
|
||||
switch (queryName) {
|
||||
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;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,58 +20,8 @@
|
||||
*/
|
||||
|
||||
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||
|
||||
export type PrometheusClusterQuery = {
|
||||
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;
|
||||
import { Singleton } from "../../common/utils";
|
||||
import logger from "../logger";
|
||||
|
||||
export type PrometheusService = {
|
||||
id: string;
|
||||
@ -80,33 +30,61 @@ export type PrometheusService = {
|
||||
port: number;
|
||||
};
|
||||
|
||||
export interface PrometheusProvider {
|
||||
id: string;
|
||||
name: string;
|
||||
getQueries(opts: PrometheusQueryOpts): PrometheusQuery | void;
|
||||
getPrometheusService(client: CoreV1Api): Promise<PrometheusService | void>;
|
||||
}
|
||||
export abstract class PrometheusProvider {
|
||||
abstract readonly id: string;
|
||||
abstract readonly name: string;
|
||||
abstract readonly rateAccuracy: string;
|
||||
abstract readonly isConfigurable: boolean;
|
||||
|
||||
export type PrometheusProviderList = {
|
||||
[key: string]: PrometheusProvider;
|
||||
};
|
||||
abstract getQuery(opts: Record<string, string>, queryName: string): string;
|
||||
abstract getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined>;
|
||||
|
||||
export class PrometheusProviderRegistry {
|
||||
private static prometheusProviders: PrometheusProviderList = {};
|
||||
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)`;
|
||||
}
|
||||
|
||||
static getProvider(type: string): PrometheusProvider {
|
||||
if (!this.prometheusProviders[type]) {
|
||||
throw "Unknown Prometheus provider";
|
||||
protected async getFirstNamespacedServer(client: CoreV1Api, ...selectors: string[]): Promise<PrometheusService | undefined> {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`${this.name}: failed to list services: ${error.toString()}`);
|
||||
}
|
||||
|
||||
return this.prometheusProviders[type];
|
||||
}
|
||||
|
||||
static registerProvider(key: string, provider: PrometheusProvider) {
|
||||
this.prometheusProviders[key] = provider;
|
||||
}
|
||||
|
||||
static getProviders(): PrometheusProvider[] {
|
||||
return Object.values(this.prometheusProviders);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class PrometheusProviderRegistry extends Singleton {
|
||||
public providers = new Map<string, PrometheusProvider>();
|
||||
|
||||
getByKind(kind: string): PrometheusProvider {
|
||||
const provider = this.providers.get(kind);
|
||||
|
||||
if (!provider) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,16 +19,18 @@
|
||||
* 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 logger from "../logger";
|
||||
import { inspect } from "util";
|
||||
|
||||
export class PrometheusStacklight implements PrometheusProvider {
|
||||
id = "stacklight";
|
||||
name = "Stacklight";
|
||||
rateAccuracy = "1m";
|
||||
export class PrometheusStacklight extends PrometheusProvider {
|
||||
readonly id: string = "stacklight";
|
||||
readonly name: string = "Stacklight";
|
||||
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 {
|
||||
const resp = await client.readNamespacedService("prometheus-server", "stacklight");
|
||||
const service = resp.body;
|
||||
@ -41,66 +43,101 @@ export class PrometheusStacklight implements PrometheusProvider {
|
||||
};
|
||||
} catch(error) {
|
||||
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) {
|
||||
case "cluster":
|
||||
return {
|
||||
memoryUsage: `
|
||||
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}"}`),
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`,
|
||||
memoryCapacity: `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}]))`,
|
||||
cpuRequests:`sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
cpuCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="cpu"}) by (component)`,
|
||||
podUsage: `sum({__name__=~"kubelet_running_pod_count|kubelet_running_pods", instance=~"${opts.nodes}"})`,
|
||||
podCapacity: `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="pods"}) by (component)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
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}"}`);
|
||||
case "memoryRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "memoryLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{node=~"${opts.nodes}", resource="memory"}) by (component)`;
|
||||
case "cpuUsage":
|
||||
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{node=~"${opts.nodes}", resource="cpu"}) by (component)`;
|
||||
case "cpuCapacity":
|
||||
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":
|
||||
return {
|
||||
memoryUsage: `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`,
|
||||
memoryCapacity: `sum(kube_node_status_capacity{resource="memory"}) by (node)`,
|
||||
cpuUsage: `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`,
|
||||
cpuCapacity: `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`,
|
||||
fsSize: `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`,
|
||||
fsUsage: `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "memoryUsage":
|
||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
||||
case "memoryCapacity":
|
||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||
case "cpuUsage":
|
||||
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":
|
||||
return {
|
||||
cpuUsage: `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
cpuRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
cpuLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryUsage: `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryRequests: `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
memoryLimits: `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="memory",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
fsUsage: `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`,
|
||||
networkReceive: `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`,
|
||||
networkTransmit: `sum(rate(container_network_transmit_bytes_total{pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "cpuUsage":
|
||||
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||
case "cpuRequests":
|
||||
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "cpuLimits":
|
||||
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}",resource="cpu",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||
case "memoryUsage":
|
||||
return `sum(container_memory_working_set_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) 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":
|
||||
return {
|
||||
diskUsage: `sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`,
|
||||
diskCapacity: `sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="${opts.pvc}",namespace="${opts.namespace}"}) by (persistentvolumeclaim, namespace)`
|
||||
};
|
||||
switch (queryName) {
|
||||
case "diskUsage":
|
||||
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":
|
||||
const bytesSent = (ingress: string, namespace: string, statuses: string) =>
|
||||
`sum(rate(nginx_ingress_controller_bytes_sent_sum{ingress="${ingress}",namespace="${namespace}",status=~"${statuses}"}[${this.rateAccuracy}])) by (ingress, namespace)`;
|
||||
|
||||
return {
|
||||
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)`
|
||||
};
|
||||
switch (queryName) {
|
||||
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;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown query name ${inspect(queryName, false, undefined, false)} for category: ${inspect(opts.category, false, undefined, false)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,6 +177,7 @@ export class Router {
|
||||
|
||||
// Metrics API
|
||||
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
|
||||
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
|
||||
|
||||
// Port-forward API
|
||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward);
|
||||
|
||||
@ -26,6 +26,7 @@ import { Cluster, ClusterMetadataKey } from "../cluster";
|
||||
import type { ClusterPrometheusMetadata } from "../../common/cluster-store";
|
||||
import logger from "../logger";
|
||||
import { getMetrics } from "../k8s-request";
|
||||
import { PrometheusProviderRegistry } from "../prometheus";
|
||||
|
||||
export type IMetricsQuery = string | string[] | {
|
||||
[metricName: string]: string;
|
||||
@ -62,6 +63,12 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa
|
||||
return Promise.all(queries.map(loadMetric));
|
||||
}
|
||||
|
||||
interface MetricProviderInfo {
|
||||
name: string;
|
||||
id: string;
|
||||
isConfigurable: boolean;
|
||||
}
|
||||
|
||||
export class MetricsRoute {
|
||||
static async routeMetrics({ response, cluster, payload, query }: LensApiRequest) {
|
||||
const queryParams: IMetricsQuery = Object.fromEntries(query.entries());
|
||||
@ -92,9 +99,10 @@ export class MetricsRoute {
|
||||
|
||||
respondJson(response, data);
|
||||
} else {
|
||||
const queries = Object.entries(payload).map(([queryName, queryOpts]) => (
|
||||
(prometheusProvider.getQueries(queryOpts) as Record<string, string>)[queryName]
|
||||
));
|
||||
const queries = Object.entries<Record<string, string>>(payload)
|
||||
.map(([queryName, queryOpts]) => (
|
||||
prometheusProvider.getQuery(queryOpts, queryName)
|
||||
));
|
||||
const result = await loadMetrics(queries, cluster, prometheusPath, queryParams);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,12 @@ export interface IMetricsResult {
|
||||
values: [number, string][];
|
||||
}
|
||||
|
||||
export interface MetricProviderInfo {
|
||||
name: string;
|
||||
id: string;
|
||||
isConfigurable: boolean;
|
||||
}
|
||||
|
||||
export interface IMetricsReqParams {
|
||||
start?: number | string; // timestamp in seconds or valid date-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 {
|
||||
|
||||
@ -52,39 +52,56 @@ export interface INodeMetrics<T = IMetrics> {
|
||||
|
||||
export interface Node {
|
||||
spec: {
|
||||
podCIDR: string;
|
||||
externalID: string;
|
||||
podCIDR?: string;
|
||||
podCIDRs?: string[];
|
||||
providerID?: string;
|
||||
/**
|
||||
* @deprecated see https://issues.k8s.io/61966
|
||||
*/
|
||||
externalID?: string;
|
||||
taints?: {
|
||||
key: string;
|
||||
value: string;
|
||||
effect: string;
|
||||
timeAdded: string;
|
||||
}[];
|
||||
unschedulable?: boolean;
|
||||
};
|
||||
status: {
|
||||
capacity: {
|
||||
capacity?: {
|
||||
cpu: string;
|
||||
["ephemeral-storage"]: string;
|
||||
["hugepages-1Gi"]: string;
|
||||
["hugepages-2Mi"]: string;
|
||||
memory: string;
|
||||
pods: string;
|
||||
};
|
||||
allocatable: {
|
||||
allocatable?: {
|
||||
cpu: string;
|
||||
["ephemeral-storage"]: string;
|
||||
["hugepages-1Gi"]: string;
|
||||
["hugepages-2Mi"]: string;
|
||||
memory: string;
|
||||
pods: string;
|
||||
};
|
||||
conditions: {
|
||||
conditions?: {
|
||||
type: string;
|
||||
status?: string;
|
||||
status: string;
|
||||
lastHeartbeatTime?: string;
|
||||
lastTransitionTime?: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
}[];
|
||||
addresses: {
|
||||
addresses?: {
|
||||
type: string;
|
||||
address: string;
|
||||
}[];
|
||||
nodeInfo: {
|
||||
daemonEndpoints?: {
|
||||
kubeletEndpoint: {
|
||||
Port: number; //it must be uppercase for backwards compatibility
|
||||
}
|
||||
}
|
||||
nodeInfo?: {
|
||||
machineID: string;
|
||||
systemUUID: string;
|
||||
bootID: string;
|
||||
@ -96,9 +113,14 @@ export interface Node {
|
||||
operatingSystem: string;
|
||||
architecture: string;
|
||||
};
|
||||
images: {
|
||||
images?: {
|
||||
names: string[];
|
||||
sizeBytes: number;
|
||||
sizeBytes?: number;
|
||||
}[];
|
||||
volumesInUse?: string[];
|
||||
volumesAttached?: {
|
||||
name: string;
|
||||
devicePath: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
48
src/renderer/components/+nodes/node-details-resources.scss
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.NodeDetailsResources {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/renderer/components/+nodes/node-details-resources.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -40,6 +40,8 @@ import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
|
||||
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
|
||||
import { ClusterStore } from "../../../common/cluster-store";
|
||||
import { NodeDetailsResources } from "./node-details-resources";
|
||||
import { DrawerTitle } from "../drawer/drawer-title";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<Node> {
|
||||
}
|
||||
@ -64,7 +66,7 @@ export class NodeDetails extends React.Component<Props> {
|
||||
|
||||
if (!node) return null;
|
||||
const { status } = node;
|
||||
const { nodeInfo, addresses, capacity, allocatable } = status;
|
||||
const { nodeInfo, addresses } = status;
|
||||
const conditions = node.getActiveConditions();
|
||||
const taints = node.getTaints();
|
||||
const childPods = podsStore.getPodsByNode(node.getName());
|
||||
@ -88,16 +90,6 @@ export class NodeDetails extends React.Component<Props> {
|
||||
</ResourceMetrics>
|
||||
)}
|
||||
<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 &&
|
||||
<DrawerItem name="Addresses">
|
||||
{
|
||||
@ -167,6 +159,10 @@ export class NodeDetails extends React.Component<Props> {
|
||||
}
|
||||
</DrawerItem>
|
||||
}
|
||||
<DrawerTitle title="Capacity"/>
|
||||
<NodeDetailsResources node={node} type={"capacity"}/>
|
||||
<DrawerTitle title="Allocatable"/>
|
||||
<NodeDetailsResources node={node} type={"allocatable"}/>
|
||||
<PodDetailsList
|
||||
pods={childPods}
|
||||
owner={node}
|
||||
|
||||
@ -21,18 +21,14 @@
|
||||
|
||||
import React from "react";
|
||||
import { observer, disposeOnUnmount } from "mobx-react";
|
||||
import { prometheusProviders } from "../../../../common/prometheus-providers";
|
||||
import type { Cluster } from "../../../../main/cluster";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { Select, SelectOption } from "../../select";
|
||||
import { Input } from "../../input";
|
||||
import { observable, computed, autorun, makeObservable } from "mobx";
|
||||
import { productName } from "../../../../common/vars";
|
||||
|
||||
const options: SelectOption<string>[] = [
|
||||
{ value: "", label: "Auto detect" },
|
||||
...prometheusProviders.map(pp => ({value: pp.id, label: pp.name}))
|
||||
];
|
||||
import { MetricProviderInfo, metricsApi } from "../../../api/endpoints/metrics.api";
|
||||
import { Spinner } from "../../spinner";
|
||||
|
||||
interface Props {
|
||||
cluster: Cluster;
|
||||
@ -42,16 +38,27 @@ interface Props {
|
||||
export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
@observable path = "";
|
||||
@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) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@computed get canEditPrometheusPath() {
|
||||
if (this.provider === "" || this.provider === "lens") return false;
|
||||
|
||||
return true;
|
||||
@computed get canEditPrometheusPath(): boolean {
|
||||
return Boolean(
|
||||
this.loadedOptions
|
||||
.find(opt => opt.id === this.provider)
|
||||
?.isConfigurable
|
||||
);
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
@ -110,15 +127,21 @@ export class ClusterPrometheusSetting extends React.Component<Props> {
|
||||
<>
|
||||
<section>
|
||||
<SubTitle title="Prometheus"/>
|
||||
<Select
|
||||
value={this.provider}
|
||||
onChange={({value}) => {
|
||||
this.provider = value;
|
||||
this.onSaveProvider();
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
<small className="hint">What query format is used to fetch metrics from Prometheus</small>
|
||||
{
|
||||
this.loading
|
||||
? <Spinner />
|
||||
: <>
|
||||
<Select
|
||||
value={this.provider}
|
||||
onChange={({ value }) => {
|
||||
this.provider = value;
|
||||
this.onSaveProvider();
|
||||
}}
|
||||
options={this.options}
|
||||
/>
|
||||
<small className="hint">What query format is used to fetch metrics from Prometheus</small>
|
||||
</>
|
||||
}
|
||||
</section>
|
||||
{this.canEditPrometheusPath && (
|
||||
<section>
|
||||
|
||||
@ -55,6 +55,20 @@ export class LogStore {
|
||||
}, { 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
|
||||
* Each time it increasing it's number, caused to fetch more logs.
|
||||
@ -70,14 +84,8 @@ export class LogStore {
|
||||
|
||||
this.refresher.start();
|
||||
this.podLogs.set(tabId, logs);
|
||||
} catch ({error}) {
|
||||
const message = [
|
||||
`Failed to load logs: ${error.message}`,
|
||||
`Reason: ${error.reason} (${error.code})`
|
||||
];
|
||||
|
||||
this.refresher.stop();
|
||||
this.podLogs.set(tabId, message);
|
||||
} catch (error) {
|
||||
this.handlerError(tabId, error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -88,14 +96,21 @@ export class LogStore {
|
||||
* @param tabId
|
||||
*/
|
||||
loadMore = async (tabId: TabId) => {
|
||||
if (!this.podLogs.get(tabId).length) return;
|
||||
const oldLogs = this.podLogs.get(tabId);
|
||||
const logs = await this.loadLogs(tabId, {
|
||||
sinceTime: this.getLastSinceTime(tabId)
|
||||
});
|
||||
if (!this.podLogs.get(tabId).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add newly received logs to bottom
|
||||
this.podLogs.set(tabId, [...oldLogs, ...logs]);
|
||||
try {
|
||||
const oldLogs = this.podLogs.get(tabId);
|
||||
const logs = await this.loadLogs(tabId, {
|
||||
sinceTime: this.getLastSinceTime(tabId)
|
||||
});
|
||||
|
||||
// Add newly received logs to bottom
|
||||
this.podLogs.set(tabId, [...oldLogs, ...logs]);
|
||||
} catch (error) {
|
||||
this.handlerError(tabId, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -103,45 +118,39 @@ export class LogStore {
|
||||
* an API request
|
||||
* @param tabId
|
||||
* @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 { selectedContainer, previous } = data;
|
||||
const pod = new Pod(data.selectedPod);
|
||||
const namespace = pod.getNs();
|
||||
const name = pod.getName();
|
||||
|
||||
return podsApi.getLogs({ namespace, name }, {
|
||||
const result = await podsApi.getLogs({ namespace, name }, {
|
||||
...params,
|
||||
timestamps: true, // Always setting timestamp to separate old logs from new ones
|
||||
container: selectedContainer.name,
|
||||
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
|
||||
* @returns {number} Length of log lines
|
||||
* @returns Length of log lines
|
||||
*/
|
||||
@computed
|
||||
get lines() {
|
||||
const id = dockStore.selectedTabId;
|
||||
const logs = this.podLogs.get(id);
|
||||
|
||||
return logs ? logs.length : 0;
|
||||
get lines(): number {
|
||||
return this.logs.length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns logs with timestamps for selected tab
|
||||
*/
|
||||
@computed
|
||||
get logs() {
|
||||
return this.podLogs.get(dockStore.selectedTabId) ?? [];
|
||||
}
|
||||
@ -150,6 +159,7 @@ export class LogStore {
|
||||
* Removes timestamps from each log line and returns changed logs
|
||||
* @returns Logs without timestamps
|
||||
*/
|
||||
@computed
|
||||
get logsWithoutTimestamps() {
|
||||
return this.logs.map(item => this.removeTimestamps(item));
|
||||
}
|
||||
|
||||
@ -20,17 +20,20 @@
|
||||
*/
|
||||
|
||||
import type { LocationDescriptor } from "history";
|
||||
import { createPath } from "history";
|
||||
import { matchPath, RouteProps } from "react-router";
|
||||
import { PageParam, PageParamInit } from "./page-param";
|
||||
import { clusterViewRoute, IClusterViewRouteParams } from "../components/cluster-manager/cluster-view.route";
|
||||
import { navigation } from "./history";
|
||||
|
||||
export function navigate(location: LocationDescriptor) {
|
||||
const currentLocation = navigation.location.pathname;
|
||||
const currentLocation = createPath(navigation.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
|
||||
}
|
||||
}
|
||||
|
||||
@ -2602,10 +2602,10 @@ array-includes@^3.1.1:
|
||||
es-abstract "^1.17.0"
|
||||
is-string "^1.0.5"
|
||||
|
||||
array-move@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-move/-/array-move-3.0.0.tgz#b646a2f4980be78f04d28d7572a72036150d364e"
|
||||
integrity sha512-kqK1ZKiAVfIdfiJjC3zpAGPg3OEkjeeKuOILwS1b+oh34dI6GTg9szgRT+oKWw48RuVF8RGjlWCSYkn6NU+Jvw==
|
||||
array-move@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-move/-/array-move-3.0.1.tgz#179645cc0987b65953a4fc06b6df9045e4ba9618"
|
||||
integrity sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==
|
||||
|
||||
array-union@^1.0.1:
|
||||
version "1.0.2"
|
||||
|
||||