mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into bundle-kubectl-1.20.2
This commit is contained in:
commit
3a39c66be7
11
.github/actions/add-card-to-project/Dockerfile
vendored
Normal file
11
.github/actions/add-card-to-project/Dockerfile
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Container image that runs your code
|
||||||
|
FROM alpine:3.10
|
||||||
|
|
||||||
|
RUN apk add --no-cache --no-progress curl jq
|
||||||
|
|
||||||
|
# Copies your code file from your action repository to the filesystem path `/` of the container
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Code file to execute when the docker container starts up (`entrypoint.sh`)
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
23
.github/actions/add-card-to-project/action.yml
vendored
Normal file
23
.github/actions/add-card-to-project/action.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: 'add_card_to_project'
|
||||||
|
description: 'A GitHub Action to add a card to a project and set the card position'
|
||||||
|
author: 'Steve Richards'
|
||||||
|
branding:
|
||||||
|
icon: 'command'
|
||||||
|
color: 'blue'
|
||||||
|
inputs:
|
||||||
|
project:
|
||||||
|
description: 'The url of the project to be assigned to.'
|
||||||
|
required: true
|
||||||
|
column_name:
|
||||||
|
description: 'The column name of the project, defaults to "To do" for issues and "In progress" for pull requests.'
|
||||||
|
required: false
|
||||||
|
card_position:
|
||||||
|
description: 'The card position of the card in the column, defaults to "bottom". Valid values are "top", "bottom" and "after:<card id>".'
|
||||||
|
required: false
|
||||||
|
runs:
|
||||||
|
using: 'docker'
|
||||||
|
image: 'Dockerfile'
|
||||||
|
args:
|
||||||
|
- ${{ inputs.project }}
|
||||||
|
- ${{ inputs.column_name }}
|
||||||
|
- ${{ inputs.card_position }}
|
||||||
157
.github/actions/add-card-to-project/entrypoint.sh
vendored
Normal file
157
.github/actions/add-card-to-project/entrypoint.sh
vendored
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
#!/bin/sh -l
|
||||||
|
|
||||||
|
PROJECT_URL="$INPUT_PROJECT"
|
||||||
|
if [ -z "$PROJECT_URL" ]; then
|
||||||
|
echo "PROJECT_URL is not defined." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
get_project_type() {
|
||||||
|
_PROJECT_URL="$1"
|
||||||
|
|
||||||
|
case "$_PROJECT_URL" in
|
||||||
|
https://github.com/orgs/*)
|
||||||
|
echo "org"
|
||||||
|
;;
|
||||||
|
https://github.com/users/*)
|
||||||
|
echo "user"
|
||||||
|
;;
|
||||||
|
https://github.com/*/projects/*)
|
||||||
|
echo "repo"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid PROJECT_URL: $_PROJECT_URL" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
unset _PROJECT_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
find_project_id() {
|
||||||
|
_PROJECT_TYPE="$1"
|
||||||
|
_PROJECT_URL="$2"
|
||||||
|
|
||||||
|
case "$_PROJECT_TYPE" in
|
||||||
|
org)
|
||||||
|
_ORG_NAME=$(echo "$_PROJECT_URL" | sed -e 's@https://github.com/orgs/\([^/]\+\)/projects/[0-9]\+@\1@')
|
||||||
|
_ENDPOINT="https://api.github.com/orgs/$_ORG_NAME/projects"
|
||||||
|
;;
|
||||||
|
user)
|
||||||
|
_USER_NAME=$(echo "$_PROJECT_URL" | sed -e 's@https://github.com/users/\([^/]\+\)/projects/[0-9]\+@\1@')
|
||||||
|
_ENDPOINT="https://api.github.com/users/$_USER_NAME/projects"
|
||||||
|
;;
|
||||||
|
repo)
|
||||||
|
_ENDPOINT="https://api.github.com/repos/$GITHUB_REPOSITORY/projects"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
_PROJECTS=$(curl -s -X GET -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
"$_ENDPOINT")
|
||||||
|
|
||||||
|
_PROJECTID=$(echo "$_PROJECTS" | jq -r ".[] | select(.html_url == \"$_PROJECT_URL\").id")
|
||||||
|
|
||||||
|
if [ "$_PROJECTID" != "" ]; then
|
||||||
|
echo "$_PROJECTID"
|
||||||
|
else
|
||||||
|
echo "No project was found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset _PROJECT_TYPE _PROJECT_URL _ORG_NAME _USER_NAME _ENDPOINT _PROJECTS _PROJECTID
|
||||||
|
}
|
||||||
|
|
||||||
|
find_column_id() {
|
||||||
|
_PROJECT_ID="$1"
|
||||||
|
_INITIAL_COLUMN_NAME="$2"
|
||||||
|
|
||||||
|
_COLUMNS=$(curl -s -X GET -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
"https://api.github.com/projects/$_PROJECT_ID/columns")
|
||||||
|
|
||||||
|
|
||||||
|
echo "$_COLUMNS" | jq -r ".[] | select(.name == \"$_INITIAL_COLUMN_NAME\").id"
|
||||||
|
unset _PROJECT_ID _INITIAL_COLUMN_NAME _COLUMNS
|
||||||
|
}
|
||||||
|
|
||||||
|
PROJECT_TYPE=$(get_project_type "${PROJECT_URL:?<Error> required this environment variable}")
|
||||||
|
|
||||||
|
if [ "$PROJECT_TYPE" = org ] || [ "$PROJECT_TYPE" = user ]; then
|
||||||
|
if [ -z "$MY_GITHUB_TOKEN" ]; then
|
||||||
|
echo "MY_GITHUB_TOKEN not defined" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN="$MY_GITHUB_TOKEN" # It's User's personal access token. It should be secret.
|
||||||
|
else
|
||||||
|
if [ -z "$GITHUB_TOKEN" ]; then
|
||||||
|
echo "GITHUB_TOKEN not defined" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN="$GITHUB_TOKEN" # GitHub sets. The scope in only the repository containing the workflow file.
|
||||||
|
fi
|
||||||
|
|
||||||
|
INITIAL_COLUMN_NAME="$INPUT_COLUMN_NAME"
|
||||||
|
if [ -z "$INITIAL_COLUMN_NAME" ]; then
|
||||||
|
# assing the column name by default
|
||||||
|
INITIAL_COLUMN_NAME='To do'
|
||||||
|
if [ "$GITHUB_EVENT_NAME" == "pull_request" ] || [ "$GITHUB_EVENT_NAME" == "pull_request_target" ]; then
|
||||||
|
echo "changing col name for PR event"
|
||||||
|
INITIAL_COLUMN_NAME='In progress'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ID=$(find_project_id "$PROJECT_TYPE" "$PROJECT_URL")
|
||||||
|
INITIAL_COLUMN_ID=$(find_column_id "$PROJECT_ID" "${INITIAL_COLUMN_NAME:?<Error> required this environment variable}")
|
||||||
|
|
||||||
|
if [ -z "$INITIAL_COLUMN_ID" ]; then
|
||||||
|
echo "INITIAL_COLUMN_ID is not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$GITHUB_EVENT_NAME" in
|
||||||
|
issues)
|
||||||
|
ISSUE_ID=$(jq -r '.issue.id' < "$GITHUB_EVENT_PATH")
|
||||||
|
|
||||||
|
# Add this issue to the project column
|
||||||
|
_CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
-d "{\"content_type\": \"Issue\", \"content_id\": $ISSUE_ID}" \
|
||||||
|
"https://api.github.com/projects/columns/$INITIAL_COLUMN_ID/cards")
|
||||||
|
;;
|
||||||
|
pull_request|pull_request_target)
|
||||||
|
PULL_REQUEST_ID=$(jq -r '.pull_request.id' < "$GITHUB_EVENT_PATH")
|
||||||
|
|
||||||
|
# Add this pull_request to the project column
|
||||||
|
_CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
-d "{\"content_type\": \"PullRequest\", \"content_id\": $PULL_REQUEST_ID}" \
|
||||||
|
"https://api.github.com/projects/columns/$INITIAL_COLUMN_ID/cards")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Nothing to be done on this action: $GITHUB_EVENT_NAME" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CARDS_ID=$(echo "$_CARDS" | jq -r .id)
|
||||||
|
unset _CARDS
|
||||||
|
|
||||||
|
if [ -z "$CARDS_ID" ]; then
|
||||||
|
echo "CARDS_ID is not found." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INITIAL_CARD_POSITION="$INPUT_CARD_POSITION"
|
||||||
|
if [ -z "$INITIAL_CARD_POSITION" ]; then
|
||||||
|
# assign the card position by default
|
||||||
|
INITIAL_CARD_POSITION='bottom'
|
||||||
|
fi
|
||||||
|
|
||||||
|
_CARDS=$(curl -s -X POST -u "$GITHUB_ACTOR:$TOKEN" --retry 3 \
|
||||||
|
-H 'Accept: application/vnd.github.inertia-preview+json' \
|
||||||
|
-d "{\"position\": \"$INITIAL_CARD_POSITION\"}" \
|
||||||
|
"https://api.github.com/projects/columns/cards/$CARDS_ID/moves")
|
||||||
33
.github/workflows/add-to-project-board.yaml
vendored
Normal file
33
.github/workflows/add-to-project-board.yaml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: Add Card to Project(s)
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened]
|
||||||
|
env:
|
||||||
|
MY_GITHUB_TOKEN: ${{ secrets.PROJECT_GITHUB_TOKEN }}
|
||||||
|
EVENT_TYPE: $GITHUB_EVENT_NAME
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
add_card_to_project:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Add Card to Project(s)
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Get Event Type
|
||||||
|
run: echo $GITHUB_EVENT_NAME
|
||||||
|
- name: Assign NEW issues to project 1
|
||||||
|
uses: ./.github/actions/add-card-to-project
|
||||||
|
if: github.event_name == 'issues' && github.event.action == 'opened'
|
||||||
|
with:
|
||||||
|
project: 'https://github.com/orgs/lensapp/projects/1'
|
||||||
|
column_name: 'Backlog'
|
||||||
|
card_position: 'bottom'
|
||||||
|
- name: Assign NEW pull requests to project 1
|
||||||
|
uses: ./.github/actions/add-card-to-project
|
||||||
|
if: github.event_name == 'pull_request_target' && github.event.action == 'opened'
|
||||||
|
with:
|
||||||
|
project: 'https://github.com/orgs/lensapp/projects/1'
|
||||||
|
column_name: 'PRs'
|
||||||
|
card_position: 'bottom'
|
||||||
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '41 3 * * 2'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
@ -77,15 +77,12 @@ jobs:
|
|||||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||||
name: Install integration test dependencies
|
name: Install integration test dependencies
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
|
||||||
|
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration
|
||||||
name: Run Linux integration tests
|
name: Run Linux integration tests
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
|
|
||||||
- run: make integration-win
|
- run: make integration
|
||||||
name: Run Windows integration tests
|
name: Run integration tests
|
||||||
shell: bash
|
shell: bash
|
||||||
if: runner.os == 'Windows'
|
if: runner.os != 'Linux'
|
||||||
|
|
||||||
- run: make integration-mac
|
|
||||||
name: Run macOS integration tests
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
|
|||||||
19
Makefile
19
Makefile
@ -50,21 +50,8 @@ tag-release:
|
|||||||
test: binaries/client
|
test: binaries/client
|
||||||
yarn run jest $(or $(CMD_ARGS), "src")
|
yarn run jest $(or $(CMD_ARGS), "src")
|
||||||
|
|
||||||
.PHONY: integration-linux
|
.PHONY: integration
|
||||||
integration-linux: binaries/client build-extension-types build-extensions
|
integration: build
|
||||||
yarn build:linux
|
|
||||||
yarn integration
|
|
||||||
|
|
||||||
.PHONY: integration-mac
|
|
||||||
integration-mac: binaries/client build-extension-types build-extensions
|
|
||||||
# rm ${HOME}/Library/Application\ Support/Lens
|
|
||||||
yarn build:mac
|
|
||||||
yarn integration
|
|
||||||
|
|
||||||
.PHONY: integration-win
|
|
||||||
integration-win: binaries/client build-extension-types build-extensions
|
|
||||||
# rm %APPDATA%/Lens
|
|
||||||
yarn build:win
|
|
||||||
yarn integration
|
yarn integration
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
@ -73,6 +60,8 @@ build: node_modules binaries/client
|
|||||||
$(MAKE) build-extensions -B
|
$(MAKE) build-extensions -B
|
||||||
yarn run compile
|
yarn run compile
|
||||||
ifeq "$(DETECTED_OS)" "Windows"
|
ifeq "$(DETECTED_OS)" "Windows"
|
||||||
|
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
||||||
|
rm -rf node_modules/win-ca/pem
|
||||||
yarn run electron-builder --publish onTag --x64 --ia32
|
yarn run electron-builder --publish onTag --x64 --ia32
|
||||||
else
|
else
|
||||||
yarn run electron-builder --publish onTag
|
yarn run electron-builder --publish onTag
|
||||||
|
|||||||
32
__mocks__/windowMock.ts
Normal file
32
__mocks__/windowMock.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Object.defineProperty(window, "requestIdleCallback", {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn().mockImplementation(callback => callback()),
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, "cancelIdleCallback", {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {};
|
||||||
@ -32,6 +32,8 @@ function getBuildChannel(): string {
|
|||||||
switch (versionInfo.prerelease?.[0]) {
|
switch (versionInfo.prerelease?.[0]) {
|
||||||
case "beta":
|
case "beta":
|
||||||
return "beta";
|
return "beta";
|
||||||
|
case undefined:
|
||||||
|
return "latest";
|
||||||
default:
|
default:
|
||||||
return "alpha";
|
return "alpha";
|
||||||
}
|
}
|
||||||
@ -57,16 +59,16 @@ async function writeOutNewVersions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
if (versionInfo.prerelease && versionInfo.prerelease.length > 1) {
|
|
||||||
const prereleaseParts: string[] = [getBuildChannel()];
|
const prereleaseParts: string[] = [getBuildChannel()];
|
||||||
|
|
||||||
|
if (versionInfo.prerelease && versionInfo.prerelease.length > 1) {
|
||||||
prereleaseParts.push(versionInfo.prerelease[1].toString());
|
prereleaseParts.push(versionInfo.prerelease[1].toString());
|
||||||
prereleaseParts.push(buildNumber);
|
|
||||||
appInfo.version = `${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}-${prereleaseParts.join(".")}`;
|
|
||||||
} else {
|
|
||||||
appInfo.version = `${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}+${buildNumber}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prereleaseParts.push(buildNumber);
|
||||||
|
|
||||||
|
appInfo.version = `${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}-${prereleaseParts.join(".")}`;
|
||||||
|
|
||||||
writeOutNewVersions()
|
writeOutNewVersions()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ In this section you will learn how this extension works under the hood.
|
|||||||
The Hello World sample extension does three things:
|
The Hello World sample extension does three things:
|
||||||
|
|
||||||
- Implements `onActivate()` and outputs a message to the console.
|
- Implements `onActivate()` and outputs a message to the console.
|
||||||
- Implements `onDectivate()` and outputs a message to the console.
|
- Implements `onDeactivate()` and outputs a message to the console.
|
||||||
- Registers `ClusterPage` so that the page is visible in the left-side menu of the cluster dashboard.
|
- Registers `ClusterPage` so that the page is visible in the left-side menu of the cluster dashboard.
|
||||||
|
|
||||||
Let's take a closer look at our Hello World sample's source code and see how these three things are achieved.
|
Let's take a closer look at our Hello World sample's source code and see how these three things are achieved.
|
||||||
|
|||||||
@ -72,4 +72,4 @@ To dive deeper, consider looking at [Common Capabilities](../capabilities/common
|
|||||||
If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues).
|
If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues).
|
||||||
You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing).
|
You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing).
|
||||||
|
|
||||||
The Generator source code is hosted at [Github](https://github.com/lensapp/generator-lens-ext).
|
The Generator source code is hosted at [GitHub](https://github.com/lensapp/generator-lens-ext).
|
||||||
|
|||||||
@ -213,7 +213,7 @@ export class CertificatePage extends React.Component<{ extension: LensRendererEx
|
|||||||
return (
|
return (
|
||||||
<TabLayout>
|
<TabLayout>
|
||||||
<KubeObjectListLayout
|
<KubeObjectListLayout
|
||||||
className="Certicates" store={certificatesStore}
|
className="Certificates" store={certificatesStore}
|
||||||
sortingCallbacks={{
|
sortingCallbacks={{
|
||||||
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
||||||
[sortBy.namespace]: (certificate: Certificate) => certificate.metadata.namespace,
|
[sortBy.namespace]: (certificate: Certificate) => certificate.metadata.namespace,
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
|||||||
```
|
```
|
||||||
|
|
||||||
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
||||||
This page would be defined in your extension's `Renderer.LensExtension` implmentation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
||||||
|
|
||||||
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ This is the cluster that the resource stack will be applied to, and the construc
|
|||||||
Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
||||||
`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
|
`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
|
||||||
|
|
||||||
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utiliize the kubernetes api to inspect the resource stack status.
|
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utilize the kubernetes api to inspect the resource stack status.
|
||||||
`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
|
`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
|
||||||
This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
|
This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
|
||||||
|
|
||||||
|
|||||||
@ -148,8 +148,8 @@ export class ExamplePreferenceInput extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="I understand appPreferences"
|
label="I understand appPreferences"
|
||||||
value={ExamplePreferencesStore.getInstace().enabled}
|
value={ExamplePreferencesStore.getInstance().enabled}
|
||||||
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
onChange={v => { ExamplePreferencesStore.getInstance().enabled = v; }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,4 +43,4 @@ Say you have your project folder at `~/my-extension/` and you want to create an
|
|||||||
npm pack
|
npm pack
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create a NPM tarball that can be hosted on Github Releases or any other publicly available file hosting service.
|
This will create a NPM tarball that can be hosted on GitHub Releases or any other publicly available file hosting service.
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "kube-object-event-status",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Adds kube object status from events",
|
|
||||||
"renderer": "dist/renderer.js",
|
|
||||||
"lens": {
|
|
||||||
"metadata": {},
|
|
||||||
"styles": []
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack && npm pack",
|
|
||||||
"dev": "webpack --watch",
|
|
||||||
"test": "echo NO TESTS"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/**/*"
|
|
||||||
],
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
|
||||||
"ts-loader": "^8.0.4",
|
|
||||||
"typescript": "^4.0.3",
|
|
||||||
"webpack": "^4.44.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
67
package.json
67
package.json
@ -3,12 +3,13 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.0.0",
|
"version": "5.1.4",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "OpenLens Authors"
|
"name": "OpenLens Authors",
|
||||||
|
"email": "info@k8slens.dev"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*",
|
"dev": "concurrently -i -k \"yarn run dev-run -C\" yarn:dev:*",
|
||||||
@ -48,7 +49,8 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"bundledKubectlVersion": "1.21.2",
|
"bundledKubectlVersion": "1.21.2",
|
||||||
"bundledHelmVersion": "3.5.4"
|
"bundledHelmVersion": "3.5.4",
|
||||||
|
"sentryDsn": ""
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12 <13"
|
"node": ">=12 <13"
|
||||||
@ -62,7 +64,7 @@
|
|||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
|
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
|
||||||
"^@lingui/macro$": "<rootDir>/__mocks__/@linguiMacro.ts"
|
"src/(.*)": "<rootDir>/__mocks__/windowMock.ts"
|
||||||
},
|
},
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
"<rootDir>/dist",
|
"<rootDir>/dist",
|
||||||
@ -182,6 +184,8 @@
|
|||||||
"@hapi/call": "^8.0.1",
|
"@hapi/call": "^8.0.1",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.14.3",
|
"@kubernetes/client-node": "^0.14.3",
|
||||||
|
"@sentry/electron": "^2.5.0",
|
||||||
|
"@sentry/integrations": "^6.8.0",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
"array-move": "^3.0.1",
|
"array-move": "^3.0.1",
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
@ -197,8 +201,8 @@
|
|||||||
"electron-updater": "^4.3.1",
|
"electron-updater": "^4.3.1",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"filehound": "^1.17.4",
|
"filehound": "^1.17.4",
|
||||||
"filenamify": "^4.1.0",
|
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
|
"glob-to-regexp": "^0.4.1",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
@ -208,23 +212,23 @@
|
|||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"jsonpath": "^1.0.2",
|
"jsonpath": "^1.0.2",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"mac-ca": "^1.0.4",
|
"mac-ca": "^1.0.6",
|
||||||
"marked": "^2.0.3",
|
"marked": "^2.0.3",
|
||||||
"md5-file": "^5.0.0",
|
"md5-file": "^5.0.0",
|
||||||
"mobx": "^6.3.0",
|
"mobx": "^6.3.0",
|
||||||
"mobx-observable-history": "^2.0.1",
|
"mobx-observable-history": "^2.0.1",
|
||||||
"mobx-react": "^7.1.0",
|
"mobx-react": "^7.1.0",
|
||||||
"mock-fs": "^4.12.0",
|
"mock-fs": "^4.14.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"moment-timezone": "^0.5.33",
|
"moment-timezone": "^0.5.33",
|
||||||
"node-pty": "^0.9.0",
|
"node-pty": "^0.10.1",
|
||||||
"npm": "^6.14.8",
|
"npm": "^6.14.8",
|
||||||
"openid-client": "^3.15.2",
|
"openid-client": "^3.15.2",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"path-to-regexp": "^6.1.0",
|
"path-to-regexp": "^6.1.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.2",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-virtualized-auto-sizer": "^1.0.5",
|
"react-virtualized-auto-sizer": "^1.0.5",
|
||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
@ -240,7 +244,7 @@
|
|||||||
"url-parse": "^1.5.1",
|
"url-parse": "^1.5.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"win-ca": "^3.2.0",
|
"win-ca": "^3.2.0",
|
||||||
"winston": "^3.2.1",
|
"winston": "^3.3.3",
|
||||||
"winston-transport-browserconsole": "^1.0.5",
|
"winston-transport-browserconsole": "^1.0.5",
|
||||||
"ws": "^7.4.6"
|
"ws": "^7.4.6"
|
||||||
},
|
},
|
||||||
@ -250,6 +254,8 @@
|
|||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
|
"@sentry/react": "^6.8.0",
|
||||||
|
"@sentry/types": "^6.8.0",
|
||||||
"@testing-library/jest-dom": "^5.13.0",
|
"@testing-library/jest-dom": "^5.13.0",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@types/byline": "^4.2.32",
|
"@types/byline": "^4.2.32",
|
||||||
@ -261,7 +267,8 @@
|
|||||||
"@types/electron-devtools-installer": "^2.2.0",
|
"@types/electron-devtools-installer": "^2.2.0",
|
||||||
"@types/electron-window-state": "^2.0.34",
|
"@types/electron-window-state": "^2.0.34",
|
||||||
"@types/fs-extra": "^9.0.1",
|
"@types/fs-extra": "^9.0.1",
|
||||||
"@types/hapi": "^18.0.5",
|
"@types/glob-to-regexp": "^0.4.1",
|
||||||
|
"@types/hapi": "^18.0.6",
|
||||||
"@types/hoist-non-react-statics": "^3.3.1",
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
"@types/html-webpack-plugin": "^3.2.3",
|
"@types/html-webpack-plugin": "^3.2.3",
|
||||||
"@types/http-proxy": "^1.17.5",
|
"@types/http-proxy": "^1.17.5",
|
||||||
@ -273,11 +280,11 @@
|
|||||||
"@types/marked": "^2.0.3",
|
"@types/marked": "^2.0.3",
|
||||||
"@types/md5-file": "^4.0.2",
|
"@types/md5-file": "^4.0.2",
|
||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
"@types/mock-fs": "^4.10.0",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/module-alias": "^2.0.0",
|
"@types/module-alias": "^2.0.0",
|
||||||
"@types/node": "12.20",
|
"@types/node": "12.20",
|
||||||
"@types/npm": "^2.0.31",
|
"@types/npm": "^2.0.31",
|
||||||
"@types/progress-bar-webpack-plugin": "^2.1.0",
|
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
"@types/proper-lockfile": "^4.1.1",
|
||||||
"@types/randomcolor": "^0.5.5",
|
"@types/randomcolor": "^0.5.5",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
@ -293,9 +300,8 @@
|
|||||||
"@types/request-promise-native": "^1.0.17",
|
"@types/request-promise-native": "^1.0.17",
|
||||||
"@types/semver": "^7.2.0",
|
"@types/semver": "^7.2.0",
|
||||||
"@types/sharp": "^0.28.3",
|
"@types/sharp": "^0.28.3",
|
||||||
"@types/shelljs": "^0.8.8",
|
|
||||||
"@types/spdy": "^3.4.4",
|
"@types/spdy": "^3.4.4",
|
||||||
"@types/tar": "^4.0.4",
|
"@types/tar": "^4.0.5",
|
||||||
"@types/tcp-port-used": "^1.0.0",
|
"@types/tcp-port-used": "^1.0.0",
|
||||||
"@types/tempy": "^0.3.0",
|
"@types/tempy": "^0.3.0",
|
||||||
"@types/url-parse": "^1.4.3",
|
"@types/url-parse": "^1.4.3",
|
||||||
@ -315,7 +321,7 @@
|
|||||||
"concurrently": "^5.2.0",
|
"concurrently": "^5.2.0",
|
||||||
"css-loader": "^5.2.6",
|
"css-loader": "^5.2.6",
|
||||||
"deepdash": "^5.3.5",
|
"deepdash": "^5.3.5",
|
||||||
"dompurify": "^2.0.11",
|
"dompurify": "^2.0.17",
|
||||||
"electron": "^9.4.4",
|
"electron": "^9.4.4",
|
||||||
"electron-builder": "^22.10.5",
|
"electron-builder": "^22.10.5",
|
||||||
"electron-notarize": "^0.3.0",
|
"electron-notarize": "^0.3.0",
|
||||||
@ -328,28 +334,27 @@
|
|||||||
"eslint-plugin-unused-imports": "^1.0.1",
|
"eslint-plugin-unused-imports": "^1.0.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"flex.box": "^3.4.4",
|
"flex.box": "^3.4.4",
|
||||||
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
"fork-ts-checker-webpack-plugin": "^5.2.1",
|
||||||
"hoist-non-react-statics": "^3.3.2",
|
"hoist-non-react-statics": "^3.3.2",
|
||||||
"html-webpack-plugin": "^4.3.0",
|
"html-webpack-plugin": "^4.5.2",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"include-media": "^1.4.9",
|
"include-media": "^1.4.9",
|
||||||
"jest": "^26.0.1",
|
"jest": "26.6.3",
|
||||||
"jest-canvas-mock": "^2.3.0",
|
"jest-canvas-mock": "^2.3.0",
|
||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.16",
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^1.6.0",
|
"mini-css-extract-plugin": "^1.6.0",
|
||||||
"node-loader": "^1.0.3",
|
"node-loader": "^1.0.3",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.12",
|
||||||
"open": "^7.3.1",
|
|
||||||
"patch-package": "^6.2.2",
|
"patch-package": "^6.2.2",
|
||||||
"postcss": "^8.2.14",
|
"postcss": "^8.2.14",
|
||||||
"postcss-loader": "~3.0.0",
|
"postcss-loader": "4.0.3",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
"progress-bar-webpack-plugin": "^2.1.0",
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"raw-loader": "^4.0.1",
|
"raw-loader": "^4.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-refresh": "^0.9.0",
|
"react-refresh": "^0.9.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
@ -361,8 +366,8 @@
|
|||||||
"sharp": "^0.26.1",
|
"sharp": "^0.26.1",
|
||||||
"spectron": "11.0.0",
|
"spectron": "11.0.0",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^2.1.2",
|
"tailwindcss": "^2.2.4",
|
||||||
"ts-jest": "26.3.0",
|
"ts-jest": "26.5.6",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"type-fest": "^1.0.2",
|
"type-fest": "^1.0.2",
|
||||||
@ -371,14 +376,14 @@
|
|||||||
"typedoc-plugin-markdown": "^3.9.0",
|
"typedoc-plugin-markdown": "^3.9.0",
|
||||||
"typeface-roboto": "^1.1.13",
|
"typeface-roboto": "^1.1.13",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.3.2",
|
||||||
"typescript-plugin-css-modules": "^3.2.0",
|
"typescript-plugin-css-modules": "^3.4.0",
|
||||||
"url-loader": "^4.1.0",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^4.44.2",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.11",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-server": "^3.11.0",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"webpack-node-externals": "^1.7.2",
|
"webpack-node-externals": "^1.7.2",
|
||||||
"what-input": "^5.2.10",
|
"what-input": "^5.2.10",
|
||||||
"xterm": "^4.12.0",
|
"xterm": "^4.12.0",
|
||||||
"xterm-addon-fit": "^0.4.0"
|
"xterm-addon-fit": "^0.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,10 +19,9 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const tailwindcss = require("tailwindcss");
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss("./tailwind.config.js")
|
require("tailwindcss/nesting"),
|
||||||
|
require("tailwindcss"),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class TestCatalogCategory2 extends CatalogCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("CatalogCategoryRegistry", () => {
|
describe("CatalogCategoryRegistry", () => {
|
||||||
it("should remove only the category registered when running the disopser", () => {
|
it("should remove only the category registered when running the disposer", () => {
|
||||||
const registry = new TestCatalogCategoryRegistry();
|
const registry = new TestCatalogCategoryRegistry();
|
||||||
|
|
||||||
expect(registry.items.length).toBe(0);
|
expect(registry.items.length).toBe(0);
|
||||||
|
|||||||
102
src/common/__tests__/event-emitter.test.ts
Normal file
102
src/common/__tests__/event-emitter.test.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 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 { EventEmitter } from "../event-emitter";
|
||||||
|
|
||||||
|
describe("EventEmitter", () => {
|
||||||
|
it("should stop early if a listener returns false", () => {
|
||||||
|
let called = false;
|
||||||
|
const e = new EventEmitter<[]>();
|
||||||
|
|
||||||
|
e.addListener(() => false, {});
|
||||||
|
e.addListener(() => { called = true; }, {});
|
||||||
|
e.emit();
|
||||||
|
|
||||||
|
expect(called).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't stop early if a listener returns 0", () => {
|
||||||
|
let called = false;
|
||||||
|
const e = new EventEmitter<[]>();
|
||||||
|
|
||||||
|
e.addListener(() => 0 as any, {});
|
||||||
|
e.addListener(() => { called = true; }, {});
|
||||||
|
e.emit();
|
||||||
|
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prepended listeners should be called before others", () => {
|
||||||
|
const callOrder: number[] = [];
|
||||||
|
const e = new EventEmitter<[]>();
|
||||||
|
|
||||||
|
e.addListener(() => { callOrder.push(1); }, {});
|
||||||
|
e.addListener(() => { callOrder.push(2); }, {});
|
||||||
|
e.addListener(() => { callOrder.push(3); }, { prepend: true });
|
||||||
|
e.emit();
|
||||||
|
|
||||||
|
expect(callOrder).toStrictEqual([3, 1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("once listeners should be called only once", () => {
|
||||||
|
const callOrder: number[] = [];
|
||||||
|
const e = new EventEmitter<[]>();
|
||||||
|
|
||||||
|
e.addListener(() => { callOrder.push(1); }, {});
|
||||||
|
e.addListener(() => { callOrder.push(2); }, {});
|
||||||
|
e.addListener(() => { callOrder.push(3); }, { once: true });
|
||||||
|
e.emit();
|
||||||
|
e.emit();
|
||||||
|
|
||||||
|
expect(callOrder).toStrictEqual([1, 2, 3, 1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeListener should stop the listener from being called", () => {
|
||||||
|
const callOrder: number[] = [];
|
||||||
|
const e = new EventEmitter<[]>();
|
||||||
|
const r = () => { callOrder.push(3); };
|
||||||
|
|
||||||
|
e.addListener(() => { callOrder.push(1); }, {});
|
||||||
|
e.addListener(() => { callOrder.push(2); }, {});
|
||||||
|
e.addListener(r);
|
||||||
|
|
||||||
|
e.emit();
|
||||||
|
e.removeListener(r);
|
||||||
|
e.emit();
|
||||||
|
|
||||||
|
expect(callOrder).toStrictEqual([1, 2, 3, 1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removeAllListeners should stop the all listeners from being called", () => {
|
||||||
|
const callOrder: number[] = [];
|
||||||
|
const e = new EventEmitter<[]>();
|
||||||
|
|
||||||
|
e.addListener(() => { callOrder.push(1); });
|
||||||
|
e.addListener(() => { callOrder.push(2); });
|
||||||
|
e.addListener(() => { callOrder.push(3); });
|
||||||
|
|
||||||
|
e.emit();
|
||||||
|
e.removeAllListeners();
|
||||||
|
e.emit();
|
||||||
|
|
||||||
|
expect(callOrder).toStrictEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -19,10 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Console } from "console";
|
|
||||||
|
|
||||||
console = new Console(process.stdout, process.stderr);
|
|
||||||
|
|
||||||
import mockFs from "mock-fs";
|
import mockFs from "mock-fs";
|
||||||
|
|
||||||
jest.mock("electron", () => {
|
jest.mock("electron", () => {
|
||||||
@ -37,26 +33,27 @@ jest.mock("electron", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { UserStore } from "../user-store";
|
import { UserStore } from "../user-store";
|
||||||
|
import { Console } from "console";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import electron from "electron";
|
import electron from "electron";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
||||||
|
import { ThemeStore } from "../../renderer/theme.store";
|
||||||
|
import type { ClusterStoreModel } from "../cluster-store";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
describe("user store tests", () => {
|
describe("user store tests", () => {
|
||||||
describe("for an empty config", () => {
|
describe("for an empty config", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
|
||||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||||
|
|
||||||
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
||||||
|
|
||||||
UserStore.getInstance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
|
UserStore.resetInstance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows setting and retrieving lastSeenAppVersion", () => {
|
it("allows setting and retrieving lastSeenAppVersion", () => {
|
||||||
@ -72,7 +69,7 @@ describe("user store tests", () => {
|
|||||||
us.httpsProxy = "abcd://defg";
|
us.httpsProxy = "abcd://defg";
|
||||||
|
|
||||||
expect(us.httpsProxy).toBe("abcd://defg");
|
expect(us.httpsProxy).toBe("abcd://defg");
|
||||||
expect(us.colorTheme).toBe(UserStore.defaultTheme);
|
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
||||||
|
|
||||||
us.colorTheme = "light";
|
us.colorTheme = "light";
|
||||||
expect(us.colorTheme).toBe("light");
|
expect(us.colorTheme).toBe("light");
|
||||||
@ -83,7 +80,7 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
us.colorTheme = "some other theme";
|
us.colorTheme = "some other theme";
|
||||||
us.resetTheme();
|
us.resetTheme();
|
||||||
expect(us.colorTheme).toBe(UserStore.defaultTheme);
|
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly calculates if the last seen version is an old release", () => {
|
it("correctly calculates if the last seen version is an old release", () => {
|
||||||
@ -98,14 +95,31 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
describe("migrations", () => {
|
describe("migrations", () => {
|
||||||
beforeEachWrapped(() => {
|
beforeEachWrapped(() => {
|
||||||
UserStore.resetInstance();
|
|
||||||
mockFs({
|
mockFs({
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"config.json": JSON.stringify({
|
"config.json": JSON.stringify({
|
||||||
user: { username: "foobar" },
|
user: { username: "foobar" },
|
||||||
preferences: { colorTheme: "light" },
|
preferences: { colorTheme: "light" },
|
||||||
lastSeenAppVersion: "1.2.3"
|
lastSeenAppVersion: "1.2.3"
|
||||||
})
|
}),
|
||||||
|
"lens-cluster-store.json": JSON.stringify({
|
||||||
|
clusters: [
|
||||||
|
{
|
||||||
|
id: "foobar",
|
||||||
|
kubeConfigPath: "tmp/extension_data/foo/bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "barfoo",
|
||||||
|
kubeConfigPath: "some/other/path",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} as ClusterStoreModel),
|
||||||
|
"extension_data": {},
|
||||||
|
},
|
||||||
|
"some": {
|
||||||
|
"other": {
|
||||||
|
"path": "is file",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -113,6 +127,7 @@ describe("user store tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
UserStore.resetInstance();
|
||||||
mockFs.restore();
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -121,5 +136,12 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.only("skips clusters for adding to kube-sync with files under extension_data/", () => {
|
||||||
|
const us = UserStore.getInstance();
|
||||||
|
|
||||||
|
expect(us.syncKubeconfigEntries.has("tmp/extension_data/foo/bar")).toBe(false);
|
||||||
|
expect(us.syncKubeconfigEntries.has("some/other/path")).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -56,9 +56,15 @@ export abstract class BaseStore<T> extends Singleton {
|
|||||||
cwd: this.cwd(),
|
cwd: this.cwd(),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`[STORE]: LOADED from ${this.path}`);
|
const res: any = this.fromStore(this.storeConfig.store);
|
||||||
this.fromStore(this.storeConfig.store);
|
|
||||||
|
if (res instanceof Promise || (typeof res === "object" && res && typeof res.then === "function")) {
|
||||||
|
console.error(`${this.name} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
|
||||||
|
}
|
||||||
|
|
||||||
this.enableSync();
|
this.enableSync();
|
||||||
|
|
||||||
|
logger.info(`[STORE]: LOADED from ${this.path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@ -157,6 +163,9 @@ export abstract class BaseStore<T> extends Singleton {
|
|||||||
/**
|
/**
|
||||||
* fromStore is called internally when a child class syncs with the file
|
* fromStore is called internally when a child class syncs with the file
|
||||||
* system.
|
* system.
|
||||||
|
*
|
||||||
|
* Note: This function **must** be synchronous.
|
||||||
|
*
|
||||||
* @param data the parsed information read from the stored JSON file
|
* @param data the parsed information read from the stored JSON file
|
||||||
*/
|
*/
|
||||||
protected abstract fromStore(data: T): void;
|
protected abstract fromStore(data: T): void;
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export interface KubernetesClusterPrometheusMetrics {
|
|||||||
export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
||||||
kubeconfigPath: string;
|
kubeconfigPath: string;
|
||||||
kubeconfigContext: string;
|
kubeconfigContext: string;
|
||||||
|
accessibleNamespaces?: string[];
|
||||||
metrics?: {
|
metrics?: {
|
||||||
source: string;
|
source: string;
|
||||||
prometheus?: KubernetesClusterPrometheusMetrics;
|
prometheus?: KubernetesClusterPrometheusMetrics;
|
||||||
@ -55,15 +56,23 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
||||||
|
distro?: string;
|
||||||
|
kubeVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
||||||
|
|
||||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||||
phase: KubernetesClusterStatusPhase;
|
phase: KubernetesClusterStatusPhase;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||||
public readonly kind = "KubernetesCluster";
|
public static readonly kind = "KubernetesCluster";
|
||||||
|
|
||||||
|
public readonly apiVersion = KubernetesCluster.apiVersion;
|
||||||
|
public readonly kind = KubernetesCluster.kind;
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (app) {
|
if (app) {
|
||||||
|
|||||||
@ -24,8 +24,10 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
|||||||
import { productName } from "../vars";
|
import { productName } from "../vars";
|
||||||
import { WeblinkStore } from "../weblink-store";
|
import { WeblinkStore } from "../weblink-store";
|
||||||
|
|
||||||
|
export type WebLinkStatusPhase = "available" | "unavailable";
|
||||||
|
|
||||||
export interface WebLinkStatus extends CatalogEntityStatus {
|
export interface WebLinkStatus extends CatalogEntityStatus {
|
||||||
phase: "available" | "unavailable";
|
phase: WebLinkStatusPhase;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WebLinkSpec = {
|
export type WebLinkSpec = {
|
||||||
|
|||||||
@ -101,6 +101,8 @@ export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
httpsProxy?: string;
|
httpsProxy?: string;
|
||||||
hiddenMetrics?: string[];
|
hiddenMetrics?: string[];
|
||||||
|
nodeShellImage?: string;
|
||||||
|
imagePullSecret?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClusterPrometheusPreferences {
|
export interface ClusterPrometheusPreferences {
|
||||||
@ -117,6 +119,8 @@ export interface ClusterPrometheusPreferences {
|
|||||||
|
|
||||||
const initialStates = "cluster:states";
|
const initialStates = "cluster:states";
|
||||||
|
|
||||||
|
export const initialNodeShellImage = "docker.io/alpine:3.13";
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
private static StateChannel = "cluster:state";
|
private static StateChannel = "cluster:state";
|
||||||
|
|
||||||
|
|||||||
@ -29,35 +29,31 @@ interface Options {
|
|||||||
type Callback<D extends [...any[]]> = (...data: D) => void | boolean;
|
type Callback<D extends [...any[]]> = (...data: D) => void | boolean;
|
||||||
|
|
||||||
export class EventEmitter<D extends [...any[]]> {
|
export class EventEmitter<D extends [...any[]]> {
|
||||||
protected listeners = new Map<Callback<D>, Options>();
|
protected listeners: [Callback<D>, Options][] = [];
|
||||||
|
|
||||||
addListener(callback: Callback<D>, options: Options = {}) {
|
addListener(callback: Callback<D>, options: Options = {}) {
|
||||||
if (options.prepend) {
|
const fn = options.prepend ? "unshift" : "push";
|
||||||
const listeners = [...this.listeners];
|
|
||||||
|
|
||||||
listeners.unshift([callback, options]);
|
this.listeners[fn]([callback, options]);
|
||||||
this.listeners = new Map(listeners);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.listeners.set(callback, options);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListener(callback: Callback<D>) {
|
removeListener(callback: Callback<D>) {
|
||||||
this.listeners.delete(callback);
|
this.listeners = this.listeners.filter(([cb]) => cb !== callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAllListeners() {
|
removeAllListeners() {
|
||||||
this.listeners.clear();
|
this.listeners.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(...data: D) {
|
emit(...data: D) {
|
||||||
[...this.listeners].every(([callback, options]) => {
|
for (const [callback, { once }] of this.listeners) {
|
||||||
if (options.once) {
|
if (once) {
|
||||||
this.removeListener(callback);
|
this.removeListener(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback(...data) !== false;
|
if (callback(...data) === false) {
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export interface HotbarStoreModel {
|
|||||||
activeHotbarId: string;
|
activeHotbarId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultHotbarCells = 12; // Number is choosen to easy hit any item with keyboard
|
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
|
||||||
|
|
||||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||||
@observable hotbars: Hotbar[] = [];
|
@observable hotbars: Hotbar[] = [];
|
||||||
@ -94,7 +94,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||||
if (!data.hotbars || !data.hotbars.length) {
|
if (!data.hotbars || !data.hotbars.length) {
|
||||||
this.hotbars = [{
|
this.hotbars = [{
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
@ -153,6 +153,19 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setHotbarName(id: string, name: string) {
|
||||||
|
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||||
|
|
||||||
|
if(index < 0) {
|
||||||
|
console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hotbars[index].name = name;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
remove(hotbar: Hotbar) {
|
remove(hotbar: Hotbar) {
|
||||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||||
@ -203,7 +216,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remvove all hotbar items that reference the `uid`.
|
* Remove all hotbar items that reference the `uid`.
|
||||||
* @param uid The `EntityId` that each hotbar item refers to
|
* @param uid The `EntityId` that each hotbar item refers to
|
||||||
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -40,14 +40,14 @@ export interface KubeApiResourceData {
|
|||||||
export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
||||||
"clusterroles": { kind: "ClusterRole", group: "rbac.authorization.k8s.io" },
|
"clusterroles": { kind: "ClusterRole", group: "rbac.authorization.k8s.io" },
|
||||||
"clusterrolebindings": { kind: "ClusterRoleBinding", group: "rbac.authorization.k8s.io" },
|
"clusterrolebindings": { kind: "ClusterRoleBinding", group: "rbac.authorization.k8s.io" },
|
||||||
"configmaps": { kind: "ConfigMap" },
|
"configmaps": { kind: "ConfigMap" }, //empty group means "core"
|
||||||
"cronjobs": { kind: "CronJob", group: "batch" },
|
"cronjobs": { kind: "CronJob", group: "batch" },
|
||||||
"customresourcedefinitions": { kind: "CustomResourceDefinition", group: "apiextensions.k8s.io" },
|
"customresourcedefinitions": { kind: "CustomResourceDefinition", group: "apiextensions.k8s.io" },
|
||||||
"daemonsets": { kind: "DaemonSet", group: "apps" },
|
"daemonsets": { kind: "DaemonSet", group: "apps" },
|
||||||
"deployments": { kind: "Deployment", group: "apps" },
|
"deployments": { kind: "Deployment", group: "apps" },
|
||||||
"endpoints": { kind: "Endpoint" },
|
"endpoints": { kind: "Endpoint" },
|
||||||
"events": { kind: "Event" },
|
"events": { kind: "Event" },
|
||||||
"horizontalpodautoscalers": { kind: "HorizontalPodAutoscaler" },
|
"horizontalpodautoscalers": { kind: "HorizontalPodAutoscaler", group: "autoscaling" },
|
||||||
"ingresses": { kind: "Ingress", group: "networking.k8s.io" },
|
"ingresses": { kind: "Ingress", group: "networking.k8s.io" },
|
||||||
"jobs": { kind: "Job", group: "batch" },
|
"jobs": { kind: "Job", group: "batch" },
|
||||||
"namespaces": { kind: "Namespace" },
|
"namespaces": { kind: "Namespace" },
|
||||||
@ -58,13 +58,13 @@ export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
|||||||
"persistentvolumeclaims": { kind: "PersistentVolumeClaim" },
|
"persistentvolumeclaims": { kind: "PersistentVolumeClaim" },
|
||||||
"pods": { kind: "Pod" },
|
"pods": { kind: "Pod" },
|
||||||
"poddisruptionbudgets": { kind: "PodDisruptionBudget", group: "policy" },
|
"poddisruptionbudgets": { kind: "PodDisruptionBudget", group: "policy" },
|
||||||
"podsecuritypolicies": { kind: "PodSecurityPolicy" },
|
"podsecuritypolicies": { kind: "PodSecurityPolicy", group: "policy" },
|
||||||
"resourcequotas": { kind: "ResourceQuota" },
|
"resourcequotas": { kind: "ResourceQuota" },
|
||||||
"replicasets": { kind: "ReplicaSet", group: "apps" },
|
"replicasets": { kind: "ReplicaSet", group: "apps" },
|
||||||
"roles": { kind: "Role", group: "rbac.authorization.k8s.io" },
|
"roles": { kind: "Role", group: "rbac.authorization.k8s.io" },
|
||||||
"rolebindings": { kind: "RoleBinding", group: "rbac.authorization.k8s.io" },
|
"rolebindings": { kind: "RoleBinding", group: "rbac.authorization.k8s.io" },
|
||||||
"secrets": { kind: "Secret" },
|
"secrets": { kind: "Secret" },
|
||||||
"serviceaccounts": { kind: "ServiceAccount", group: "core" },
|
"serviceaccounts": { kind: "ServiceAccount" },
|
||||||
"services": { kind: "Service" },
|
"services": { kind: "Service" },
|
||||||
"statefulsets": { kind: "StatefulSet", group: "apps" },
|
"statefulsets": { kind: "StatefulSet", group: "apps" },
|
||||||
"storageclasses": { kind: "StorageClass", group: "storage.k8s.io" },
|
"storageclasses": { kind: "StorageClass", group: "storage.k8s.io" },
|
||||||
|
|||||||
77
src/common/sentry.ts
Normal file
77
src/common/sentry.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* 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 { CaptureConsole, Dedupe, Offline } from "@sentry/integrations";
|
||||||
|
import * as Sentry from "@sentry/electron";
|
||||||
|
import { sentryDsn, isProduction } from "./vars";
|
||||||
|
import { UserStore } from "./user-store";
|
||||||
|
import logger from "../main/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Translate" 'browser' to 'main' as Lens developer more familiar with the term 'main'
|
||||||
|
*/
|
||||||
|
function mapProcessName(processType: string) {
|
||||||
|
if (processType === "browser") {
|
||||||
|
return "main";
|
||||||
|
}
|
||||||
|
|
||||||
|
return processType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Sentry for the current process so to send errors for debugging.
|
||||||
|
*/
|
||||||
|
export function SentryInit() {
|
||||||
|
const processName = mapProcessName(process.type);
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
beforeSend: (event) => {
|
||||||
|
// default to false, in case instance of UserStore is not created (yet)
|
||||||
|
const allowErrorReporting = UserStore.getInstance(false)?.allowErrorReporting ?? false;
|
||||||
|
|
||||||
|
if (allowErrorReporting) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`🔒 [SENTRY-BEFORE-SEND-HOOK]: allowErrorReporting: ${allowErrorReporting}. Sentry event is caught but not sent to server.`);
|
||||||
|
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === START OF SENTRY EVENT ===");
|
||||||
|
logger.info(event);
|
||||||
|
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === END OF SENTRY EVENT ===");
|
||||||
|
|
||||||
|
// if return null, the event won't be sent
|
||||||
|
// ref https://github.com/getsentry/sentry-javascript/issues/2039
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
dsn: sentryDsn,
|
||||||
|
integrations: [
|
||||||
|
new CaptureConsole({ levels: ["error"] }),
|
||||||
|
new Dedupe(),
|
||||||
|
new Offline()
|
||||||
|
],
|
||||||
|
initialScope: {
|
||||||
|
tags: {
|
||||||
|
|
||||||
|
"process": processName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
environment: isProduction ? "production" : "development",
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -33,5 +33,9 @@ if (isMac) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
|
try {
|
||||||
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[CA]: failed to force load: ${error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/common/user-store/index.ts
Normal file
23
src/common/user-store/index.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./user-store";
|
||||||
|
export type { KubeconfigSyncEntry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
|
||||||
251
src/common/user-store/preferences-helpers.ts
Normal file
251
src/common/user-store/preferences-helpers.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* 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 moment from "moment-timezone";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import { ThemeStore } from "../../renderer/theme.store";
|
||||||
|
import { ObservableToggleSet } from "../utils";
|
||||||
|
|
||||||
|
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeconfigSyncValue { }
|
||||||
|
|
||||||
|
interface PreferenceDescription<T, R = T> {
|
||||||
|
fromStore(val: T | undefined): R;
|
||||||
|
toStore(val: R): T | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpsProxy: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
return val || undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const shell: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
return val || undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const colorTheme: PreferenceDescription<string> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val || ThemeStore.defaultTheme;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val || val === ThemeStore.defaultTheme) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const localeTimezone: PreferenceDescription<string> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val || moment.tz.guess(true) || "UTC";
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val || val === moment.tz.guess(true) || val === "UTC") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowUntrustedCAs: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? false;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowTelemetry: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? true;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val === true) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowErrorReporting: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? true;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val === true) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadMirror: PreferenceDescription<string> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? "default";
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val || val === "default") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadKubectlBinaries: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? true;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val === true) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadBinariesPath: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const kubectlBinariesPath: PreferenceDescription<string | undefined> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAtLogin: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? false;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, ObservableToggleSet<string>>> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return new Map(
|
||||||
|
(val ?? []).map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
const res: [string, string[]][] = [];
|
||||||
|
|
||||||
|
for (const [table, columns] of val) {
|
||||||
|
if (columns.size) {
|
||||||
|
res.push([table, Array.from(columns)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.length ? res : undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mainKubeFolder = path.join(os.homedir(), ".kube");
|
||||||
|
|
||||||
|
const syncKubeconfigEntries: PreferenceDescription<KubeconfigSyncEntry[], Map<string, KubeconfigSyncValue>> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return new Map(
|
||||||
|
val
|
||||||
|
?.map(({ filePath, ...rest }) => [filePath, rest])
|
||||||
|
?? [[mainKubeFolder, {}]]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (val.size === 1 && val.has(mainKubeFolder)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(val, ([filePath, rest]) => ({ filePath, ...rest }));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
|
||||||
|
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
|
||||||
|
|
||||||
|
export type UserStoreFlatModel = {
|
||||||
|
[field in keyof typeof DESCRIPTORS]: UserStoreModelType<field>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserPreferencesModel = {
|
||||||
|
[field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DESCRIPTORS = {
|
||||||
|
httpsProxy,
|
||||||
|
shell,
|
||||||
|
colorTheme,
|
||||||
|
localeTimezone,
|
||||||
|
allowUntrustedCAs,
|
||||||
|
allowTelemetry,
|
||||||
|
allowErrorReporting,
|
||||||
|
downloadMirror,
|
||||||
|
downloadKubectlBinaries,
|
||||||
|
downloadBinariesPath,
|
||||||
|
kubectlBinariesPath,
|
||||||
|
openAtLogin,
|
||||||
|
hiddenTableColumns,
|
||||||
|
syncKubeconfigEntries,
|
||||||
|
};
|
||||||
@ -19,50 +19,26 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThemeId } from "../renderer/theme.store";
|
|
||||||
import { app, remote } from "electron";
|
import { app, remote } from "electron";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||||
import moment from "moment-timezone";
|
import { BaseStore } from "../base-store";
|
||||||
import { BaseStore } from "./base-store";
|
import migrations from "../../migrations/user-store";
|
||||||
import migrations from "../migrations/user-store";
|
import { getAppVersion } from "../utils/app-version";
|
||||||
import { getAppVersion } from "./utils/app-version";
|
import { kubeConfigDefaultPath } from "../kube-helpers";
|
||||||
import { appEventBus } from "./event-bus";
|
import { appEventBus } from "../event-bus";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import { fileNameMigration } from "../../migrations/user-store";
|
||||||
import { fileNameMigration } from "../migrations/user-store";
|
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
||||||
import { ObservableToggleSet, toJS } from "../renderer/utils";
|
import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
|
||||||
export interface UserStoreModel {
|
export interface UserStoreModel {
|
||||||
lastSeenAppVersion: string;
|
lastSeenAppVersion: string;
|
||||||
preferences: UserPreferencesModel;
|
preferences: UserPreferencesModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
|
||||||
filePath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KubeconfigSyncValue { }
|
|
||||||
|
|
||||||
export interface UserPreferencesModel {
|
|
||||||
httpsProxy?: string;
|
|
||||||
shell?: string;
|
|
||||||
colorTheme?: string;
|
|
||||||
localeTimezone?: string;
|
|
||||||
allowUntrustedCAs?: boolean;
|
|
||||||
allowTelemetry?: boolean;
|
|
||||||
downloadMirror?: string | "default";
|
|
||||||
downloadKubectlBinaries?: boolean;
|
|
||||||
downloadBinariesPath?: string;
|
|
||||||
kubectlBinariesPath?: string;
|
|
||||||
openAtLogin?: boolean;
|
|
||||||
hiddenTableColumns?: [string, string[]][];
|
|
||||||
syncKubeconfigEntries?: KubeconfigSyncEntry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserStore extends BaseStore<UserStoreModel> {
|
|
||||||
static readonly defaultTheme: ThemeId = "lens-dark";
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
configName: "lens-user-store",
|
configName: "lens-user-store",
|
||||||
@ -75,11 +51,19 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observable lastSeenAppVersion = "0.0.0";
|
@observable lastSeenAppVersion = "0.0.0";
|
||||||
@observable allowTelemetry = true;
|
|
||||||
@observable allowUntrustedCAs = false;
|
/**
|
||||||
@observable colorTheme = UserStore.defaultTheme;
|
* used in add-cluster page for providing context
|
||||||
@observable localeTimezone = moment.tz.guess(true) || "UTC";
|
*/
|
||||||
@observable downloadMirror = "default";
|
@observable kubeConfigPath = kubeConfigDefaultPath;
|
||||||
|
@observable seenContexts = observable.set<string>();
|
||||||
|
@observable newContexts = observable.set<string>();
|
||||||
|
@observable allowTelemetry: boolean;
|
||||||
|
@observable allowErrorReporting: boolean;
|
||||||
|
@observable allowUntrustedCAs: boolean;
|
||||||
|
@observable colorTheme: string;
|
||||||
|
@observable localeTimezone: string;
|
||||||
|
@observable downloadMirror: string;
|
||||||
@observable httpsProxy?: string;
|
@observable httpsProxy?: string;
|
||||||
@observable shell?: string;
|
@observable shell?: string;
|
||||||
@observable downloadBinariesPath?: string;
|
@observable downloadBinariesPath?: string;
|
||||||
@ -88,8 +72,8 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
/**
|
/**
|
||||||
* Download kubectl binaries matching cluster version
|
* Download kubectl binaries matching cluster version
|
||||||
*/
|
*/
|
||||||
@observable downloadKubectlBinaries = true;
|
@observable downloadKubectlBinaries: boolean;
|
||||||
@observable openAtLogin = false;
|
@observable openAtLogin: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The column IDs under each configurable table ID that have been configured
|
* The column IDs under each configurable table ID that have been configured
|
||||||
@ -100,9 +84,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
/**
|
/**
|
||||||
* The set of file/folder paths to be synced
|
* The set of file/folder paths to be synced
|
||||||
*/
|
*/
|
||||||
syncKubeconfigEntries = observable.map<string, KubeconfigSyncValue>([
|
syncKubeconfigEntries = observable.map<string, KubeconfigSyncValue>();
|
||||||
[path.join(os.homedir(), ".kube"), {}]
|
|
||||||
]);
|
|
||||||
|
|
||||||
@computed get isNewVersion() {
|
@computed get isNewVersion() {
|
||||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||||
@ -138,13 +120,13 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
*/
|
*/
|
||||||
isTableColumnHidden(tableId: string, ...columnIds: string[]): boolean {
|
isTableColumnHidden(tableId: string, ...columnIds: string[]): boolean {
|
||||||
if (columnIds.length === 0) {
|
if (columnIds.length === 0) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = this.hiddenTableColumns.get(tableId);
|
const config = this.hiddenTableColumns.get(tableId);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return columnIds.some(columnId => config.has(columnId));
|
return columnIds.some(columnId => config.has(columnId));
|
||||||
@ -155,12 +137,16 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
* Toggles the hidden configuration of a table's column
|
* Toggles the hidden configuration of a table's column
|
||||||
*/
|
*/
|
||||||
toggleTableColumnVisibility(tableId: string, columnId: string) {
|
toggleTableColumnVisibility(tableId: string, columnId: string) {
|
||||||
this.hiddenTableColumns.get(tableId)?.toggle(columnId);
|
if (!this.hiddenTableColumns.get(tableId)) {
|
||||||
|
this.hiddenTableColumns.set(tableId, new ObservableToggleSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hiddenTableColumns.get(tableId).toggle(columnId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resetTheme() {
|
resetTheme() {
|
||||||
this.colorTheme = UserStore.defaultTheme;
|
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -175,71 +161,47 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
protected fromStore({ lastSeenAppVersion, preferences }: Partial<UserStoreModel> = {}) {
|
||||||
const { lastSeenAppVersion, preferences } = data;
|
logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences });
|
||||||
|
|
||||||
if (lastSeenAppVersion) {
|
if (lastSeenAppVersion) {
|
||||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preferences) {
|
this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy);
|
||||||
return;
|
this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell);
|
||||||
}
|
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme);
|
||||||
|
this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone);
|
||||||
this.httpsProxy = preferences.httpsProxy;
|
this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs);
|
||||||
this.shell = preferences.shell;
|
this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry);
|
||||||
this.colorTheme = preferences.colorTheme;
|
this.allowErrorReporting = DESCRIPTORS.allowErrorReporting.fromStore(preferences?.allowErrorReporting);
|
||||||
this.localeTimezone = preferences.localeTimezone;
|
this.downloadMirror = DESCRIPTORS.downloadMirror.fromStore(preferences?.downloadMirror);
|
||||||
this.allowUntrustedCAs = preferences.allowUntrustedCAs;
|
this.downloadKubectlBinaries = DESCRIPTORS.downloadKubectlBinaries.fromStore(preferences?.downloadKubectlBinaries);
|
||||||
this.allowTelemetry = preferences.allowTelemetry;
|
this.downloadBinariesPath = DESCRIPTORS.downloadBinariesPath.fromStore(preferences?.downloadBinariesPath);
|
||||||
this.downloadMirror = preferences.downloadMirror;
|
this.kubectlBinariesPath = DESCRIPTORS.kubectlBinariesPath.fromStore(preferences?.kubectlBinariesPath);
|
||||||
this.downloadKubectlBinaries = preferences.downloadKubectlBinaries;
|
this.openAtLogin = DESCRIPTORS.openAtLogin.fromStore(preferences?.openAtLogin);
|
||||||
this.downloadBinariesPath = preferences.downloadBinariesPath;
|
this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns));
|
||||||
this.kubectlBinariesPath = preferences.kubectlBinariesPath;
|
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
||||||
this.openAtLogin = preferences.openAtLogin;
|
|
||||||
|
|
||||||
if (preferences.hiddenTableColumns) {
|
|
||||||
this.hiddenTableColumns.replace(
|
|
||||||
preferences.hiddenTableColumns
|
|
||||||
.map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.syncKubeconfigEntries) {
|
|
||||||
this.syncKubeconfigEntries.replace(
|
|
||||||
preferences.syncKubeconfigEntries.map(({ filePath, ...rest }) => [filePath, rest])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): UserStoreModel {
|
toJSON(): UserStoreModel {
|
||||||
const hiddenTableColumns: [string, string[]][] = [];
|
|
||||||
const syncKubeconfigEntries: KubeconfigSyncEntry[] = [];
|
|
||||||
|
|
||||||
for (const [key, values] of this.hiddenTableColumns.entries()) {
|
|
||||||
hiddenTableColumns.push([key, Array.from(values)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [filePath, rest] of this.syncKubeconfigEntries) {
|
|
||||||
syncKubeconfigEntries.push({ filePath, ...rest });
|
|
||||||
}
|
|
||||||
|
|
||||||
const model: UserStoreModel = {
|
const model: UserStoreModel = {
|
||||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||||
preferences: {
|
preferences: {
|
||||||
httpsProxy: toJS(this.httpsProxy),
|
httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy),
|
||||||
shell: toJS(this.shell),
|
shell: DESCRIPTORS.shell.toStore(this.shell),
|
||||||
colorTheme: toJS(this.colorTheme),
|
colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme),
|
||||||
localeTimezone: toJS(this.localeTimezone),
|
localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone),
|
||||||
allowUntrustedCAs: toJS(this.allowUntrustedCAs),
|
allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs),
|
||||||
allowTelemetry: toJS(this.allowTelemetry),
|
allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry),
|
||||||
downloadMirror: toJS(this.downloadMirror),
|
allowErrorReporting: DESCRIPTORS.allowErrorReporting.toStore(this.allowErrorReporting),
|
||||||
downloadKubectlBinaries: toJS(this.downloadKubectlBinaries),
|
downloadMirror: DESCRIPTORS.downloadMirror.toStore(this.downloadMirror),
|
||||||
downloadBinariesPath: toJS(this.downloadBinariesPath),
|
downloadKubectlBinaries: DESCRIPTORS.downloadKubectlBinaries.toStore(this.downloadKubectlBinaries),
|
||||||
kubectlBinariesPath: toJS(this.kubectlBinariesPath),
|
downloadBinariesPath: DESCRIPTORS.downloadBinariesPath.toStore(this.downloadBinariesPath),
|
||||||
openAtLogin: toJS(this.openAtLogin),
|
kubectlBinariesPath: DESCRIPTORS.kubectlBinariesPath.toStore(this.kubectlBinariesPath),
|
||||||
hiddenTableColumns,
|
openAtLogin: DESCRIPTORS.openAtLogin.toStore(this.openAtLogin),
|
||||||
syncKubeconfigEntries,
|
hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns),
|
||||||
|
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
130
src/common/utils/__tests__/paths.test.ts
Normal file
130
src/common/utils/__tests__/paths.test.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* 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 { describeIf } from "../../../../integration/helpers/utils";
|
||||||
|
import { isWindows } from "../../vars";
|
||||||
|
import { isLogicalChildPath } from "../paths";
|
||||||
|
|
||||||
|
describe("isLogicalChildPath", () => {
|
||||||
|
describeIf(isWindows)("windows tests", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:\\Foo\\Bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:\\Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:/Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "C:/Foo/Bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "C:\\Foo",
|
||||||
|
testPath: "D:\\Foo\\Bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("test %#", (testData) => {
|
||||||
|
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describeIf(!isWindows)("posix tests", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foobar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/../bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/./bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/.bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/..bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/...bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/..\\.bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/bar/../foo",
|
||||||
|
testPath: "/foo/bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "/foo/\\bar",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parentPath: "/foo",
|
||||||
|
testPath: "./bar",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("test %#", (testData) => {
|
||||||
|
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -27,7 +27,7 @@ export class ExtendedMap<K, V> extends Map<K, V> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the value behind `key`. If it was not pressent, first insert the value returned by `getVal`
|
* Get the value behind `key`. If it was not present, first insert the value returned by `getVal`
|
||||||
* @param key The key to insert into the map with
|
* @param key The key to insert into the map with
|
||||||
* @param getVal A function that returns a new instance of `V`.
|
* @param getVal A function that returns a new instance of `V`.
|
||||||
* @returns The value in the map
|
* @returns The value in the map
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export * from "./openExternal";
|
|||||||
export * from "./paths";
|
export * from "./paths";
|
||||||
export * from "./reject-promise";
|
export * from "./reject-promise";
|
||||||
export * from "./singleton";
|
export * from "./singleton";
|
||||||
|
export * from "./sort-compare";
|
||||||
export * from "./splitArray";
|
export * from "./splitArray";
|
||||||
export * from "./tar";
|
export * from "./tar";
|
||||||
export * from "./toggle-set";
|
export * from "./toggle-set";
|
||||||
|
|||||||
@ -33,3 +33,35 @@ function resolveTilde(filePath: string) {
|
|||||||
export function resolvePath(filePath: string): string {
|
export function resolvePath(filePath: string): string {
|
||||||
return path.resolve(resolveTilde(filePath));
|
return path.resolve(resolveTilde(filePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if `testPath` represents a potential filesystem entry that would be
|
||||||
|
* logically "within" the `parentPath` directory.
|
||||||
|
*
|
||||||
|
* This function will return `true` in the above case, and `false` otherwise.
|
||||||
|
* It will return `false` if the two paths are the same (after resolving them).
|
||||||
|
*
|
||||||
|
* The function makes no FS calls and is platform dependant. Meaning that the
|
||||||
|
* results are only guaranteed to be correct for the platform you are running
|
||||||
|
* on.
|
||||||
|
* @param parentPath The known path of a directory
|
||||||
|
* @param testPath The path that is to be tested
|
||||||
|
*/
|
||||||
|
export function isLogicalChildPath(parentPath: string, testPath: string): boolean {
|
||||||
|
parentPath = path.resolve(parentPath);
|
||||||
|
testPath = path.resolve(testPath);
|
||||||
|
|
||||||
|
if (parentPath === testPath) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (testPath.length >= parentPath.length) {
|
||||||
|
if (testPath === parentPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
testPath = path.dirname(testPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@ -46,13 +46,16 @@ export class Singleton {
|
|||||||
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
||||||
if (!Singleton.instances.has(this)) {
|
if (!Singleton.instances.has(this)) {
|
||||||
if (Singleton.creating.length > 0) {
|
if (Singleton.creating.length > 0) {
|
||||||
throw new TypeError("Cannot create a second singleton while creating a first");
|
throw new TypeError(`Cannot create a second singleton (${this.name}) while creating a first (${Singleton.creating})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
Singleton.creating = this.name;
|
Singleton.creating = this.name;
|
||||||
Singleton.instances.set(this, new this(...args));
|
Singleton.instances.set(this, new this(...args));
|
||||||
|
} finally {
|
||||||
Singleton.creating = "";
|
Singleton.creating = "";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Singleton.instances.get(this) as T;
|
return Singleton.instances.get(this) as T;
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/common/utils/sort-compare.ts
Normal file
55
src/common/utils/sort-compare.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 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 semver, { SemVer } from "semver";
|
||||||
|
|
||||||
|
export function sortCompare<T>(left: T, right: T): -1 | 0 | 1 {
|
||||||
|
if (left < right) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left === right) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartVersion {
|
||||||
|
version: string;
|
||||||
|
__version?: SemVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion): -1 | 0 | 1 {
|
||||||
|
if (left.__version && right.__version) {
|
||||||
|
return semver.compare(right.__version, left.__version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!left.__version && right.__version) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.__version && !right.__version) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortCompare(left.version, right.version);
|
||||||
|
}
|
||||||
@ -69,4 +69,6 @@ export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5
|
|||||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||||
|
|
||||||
export const appSemVer = new SemVer(packageInfo.version);
|
export const appSemVer = new SemVer(packageInfo.version);
|
||||||
export const docsUrl = `https://docs.k8slens.dev/main/` as string;
|
export const docsUrl = "https://docs.k8slens.dev/main/" as string;
|
||||||
|
|
||||||
|
export const sentryDsn = packageInfo.config?.sentryDsn ?? "";
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import { action, comparer, observable, makeObservable } from "mobx";
|
import { action, comparer, observable, makeObservable } from "mobx";
|
||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import migrations from "../migrations/hotbar-store";
|
import migrations from "../migrations/weblinks-store";
|
||||||
import * as uuid from "uuid";
|
import * as uuid from "uuid";
|
||||||
import { toJS } from "./utils";
|
import { toJS } from "./utils";
|
||||||
|
|
||||||
@ -58,7 +58,8 @@ export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action protected async fromStore(data: Partial<WeblinkStoreModel> = {}) {
|
@action
|
||||||
|
protected fromStore(data: Partial<WeblinkStoreModel> = {}) {
|
||||||
this.weblinks = data.weblinks || [];
|
this.weblinks = data.weblinks || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
138
src/extensions/__tests__/extension-compatibility.test.ts
Normal file
138
src/extensions/__tests__/extension-compatibility.test.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 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 { isCompatibleExtension } from "../extension-compatibility";
|
||||||
|
import { Console } from "console";
|
||||||
|
import { stdout, stderr } from "process";
|
||||||
|
import type { LensExtensionManifest } from "../lens-extension";
|
||||||
|
import { appSemVer } from "../../common/vars";
|
||||||
|
|
||||||
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
|
describe("extension compatibility", () => {
|
||||||
|
describe("appSemVer with no prerelease tag", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
appSemVer.major = 5;
|
||||||
|
appSemVer.minor = 0;
|
||||||
|
appSemVer.patch = 3;
|
||||||
|
appSemVer.prerelease = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no extension comparator", () => {
|
||||||
|
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "bad comparator",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0-alpha.1",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||||
|
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("appSemVer with prerelease tag", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
appSemVer.major = 5;
|
||||||
|
appSemVer.minor = 0;
|
||||||
|
appSemVer.patch = 3;
|
||||||
|
appSemVer.prerelease = ["beta", 3];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no extension comparator", () => {
|
||||||
|
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
comparator: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "bad comparator",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^4.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^5.0.0-alpha.1",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comparator: "^6.0.0-alpha.1",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||||
|
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||||
|
|
||||||
|
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -84,7 +84,7 @@ jest.mock(
|
|||||||
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||||
if (channel === "extensions:main") {
|
if (channel === "extensions:main") {
|
||||||
// First initialize with extensions 1 and 2
|
// First initialize with extensions 1 and 2
|
||||||
// and then broadcast event to remove extensioin 2 and add extension number 3
|
// and then broadcast event to remove extension 2 and add extension number 3
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
listener({}, [
|
listener({}, [
|
||||||
[
|
[
|
||||||
|
|||||||
37
src/extensions/extension-compatibility.ts
Normal file
37
src/extensions/extension-compatibility.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import semver from "semver";
|
||||||
|
import { appSemVer, isProduction } from "../common/vars";
|
||||||
|
import type { LensExtensionManifest } from "./lens-extension";
|
||||||
|
|
||||||
|
export function isCompatibleExtension(manifest: LensExtensionManifest): boolean {
|
||||||
|
if (manifest.engines?.lens) {
|
||||||
|
/* include Lens's prerelease tag in the matching so the extension's compatibility is not limited by it */
|
||||||
|
return semver.satisfies(appSemVer, manifest.engines.lens, { includePrerelease: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCompatibleBundledExtension(manifest: LensExtensionManifest): boolean {
|
||||||
|
return !isProduction || manifest.version === appSemVer.raw;
|
||||||
|
}
|
||||||
@ -34,9 +34,8 @@ import { extensionInstaller } from "./extension-installer";
|
|||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||||
import semver from "semver";
|
|
||||||
import { appSemVer } from "../common/vars";
|
|
||||||
import { isProduction } from "../common/vars";
|
import { isProduction } from "../common/vars";
|
||||||
|
import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility";
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
id: LensExtensionId;
|
id: LensExtensionId;
|
||||||
@ -362,11 +361,7 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
const extensionDir = path.dirname(manifestPath);
|
const extensionDir = path.dirname(manifestPath);
|
||||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||||
let isCompatible = isBundled;
|
const isCompatible = (isBundled && isCompatibleBundledExtension(manifest)) || isCompatibleExtension(manifest);
|
||||||
|
|
||||||
if (manifest.engines?.lens) {
|
|
||||||
isCompatible = semver.satisfies(appSemVer, manifest.engines.lens);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@ -25,23 +25,31 @@ import type { LensMainExtension } from "../lens-main-extension";
|
|||||||
import type { Disposer } from "../../common/utils";
|
import type { Disposer } from "../../common/utils";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
import { ipcMainHandle } from "../../common/ipc";
|
import { ipcMainHandle } from "../../common/ipc";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
|
||||||
export abstract class IpcMain extends IpcRegistrar {
|
export abstract class IpcMain extends IpcRegistrar {
|
||||||
constructor(extension: LensMainExtension) {
|
constructor(extension: LensMainExtension) {
|
||||||
super(extension);
|
super(extension);
|
||||||
extension[Disposers].push(() => IpcMain.resetInstance());
|
|
||||||
|
// Call the static method on the bottom child class.
|
||||||
|
extension[Disposers].push(() => (this.constructor as typeof IpcMain).resetInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for broadcasts within your extension
|
* Listen for broadcasts within your extension
|
||||||
* @param channel The channel to listen for broadcasts on
|
* @param channel The channel to listen for broadcasts on
|
||||||
* @param listener The function that will be called with the arguments of the broadcast
|
* @param listener The function that will be called with the arguments of the broadcast
|
||||||
* @returns An optional disopser, Lens will cleanup when the extension is disabled or uninstalled even if this is not called
|
* @returns An optional disposer, Lens will cleanup when the extension is disabled or uninstalled even if this is not called
|
||||||
*/
|
*/
|
||||||
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
||||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||||
const cleanup = once(() => ipcMain.removeListener(prefixedChannel, listener));
|
const cleanup = once(() => {
|
||||||
|
logger.info(`[IPC-RENDERER]: removing extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version } });
|
||||||
|
|
||||||
|
return ipcMain.removeListener(prefixedChannel, listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[IPC-RENDERER]: adding extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version } });
|
||||||
ipcMain.addListener(prefixedChannel, listener);
|
ipcMain.addListener(prefixedChannel, listener);
|
||||||
this.extension[Disposers].push(cleanup);
|
this.extension[Disposers].push(cleanup);
|
||||||
|
|
||||||
@ -56,7 +64,12 @@ export abstract class IpcMain extends IpcRegistrar {
|
|||||||
handle(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void {
|
handle(channel: string, handler: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any): void {
|
||||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||||
|
|
||||||
|
logger.info(`[IPC-RENDERER]: adding extension handler`, { channel, extension: { name: this.extension.name, version: this.extension.version } });
|
||||||
ipcMainHandle(prefixedChannel, handler);
|
ipcMainHandle(prefixedChannel, handler);
|
||||||
this.extension[Disposers].push(() => ipcMain.removeHandler(prefixedChannel));
|
this.extension[Disposers].push(() => {
|
||||||
|
logger.info(`[IPC-RENDERER]: removing extension handler`, { channel, extension: { name: this.extension.name, version: this.extension.version } });
|
||||||
|
|
||||||
|
return ipcMain.removeHandler(prefixedChannel);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,7 +28,9 @@ import { once } from "lodash";
|
|||||||
export abstract class IpcRenderer extends IpcRegistrar {
|
export abstract class IpcRenderer extends IpcRegistrar {
|
||||||
constructor(extension: LensRendererExtension) {
|
constructor(extension: LensRendererExtension) {
|
||||||
super(extension);
|
super(extension);
|
||||||
extension[Disposers].push(() => IpcRenderer.resetInstance());
|
|
||||||
|
// Call the static method on the bottom child class.
|
||||||
|
extension[Disposers].push(() => (this.constructor as typeof IpcRenderer).resetInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,8 +43,13 @@ export abstract class IpcRenderer extends IpcRegistrar {
|
|||||||
*/
|
*/
|
||||||
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
||||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||||
const cleanup = once(() => ipcRenderer.removeListener(prefixedChannel, listener));
|
const cleanup = once(() => {
|
||||||
|
console.info(`[IPC-RENDERER]: removing extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version } });
|
||||||
|
|
||||||
|
return ipcRenderer.removeListener(prefixedChannel, listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`[IPC-RENDERER]: adding extension listener`, { channel, extension: { name: this.extension.name, version: this.extension.version } });
|
||||||
ipcRenderer.addListener(prefixedChannel, listener);
|
ipcRenderer.addListener(prefixedChannel, listener);
|
||||||
this.extension[Disposers].push(cleanup);
|
this.extension[Disposers].push(cleanup);
|
||||||
|
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export class LensExtension {
|
|||||||
* getExtensionFileFolder returns the path to an already created folder. This
|
* getExtensionFileFolder returns the path to an already created folder. This
|
||||||
* folder is for the sole use of this extension.
|
* folder is for the sole use of this extension.
|
||||||
*
|
*
|
||||||
* Note: there is no security done on this folder, only obfiscation of the
|
* Note: there is no security done on this folder, only obfuscation of the
|
||||||
* folder name.
|
* folder name.
|
||||||
*/
|
*/
|
||||||
async getExtensionFileFolder(): Promise<string> {
|
async getExtensionFileFolder(): Promise<string> {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import type * as registries from "./registries";
|
|||||||
import type { Cluster } from "../main/cluster";
|
import type { Cluster } from "../main/cluster";
|
||||||
import { LensExtension } from "./lens-extension";
|
import { LensExtension } from "./lens-extension";
|
||||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||||
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -37,7 +38,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: registries.CommandRegistration[] = [];
|
commands: registries.CommandRegistration[] = [];
|
||||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: registries.TopBarRegistration[] = [];
|
topBarItems: registries.TopBarRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
|
|||||||
@ -22,14 +22,24 @@
|
|||||||
import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry";
|
import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry";
|
||||||
import { LensExtension } from "../../lens-extension";
|
import { LensExtension } from "../../lens-extension";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import fse from "fs-extra";
|
||||||
import { Console } from "console";
|
import { Console } from "console";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
|
import { ThemeStore } from "../../../renderer/theme.store";
|
||||||
|
import { TerminalStore } from "../../renderer-api/components";
|
||||||
|
import { UserStore } from "../../../common/user-store";
|
||||||
|
|
||||||
|
jest.mock("electron", () => ({
|
||||||
|
app: {
|
||||||
|
getPath: () => "tmp",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
|
|
||||||
let ext: LensExtension = null;
|
let ext: LensExtension = null;
|
||||||
|
|
||||||
describe("getPageUrl", () => {
|
describe("page registry tests", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
ext = new LensExtension({
|
ext = new LensExtension({
|
||||||
manifest: {
|
manifest: {
|
||||||
@ -43,6 +53,9 @@ describe("getPageUrl", () => {
|
|||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
isCompatible: true
|
isCompatible: true
|
||||||
});
|
});
|
||||||
|
UserStore.createInstance();
|
||||||
|
ThemeStore.createInstance();
|
||||||
|
TerminalStore.createInstance();
|
||||||
ClusterPageRegistry.createInstance();
|
ClusterPageRegistry.createInstance();
|
||||||
GlobalPageRegistry.createInstance().add({
|
GlobalPageRegistry.createInstance().add({
|
||||||
id: "page-with-params",
|
id: "page-with-params",
|
||||||
@ -54,13 +67,37 @@ describe("getPageUrl", () => {
|
|||||||
test2: "" // no default value, just declaration
|
test2: "" // no default value, just declaration
|
||||||
},
|
},
|
||||||
}, ext);
|
}, ext);
|
||||||
|
GlobalPageRegistry.createInstance().add([
|
||||||
|
{
|
||||||
|
id: "test-page",
|
||||||
|
components: {
|
||||||
|
Page: () => React.createElement("Text")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "another-page",
|
||||||
|
components: {
|
||||||
|
Page: () => React.createElement("Text")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
components: {
|
||||||
|
Page: () => React.createElement("Default")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
], ext);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
GlobalPageRegistry.resetInstance();
|
GlobalPageRegistry.resetInstance();
|
||||||
ClusterPageRegistry.resetInstance();
|
ClusterPageRegistry.resetInstance();
|
||||||
|
TerminalStore.resetInstance();
|
||||||
|
ThemeStore.resetInstance();
|
||||||
|
UserStore.resetInstance();
|
||||||
|
fse.remove("tmp");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getPageUrl", () => {
|
||||||
it("returns a page url for extension", () => {
|
it("returns a page url for extension", () => {
|
||||||
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
||||||
});
|
});
|
||||||
@ -101,49 +138,9 @@ describe("getPageUrl", () => {
|
|||||||
|
|
||||||
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe("globalPageRegistry", () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
ext = new LensExtension({
|
|
||||||
manifest: {
|
|
||||||
name: "@acme/foo-bar",
|
|
||||||
version: "0.1.1"
|
|
||||||
},
|
|
||||||
id: "/this/is/fake/package.json",
|
|
||||||
absolutePath: "/absolute/fake/",
|
|
||||||
manifestPath: "/this/is/fake/package.json",
|
|
||||||
isBundled: false,
|
|
||||||
isEnabled: true,
|
|
||||||
isCompatible: true
|
|
||||||
});
|
|
||||||
ClusterPageRegistry.createInstance();
|
|
||||||
GlobalPageRegistry.createInstance().add([
|
|
||||||
{
|
|
||||||
id: "test-page",
|
|
||||||
components: {
|
|
||||||
Page: () => React.createElement("Text")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "another-page",
|
|
||||||
components: {
|
|
||||||
Page: () => React.createElement("Text")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
components: {
|
|
||||||
Page: () => React.createElement("Default")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
], ext);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
GlobalPageRegistry.resetInstance();
|
|
||||||
ClusterPageRegistry.resetInstance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("globalPageRegistry", () => {
|
||||||
describe("getByPageTarget", () => {
|
describe("getByPageTarget", () => {
|
||||||
it("matching to first registered page without id", () => {
|
it("matching to first registered page without id", () => {
|
||||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
||||||
@ -171,4 +168,5 @@ describe("globalPageRegistry", () => {
|
|||||||
expect(page).toBeNull();
|
expect(page).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -20,20 +20,25 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
import type { CatalogEntity } from "../common-api/catalog";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface CatalogEntityDetailComponents {
|
export interface CatalogEntityDetailsProps<T extends CatalogEntity> {
|
||||||
Details: React.ComponentType<any>;
|
entity: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogEntityDetailRegistration {
|
export interface CatalogEntityDetailComponents<T extends CatalogEntity> {
|
||||||
|
Details: React.ComponentType<CatalogEntityDetailsProps<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
components: CatalogEntityDetailComponents;
|
components: CatalogEntityDetailComponents<T>;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration> {
|
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration<CatalogEntity>> {
|
||||||
getItemsForKind(kind: string, apiVersion: string) {
|
getItemsForKind(kind: string, apiVersion: string) {
|
||||||
const items = this.getItems().filter((item) => {
|
const items = this.getItems().filter((item) => {
|
||||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||||
|
|||||||
@ -24,14 +24,14 @@ import type { KubeObjectDetailsProps } from "../renderer-api/components";
|
|||||||
import type { KubeObject } from "../renderer-api/k8s-api";
|
import type { KubeObject } from "../renderer-api/k8s-api";
|
||||||
import { BaseRegistry } from "./base-registry";
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
export interface KubeObjectDetailComponents {
|
export interface KubeObjectDetailComponents<T extends KubeObject = KubeObject> {
|
||||||
Details: React.ComponentType<KubeObjectDetailsProps<KubeObject>>;
|
Details: React.ComponentType<KubeObjectDetailsProps<T>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KubeObjectDetailRegistration {
|
export interface KubeObjectDetailRegistration {
|
||||||
kind: string;
|
kind: string;
|
||||||
apiVersions: string[];
|
apiVersions: string[];
|
||||||
components: KubeObjectDetailComponents;
|
components: KubeObjectDetailComponents<KubeObject>;
|
||||||
priority?: number;
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -67,5 +67,5 @@ export * from "../../renderer/components/+events/kube-event-details";
|
|||||||
|
|
||||||
// specific exports
|
// specific exports
|
||||||
export * from "../../renderer/components/status-brick";
|
export * from "../../renderer/components/status-brick";
|
||||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
export { terminalStore, createTerminalTab, TerminalStore } from "../../renderer/components/dock/terminal.store";
|
||||||
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
||||||
|
|||||||
@ -207,7 +207,7 @@ describe("ContextHandler", () => {
|
|||||||
expect(service.id === "id_0");
|
expect(service.id === "id_0");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shouldn't pick the second provider of 2 succcess(es) after 1 failure(s)", async () => {
|
it("shouldn't pick the second provider of 2 success(es) after 1 failure(s)", async () => {
|
||||||
const reg = PrometheusProviderRegistry.getInstance();
|
const reg = PrometheusProviderRegistry.getInstance();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import type { CatalogEntity } from "../../common/catalog";
|
|||||||
import { catalogEntityRegistry } from "../../main/catalog";
|
import { catalogEntityRegistry } from "../../main/catalog";
|
||||||
import { watch } from "chokidar";
|
import { watch } from "chokidar";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import type stream from "stream";
|
import type stream from "stream";
|
||||||
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
|
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
|
||||||
@ -36,9 +37,23 @@ import { UserStore } from "../../common/user-store";
|
|||||||
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
|
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { homedir } from "os";
|
import { homedir } from "os";
|
||||||
|
import globToRegExp from "glob-to-regexp";
|
||||||
|
import { inspect } from "util";
|
||||||
|
|
||||||
const logPrefix = "[KUBECONFIG-SYNC]:";
|
const logPrefix = "[KUBECONFIG-SYNC]:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the list of globs of which files are ignored when under a folder sync
|
||||||
|
*/
|
||||||
|
const ignoreGlobs = [
|
||||||
|
"*.lock", // kubectl lock files
|
||||||
|
"*.swp", // vim swap files
|
||||||
|
".DS_Store", // macOS specific
|
||||||
|
].map(rawGlob => ({
|
||||||
|
rawGlob,
|
||||||
|
matcher: globToRegExp(rawGlob),
|
||||||
|
}));
|
||||||
|
|
||||||
export class KubeconfigSyncManager extends Singleton {
|
export class KubeconfigSyncManager extends Singleton {
|
||||||
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
||||||
protected syncing = false;
|
protected syncing = false;
|
||||||
@ -260,26 +275,50 @@ function diffChangedConfig(filePath: string, source: RootSource): Disposer {
|
|||||||
|
|
||||||
async function watchFileChanges(filePath: string): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> {
|
async function watchFileChanges(filePath: string): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> {
|
||||||
const stat = await fse.stat(filePath); // traverses symlinks, is a race condition
|
const stat = await fse.stat(filePath); // traverses symlinks, is a race condition
|
||||||
|
const isFolderSync = stat.isDirectory();
|
||||||
const watcher = watch(filePath, {
|
const watcher = watch(filePath, {
|
||||||
followSymlinks: true,
|
followSymlinks: true,
|
||||||
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
|
depth: isFolderSync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
|
||||||
disableGlobbing: true,
|
disableGlobbing: true,
|
||||||
|
ignorePermissionErrors: true,
|
||||||
|
usePolling: false,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
pollInterval: 100,
|
||||||
|
stabilityThreshold: 1000,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
|
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
|
||||||
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
|
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
|
||||||
const stoppers = new Map<string, Disposer>();
|
const cleanupFns = new Map<string, Disposer>();
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on("change", (childFilePath) => {
|
.on("change", (childFilePath) => {
|
||||||
stoppers.get(childFilePath)();
|
const cleanup = cleanupFns.get(childFilePath);
|
||||||
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
|
|
||||||
|
if (!cleanup) {
|
||||||
|
// file was previously ignored, do nothing
|
||||||
|
return void logger.debug(`${logPrefix} ${inspect(childFilePath)} that should have been previously ignored has changed. Doing nothing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
cleanupFns.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
|
||||||
})
|
})
|
||||||
.on("add", (childFilePath) => {
|
.on("add", (childFilePath) => {
|
||||||
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
|
if (isFolderSync) {
|
||||||
|
const fileName = path.basename(childFilePath);
|
||||||
|
|
||||||
|
for (const ignoreGlob of ignoreGlobs) {
|
||||||
|
if (ignoreGlob.matcher.test(fileName)) {
|
||||||
|
return void logger.info(`${logPrefix} ignoring ${inspect(childFilePath)} due to ignore glob: ${ignoreGlob.rawGlob}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupFns.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrInsert(childFilePath, observable.map)));
|
||||||
})
|
})
|
||||||
.on("unlink", (childFilePath) => {
|
.on("unlink", (childFilePath) => {
|
||||||
stoppers.get(childFilePath)();
|
cleanupFns.get(childFilePath)?.();
|
||||||
stoppers.delete(childFilePath);
|
cleanupFns.delete(childFilePath);
|
||||||
rootSource.delete(childFilePath);
|
rootSource.delete(childFilePath);
|
||||||
})
|
})
|
||||||
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
|
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
|
||||||
|
|||||||
@ -19,43 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { observable, reaction } from "mobx";
|
import { computed, observable, reaction } from "mobx";
|
||||||
import { WeblinkStore } from "../../common/weblink-store";
|
import { WeblinkStore } from "../../common/weblink-store";
|
||||||
import { WebLink } from "../../common/catalog-entities";
|
import { WebLink } from "../../common/catalog-entities";
|
||||||
import { catalogEntityRegistry } from "../catalog";
|
import { catalogEntityRegistry } from "../catalog";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
import logger from "../logger";
|
import type { Disposer } from "../../common/utils";
|
||||||
import { docsUrl, slackUrl } from "../../common/vars";
|
import { random } from "lodash";
|
||||||
|
|
||||||
const defaultLinks = [
|
|
||||||
{ title: "Lens Website", url: "https://k8slens.dev" },
|
|
||||||
{ title: "Lens Documentation", url: docsUrl },
|
|
||||||
{ title: "Lens Community Slack", url: slackUrl },
|
|
||||||
{ title: "Kubernetes Documentation", url: "https://kubernetes.io/docs/home/" },
|
|
||||||
{ title: "Lens on Twitter", url: "https://twitter.com/k8slens" },
|
|
||||||
{ title: "Lens Official Blog", url: "https://medium.com/k8slens" }
|
|
||||||
].map((link) => (
|
|
||||||
new WebLink({
|
|
||||||
metadata: {
|
|
||||||
uid: link.url,
|
|
||||||
name: link.title,
|
|
||||||
source: "app",
|
|
||||||
labels: {}
|
|
||||||
},
|
|
||||||
spec: {
|
|
||||||
url: link.url
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
phase: "available",
|
|
||||||
active: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
async function validateLink(link: WebLink) {
|
async function validateLink(link: WebLink) {
|
||||||
try {
|
try {
|
||||||
const response = await got.get(link.spec.url, {
|
const response = await got.get(link.spec.url, {
|
||||||
throwHttpErrors: false
|
throwHttpErrors: false,
|
||||||
|
timeout: 20_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode >= 200 && response.statusCode < 500) {
|
if (response.statusCode >= 200 && response.statusCode < 500) {
|
||||||
@ -63,7 +39,7 @@ async function validateLink(link: WebLink) {
|
|||||||
} else {
|
} else {
|
||||||
link.status.phase = "unavailable";
|
link.status.phase = "unavailable";
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch {
|
||||||
link.status.phase = "unavailable";
|
link.status.phase = "unavailable";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,32 +47,60 @@ async function validateLink(link: WebLink) {
|
|||||||
|
|
||||||
export function syncWeblinks() {
|
export function syncWeblinks() {
|
||||||
const weblinkStore = WeblinkStore.getInstance();
|
const weblinkStore = WeblinkStore.getInstance();
|
||||||
const weblinks = observable.array(defaultLinks);
|
const webLinkEntities = observable.map<string, [WebLink, Disposer]>();
|
||||||
|
|
||||||
|
function periodicallyCheckLink(link: WebLink): Disposer {
|
||||||
|
validateLink(link);
|
||||||
|
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
interval = setInterval(() => validateLink(link), 60 * 60 * 1000); // every 60 minutes
|
||||||
|
}, random(0, 5 * 60 * 1000, false)); // spread out over 5 minutes
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
reaction(() => weblinkStore.weblinks, (links) => {
|
reaction(() => weblinkStore.weblinks, (links) => {
|
||||||
weblinks.replace(links.map((data) => new WebLink({
|
const seenWeblinks = new Set<string>();
|
||||||
|
|
||||||
|
for (const weblinkData of links) {
|
||||||
|
seenWeblinks.add(weblinkData.id);
|
||||||
|
|
||||||
|
if (!webLinkEntities.has(weblinkData.id)) {
|
||||||
|
const link = new WebLink({
|
||||||
metadata: {
|
metadata: {
|
||||||
uid: data.id,
|
uid: weblinkData.id,
|
||||||
name: data.name,
|
name: weblinkData.name,
|
||||||
source: "local",
|
source: "local",
|
||||||
labels: {}
|
labels: {}
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
url: data.url
|
url: weblinkData.url
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: "available",
|
phase: "available",
|
||||||
active: true
|
active: true
|
||||||
}
|
}
|
||||||
})));
|
|
||||||
weblinks.push(...defaultLinks);
|
|
||||||
|
|
||||||
for (const link of weblinks) {
|
|
||||||
validateLink(link).catch((error) => {
|
|
||||||
logger.error(`failed to validate link ${link.spec.url}: %s`, error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
webLinkEntities.set(weblinkData.id, [
|
||||||
|
link,
|
||||||
|
periodicallyCheckLink(link),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop checking and remove weblinks that are no longer in the store
|
||||||
|
for (const [weblinkId, [, disposer]] of webLinkEntities) {
|
||||||
|
if (!seenWeblinks.has(weblinkId)) {
|
||||||
|
disposer();
|
||||||
|
webLinkEntities.delete(weblinkId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, {fireImmediately: true});
|
}, {fireImmediately: true});
|
||||||
|
|
||||||
catalogEntityRegistry.addObservableSource("weblinks", weblinks);
|
catalogEntityRegistry.addComputedSource("weblinks", computed(() => Array.from(webLinkEntities.values(), ([link]) => link)));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
||||||
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog";
|
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityKindData } from "../../common/catalog";
|
||||||
import { iter } from "../../common/utils";
|
import { iter } from "../../common/utils";
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@ -62,6 +62,10 @@ export class CatalogEntityRegistry {
|
|||||||
|
|
||||||
return items as T[];
|
return items as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData): T[] {
|
||||||
|
return this.getItemsForApiKind(apiVersion, kind);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import "../common/cluster-ipc";
|
|||||||
import type http from "http";
|
import type http from "http";
|
||||||
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||||
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||||
import type { Cluster } from "./cluster";
|
import { Cluster } from "./cluster";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { Singleton } from "../common/utils";
|
import { Singleton } from "../common/utils";
|
||||||
@ -32,6 +32,8 @@ import { KubernetesCluster, KubernetesClusterPrometheusMetrics, KubernetesCluste
|
|||||||
import { ipcMainOn } from "../common/ipc";
|
import { ipcMainOn } from "../common/ipc";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
|
|
||||||
|
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||||
|
|
||||||
export class ClusterManager extends Singleton {
|
export class ClusterManager extends Singleton {
|
||||||
private store = ClusterStore.getInstance();
|
private store = ClusterStore.getInstance();
|
||||||
deleting = observable.set<ClusterId>();
|
deleting = observable.set<ClusterId>();
|
||||||
@ -73,7 +75,7 @@ export class ClusterManager extends Singleton {
|
|||||||
if (removedClusters.length > 0) {
|
if (removedClusters.length > 0) {
|
||||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||||
|
|
||||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
logger.info(`${logPrefix} removing clusters`, meta);
|
||||||
removedClusters.forEach(cluster => cluster.disconnect());
|
removedClusters.forEach(cluster => cluster.disconnect());
|
||||||
this.store.removedClusters.clear();
|
this.store.removedClusters.clear();
|
||||||
}
|
}
|
||||||
@ -103,10 +105,18 @@ export class ClusterManager extends Singleton {
|
|||||||
|
|
||||||
this.updateEntityStatus(entity, cluster);
|
this.updateEntityStatus(entity, cluster);
|
||||||
|
|
||||||
entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
|
entity.metadata.labels = {
|
||||||
entity.metadata.labels.distro = cluster.distribution;
|
...entity.metadata.labels,
|
||||||
|
...cluster.labels,
|
||||||
|
};
|
||||||
|
entity.metadata.distro = cluster.distribution;
|
||||||
|
entity.metadata.kubeVersion = cluster.version;
|
||||||
|
|
||||||
if (cluster.preferences?.clusterName) {
|
if (cluster.preferences?.clusterName) {
|
||||||
|
/**
|
||||||
|
* Only set the name if the it is overriden in preferences. If it isn't
|
||||||
|
* set then the name of the entity has been explicitly set by its source
|
||||||
|
*/
|
||||||
entity.metadata.name = cluster.preferences.clusterName;
|
entity.metadata.name = cluster.preferences.clusterName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,17 +171,28 @@ export class ClusterManager extends Singleton {
|
|||||||
const cluster = this.store.getById(entity.metadata.uid);
|
const cluster = this.store.getById(entity.metadata.uid);
|
||||||
|
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* Add the bare minimum of data to ClusterStore. And especially no
|
||||||
|
* preferences, as those might be configured by the entity's source
|
||||||
|
*/
|
||||||
this.store.addCluster({
|
this.store.addCluster({
|
||||||
id: entity.metadata.uid,
|
id: entity.metadata.uid,
|
||||||
preferences: {
|
|
||||||
clusterName: entity.metadata.name
|
|
||||||
},
|
|
||||||
kubeConfigPath: entity.spec.kubeconfigPath,
|
kubeConfigPath: entity.spec.kubeconfigPath,
|
||||||
contextName: entity.spec.kubeconfigContext
|
contextName: entity.spec.kubeconfigContext,
|
||||||
|
accessibleNamespaces: entity.spec.accessibleNamespaces ?? [],
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||||
|
logger.warn(`${logPrefix} kubeconfig file disappeared`, { path: entity.spec.kubeconfigPath });
|
||||||
|
} else {
|
||||||
|
logger.error(`${logPrefix} failed to add cluster: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||||
cluster.contextName = entity.spec.kubeconfigContext;
|
cluster.contextName = entity.spec.kubeconfigContext;
|
||||||
|
cluster.accessibleNamespaces = entity.spec.accessibleNamespaces ?? [];
|
||||||
|
|
||||||
this.updateEntityFromCluster(cluster);
|
this.updateEntityFromCluster(cluster);
|
||||||
}
|
}
|
||||||
@ -179,7 +200,7 @@ export class ClusterManager extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onNetworkOffline = () => {
|
protected onNetworkOffline = () => {
|
||||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
logger.info(`${logPrefix} network is offline`);
|
||||||
this.store.clustersList.forEach((cluster) => {
|
this.store.clustersList.forEach((cluster) => {
|
||||||
if (!cluster.disconnected) {
|
if (!cluster.disconnected) {
|
||||||
cluster.online = false;
|
cluster.online = false;
|
||||||
@ -190,7 +211,7 @@ export class ClusterManager extends Singleton {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected onNetworkOnline = () => {
|
protected onNetworkOnline = () => {
|
||||||
logger.info("[CLUSTER-MANAGER]: network is online");
|
logger.info(`${logPrefix} network is online`);
|
||||||
this.store.clustersList.forEach((cluster) => {
|
this.store.clustersList.forEach((cluster) => {
|
||||||
if (!cluster.disconnected) {
|
if (!cluster.disconnected) {
|
||||||
cluster.refreshConnectionStatus().catch((e) => e);
|
cluster.refreshConnectionStatus().catch((e) => e);
|
||||||
@ -236,8 +257,10 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
|||||||
name: cluster.name,
|
name: cluster.name,
|
||||||
source: "local",
|
source: "local",
|
||||||
labels: {
|
labels: {
|
||||||
|
...cluster.labels,
|
||||||
|
},
|
||||||
distro: cluster.distribution,
|
distro: cluster.distribution,
|
||||||
}
|
kubeVersion: cluster.version,
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
kubeconfigPath: cluster.kubeConfigPath,
|
kubeconfigPath: cluster.kubeConfigPath,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { VersionDetector } from "./cluster-detectors/version-detector";
|
|||||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||||
import plimit from "p-limit";
|
import plimit from "p-limit";
|
||||||
import { toJS } from "../common/utils";
|
import { toJS } from "../common/utils";
|
||||||
|
import { initialNodeShellImage } from "../common/cluster-store";
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
AccessGranted = 2,
|
AccessGranted = 2,
|
||||||
@ -61,6 +62,8 @@ export enum ClusterMetricsResourceType {
|
|||||||
VolumeClaim = "VolumeClaim",
|
VolumeClaim = "VolumeClaim",
|
||||||
ReplicaSet = "ReplicaSet",
|
ReplicaSet = "ReplicaSet",
|
||||||
DaemonSet = "DaemonSet",
|
DaemonSet = "DaemonSet",
|
||||||
|
Job = "Job",
|
||||||
|
Namespace = "Namespace"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClusterRefreshOptions = {
|
export type ClusterRefreshOptions = {
|
||||||
@ -234,8 +237,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detected kubernetes distribution
|
||||||
|
*/
|
||||||
@computed get distribution(): string {
|
@computed get distribution(): string {
|
||||||
return this.metadata.distribution?.toString() || "unknown";
|
return this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The detected kubernetes version
|
||||||
|
*/
|
||||||
|
@computed get version(): string {
|
||||||
|
return this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -250,13 +263,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return toJS({ prometheus, prometheusProvider });
|
return toJS({ prometheus, prometheusProvider });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Kubernetes version
|
|
||||||
*/
|
|
||||||
get version(): string {
|
|
||||||
return String(this.metadata?.version ?? "");
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.id = model.id;
|
this.id = model.id;
|
||||||
@ -745,4 +751,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
||||||
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get nodeShellImage(): string {
|
||||||
|
return this.preferences.nodeShellImage || initialNodeShellImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
get imagePullSecret(): string {
|
||||||
|
return this.preferences.imagePullSecret || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,8 +146,10 @@ export class ContextHandler {
|
|||||||
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy;
|
||||||
}
|
}
|
||||||
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, proxyEnv);
|
this.kubeAuthProxy = new KubeAuthProxy(this.cluster, proxyEnv);
|
||||||
await this.kubeAuthProxy.run();
|
this.kubeAuthProxy.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.kubeAuthProxy.whenReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
stopServer() {
|
stopServer() {
|
||||||
|
|||||||
@ -34,6 +34,36 @@ export class HelmChartManager {
|
|||||||
switch (this.repo.name) {
|
switch (this.repo.name) {
|
||||||
case "stable":
|
case "stable":
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
|
"invalid-semver": [
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.3.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver but more",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.4.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
],
|
||||||
"apm-server": [
|
"apm-server": [
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
|
|||||||
@ -62,6 +62,36 @@ describe("Helm Service tests", () => {
|
|||||||
digest: "test"
|
digest: "test"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"invalid-semver": [
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.4.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "v4.3.0",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
apiVersion: "3.0.0",
|
||||||
|
name: "weird-versioning",
|
||||||
|
version: "I am not semver but more",
|
||||||
|
repo: "stable",
|
||||||
|
digest: "test"
|
||||||
|
},
|
||||||
|
],
|
||||||
"redis": [
|
"redis": [
|
||||||
{
|
{
|
||||||
apiVersion: "3.0.0",
|
apiVersion: "3.0.0",
|
||||||
|
|||||||
@ -146,7 +146,7 @@ export class HelmRepoManager extends Singleton {
|
|||||||
return stdout;
|
return stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async addСustomRepo(repoAttributes : HelmRepo) {
|
public static async addCustomRepo(repoAttributes : HelmRepo) {
|
||||||
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`);
|
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`);
|
||||||
const helm = await helmCli.binaryPath();
|
const helm = await helmCli.binaryPath();
|
||||||
|
|
||||||
|
|||||||
@ -19,13 +19,14 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import semver from "semver";
|
import semver, { SemVer } from "semver";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { HelmRepoManager } from "./helm-repo-manager";
|
import { HelmRepoManager } from "./helm-repo-manager";
|
||||||
import { HelmChartManager } from "./helm-chart-manager";
|
import { HelmChartManager } from "./helm-chart-manager";
|
||||||
import type { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
import type { HelmChart, HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
||||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||||
|
import { iter, sortCompareChartVersions } from "../../common/utils";
|
||||||
|
|
||||||
interface GetReleaseValuesArgs {
|
interface GetReleaseValuesArgs {
|
||||||
cluster: Cluster;
|
cluster: Cluster;
|
||||||
@ -132,28 +133,55 @@ class HelmService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
|
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
|
||||||
const groups = new Map(Object.entries(chartGroups));
|
return Object.fromEntries(
|
||||||
|
iter.filterMap(
|
||||||
for (const [chartName, charts] of groups) {
|
Object.entries(chartGroups),
|
||||||
if (charts[0].deprecated) {
|
([name, charts]) => {
|
||||||
groups.delete(chartName);
|
for (const chart of charts) {
|
||||||
|
if (chart.deprecated) {
|
||||||
|
// ignore chart group if any chart is deprecated
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.fromEntries(groups);
|
return [name, charts];
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sortCharts(charts: HelmChart[]) {
|
||||||
|
interface ExtendedHelmChart extends HelmChart {
|
||||||
|
__version: SemVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartsWithVersion = Array.from(
|
||||||
|
iter.map(
|
||||||
|
charts,
|
||||||
|
(chart => {
|
||||||
|
const __version = semver.coerce(chart.version, { includePrerelease: true, loose: true });
|
||||||
|
|
||||||
|
if (!__version) {
|
||||||
|
logger.error(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
|
||||||
|
}
|
||||||
|
|
||||||
|
(chart as ExtendedHelmChart).__version = __version;
|
||||||
|
|
||||||
|
return chart as ExtendedHelmChart;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return chartsWithVersion
|
||||||
|
.sort(sortCompareChartVersions)
|
||||||
|
.map(chart => (delete chart.__version, chart as HelmChart));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
|
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
|
||||||
for (const key in chartGroups) {
|
return Object.fromEntries(
|
||||||
chartGroups[key] = chartGroups[key].sort((first, second) => {
|
Object.entries(chartGroups)
|
||||||
const firstVersion = semver.coerce(first.version || 0);
|
.map(([name, charts]) => [name, this.sortCharts(charts)])
|
||||||
const secondVersion = semver.coerce(second.version || 0);
|
);
|
||||||
|
|
||||||
return semver.compare(secondVersion, firstVersion);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return chartGroups;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,11 @@ import { UserStore } from "../common/user-store";
|
|||||||
import { WeblinkStore } from "../common/weblink-store";
|
import { WeblinkStore } from "../common/weblink-store";
|
||||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||||
|
import { SentryInit } from "../common/sentry";
|
||||||
|
|
||||||
|
// This has to be called before start using winton-based logger
|
||||||
|
// For example, before any logger.log
|
||||||
|
SentryInit();
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
const cleanup = disposer();
|
const cleanup = disposer();
|
||||||
@ -132,15 +137,20 @@ app.on("ready", async () => {
|
|||||||
/**
|
/**
|
||||||
* The following sync MUST be done before HotbarStore creation, because that
|
* The following sync MUST be done before HotbarStore creation, because that
|
||||||
* store has migrations that will remove items that previous migrations add
|
* store has migrations that will remove items that previous migrations add
|
||||||
* if this is not presant
|
* if this is not present
|
||||||
*/
|
*/
|
||||||
syncGeneralEntities();
|
syncGeneralEntities();
|
||||||
|
|
||||||
logger.info("💾 Loading stores");
|
logger.info("💾 Loading stores");
|
||||||
|
|
||||||
UserStore.createInstance().startMainReactions();
|
UserStore.createInstance().startMainReactions();
|
||||||
|
|
||||||
|
// ClusterStore depends on: UserStore
|
||||||
ClusterStore.createInstance().provideInitialFromMain();
|
ClusterStore.createInstance().provideInitialFromMain();
|
||||||
|
|
||||||
|
// HotbarStore depends on: ClusterStore
|
||||||
HotbarStore.createInstance();
|
HotbarStore.createInstance();
|
||||||
|
|
||||||
ExtensionsStore.createInstance();
|
ExtensionsStore.createInstance();
|
||||||
FilesystemProvisionerStore.createInstance();
|
FilesystemProvisionerStore.createInstance();
|
||||||
WeblinkStore.createInstance();
|
WeblinkStore.createInstance();
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IpcMainInvokeEvent } from "electron";
|
import type { IpcMainInvokeEvent } from "electron";
|
||||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
||||||
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
||||||
@ -50,9 +50,9 @@ export function initIpcMainHandlers() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
||||||
const entity = catalogEntityRegistry.getById<KubernetesCluster>(clusterId);
|
const entity = catalogEntityRegistry.getById(clusterId);
|
||||||
|
|
||||||
for (const kubeEntity of catalogEntityRegistry.getItemsForApiKind(entity.apiVersion, entity.kind)) {
|
for (const kubeEntity of catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster)) {
|
||||||
kubeEntity.status.active = false;
|
kubeEntity.status.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { Kubectl } from "./kubectl";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
import { getPortFrom } from "./utils/get-port";
|
import { getPortFrom } from "./utils/get-port";
|
||||||
|
import { makeObservable, observable, when } from "mobx";
|
||||||
|
|
||||||
export interface KubeAuthProxyLog {
|
export interface KubeAuthProxyLog {
|
||||||
data: string;
|
data: string;
|
||||||
@ -47,8 +48,11 @@ export class KubeAuthProxy {
|
|||||||
protected env: NodeJS.ProcessEnv = null;
|
protected env: NodeJS.ProcessEnv = null;
|
||||||
protected proxyProcess: ChildProcess;
|
protected proxyProcess: ChildProcess;
|
||||||
protected kubectl: Kubectl;
|
protected kubectl: Kubectl;
|
||||||
|
@observable protected ready: boolean;
|
||||||
|
|
||||||
constructor(cluster: Cluster, env: NodeJS.ProcessEnv) {
|
constructor(cluster: Cluster, env: NodeJS.ProcessEnv) {
|
||||||
|
makeObservable(this);
|
||||||
|
this.ready = false;
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.cluster = cluster;
|
this.cluster = cluster;
|
||||||
this.kubectl = Kubectl.bundled();
|
this.kubectl = Kubectl.bundled();
|
||||||
@ -58,9 +62,13 @@ export class KubeAuthProxy {
|
|||||||
return url.parse(this.cluster.apiUrl).hostname;
|
return url.parse(this.cluster.apiUrl).hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get whenReady() {
|
||||||
|
return when(() => this.ready);
|
||||||
|
}
|
||||||
|
|
||||||
public async run(): Promise<void> {
|
public async run(): Promise<void> {
|
||||||
if (this.proxyProcess) {
|
if (this.proxyProcess) {
|
||||||
return;
|
return this.whenReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxyBin = await this.kubectl.getPath();
|
const proxyBin = await this.kubectl.getPath();
|
||||||
@ -103,7 +111,9 @@ export class KubeAuthProxy {
|
|||||||
this.sendIpcLogMessage({ data: data.toString() });
|
this.sendIpcLogMessage({ data: data.toString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
return waitUntilUsed(this.port, 500, 10000);
|
await waitUntilUsed(this.port, 500, 10000);
|
||||||
|
|
||||||
|
this.ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected parseError(data: string) {
|
protected parseError(data: string) {
|
||||||
@ -132,6 +142,7 @@ export class KubeAuthProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public exit() {
|
public exit() {
|
||||||
|
this.ready = false;
|
||||||
if (!this.proxyProcess) return;
|
if (!this.proxyProcess) return;
|
||||||
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
|
logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta());
|
||||||
this.proxyProcess.kill();
|
this.proxyProcess.kill();
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class PrometheusHelm extends PrometheusLens {
|
|||||||
readonly id: string = "helm";
|
readonly id: string = "helm";
|
||||||
readonly name: string = "Helm";
|
readonly name: string = "Helm";
|
||||||
readonly rateAccuracy: string = "5m";
|
readonly rateAccuracy: string = "5m";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class PrometheusLens extends PrometheusProvider {
|
|||||||
readonly id: string = "lens";
|
readonly id: string = "lens";
|
||||||
readonly name: string = "Lens";
|
readonly name: string = "Lens";
|
||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = true;
|
readonly isConfigurable: boolean = false;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly id: string = "operator";
|
readonly id: string = "operator";
|
||||||
readonly name: string = "Prometheus Operator";
|
readonly name: string = "Prometheus Operator";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
||||||
|
|||||||
@ -28,7 +28,7 @@ export class PrometheusStacklight extends PrometheusProvider {
|
|||||||
readonly id: string = "stacklight";
|
readonly id: string = "stacklight";
|
||||||
readonly name: string = "Stacklight";
|
readonly name: string = "Stacklight";
|
||||||
readonly rateAccuracy: string = "1m";
|
readonly rateAccuracy: string = "1m";
|
||||||
readonly isConfigurable: boolean = false;
|
readonly isConfigurable: boolean = true;
|
||||||
|
|
||||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -61,7 +61,8 @@ export class LensProxy extends Singleton {
|
|||||||
if (req.url.startsWith(`${apiPrefix}?`)) {
|
if (req.url.startsWith(`${apiPrefix}?`)) {
|
||||||
handleWsUpgrade(req, socket, head);
|
handleWsUpgrade(req, socket, head);
|
||||||
} else {
|
} else {
|
||||||
this.handleProxyUpgrade(proxy, req, socket, head);
|
this.handleProxyUpgrade(proxy, req, socket, head)
|
||||||
|
.catch(error => logger.error(`[LENS-PROXY]: failed to handle proxy upgrade: ${error}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -168,7 +169,10 @@ export class LensProxy extends Singleton {
|
|||||||
if (!res.headersSent && req.url) {
|
if (!res.headersSent && req.url) {
|
||||||
const url = new URL(req.url, "http://localhost");
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
|
||||||
if (url.searchParams.has("watch")) res.flushHeaders();
|
if (url.searchParams.has("watch")) {
|
||||||
|
res.statusCode = proxyRes.statusCode;
|
||||||
|
res.flushHeaders();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -177,6 +181,8 @@ export class LensProxy extends Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url });
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
||||||
|
|
||||||
@ -189,14 +195,15 @@ export class LensProxy extends Singleton {
|
|||||||
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.retryCounters.set(reqId, retryCount + 1);
|
this.retryCounters.set(reqId, retryCount + 1);
|
||||||
this.handleRequest(proxy, req, res);
|
this.handleRequest(proxy, req, res)
|
||||||
|
.catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
res.writeHead(500).end("Oops, something went wrong.");
|
res.writeHead(500).end(`Oops, something went wrong.\n${error}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
|
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,13 +134,24 @@ export class PortForwardRoute {
|
|||||||
name: resourceName,
|
name: resourceName,
|
||||||
port,
|
port,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
const started = await portForward.start();
|
const started = await portForward.start();
|
||||||
|
|
||||||
if (!started) {
|
if (!started) {
|
||||||
|
logger.warn("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
||||||
|
|
||||||
return respondJson(response, {
|
return respondJson(response, {
|
||||||
message: "Failed to open port-forward"
|
message: "Failed to open port-forward"
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName });
|
||||||
|
|
||||||
|
return respondJson(response, {
|
||||||
|
message: error?.toString() || "Failed to open port-forward",
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
portForward.open();
|
portForward.open();
|
||||||
|
|||||||
@ -31,8 +31,11 @@ export class LocalShellSession extends ShellSession {
|
|||||||
return [helmCli.getBinaryDir()];
|
return [helmCli.getBinaryDir()];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async open() {
|
protected get cwd(): string | undefined {
|
||||||
|
return this.cluster.preferences?.terminalCWD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async open() {
|
||||||
const env = await this.getCachedShellEnv();
|
const env = await this.getCachedShellEnv();
|
||||||
const shell = env.PTYSHELL;
|
const shell = env.PTYSHELL;
|
||||||
const args = await this.getShellArgs(shell);
|
const args = await this.getShellArgs(shell);
|
||||||
@ -51,7 +54,7 @@ export class LocalShellSession extends ShellSession {
|
|||||||
case "bash":
|
case "bash":
|
||||||
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
|
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
|
||||||
case "fish":
|
case "fish":
|
||||||
return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPathP}"`];
|
return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return ["--login"];
|
return ["--login"];
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -32,6 +32,10 @@ export class NodeShellSession extends ShellSession {
|
|||||||
protected podId = `node-shell-${uuid()}`;
|
protected podId = `node-shell-${uuid()}`;
|
||||||
protected kc: KubeConfig;
|
protected kc: KubeConfig;
|
||||||
|
|
||||||
|
protected get cwd(): string | undefined {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
||||||
super(socket, cluster);
|
super(socket, cluster);
|
||||||
}
|
}
|
||||||
@ -77,13 +81,16 @@ export class NodeShellSession extends ShellSession {
|
|||||||
}],
|
}],
|
||||||
containers: [{
|
containers: [{
|
||||||
name: "shell",
|
name: "shell",
|
||||||
image: "docker.io/alpine:3.13",
|
image: this.cluster.nodeShellImage,
|
||||||
securityContext: {
|
securityContext: {
|
||||||
privileged: true,
|
privileged: true,
|
||||||
},
|
},
|
||||||
command: ["nsenter"],
|
command: ["nsenter"],
|
||||||
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
|
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
|
||||||
}],
|
}],
|
||||||
|
imagePullSecrets: [{
|
||||||
|
name: this.cluster.imagePullSecret,
|
||||||
|
}]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fse from "fs-extra";
|
||||||
import type { Cluster } from "../cluster";
|
import type { Cluster } from "../cluster";
|
||||||
import { Kubectl } from "../kubectl";
|
import { Kubectl } from "../kubectl";
|
||||||
import type * as WebSocket from "ws";
|
import type * as WebSocket from "ws";
|
||||||
@ -50,9 +51,7 @@ export abstract class ShellSession {
|
|||||||
protected kubectlBinDirP: Promise<string>;
|
protected kubectlBinDirP: Promise<string>;
|
||||||
protected kubeconfigPathP: Promise<string>;
|
protected kubeconfigPathP: Promise<string>;
|
||||||
|
|
||||||
protected get cwd(): string | undefined {
|
protected abstract get cwd(): string | undefined;
|
||||||
return this.cluster.preferences?.terminalCWD;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(protected websocket: WebSocket, protected cluster: Cluster) {
|
constructor(protected websocket: WebSocket, protected cluster: Cluster) {
|
||||||
this.kubectl = new Kubectl(cluster.version);
|
this.kubectl = new Kubectl(cluster.version);
|
||||||
@ -60,10 +59,14 @@ export abstract class ShellSession {
|
|||||||
this.kubectlBinDirP = this.kubectl.binDir();
|
this.kubectlBinDirP = this.kubectl.binDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
open(shell: string, args: string[], env: Record<string, any>): void {
|
protected async open(shell: string, args: string[], env: Record<string, any>) {
|
||||||
|
const cwd = (this.cwd && await fse.pathExists(this.cwd))
|
||||||
|
? this.cwd
|
||||||
|
: env.HOME;
|
||||||
|
|
||||||
this.shellProcess = pty.spawn(shell, args, {
|
this.shellProcess = pty.spawn(shell, args, {
|
||||||
cols: 80,
|
cols: 80,
|
||||||
cwd: this.cwd || env.HOME,
|
cwd,
|
||||||
env,
|
env,
|
||||||
name: "xterm-256color",
|
name: "xterm-256color",
|
||||||
rows: 30,
|
rows: 30,
|
||||||
|
|||||||
@ -93,7 +93,7 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
click() {
|
click() {
|
||||||
windowManager
|
windowManager
|
||||||
.navigate(preferencesURL())
|
.navigate(preferencesURL())
|
||||||
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to nativate to Preferences`, { error }));
|
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to navigate to Preferences`, { error }));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import type { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
import URLParse from "url-parse";
|
import URLParse from "url-parse";
|
||||||
|
import logger from "../logger";
|
||||||
|
|
||||||
interface GetPortArgs {
|
interface GetPortArgs {
|
||||||
/**
|
/**
|
||||||
@ -47,11 +48,15 @@ interface GetPortArgs {
|
|||||||
* @returns A Promise for port number
|
* @returns A Promise for port number
|
||||||
*/
|
*/
|
||||||
export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number> {
|
export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number> {
|
||||||
|
const logLines: string[] = [];
|
||||||
|
|
||||||
return new Promise<number>((resolve, reject) => {
|
return new Promise<number>((resolve, reject) => {
|
||||||
const handler = (data: any) => {
|
const handler = (data: any) => {
|
||||||
const logItem: string = data.toString();
|
const logItem: string = data.toString();
|
||||||
const match = logItem.match(args.lineRegex);
|
const match = logItem.match(args.lineRegex);
|
||||||
|
|
||||||
|
logLines.push(logItem);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
// use unknown protocol so that there is no default port
|
// use unknown protocol so that there is no default port
|
||||||
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
||||||
@ -64,8 +69,9 @@ export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number
|
|||||||
};
|
};
|
||||||
const timeoutID = setTimeout(() => {
|
const timeoutID = setTimeout(() => {
|
||||||
stream.off("data", handler);
|
stream.off("data", handler);
|
||||||
|
logger.warn(`[getPortFrom]: failed to retrieve port via ${args.lineRegex.toString()}: ${logLines}`);
|
||||||
reject(new Error("failed to retrieve port from stream"));
|
reject(new Error("failed to retrieve port from stream"));
|
||||||
}, args.timeout ?? 5000);
|
}, args.timeout ?? 15000);
|
||||||
|
|
||||||
stream.on("data", handler);
|
stream.on("data", handler);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export default {
|
|||||||
workspaces.set(id, name);
|
workspaces.set(id, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clusters: ClusterModel[] = store.get("clusters");
|
const clusters: ClusterModel[] = store.get("clusters") ?? [];
|
||||||
|
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
||||||
|
|||||||
@ -108,7 +108,7 @@ export default {
|
|||||||
run(store) {
|
run(store) {
|
||||||
const folder = path.resolve(app.getPath("userData"), "lens-local-storage");
|
const folder = path.resolve(app.getPath("userData"), "lens-local-storage");
|
||||||
|
|
||||||
const oldClusters: ClusterModel[] = store.get("clusters");
|
const oldClusters: ClusterModel[] = store.get("clusters") ?? [];
|
||||||
const clusters = new Map<string, ClusterModel>();
|
const clusters = new Map<string, ClusterModel>();
|
||||||
|
|
||||||
for (const { id: oldId, ...cluster } of oldClusters) {
|
for (const { id: oldId, ...cluster } of oldClusters) {
|
||||||
|
|||||||
@ -46,6 +46,8 @@ export function joinMigrations(...declarations: MigrationDeclaration[]): Migrati
|
|||||||
iter.map(
|
iter.map(
|
||||||
migrations,
|
migrations,
|
||||||
([v, fns]) => [v, (store: Conf<any>) => {
|
([v, fns]) => [v, (store: Conf<any>) => {
|
||||||
|
migrationLog(`Running ${v} migration for ${store.path}`);
|
||||||
|
|
||||||
for (const fn of fns) {
|
for (const fn of fns) {
|
||||||
fn(store);
|
fn(store);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ interface Pre500WorkspaceStoreModel {
|
|||||||
export default {
|
export default {
|
||||||
version: "5.0.0-beta.10",
|
version: "5.0.0-beta.10",
|
||||||
run(store) {
|
run(store) {
|
||||||
const hotbars: Hotbar[] = store.get("hotbars");
|
const hotbars = (store.get("hotbars") as Hotbar[] ?? []).filter(Boolean);
|
||||||
const userDataPath = app.getPath("userData");
|
const userDataPath = app.getPath("userData");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -75,6 +75,11 @@ export default {
|
|||||||
for (const workspaceId of cluster.workspaces ?? [cluster.workspace].filter(Boolean)) {
|
for (const workspaceId of cluster.workspaces ?? [cluster.workspace].filter(Boolean)) {
|
||||||
const workspaceHotbar = workspaceHotbars.get(workspaceId);
|
const workspaceHotbar = workspaceHotbars.get(workspaceId);
|
||||||
|
|
||||||
|
if (!workspaceHotbar) {
|
||||||
|
migrationLog(`Cluster ${uid} has unknown workspace ID, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
migrationLog(`Adding cluster ${uid} to ${workspaceHotbar.name}`);
|
migrationLog(`Adding cluster ${uid} to ${workspaceHotbar.name}`);
|
||||||
|
|
||||||
if (workspaceHotbar?.items.length < defaultHotbarCells) {
|
if (workspaceHotbar?.items.length < defaultHotbarCells) {
|
||||||
@ -143,8 +148,8 @@ export default {
|
|||||||
|
|
||||||
store.set("hotbars", hotbars);
|
store.set("hotbars", hotbars);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
|
// ignore files being missing
|
||||||
// ignore lens-workspace-store.json being missing
|
if (error.code !== "ENOENT") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/migrations/user-store/5.0.3-beta.1.ts
Normal file
80
src/migrations/user-store/5.0.3-beta.1.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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 { app } from "electron";
|
||||||
|
import { existsSync, readFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import os from "os";
|
||||||
|
import { ClusterStore, ClusterStoreModel } from "../../common/cluster-store";
|
||||||
|
import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store";
|
||||||
|
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||||
|
import { isLogicalChildPath } from "../../common/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
version: "5.0.3-beta.1",
|
||||||
|
run(store) {
|
||||||
|
try {
|
||||||
|
const { syncKubeconfigEntries = [], ...preferences }: UserPreferencesModel = store.get("preferences") ?? {};
|
||||||
|
const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(path.resolve(app.getPath("userData"), "lens-cluster-store.json"), "utf-8")) ?? {};
|
||||||
|
const extensionDataDir = path.resolve(app.getPath("userData"), "extension_data");
|
||||||
|
const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath));
|
||||||
|
|
||||||
|
syncPaths.add(path.join(os.homedir(), ".kube"));
|
||||||
|
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
const dirOfKubeconfig = path.dirname(cluster.kubeConfigPath);
|
||||||
|
|
||||||
|
if (dirOfKubeconfig === ClusterStore.storedKubeConfigFolder) {
|
||||||
|
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is under ClusterStore.storedKubeConfigFolder`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncPaths.has(cluster.kubeConfigPath) || syncPaths.has(dirOfKubeconfig)) {
|
||||||
|
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is already being synced`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLogicalChildPath(extensionDataDir, cluster.kubeConfigPath)) {
|
||||||
|
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is placed under an extension_data folder`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(cluster.kubeConfigPath)) {
|
||||||
|
migrationLog(`Skipping ${cluster.id} because kubeConfigPath no longer exists`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationLog(`Adding ${cluster.kubeConfigPath} from ${cluster.id} to sync paths`);
|
||||||
|
syncPaths.add(cluster.kubeConfigPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSyncEntries: KubeconfigSyncEntry[] = [...syncPaths].map(filePath => ({ filePath }));
|
||||||
|
|
||||||
|
migrationLog("Final list of synced paths", updatedSyncEntries);
|
||||||
|
store.set("preferences", { ...preferences, syncKubeconfigEntries: updatedSyncEntries });
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ENOENT") {
|
||||||
|
// ignore files being missing
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as MigrationDeclaration;
|
||||||
@ -25,6 +25,7 @@ import { joinMigrations } from "../helpers";
|
|||||||
|
|
||||||
import version210Beta4 from "./2.1.0-beta.4";
|
import version210Beta4 from "./2.1.0-beta.4";
|
||||||
import version500Alpha3 from "./5.0.0-alpha.3";
|
import version500Alpha3 from "./5.0.0-alpha.3";
|
||||||
|
import version503Beta1 from "./5.0.3-beta.1";
|
||||||
import { fileNameMigration } from "./file-name-migration";
|
import { fileNameMigration } from "./file-name-migration";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -34,4 +35,5 @@ export {
|
|||||||
export default joinMigrations(
|
export default joinMigrations(
|
||||||
version210Beta4,
|
version210Beta4,
|
||||||
version500Alpha3,
|
version500Alpha3,
|
||||||
|
version503Beta1,
|
||||||
);
|
);
|
||||||
|
|||||||
43
src/migrations/weblinks-store/5.1.4.ts
Normal file
43
src/migrations/weblinks-store/5.1.4.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 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 { docsUrl, slackUrl } from "../../common/vars";
|
||||||
|
import type { WeblinkData } from "../../common/weblink-store";
|
||||||
|
import type { MigrationDeclaration } from "../helpers";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
version: "5.1.4",
|
||||||
|
run(store) {
|
||||||
|
const weblinksRaw: any = store.get("weblinks");
|
||||||
|
const weblinks = (Array.isArray(weblinksRaw) ? weblinksRaw : []) as WeblinkData[];
|
||||||
|
|
||||||
|
weblinks.push(
|
||||||
|
{ id: "https://k8slens.dev", name: "Lens Website", url: "https://k8slens.dev" },
|
||||||
|
{ id: docsUrl, name: "Lens Documentation", url: docsUrl },
|
||||||
|
{ id: slackUrl, name: "Lens Community Slack", url: slackUrl },
|
||||||
|
{ id: "https://kubernetes.io/docs/home/", name: "Kubernetes Documentation", url: "https://kubernetes.io/docs/home/" },
|
||||||
|
{ id: "https://twitter.com/k8slens", name: "Lens on Twitter", url: "https://twitter.com/k8slens" },
|
||||||
|
{ id: "https://medium.com/k8slens", name: "Lens Official Blog", url: "https://medium.com/k8slens" }
|
||||||
|
);
|
||||||
|
|
||||||
|
store.set("weblinks", weblinks);
|
||||||
|
}
|
||||||
|
} as MigrationDeclaration;
|
||||||
@ -19,11 +19,10 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.AppInit {
|
import { joinMigrations } from "../helpers";
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.waiting-services {
|
import version514 from "./5.1.4";
|
||||||
font-size: small;
|
|
||||||
opacity: .75;
|
export default joinMigrations(
|
||||||
}
|
version514,
|
||||||
}
|
);
|
||||||
@ -22,8 +22,9 @@
|
|||||||
import { ingressStore } from "../../components/+network-ingresses/ingress.store";
|
import { ingressStore } from "../../components/+network-ingresses/ingress.store";
|
||||||
import { apiManager } from "../api-manager";
|
import { apiManager } from "../api-manager";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
import { KubeObject } from "../kube-object";
|
||||||
|
|
||||||
class TestApi extends KubeApi {
|
class TestApi extends KubeApi<KubeObject> {
|
||||||
|
|
||||||
protected async checkPreferredVersion() {
|
protected async checkPreferredVersion() {
|
||||||
return;
|
return;
|
||||||
@ -36,6 +37,7 @@ describe("ApiManager", () => {
|
|||||||
const apiBase = "apis/v1/foo";
|
const apiBase = "apis/v1/foo";
|
||||||
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
|
const fallbackApiBase = "/apis/extensions/v1beta1/foo";
|
||||||
const kubeApi = new TestApi({
|
const kubeApi = new TestApi({
|
||||||
|
objectConstructor: KubeObject,
|
||||||
apiBase,
|
apiBase,
|
||||||
fallbackApiBases: [fallbackApiBase],
|
fallbackApiBases: [fallbackApiBase],
|
||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CustomResourceDefinition } from "../endpoints";
|
import { CustomResourceDefinition } from "../endpoints";
|
||||||
import type { IKubeObjectMetadata } from "../kube-object";
|
import type { KubeObjectMetadata } from "../kube-object";
|
||||||
|
|
||||||
describe("Crds", () => {
|
describe("Crds", () => {
|
||||||
describe("getVersion", () => {
|
describe("getVersion", () => {
|
||||||
@ -28,7 +28,7 @@ describe("Crds", () => {
|
|||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "foo",
|
apiVersion: "foo",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {} as IKubeObjectMetadata,
|
metadata: {} as KubeObjectMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
crd.spec = {
|
crd.spec = {
|
||||||
@ -48,7 +48,7 @@ describe("Crds", () => {
|
|||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "foo",
|
apiVersion: "foo",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {} as IKubeObjectMetadata,
|
metadata: {} as KubeObjectMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
crd.spec = {
|
crd.spec = {
|
||||||
@ -73,7 +73,7 @@ describe("Crds", () => {
|
|||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "foo",
|
apiVersion: "foo",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {} as IKubeObjectMetadata,
|
metadata: {} as KubeObjectMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
crd.spec = {
|
crd.spec = {
|
||||||
@ -99,7 +99,7 @@ describe("Crds", () => {
|
|||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "foo",
|
apiVersion: "foo",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {} as IKubeObjectMetadata,
|
metadata: {} as KubeObjectMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
crd.spec = {
|
crd.spec = {
|
||||||
|
|||||||
60
src/renderer/api/__tests__/endpoint.api.test.ts
Normal file
60
src/renderer/api/__tests__/endpoint.api.test.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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 { EndpointSubset } from "../endpoints";
|
||||||
|
|
||||||
|
describe("endpoint tests", () => {
|
||||||
|
describe("EndpointSubset", () => {
|
||||||
|
it.each([
|
||||||
|
4,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
{},
|
||||||
|
[],
|
||||||
|
"ahe",
|
||||||
|
/a/,
|
||||||
|
])("should always initialize fields when given %j", (data: any) => {
|
||||||
|
const sub = new EndpointSubset(data);
|
||||||
|
|
||||||
|
expect(sub.addresses).toStrictEqual([]);
|
||||||
|
expect(sub.notReadyAddresses).toStrictEqual([]);
|
||||||
|
expect(sub.ports).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toString should be addresses X ports", () => {
|
||||||
|
const sub = new EndpointSubset({
|
||||||
|
addresses: [{
|
||||||
|
ip: "1.1.1.1",
|
||||||
|
}, {
|
||||||
|
ip: "1.1.1.2",
|
||||||
|
}] as any,
|
||||||
|
notReadyAddresses: [],
|
||||||
|
ports: [{
|
||||||
|
port: "81",
|
||||||
|
}, {
|
||||||
|
port: "82",
|
||||||
|
}] as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(sub.toString()).toBe("1.1.1.1:81, 1.1.1.1:82, 1.1.1.2:81, 1.1.1.2:82");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -129,8 +129,18 @@ const tests: KubeApiParseTestData[] = [
|
|||||||
}],
|
}],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const throwtests = [
|
||||||
|
undefined,
|
||||||
|
"",
|
||||||
|
"ajklsmh"
|
||||||
|
];
|
||||||
|
|
||||||
describe("parseApi unit tests", () => {
|
describe("parseApi unit tests", () => {
|
||||||
it.each(tests)("testing %s", (url, expected) => {
|
it.each(tests)("testing %j", (url, expected) => {
|
||||||
expect(parseKubeApi(url)).toStrictEqual(expected);
|
expect(parseKubeApi(url)).toStrictEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(throwtests)("testing %j should throw", (url) => {
|
||||||
|
expect(() => parseKubeApi(url)).toThrowError("invalid apiPath");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
import { KubeObject } from "../kube-object";
|
||||||
|
|
||||||
describe("KubeApi", () => {
|
describe("KubeApi", () => {
|
||||||
it("uses url from apiBase if apiBase contains the resource", async () => {
|
it("uses url from apiBase if apiBase contains the resource", async () => {
|
||||||
@ -29,7 +30,7 @@ describe("KubeApi", () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resources: [{
|
resources: [{
|
||||||
name: "ingresses"
|
name: "ingresses"
|
||||||
}] as any []
|
}] as any[]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
||||||
@ -38,13 +39,13 @@ describe("KubeApi", () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resources: [{
|
resources: [{
|
||||||
name: "ingresses"
|
name: "ingresses"
|
||||||
}] as any []
|
}] as any[]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resources: [] as any []
|
resources: [] as any[]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -53,6 +54,7 @@ describe("KubeApi", () => {
|
|||||||
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
const apiBase = "/apis/networking.k8s.io/v1/ingresses";
|
||||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||||
const kubeApi = new KubeApi({
|
const kubeApi = new KubeApi({
|
||||||
|
objectConstructor: KubeObject,
|
||||||
apiBase,
|
apiBase,
|
||||||
fallbackApiBases: [fallbackApiBase],
|
fallbackApiBases: [fallbackApiBase],
|
||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
@ -68,7 +70,7 @@ describe("KubeApi", () => {
|
|||||||
if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
|
if (request.url === "/api-kube/apis/networking.k8s.io/v1") {
|
||||||
return {
|
return {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resources: [] as any []
|
resources: [] as any[]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
} else if (request.url === "/api-kube/apis/extensions/v1beta1") {
|
||||||
@ -76,13 +78,13 @@ describe("KubeApi", () => {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resources: [{
|
resources: [{
|
||||||
name: "ingresses"
|
name: "ingresses"
|
||||||
}] as any []
|
}] as any[]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
resources: [] as any []
|
resources: [] as any[]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -91,6 +93,7 @@ describe("KubeApi", () => {
|
|||||||
const apiBase = "apis/networking.k8s.io/v1/ingresses";
|
const apiBase = "apis/networking.k8s.io/v1/ingresses";
|
||||||
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
const fallbackApiBase = "/apis/extensions/v1beta1/ingresses";
|
||||||
const kubeApi = new KubeApi({
|
const kubeApi = new KubeApi({
|
||||||
|
objectConstructor: KubeObject,
|
||||||
apiBase,
|
apiBase,
|
||||||
fallbackApiBases: [fallbackApiBase],
|
fallbackApiBases: [fallbackApiBase],
|
||||||
checkPreferredVersion: true,
|
checkPreferredVersion: true,
|
||||||
|
|||||||
38
src/renderer/api/__tests__/pods.api.test.ts
Normal file
38
src/renderer/api/__tests__/pods.api.test.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Pod } from "../endpoints";
|
||||||
|
|
||||||
|
describe("Pod tests", () => {
|
||||||
|
it("getAllContainers() should never throw", () => {
|
||||||
|
const pod = new Pod({
|
||||||
|
apiVersion: "foo",
|
||||||
|
kind: "Pod",
|
||||||
|
metadata: {
|
||||||
|
name: "foobar",
|
||||||
|
resourceVersion: "foobar",
|
||||||
|
uid: "foobar",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(pod.getAllContainers()).toStrictEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -22,34 +22,35 @@
|
|||||||
import type { KubeObjectStore } from "../kube-object.store";
|
import type { KubeObjectStore } from "../kube-object.store";
|
||||||
|
|
||||||
import { action, observable, makeObservable } from "mobx";
|
import { action, observable, makeObservable } from "mobx";
|
||||||
import { autoBind } from "../utils";
|
import { autoBind, iter } from "../utils";
|
||||||
import { KubeApi, parseKubeApi } from "./kube-api";
|
import { KubeApi, parseKubeApi } from "./kube-api";
|
||||||
|
import type { KubeObject } from "./kube-object";
|
||||||
|
|
||||||
export class ApiManager {
|
export class ApiManager {
|
||||||
private apis = observable.map<string, KubeApi>();
|
private apis = observable.map<string, KubeApi<KubeObject>>();
|
||||||
private stores = observable.map<string, KubeObjectStore>();
|
private stores = observable.map<string, KubeObjectStore<KubeObject>>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) {
|
getApi(pathOrCallback: string | ((api: KubeApi<KubeObject>) => boolean)) {
|
||||||
if (typeof pathOrCallback === "string") {
|
if (typeof pathOrCallback === "string") {
|
||||||
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(this.apis.values()).find(pathOrCallback ?? (() => true));
|
return iter.find(this.apis.values(), pathOrCallback ?? (() => true));
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiByKind(kind: string, apiVersion: string) {
|
getApiByKind(kind: string, apiVersion: string) {
|
||||||
return Array.from(this.apis.values()).find((api) => api.kind === kind && api.apiVersionWithGroup === apiVersion);
|
return iter.find(this.apis.values(), api => api.kind === kind && api.apiVersionWithGroup === apiVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerApi(apiBase: string, api: KubeApi) {
|
registerApi(apiBase: string, api: KubeApi<KubeObject>) {
|
||||||
if (!this.apis.has(apiBase)) {
|
if (!this.apis.has(apiBase)) {
|
||||||
this.stores.forEach((store) => {
|
this.stores.forEach((store) => {
|
||||||
if(store.api === api) {
|
if (store.api === api) {
|
||||||
this.stores.set(apiBase, store);
|
this.stores.set(apiBase, store);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -58,13 +59,19 @@ export class ApiManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected resolveApi(api: string | KubeApi): KubeApi {
|
protected resolveApi<K extends KubeObject>(api?: string | KubeApi<K>): KubeApi<K> | undefined {
|
||||||
if (typeof api === "string") return this.getApi(api);
|
if (!api) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof api === "string") {
|
||||||
|
return this.getApi(api) as KubeApi<K>;
|
||||||
|
}
|
||||||
|
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
|
|
||||||
unregisterApi(api: string | KubeApi) {
|
unregisterApi(api: string | KubeApi<KubeObject>) {
|
||||||
if (typeof api === "string") this.apis.delete(api);
|
if (typeof api === "string") this.apis.delete(api);
|
||||||
else {
|
else {
|
||||||
const apis = Array.from(this.apis.entries());
|
const apis = Array.from(this.apis.entries());
|
||||||
@ -75,13 +82,13 @@ export class ApiManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) {
|
registerStore(store: KubeObjectStore<KubeObject>, apis: KubeApi<KubeObject>[] = [store.api]) {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
this.stores.set(api.apiBase, store);
|
this.stores.set(api.apiBase, store);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStore<S extends KubeObjectStore>(api: string | KubeApi): S {
|
getStore<S extends KubeObjectStore<KubeObject>>(api: string | KubeApi<KubeObject>): S | undefined {
|
||||||
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,8 +26,9 @@ import { KubeApi } from "../kube-api";
|
|||||||
export class ClusterApi extends KubeApi<Cluster> {
|
export class ClusterApi extends KubeApi<Cluster> {
|
||||||
static kind = "Cluster";
|
static kind = "Cluster";
|
||||||
static namespaced = true;
|
static namespaced = true;
|
||||||
|
}
|
||||||
|
|
||||||
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
export function getMetricsByNodeNames(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
||||||
const nodes = nodeNames.join("|");
|
const nodes = nodeNames.join("|");
|
||||||
const opts = { category: "cluster", nodes };
|
const opts = { category: "cluster", nodes };
|
||||||
|
|
||||||
@ -49,7 +50,6 @@ export class ClusterApi extends KubeApi<Cluster> {
|
|||||||
fsSize: opts,
|
fsSize: opts,
|
||||||
fsUsage: opts
|
fsUsage: opts
|
||||||
}, params);
|
}, params);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ClusterStatus {
|
export enum ClusterStatus {
|
||||||
|
|||||||
@ -20,11 +20,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import type { IPodContainer } from "./pods.api";
|
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { autoBind } from "../../utils";
|
import { autoBind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
import { metricsApi } from "./metrics.api";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||||
|
|
||||||
export class DaemonSet extends WorkloadKubeObject {
|
export class DaemonSet extends WorkloadKubeObject {
|
||||||
static kind = "DaemonSet";
|
static kind = "DaemonSet";
|
||||||
@ -97,6 +98,24 @@ export class DaemonSet extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const daemonSetApi = new KubeApi({
|
export class DaemonSetApi extends KubeApi<DaemonSet> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMetricsForDaemonSets(daemonsets: DaemonSet[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||||
|
const podSelector = daemonsets.map(daemonset => `${daemonset.getName()}-[[:alnum:]]{5}`).join("|");
|
||||||
|
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||||
|
|
||||||
|
return metricsApi.getMetrics({
|
||||||
|
cpuUsage: opts,
|
||||||
|
memoryUsage: opts,
|
||||||
|
fsUsage: opts,
|
||||||
|
networkReceive: opts,
|
||||||
|
networkTransmit: opts,
|
||||||
|
}, {
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const daemonSetApi = new DaemonSetApi({
|
||||||
objectConstructor: DaemonSet,
|
objectConstructor: DaemonSet,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import moment from "moment";
|
|||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import { autoBind } from "../../utils";
|
import { autoBind } from "../../utils";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
import { metricsApi } from "./metrics.api";
|
||||||
|
import type { IPodMetrics } from "./pods.api";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
export class DeploymentApi extends KubeApi<Deployment> {
|
export class DeploymentApi extends KubeApi<Deployment> {
|
||||||
@ -68,6 +70,21 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMetricsForDeployments(deployments: Deployment[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||||
|
const podSelector = deployments.map(deployment => `${deployment.getName()}-[[:alnum:]]{9,}-[[:alnum:]]{5}`).join("|");
|
||||||
|
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||||
|
|
||||||
|
return metricsApi.getMetrics({
|
||||||
|
cpuUsage: opts,
|
||||||
|
memoryUsage: opts,
|
||||||
|
fsUsage: opts,
|
||||||
|
networkReceive: opts,
|
||||||
|
networkTransmit: opts,
|
||||||
|
}, {
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
interface IContainerProbe {
|
interface IContainerProbe {
|
||||||
httpGet?: {
|
httpGet?: {
|
||||||
path?: string;
|
path?: string;
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { autoBind } from "../../utils";
|
|||||||
import { KubeObject } from "../kube-object";
|
import { KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
import { get } from "lodash";
|
||||||
|
|
||||||
export interface IEndpointPort {
|
export interface IEndpointPort {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -63,6 +64,10 @@ export class EndpointAddress implements IEndpointAddress {
|
|||||||
resourceVersion: string;
|
resourceVersion: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static create(data: IEndpointAddress): EndpointAddress {
|
||||||
|
return new EndpointAddress(data);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(data: IEndpointAddress) {
|
constructor(data: IEndpointAddress) {
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
}
|
}
|
||||||
@ -90,35 +95,27 @@ export class EndpointSubset implements IEndpointSubset {
|
|||||||
ports: IEndpointPort[];
|
ports: IEndpointPort[];
|
||||||
|
|
||||||
constructor(data: IEndpointSubset) {
|
constructor(data: IEndpointSubset) {
|
||||||
Object.assign(this, data);
|
this.addresses = get(data, "addresses", []);
|
||||||
|
this.notReadyAddresses = get(data, "notReadyAddresses", []);
|
||||||
|
this.ports = get(data, "ports", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddresses(): EndpointAddress[] {
|
getAddresses(): EndpointAddress[] {
|
||||||
const addresses = this.addresses || [];
|
return this.addresses.map(EndpointAddress.create);
|
||||||
|
|
||||||
return addresses.map(a => new EndpointAddress(a));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getNotReadyAddresses(): EndpointAddress[] {
|
getNotReadyAddresses(): EndpointAddress[] {
|
||||||
const notReadyAddresses = this.notReadyAddresses || [];
|
return this.notReadyAddresses.map(EndpointAddress.create);
|
||||||
|
|
||||||
return notReadyAddresses.map(a => new EndpointAddress(a));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
if(!this.addresses) {
|
return this.addresses
|
||||||
return "";
|
.map(address => (
|
||||||
}
|
this.ports
|
||||||
|
.map(port => `${address.ip}:${port.port}`)
|
||||||
return this.addresses.map(address => {
|
.join(", ")
|
||||||
if (!this.ports) {
|
))
|
||||||
return address.ip;
|
.join(", ");
|
||||||
}
|
|
||||||
|
|
||||||
return this.ports.map(port => {
|
|
||||||
return `${address.ip}:${port.port}`;
|
|
||||||
}).join(", ");
|
|
||||||
}).join(", ");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,8 +26,10 @@ import { KubeApi } from "../kube-api";
|
|||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
export class IngressApi extends KubeApi<Ingress> {
|
export class IngressApi extends KubeApi<Ingress> {
|
||||||
getMetrics(ingress: string, namespace: string): Promise<IIngressMetrics> {
|
}
|
||||||
const opts = { category: "ingress", ingress, namespace };
|
|
||||||
|
export function getMetricsForIngress(ingress: string, namespace: string): Promise<IIngressMetrics> {
|
||||||
|
const opts = { category: "ingress", ingress };
|
||||||
|
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
bytesSentSuccess: opts,
|
bytesSentSuccess: opts,
|
||||||
@ -37,7 +39,6 @@ export class IngressApi extends KubeApi<Ingress> {
|
|||||||
}, {
|
}, {
|
||||||
namespace,
|
namespace,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIngressMetrics<T = IMetrics> {
|
export interface IIngressMetrics<T = IMetrics> {
|
||||||
|
|||||||
@ -22,10 +22,11 @@
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import { autoBind } from "../../utils";
|
import { autoBind } from "../../utils";
|
||||||
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
import { IAffinity, WorkloadKubeObject } from "../workload-kube-object";
|
||||||
import type { IPodContainer } from "./pods.api";
|
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
|
import { metricsApi } from "./metrics.api";
|
||||||
import type { JsonApiParams } from "../json-api";
|
import type { JsonApiParams } from "../json-api";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
import type { IPodContainer, IPodMetrics } from "./pods.api";
|
||||||
|
|
||||||
export class Job extends WorkloadKubeObject {
|
export class Job extends WorkloadKubeObject {
|
||||||
static kind = "Job";
|
static kind = "Job";
|
||||||
@ -129,6 +130,24 @@ export class Job extends WorkloadKubeObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jobApi = new KubeApi({
|
export class JobApi extends KubeApi<Job> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMetricsForJobs(jobs: Job[], namespace: string, selector = ""): Promise<IPodMetrics> {
|
||||||
|
const podSelector = jobs.map(job => `${job.getName()}-[[:alnum:]]{5}`).join("|");
|
||||||
|
const opts = { category: "pods", pods: podSelector, namespace, selector };
|
||||||
|
|
||||||
|
return metricsApi.getMetrics({
|
||||||
|
cpuUsage: opts,
|
||||||
|
memoryUsage: opts,
|
||||||
|
fsUsage: opts,
|
||||||
|
networkReceive: opts,
|
||||||
|
networkTransmit: opts,
|
||||||
|
}, {
|
||||||
|
namespace,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jobApi = new JobApi({
|
||||||
objectConstructor: Job,
|
objectConstructor: Job,
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user