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

Merge branch 'master' into wrap-pod-logs

This commit is contained in:
Alex Andreev 2022-11-28 10:11:28 +03:00
commit 31c7ae118d
1328 changed files with 73244 additions and 43415 deletions

View File

@ -1,8 +0,0 @@
{
"extensions": [
"pod-menu",
"node-menu",
"metrics-cluster-feature",
"kube-object-event-status"
]
}

View File

@ -12,11 +12,21 @@ module.exports = {
"**/static/**/*", "**/static/**/*",
"**/site/**/*", "**/site/**/*",
"extensions/*/*.tgz", "extensions/*/*.tgz",
"build/webpack/**/*",
], ],
settings: { settings: {
react: { react: {
version: packageJson.devDependencies.react || "detect", version: packageJson.devDependencies.react || "detect",
}, },
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "./tsconfig.json",
},
},
}, },
overrides: [ overrides: [
{ {

View File

@ -13,21 +13,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Add Card to Project(s) name: Add Card to Project(s)
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Get Event Type - name: Get Event Type
run: echo $GITHUB_EVENT_NAME run: echo $GITHUB_EVENT_NAME
- name: Assign NEW issues to project 1 - name: Assign NEW issues to project 1
uses: ./.github/actions/add-card-to-project uses: ./.github/actions/add-card-to-project
if: github.event_name == 'issues' && github.event.action == 'opened' if: github.event_name == 'issues' && github.event.action == 'opened'
with: with:
project: 'https://github.com/orgs/lensapp/projects/1' project: "https://github.com/orgs/lensapp/projects/1"
column_name: 'Backlog' column_name: "Backlog"
card_position: 'bottom' card_position: "bottom"
- name: Assign NEW pull requests to project 1 - name: Assign NEW pull requests to project 1
uses: ./.github/actions/add-card-to-project uses: ./.github/actions/add-card-to-project
if: github.event_name == 'pull_request_target' && github.event.action == 'opened' if: github.event_name == 'pull_request_target' && github.event.action == 'opened'
with: with:
project: 'https://github.com/orgs/lensapp/projects/1' project: "https://github.com/orgs/lensapp/projects/1"
column_name: 'PRs' column_name: "PRs"
card_position: 'bottom' card_position: "bottom"

View File

@ -0,0 +1,35 @@
name: Bump Version on master
on:
pull_request:
types:
- closed
branches:
- master
workflow_dispatch:
jobs:
release:
name: Release
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') }}
steps:
- name: Checkout Release from lens
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install dependencies
run: |
npm i --location=global semver
- name: Bump version to first alpha of next minor version
run: |
NEW_VERSION=$(cat package.json | jq .version --raw-output| xargs semver -i preminor --preid alpha)
cat package.json | jq --arg new_version "$NEW_VERSION" '.version = $new_version' > new-package.json
mv new-package.json package.json
- uses: peter-evans/create-pull-request@v4
with:
add-paths: package.json
commit-message: Update package.json version to next preminor because of recent release
signoff: true
delete-branch: true
title: Update version to next preminor
labels: chore

View File

@ -12,12 +12,12 @@ jobs:
node-version: [16.x] node-version: [16.x]
steps: steps:
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}

View File

@ -13,12 +13,12 @@ name: "CodeQL"
on: on:
push: push:
branches: [ master ] branches: [master]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ master ] branches: [master]
schedule: schedule:
- cron: '41 3 * * 2' - cron: "41 3 * * 2"
jobs: jobs:
analyze: analyze:
@ -32,40 +32,40 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'javascript' ] language: ["javascript"]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more: # 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 # 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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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. # 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. # 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 # queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # ✏️ 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 # and modify them (or add more) to build your code if your project
# uses a compiled language # uses a compiled language
#- run: | #- run: |
# make bootstrap # make bootstrap
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@v1

View File

@ -10,9 +10,9 @@ jobs:
build_job: build_job:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
node-version: "16" node-version: "16"

View File

@ -11,18 +11,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Golang - name: Set up Golang
uses: actions/setup-go@v2 uses: actions/setup-go@v2
with: with:
go-version: '^1.18.0' go-version: "^1.18.0"
- name: Install addlicense - name: Install addlicense
run: | run: |
export PATH=${PATH}:`go env GOPATH`/bin export PATH=${PATH}:`go env GOPATH`/bin
go install github.com/google/addlicense@v1.0.0 go install github.com/google/addlicense@v1.0.0
- name: Check license headers - name: Check license headers
run: | run: |
set -e set -e
export PATH=${PATH}:`go env GOPATH`/bin export PATH=${PATH}:`go env GOPATH`/bin
addlicense -check -l mit -c "OpenLens Authors" src/**/*.?css addlicense -check -l mit -c "OpenLens Authors" src/**/*.?css

View File

@ -10,12 +10,12 @@ jobs:
node-version: [16.x] node-version: [16.x]
steps: steps:
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}

View File

@ -23,12 +23,12 @@ jobs:
python-version: "3.x" python-version: "3.x"
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@ -61,7 +61,7 @@ jobs:
pip install mike pip install mike
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
@ -71,7 +71,7 @@ jobs:
git config --local user.name "GitHub Action" git config --local user.name "GitHub Action"
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@ -88,7 +88,7 @@ jobs:
- name: Get the release version - name: Get the release version
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
id: get_version id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
- name: mkdocs deploy new release - name: mkdocs deploy new release
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease

View File

@ -13,7 +13,7 @@ jobs:
- name: Set up Python 3.7 - name: Set up Python 3.7
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.x' python-version: "3.x"
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -22,17 +22,15 @@ jobs:
pip install mike pip install mike
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: git config - name: git config
run: | run: |
git config --local user.email "action@github.com" git config --local user.email "action@github.com"
git config --local user.name "GitHub Action" git config --local user.name "GitHub Action"
- name: mkdocs delete version - name: mkdocs delete version
run: | run: |
mike delete --push ${{ github.event.inputs.version }} mike delete --push ${{ github.event.inputs.version }}

View File

@ -25,13 +25,13 @@ jobs:
pip install mike pip install mike
- name: Checkout Version from lens - name: Checkout Version from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
ref: "${{ github.event.inputs.version }}" ref: "${{ github.event.inputs.version }}"
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@ -41,7 +41,7 @@ jobs:
yarn typedocs-extensions-api yarn typedocs-extensions-api
- name: Checkout master branch from lens - name: Checkout master branch from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
path: "master" path: "master"
ref: "master" ref: "master"
@ -53,9 +53,9 @@ jobs:
rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting
sed -i '/Protocol Handlers/d' ./mkdocs.yml sed -i '/Protocol Handlers/d' ./mkdocs.yml
sed -i '/IPC/d' ./mkdocs.yml sed -i '/IPC/d' ./mkdocs.yml
sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/getting-started/add-cluster/#g' ./docs/extensions/get-started/your-first-extension.md
sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev//getting-started/adding-clusters/#g' ./docs/README.md
sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/contributing/#g' ./docs/extensions/guides/generator.md
- name: git config - name: git config
run: | run: |

View File

@ -13,7 +13,7 @@ jobs:
- name: Set up Python 3.7 - name: Set up Python 3.7
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: '3.x' python-version: "3.x"
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -22,17 +22,15 @@ jobs:
pip install mike pip install mike
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: git config - name: git config
run: | run: |
git config --local user.email "action@github.com" git config --local user.email "action@github.com"
git config --local user.name "GitHub Action" git config --local user.name "GitHub Action"
- name: mkdocs update default version - name: mkdocs update default version
run: | run: |
mike set-default --push ${{ github.event.inputs.version }} mike set-default --push ${{ github.event.inputs.version }}

View File

@ -17,12 +17,12 @@ jobs:
node-version: [16.x] node-version: [16.x]
steps: steps:
- name: Checkout Release - name: Checkout Release
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}

View File

@ -12,12 +12,12 @@ jobs:
node-version: [16.x] node-version: [16.x]
steps: steps:
- name: Checkout Release - name: Checkout Release
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}

View File

@ -1,16 +0,0 @@
name: Release Drafter
on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- master
jobs:
update_release_draft:
runs-on: ubuntu-latest
steps:
# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

30
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Release Open Lens
on:
pull_request:
types:
- closed
branches:
- master
- release/v*.*
jobs:
release:
name: Release
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') }}
steps:
- name: Checkout Release from lens
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: butlerlogic/action-autotag@stable
id: tagger
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
- uses: ncipollo/release-action@v1
if: ${{ steps.tagger.outputs.tagname != '' }}
with:
name: ${{ steps.tagger.outputs.tagname }}
commit: master
tag: ${{ steps.tagger.outputs.tagname }}
body: ${{ github.event.pull_request.body }}

14
.github/workflows/require-milestone.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Require Milestone
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
milestone:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Require Milestone
run: |
exit $(gh pr view ${{ github.event.pull_request.number }} --json milestone | jq 'if .milestone == null then 1 else 0 end')
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}

View File

@ -7,17 +7,18 @@ on:
branches: branches:
- master - master
jobs: jobs:
build: test:
name: Test name: ${{ matrix.type }} tests on ${{ matrix.os }}
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, macos-11, windows-2019] os: [ubuntu-20.04, macos-11, windows-2019]
type: [unit, smoke]
node-version: [16.x] node-version: [16.x]
steps: steps:
- name: Checkout Release from lens - name: Checkout Release from lens
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
@ -27,15 +28,16 @@ jobs:
echo -e "$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1)\t$(hostname -f) $(hostname -s)" | sudo tee -a /etc/hosts echo -e "$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1)\t$(hostname -f) $(hostname -s)" | sudo tee -a /etc/hosts
- name: Using Node.js ${{ matrix.node-version }} - name: Using Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" shell: bash
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v2 - uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@ -51,25 +53,16 @@ jobs:
retry_on: error retry_on: error
command: make node_modules command: make node_modules
- run: make build-npm
name: Generate npm package
- uses: nick-fields/retry@v2
name: Build bundled extensions
with:
timeout_minutes: 15
max_attempts: 3
retry_on: error
command: make -j2 build-extensions
- run: make test - run: make test
name: Run tests name: Run tests
if: ${{ matrix.type == 'unit' }}
- run: make test-extensions - run: make test-extensions
name: Run In-tree Extension tests name: Run In-tree Extension tests
if: ${{ matrix.type == 'unit' }}
- run: make ci-validate-dev - run: make ci-validate-dev
if: contains(github.event.pull_request.labels.*.name, 'dependencies') if: ${{ contains(github.event.pull_request.labels.*.name, 'dependencies') && matrix.type == 'unit' }}
name: Validate dev mode will work name: Validate dev mode will work
- name: Install integration test dependencies - name: Install integration test dependencies
@ -77,22 +70,22 @@ jobs:
uses: medyagh/setup-minikube@master uses: medyagh/setup-minikube@master
with: with:
minikube-version: latest minikube-version: latest
if: runner.os == 'Linux' if: ${{ runner.os == 'Linux' && matrix.type == 'smoke' }}
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration - 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' && matrix.type == 'smoke' }}
- run: make integration - run: make integration
name: Run macOS integration tests name: Run macOS integration tests
shell: bash shell: bash
env: env:
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --arm64" ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --arm64"
if: runner.os == 'macOS' if: ${{ runner.os == 'macOS' && matrix.type == 'smoke' }}
- run: make integration - run: make integration
name: Run Windows integration tests name: Run Windows integration tests
shell: bash shell: bash
env: env:
ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --ia32" ELECTRON_BUILDER_EXTRA_ARGS: "--x64 --ia32"
if: runner.os == 'Windows' if: ${{ runner.os == 'Windows' && matrix.type == 'smoke' }}

1
.gitignore vendored
View File

@ -17,3 +17,4 @@ types/extension-renderer-api.d.ts
extensions/*/dist extensions/*/dist
docs/extensions/api docs/extensions/api
site/ site/
build/webpack/

View File

@ -1,3 +1,3 @@
# Contributing to Lens # Contributing to Lens
See [Contributing to Lens](https://docs.k8slens.dev/latest/contributing/) documentation. See [Contributing to Lens](https://docs.k8slens.dev/contributing/) documentation.

View File

@ -44,7 +44,7 @@ tag-release:
scripts/tag-release.sh $(CMD_ARGS) scripts/tag-release.sh $(CMD_ARGS)
.PHONY: test .PHONY: test
test: binaries/client test: node_modules binaries/client
yarn run jest $(or $(CMD_ARGS), "src") yarn run jest $(or $(CMD_ARGS), "src")
.PHONY: integration .PHONY: integration
@ -53,7 +53,6 @@ integration: build
.PHONY: build .PHONY: build
build: node_modules binaries/client build: node_modules binaries/client
yarn run npm:fix-build-version
$(MAKE) build-extensions -B $(MAKE) build-extensions -B
yarn run build:tray-icons yarn run build:tray-icons
yarn run compile yarn run compile
@ -63,10 +62,6 @@ ifeq "$(DETECTED_OS)" "Windows"
endif endif
yarn run electron-builder --publish onTag $(ELECTRON_BUILDER_EXTRA_ARGS) yarn run electron-builder --publish onTag $(ELECTRON_BUILDER_EXTRA_ARGS)
.PHONY: update-extension-locks
update-extension-locks:
$(foreach dir, $(extensions), (cd $(dir) && rm package-lock.json && ../../node_modules/.bin/npm install --package-lock-only);)
.NOTPARALLEL: $(extension_node_modules) .NOTPARALLEL: $(extension_node_modules)
$(extension_node_modules): node_modules $(extension_node_modules): node_modules
cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund --no-save cd $(@:/node_modules=) && ../../node_modules/.bin/npm install --no-audit --no-fund --no-save
@ -92,7 +87,7 @@ src/extensions/npm/extensions/dist: src/extensions/npm/extensions/node_modules
yarn compile:extension-types yarn compile:extension-types
src/extensions/npm/extensions/node_modules: src/extensions/npm/extensions/package.json src/extensions/npm/extensions/node_modules: src/extensions/npm/extensions/package.json
cd src/extensions/npm/extensions/ && ../../../../node_modules/.bin/npm install --no-audit --no-fund cd src/extensions/npm/extensions/ && ../../../../node_modules/.bin/npm install --no-audit --no-fund --no-save
.PHONY: build-npm .PHONY: build-npm
build-npm: build-extension-types src/extensions/npm/extensions/__mocks__ build-npm: build-extension-types src/extensions/npm/extensions/__mocks__

View File

@ -19,12 +19,16 @@ Lens IDE a standalone application for MacOS, Windows and Linux operating systems
## Installation ## Installation
See [Getting Started](https://docs.k8slens.dev/main/getting-started/install-lens/) page. See [Getting Started](https://docs.k8slens.dev/getting-started/install-lens/) page.
## Development ## Development
See [Development](https://docs.k8slens.dev/latest/contributing/development/) page. See [Development](https://docs.k8slens.dev/contributing/development/) page.
## Contributing ## Contributing
See [Contributing](https://docs.k8slens.dev/latest/contributing/) page. See [Contributing](https://docs.k8slens.dev/contributing/) page.
## License
See [License](LICENSE).

19
RELEASE_GUIDE.md Normal file
View File

@ -0,0 +1,19 @@
# Release Guide
Releases for this repository are made via running the `create-release-pr` script defined in the `package.json`.
All releases will be made by creating a PR which bumps the version field in the `package.json` and, if necessary, cherry pick the relavent commits from master.
## Prerequisites
- `yarn`
- Running `yarn` (to install all dependencies)
- `gh` (Github's CLI) with a version at least 2.15.0
## Steps
1. If you are making a minor or major release (or prereleases for one) make sure you are on the `master` branch.
1. If you are making a patch release (or a prerelease for one) make sure you are on the `release/v<MAJOR>.<MINOR>` branch.
1. Run `yarn create-release-pr <release-type>`. If you are making a subsequent prerelease release, provide the `--check-commits` flag.
1. If you are checking the commits, type `y<ENTER>` to pick a commit, and `n<ENTER>` to skip it. You will want to skip the commits that were part of previous prerelease releases.
1. Once the PR is created, approved, and then merged the `Release Open Lens` workflow will create a tag and release for you.
1. If you are making a major or minor release, create a `release/v<MAJOR>.<MINOR>` branch and push it to `origin` so that future patch releases can be made from it.

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Mock the global window variable
*/
export function mockWindow() {
Object.defineProperty(window, "requestIdleCallback", {
writable: true,
value: jest.fn().mockImplementation(callback => callback()),
});
Object.defineProperty(window, "cancelIdleCallback", {
writable: true,
value: jest.fn(),
});
}

View File

@ -8,18 +8,30 @@ import { open } from "fs/promises";
import type { WriteStream } from "fs-extra"; import type { WriteStream } from "fs-extra";
import { constants, ensureDir, unlink } from "fs-extra"; import { constants, ensureDir, unlink } from "fs-extra";
import path from "path"; import path from "path";
import fetch from "node-fetch"; import type * as FetchModule from "node-fetch";
import { promisify } from "util"; import { promisify } from "util";
import { pipeline as _pipeline, Transform, Writable } from "stream"; import { pipeline as _pipeline, Transform, Writable } from "stream";
import type { SingleBar } from "cli-progress"; import type { SingleBar } from "cli-progress";
import { MultiBar } from "cli-progress"; import { MultiBar } from "cli-progress";
import { extract } from "tar-stream"; import { extract } from "tar-stream";
import gunzip from "gunzip-maybe"; import gunzip from "gunzip-maybe";
import { getBinaryName, normalizedPlatform } from "../src/common/vars"; import { isErrnoException, setTimeoutFor } from "../src/common/utils";
import { isErrnoException } from "../src/common/utils"; import AbortController from "abort-controller";
type Response = FetchModule.Response;
type RequestInfo = FetchModule.RequestInfo;
type RequestInit = FetchModule.RequestInit;
const pipeline = promisify(_pipeline); const pipeline = promisify(_pipeline);
const getBinaryName = (binaryName: string, { forPlatform }: { forPlatform : string }) => {
if (forPlatform === "windows") {
return `${binaryName}.exe`;
}
return binaryName;
};
interface BinaryDownloaderArgs { interface BinaryDownloaderArgs {
readonly version: string; readonly version: string;
readonly platform: SupportedPlatform; readonly platform: SupportedPlatform;
@ -29,6 +41,10 @@ interface BinaryDownloaderArgs {
readonly baseDir: string; readonly baseDir: string;
} }
interface BinaryDownloaderDependencies {
fetch: (url: RequestInfo, init?: RequestInit) => Promise<Response>;
}
abstract class BinaryDownloader { abstract class BinaryDownloader {
protected abstract readonly url: string; protected abstract readonly url: string;
protected readonly bar: SingleBar; protected readonly bar: SingleBar;
@ -38,7 +54,7 @@ abstract class BinaryDownloader {
return [file]; return [file];
} }
constructor(public readonly args: BinaryDownloaderArgs, multiBar: MultiBar) { constructor(protected readonly dependencies: BinaryDownloaderDependencies, public readonly args: BinaryDownloaderArgs, multiBar: MultiBar) {
this.bar = multiBar.create(1, 0, args); this.bar = multiBar.create(1, 0, args);
this.target = path.join(args.baseDir, args.platform, args.fileArch, args.binaryName); this.target = path.join(args.baseDir, args.platform, args.fileArch, args.binaryName);
} }
@ -49,8 +65,10 @@ abstract class BinaryDownloader {
} }
const controller = new AbortController(); const controller = new AbortController();
const stream = await fetch(this.url, {
timeout: 15 * 60 * 1000, // 15min setTimeoutFor(controller, 15 * 60 * 1000);
const stream = await this.dependencies.fetch(this.url, {
signal: controller.signal, signal: controller.signal,
}); });
const total = Number(stream.headers.get("content-length")); const total = Number(stream.headers.get("content-length"));
@ -72,6 +90,10 @@ abstract class BinaryDownloader {
*/ */
const handle = fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL); const handle = fileHandle = await open(this.target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
if (!stream.body) {
throw new Error("no body on stream");
}
await pipeline( await pipeline(
stream.body, stream.body,
new Transform({ new Transform({
@ -108,10 +130,10 @@ abstract class BinaryDownloader {
class LensK8sProxyDownloader extends BinaryDownloader { class LensK8sProxyDownloader extends BinaryDownloader {
protected readonly url: string; protected readonly url: string;
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) { constructor(deps: BinaryDownloaderDependencies, args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
const binaryName = getBinaryName("lens-k8s-proxy", { forPlatform: args.platform }); const binaryName = getBinaryName("lens-k8s-proxy", { forPlatform: args.platform });
super({ ...args, binaryName }, bar); super(deps, { ...args, binaryName }, bar);
this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${args.version}/lens-k8s-proxy-${args.platform}-${args.downloadArch}`; this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${args.version}/lens-k8s-proxy-${args.platform}-${args.downloadArch}`;
} }
} }
@ -119,10 +141,10 @@ class LensK8sProxyDownloader extends BinaryDownloader {
class KubectlDownloader extends BinaryDownloader { class KubectlDownloader extends BinaryDownloader {
protected readonly url: string; protected readonly url: string;
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) { constructor(deps: BinaryDownloaderDependencies, args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
const binaryName = getBinaryName("kubectl", { forPlatform: args.platform }); const binaryName = getBinaryName("kubectl", { forPlatform: args.platform });
super({ ...args, binaryName }, bar); super(deps, { ...args, binaryName }, bar);
this.url = `https://storage.googleapis.com/kubernetes-release/release/v${args.version}/bin/${args.platform}/${args.downloadArch}/${binaryName}`; this.url = `https://storage.googleapis.com/kubernetes-release/release/v${args.version}/bin/${args.platform}/${args.downloadArch}/${binaryName}`;
} }
} }
@ -130,10 +152,10 @@ class KubectlDownloader extends BinaryDownloader {
class HelmDownloader extends BinaryDownloader { class HelmDownloader extends BinaryDownloader {
protected readonly url: string; protected readonly url: string;
constructor(args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) { constructor(deps: BinaryDownloaderDependencies, args: Omit<BinaryDownloaderArgs, "binaryName">, bar: MultiBar) {
const binaryName = getBinaryName("helm", { forPlatform: args.platform }); const binaryName = getBinaryName("helm", { forPlatform: args.platform });
super({ ...args, binaryName }, bar); super(deps, { ...args, binaryName }, bar);
this.url = `https://get.helm.sh/helm-v${args.version}-${args.platform}-${args.downloadArch}.tar.gz`; this.url = `https://get.helm.sh/helm-v${args.version}-${args.platform}-${args.downloadArch}.tar.gz`;
} }
@ -160,7 +182,24 @@ class HelmDownloader extends BinaryDownloader {
type SupportedPlatform = "darwin" | "linux" | "windows"; type SupportedPlatform = "darwin" | "linux" | "windows";
const importFetchModule = new Function('return import("node-fetch")') as () => Promise<typeof FetchModule>;
async function main() { async function main() {
const deps: BinaryDownloaderDependencies = {
fetch: (await importFetchModule()).default,
};
const normalizedPlatform = (() => {
switch (process.platform) {
case "darwin":
return "darwin";
case "linux":
return "linux";
case "win32":
return "windows";
default:
throw new Error(`platform=${process.platform} is unsupported`);
}
})();
const multiBar = new MultiBar({ const multiBar = new MultiBar({
align: "left", align: "left",
clearOnComplete: false, clearOnComplete: false,
@ -171,21 +210,21 @@ async function main() {
}); });
const baseDir = path.join(__dirname, "..", "binaries", "client"); const baseDir = path.join(__dirname, "..", "binaries", "client");
const downloaders: BinaryDownloader[] = [ const downloaders: BinaryDownloader[] = [
new LensK8sProxyDownloader({ new LensK8sProxyDownloader(deps, {
version: packageInfo.config.k8sProxyVersion, version: packageInfo.config.k8sProxyVersion,
platform: normalizedPlatform, platform: normalizedPlatform,
downloadArch: "amd64", downloadArch: "amd64",
fileArch: "x64", fileArch: "x64",
baseDir, baseDir,
}, multiBar), }, multiBar),
new KubectlDownloader({ new KubectlDownloader(deps, {
version: packageInfo.config.bundledKubectlVersion, version: packageInfo.config.bundledKubectlVersion,
platform: normalizedPlatform, platform: normalizedPlatform,
downloadArch: "amd64", downloadArch: "amd64",
fileArch: "x64", fileArch: "x64",
baseDir, baseDir,
}, multiBar), }, multiBar),
new HelmDownloader({ new HelmDownloader(deps, {
version: packageInfo.config.bundledHelmVersion, version: packageInfo.config.bundledHelmVersion,
platform: normalizedPlatform, platform: normalizedPlatform,
downloadArch: "amd64", downloadArch: "amd64",
@ -194,23 +233,23 @@ async function main() {
}, multiBar), }, multiBar),
]; ];
if (normalizedPlatform === "darwin") { if (normalizedPlatform !== "windows") {
downloaders.push( downloaders.push(
new LensK8sProxyDownloader({ new LensK8sProxyDownloader(deps, {
version: packageInfo.config.k8sProxyVersion, version: packageInfo.config.k8sProxyVersion,
platform: normalizedPlatform, platform: normalizedPlatform,
downloadArch: "arm64", downloadArch: "arm64",
fileArch: "arm64", fileArch: "arm64",
baseDir, baseDir,
}, multiBar), }, multiBar),
new KubectlDownloader({ new KubectlDownloader(deps, {
version: packageInfo.config.bundledKubectlVersion, version: packageInfo.config.bundledKubectlVersion,
platform: normalizedPlatform, platform: normalizedPlatform,
downloadArch: "arm64", downloadArch: "arm64",
fileArch: "arm64", fileArch: "arm64",
baseDir, baseDir,
}, multiBar), }, multiBar),
new HelmDownloader({ new HelmDownloader(deps, {
version: packageInfo.config.bundledHelmVersion, version: packageInfo.config.bundledHelmVersion,
platform: normalizedPlatform, platform: normalizedPlatform,
downloadArch: "arm64", downloadArch: "arm64",

View File

@ -1,4 +1,7 @@
!macro customInit !macro customInit
; Make sure all old extensions are removed
RMDir /r "$INSTDIR\resources\extensions"
; Workaround for installer handing when the app directory is removed manually ; Workaround for installer handing when the app directory is removed manually
${ifNot} ${FileExists} "$INSTDIR" ${ifNot} ${FileExists} "$INSTDIR"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{${UNINSTALL_APP_KEY}}" DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{${UNINSTALL_APP_KEY}}"

View File

@ -22,5 +22,6 @@ exports.default = async function notarizing(context) {
appPath: `${appOutDir}/${appName}.app`, appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLEID, appleId: process.env.APPLEID,
appleIdPassword: process.env.APPLEIDPASS, appleIdPassword: process.env.APPLEIDPASS,
ascProvider:process.env.ASCPROVIDER,
}); });
}; };

View File

@ -1,68 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as fse from "fs-extra";
import * as path from "path";
import appInfo from "../package.json";
import semver from "semver";
import fastGlob from "fast-glob";
const packagePath = path.join(__dirname, "../package.json");
const versionInfo = semver.parse(appInfo.version);
const buildNumber = process.env.BUILD_NUMBER || Date.now().toString();
function getBuildChannel(): string {
const preRelease = versionInfo.prerelease?.[0];
switch (preRelease) {
case "alpha":
case "beta":
case "rc":
return preRelease;
case undefined:
case "latest":
return "latest"; // needed because electron-updater does not take build information into account when resolving if update is available
default:
throw new Error(`invalid pre-release ${preRelease}`);
}
}
async function writeOutExtensionVersion(manifestPath: string) {
const extensionPackageJson = await fse.readJson(manifestPath);
extensionPackageJson.version = appInfo.version;
return fse.writeJson(manifestPath, extensionPackageJson, {
spaces: 2,
});
}
async function writeOutNewVersions() {
await Promise.all([
fse.writeJson(packagePath, appInfo, {
spaces: 2,
}),
...(await fastGlob(["extensions/*/package.json"])).map(writeOutExtensionVersion),
]);
}
function main() {
const prereleaseParts: string[] = [getBuildChannel()];
if (versionInfo.prerelease && versionInfo.prerelease.length > 1) {
prereleaseParts.push(versionInfo.prerelease[1].toString());
}
prereleaseParts.push(buildNumber);
appInfo.version = `${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}-${prereleaseParts.join(".")}`;
writeOutNewVersions()
.catch((error) => {
console.error(error);
process.exit(1);
});
}
main();

View File

@ -78,7 +78,7 @@ npm run dev
You must restart Lens for the extension to load. You must restart Lens for the extension to load.
After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds. After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds.
With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/main/catalog/) for details on how to add a cluster in Lens IDE. With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/getting-started/add-cluster/) for details on how to add a cluster in Lens IDE.
You will see the "Hello World" page in the left-side cluster menu. You will see the "Hello World" page in the left-side cluster menu.
## Develop the Extension ## Develop the Extension

View File

@ -46,14 +46,14 @@ Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hel
```typescript ```typescript
clusterPageMenus = [ clusterPageMenus = [
{ {
target: { pageId: "hello" }, target: { pageId: "hello" },
title: "Hello Lens", title: "Hello Lens",
components: { components: {
Icon: ExampleIcon, Icon: ExampleIcon,
} },
} },
] ];
``` ```
Reload Lens and you will see that the menu item text has changed to "Hello Lens". Reload Lens and you will see that the menu item text has changed to "Hello Lens".
@ -70,6 +70,6 @@ To debug your extension, please see our instructions on [Testing Extensions](../
To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md). To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md).
If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues). If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues).
You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing). You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/contributing).
The Generator source code is hosted at [GitHub](https://github.com/lensapp/generator-lens-ext). The Generator source code is hosted at [GitHub](https://github.com/lensapp/generator-lens-ext).

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "kube-object-event-status", "name": "kube-object-event-status",
"version": "0.0.1", "version": "6.1.1",
"description": "Adds kube object status from events", "description": "Adds kube object status from events",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -35,12 +35,18 @@ export default class EventResourceStatusRendererExtension extends Renderer.LensE
}, },
{ {
kind: "Job", kind: "Job",
apiVersions: ["batch/v1"], apiVersions: [
"batch/v1",
"batch/v1beta1",
],
resolve: (job: Renderer.K8sApi.Job) => resolveStatus(job), resolve: (job: Renderer.K8sApi.Job) => resolveStatus(job),
}, },
{ {
kind: "CronJob", kind: "CronJob",
apiVersions: ["batch/v1"], apiVersions: [
"batch/v1",
"batch/v1beta1",
],
resolve: (cronJob: Renderer.K8sApi.CronJob) => resolveStatusForCronJobs(cronJob), resolve: (cronJob: Renderer.K8sApi.CronJob) => resolveStatusForCronJobs(cronJob),
}, },
]; ];

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-metrics-cluster-feature", "name": "lens-metrics-cluster-feature",
"version": "0.0.1", "version": "6.1.0",
"description": "Lens metrics cluster feature", "description": "Lens metrics cluster feature",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -190,7 +190,7 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
render() { render() {
return ( return (
<> <section style={{ display: "flex", flexDirection: "column", rowGap: "1.5rem" }}>
{ this.props.cluster.status.phase !== "connected" && ( { this.props.cluster.status.phase !== "connected" && (
<section> <section>
<p style={ { color: "var(--colorError)" } }> <p style={ { color: "var(--colorError)" } }>
@ -270,7 +270,7 @@ export class MetricsSettings extends React.Component<MetricsSettingsProps> {
)} )}
</div> </div>
</section> </section>
</> </section>
); );
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-node-menu", "name": "lens-node-menu",
"version": "0.0.1", "version": "6.1.0",
"description": "Lens node menu", "description": "Lens node menu",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -1,6 +1,6 @@
{ {
"name": "lens-pod-menu", "name": "lens-pod-menu",
"version": "0.0.1", "version": "6.1.0",
"description": "Lens pod menu", "description": "Lens pod menu",
"renderer": "dist/renderer.js", "renderer": "dist/renderer.js",
"lens": { "lens": {

View File

@ -13,7 +13,8 @@ import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
describe("preferences page tests", () => { describe("preferences page tests", () => {
let window: Page, cleanup: () => Promise<void>; let window: Page;
let cleanup: undefined | (() => Promise<void>);
beforeEach(async () => { beforeEach(async () => {
let app: ElectronApplication; let app: ElectronApplication;
@ -23,20 +24,21 @@ describe("preferences page tests", () => {
await app.evaluate(async ({ app }) => { await app.evaluate(async ({ app }) => {
await app.applicationMenu await app.applicationMenu
.getMenuItemById(process.platform === "darwin" ? "root" : "file") ?.getMenuItemById(process.platform === "darwin" ? "mac" : "file")
.submenu.getMenuItemById("preferences") ?.submenu
.click(); ?.getMenuItemById("navigate-to-preferences")
?.click();
}); });
}, 10*60*1000); }, 10*60*1000);
afterEach(async () => { afterEach(async () => {
await cleanup(); await cleanup?.();
}, 10*60*1000); }, 10*60*1000);
it('shows "preferences" and can navigate through the tabs', async () => { it('shows "preferences" and can navigate through the tabs', async () => {
const pages = [ const pages = [
{ {
id: "application", id: "app",
header: "Application", header: "Application",
}, },
{ {
@ -50,8 +52,8 @@ describe("preferences page tests", () => {
]; ];
for (const { id, header } of pages) { for (const { id, header } of pages) {
await window.click(`[data-testid=tab-link-for-${id}]`); await window.click(`[data-preference-tab-link-test=${id}]`);
await window.waitForSelector(`[data-testid=${id}-header] >> text=${header}`); await window.waitForSelector(`[data-preference-page-title-test] >> text=${header}`);
} }
}, 10*60*1000); }, 10*60*1000);

View File

@ -14,11 +14,14 @@ import { minikubeReady } from "../helpers/minikube";
import type { Frame, Page } from "playwright"; import type { Frame, Page } from "playwright";
import { groupBy, toPairs } from "lodash/fp"; import { groupBy, toPairs } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp"; import { pipeline } from "@ogre-tools/fp";
import { describeIf } from "../../src/test-utils/skippers";
const TEST_NAMESPACE = "integration-tests"; const TEST_NAMESPACE = "integration-tests";
utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
let window: Page, cleanup: () => Promise<void>, frame: Frame; let window: Page;
let cleanup: undefined | (() => Promise<void>);
let frame: Frame;
beforeEach(async () => { beforeEach(async () => {
({ window, cleanup } = await utils.start()); ({ window, cleanup } = await utils.start());
@ -28,7 +31,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
}, 10 * 60 * 1000); }, 10 * 60 * 1000);
afterEach(async () => { afterEach(async () => {
await cleanup(); await cleanup?.();
}, 10 * 60 * 1000); }, 10 * 60 * 1000);
it("shows cluster context menu in sidebar", async () => { it("shows cluster context menu in sidebar", async () => {
@ -388,12 +391,6 @@ const scenarios = [
sidebarItemTestId: "sidebar-item-link-for-service-accounts", sidebarItemTestId: "sidebar-item-link-for-service-accounts",
}, },
{
expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management",
sidebarItemTestId: "sidebar-item-link-for-roles",
},
{ {
expectedSelector: "h5.title", expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management", parentSidebarItemTestId: "sidebar-item-link-for-user-management",
@ -403,7 +400,7 @@ const scenarios = [
{ {
expectedSelector: "h5.title", expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management", parentSidebarItemTestId: "sidebar-item-link-for-user-management",
sidebarItemTestId: "sidebar-item-link-for-role-bindings", sidebarItemTestId: "sidebar-item-link-for-roles",
}, },
{ {
@ -415,7 +412,7 @@ const scenarios = [
{ {
expectedSelector: "h5.title", expectedSelector: "h5.title",
parentSidebarItemTestId: "sidebar-item-link-for-user-management", parentSidebarItemTestId: "sidebar-item-link-for-user-management",
sidebarItemTestId: "sidebar-item-link-for-pod-security-policies", sidebarItemTestId: "sidebar-item-link-for-role-bindings",
}, },
{ {

View File

@ -7,7 +7,9 @@ import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
describe("Lens command palette", () => { describe("Lens command palette", () => {
let window: Page, cleanup: () => Promise<void>, app: ElectronApplication; let window: Page;
let cleanup: undefined | (() => Promise<void>);
let app: ElectronApplication;
beforeEach(async () => { beforeEach(async () => {
({ window, cleanup, app } = await utils.start()); ({ window, cleanup, app } = await utils.start());
@ -15,7 +17,7 @@ describe("Lens command palette", () => {
}, 10*60*1000); }, 10*60*1000);
afterEach(async () => { afterEach(async () => {
await cleanup(); await cleanup?.();
}, 10*60*1000); }, 10*60*1000);
describe("menu", () => { describe("menu", () => {
@ -23,7 +25,7 @@ describe("Lens command palette", () => {
await app.evaluate(async ({ app }) => { await app.evaluate(async ({ app }) => {
await app.applicationMenu await app.applicationMenu
?.getMenuItemById("view") ?.getMenuItemById("view")
?.submenu?.getMenuItemById("command-palette") ?.submenu?.getMenuItemById("open-command-palette")
?.click(); ?.click();
}); });
await window.waitForSelector(".Select__option >> text=Hotbar: Switch"); await window.waitForSelector(".Select__option >> text=Hotbar: Switch");

View File

@ -10,6 +10,7 @@ import * as uuid from "uuid";
import type { ElectronApplication, Frame, Page } from "playwright"; import type { ElectronApplication, Frame, Page } from "playwright";
import { _electron as electron } from "playwright"; import { _electron as electron } from "playwright";
import { noop } from "lodash"; import { noop } from "lodash";
import { disposer } from "../../src/common/utils";
export const appPaths: Partial<Record<NodeJS.Platform, string>> = { export const appPaths: Partial<Record<NodeJS.Platform, string>> = {
"win32": "./dist/win-unpacked/OpenLens.exe", "win32": "./dist/win-unpacked/OpenLens.exe",
@ -17,28 +18,47 @@ export const appPaths: Partial<Record<NodeJS.Platform, string>> = {
"darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens", "darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens",
}; };
export function itIf(condition: boolean) {
return condition ? it : it.skip;
}
export function describeIf(condition: boolean) {
return condition ? describe : describe.skip;
}
async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promise<Page> { async function getMainWindow(app: ElectronApplication, timeout = 50_000): Promise<Page> {
const deadline = Date.now() + timeout; return new Promise((resolve, reject) => {
const cleanup = disposer();
let stdoutBuf = "";
const onWindow = (page: Page) => {
console.log(`Page opened: ${page.url()}`);
for (; Date.now() < deadline;) {
for (const page of app.windows()) {
if (page.url().startsWith("http://localhost")) { if (page.url().startsWith("http://localhost")) {
return page; cleanup();
console.log(stdoutBuf);
resolve(page);
} }
} };
await new Promise(resolve => setTimeout(resolve, 2_000)); app.on("window", onWindow);
} cleanup.push(() => app.off("window", onWindow));
throw new Error(`Lens did not open the main window within ${timeout}ms`); const onClose = () => {
cleanup();
reject(new Error("App has unnexpectedly closed"));
};
app.on("close", onClose);
cleanup.push(() => app.off("close", onClose));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const stdout = app.process().stdout!;
const onData = (chunk: any) => stdoutBuf += chunk.toString();
stdout.on("data", onData);
cleanup.push(() => stdout.off("data", onData));
const timeoutId = setTimeout(() => {
cleanup();
console.log(stdoutBuf);
reject(new Error(`Lens did not open the main window within ${timeout}ms`));
}, timeout);
cleanup.push(() => clearTimeout(timeoutId));
});
} }
async function attemptStart() { async function attemptStart() {
@ -57,7 +77,7 @@ async function attemptStart() {
...process.env, ...process.env,
}, },
timeout: 100_000, timeout: 100_000,
} as Parameters<typeof electron["launch"]>[0]); });
try { try {
const window = await getMainWindow(app); const window = await getMainWindow(app);

View File

@ -24,7 +24,7 @@
}, },
}, },
}, },
prometheus+:: { kubernetesControlPlane+:: {
serviceMonitorKubelet+: { serviceMonitorKubelet+: {
spec+: { spec+: {
endpoints: std.map(function(endpoint) endpoints: std.map(function(endpoint)

View File

@ -3,7 +3,7 @@
"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": "6.0.0", "version": "6.3.0-alpha.0",
"main": "static/build/main.js", "main": "static/build/main.js",
"copyright": "© 2022 OpenLens Authors", "copyright": "© 2022 OpenLens Authors",
"license": "MIT", "license": "MIT",
@ -26,7 +26,8 @@
"compile:main": "yarn run webpack --config webpack/main.ts", "compile:main": "yarn run webpack --config webpack/main.ts",
"compile:renderer": "yarn run webpack --config webpack/renderer.ts", "compile:renderer": "yarn run webpack --config webpack/renderer.ts",
"compile:extension-types": "yarn run webpack --config webpack/extensions.ts", "compile:extension-types": "yarn run webpack --config webpack/extensions.ts",
"npm:fix-build-version": "yarn run ts-node build/set_build_version.ts", "compile:node-fetch": "yarn run webpack --config ./webpack/node-fetch.ts",
"postinstall": "yarn run compile:node-fetch",
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts", "npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
"build:linux": "yarn run compile && electron-builder --linux --dir", "build:linux": "yarn run compile && electron-builder --linux --dir",
"build:mac": "yarn run compile && electron-builder --mac --dir", "build:mac": "yarn run compile && electron-builder --mac --dir",
@ -52,11 +53,12 @@
"create-release-pr": "node ./scripts/create-release-pr.mjs" "create-release-pr": "node ./scripts/create-release-pr.mjs"
}, },
"config": { "config": {
"k8sProxyVersion": "0.2.1", "k8sProxyVersion": "0.3.0",
"bundledKubectlVersion": "1.23.3", "bundledKubectlVersion": "1.23.3",
"bundledHelmVersion": "3.7.2", "bundledHelmVersion": "3.7.2",
"sentryDsn": "", "sentryDsn": "",
"contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:" "contentSecurityPolicy": "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:",
"welcomeRoute": "/welcome"
}, },
"engines": { "engines": {
"node": ">=16 <17" "node": ">=16 <17"
@ -216,14 +218,14 @@
"@astronautlabs/jsonpath": "^1.1.0", "@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.0", "@hapi/call": "^9.0.0",
"@hapi/subtext": "^7.0.4", "@hapi/subtext": "^7.0.4",
"@kubernetes/client-node": "^0.17.0", "@kubernetes/client-node": "^0.17.1",
"@material-ui/styles": "^4.11.5", "@material-ui/styles": "^4.11.5",
"@ogre-tools/fp": "10.1.0", "@ogre-tools/fp": "^12.0.1",
"@ogre-tools/injectable": "10.1.0", "@ogre-tools/injectable": "^12.0.1",
"@ogre-tools/injectable-extension-for-auto-registration": "10.1.0", "@ogre-tools/injectable-extension-for-auto-registration": "^12.0.1",
"@ogre-tools/injectable-extension-for-mobx": "10.1.0", "@ogre-tools/injectable-extension-for-mobx": "^12.0.1",
"@ogre-tools/injectable-react": "10.1.0", "@ogre-tools/injectable-react": "^12.0.1",
"@sentry/electron": "^3.0.7", "@sentry/electron": "^3.0.8",
"@sentry/integrations": "^6.19.3", "@sentry/integrations": "^6.19.3",
"@side/jest-runtime": "^1.0.1", "@side/jest-runtime": "^1.0.1",
"@tanstack/react-virtual": "3.0.0-beta.18", "@tanstack/react-virtual": "3.0.0-beta.18",
@ -246,43 +248,40 @@
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"history": "^4.10.1", "history": "^4.10.1",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"immer": "^9.0.15", "immer": "^9.0.16",
"joi": "^17.6.0", "joi": "^17.7.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsdom": "^16.7.0", "jsdom": "^16.7.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mac-ca": "^1.0.6", "marked": "^4.2.3",
"marked": "^4.1.0",
"md5-file": "^5.0.0", "md5-file": "^5.0.0",
"mobx": "^6.6.2", "mobx": "^6.7.0",
"mobx-observable-history": "^2.0.3", "mobx-observable-history": "^2.0.3",
"mobx-react": "^7.5.3", "mobx-react": "^7.6.0",
"mobx-utils": "^6.0.4", "mobx-utils": "^6.0.4",
"mock-fs": "^5.1.4", "mock-fs": "^5.2.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-timezone": "^0.5.37", "moment-timezone": "^0.5.39",
"monaco-editor": "^0.29.1", "monaco-editor": "^0.29.1",
"monaco-editor-webpack-plugin": "^5.0.0", "monaco-editor-webpack-plugin": "^5.0.0",
"node-fetch": "^2.6.7", "node-fetch": "^3.3.0",
"node-pty": "0.10.1", "node-pty": "0.10.1",
"npm": "^8.19.1", "npm": "^8.19.3",
"p-limit": "^3.1.0", "p-limit": "^3.1.0",
"path-to-regexp": "^6.2.0", "path-to-regexp": "^6.2.0",
"proper-lockfile": "^4.1.2", "proper-lockfile": "^4.1.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-material-ui-carousel": "^2.3.11", "react-material-ui-carousel": "^2.3.11",
"react-router": "^5.2.0", "react-router": "^5.3.4",
"react-virtualized-auto-sizer": "^1.0.6", "react-virtualized-auto-sizer": "^1.0.7",
"readable-stream": "^3.6.0", "readable-stream": "^3.6.0",
"request": "^2.88.2", "request": "^2.88.2",
"request-promise-native": "^1.0.9", "request-promise-native": "^1.0.9",
"rfc6902": "^4.0.2", "rfc6902": "^4.0.2",
"selfsigned": "^2.1.1", "selfsigned": "^2.1.1",
"semver": "^7.3.7", "semver": "^7.3.8",
"shell-env": "^3.0.1", "tar": "^6.1.12",
"spdy": "^4.0.2",
"tar": "^6.1.11",
"tcp-port-used": "^1.0.2", "tcp-port-used": "^1.0.2",
"tempy": "1.0.1", "tempy": "1.0.1",
"typed-regex": "^0.0.8", "typed-regex": "^0.0.8",
@ -290,9 +289,8 @@
"uuid": "^8.3.2", "uuid": "^8.3.2",
"win-ca": "^3.5.0", "win-ca": "^3.5.0",
"winston": "^3.8.2", "winston": "^3.8.2",
"winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5", "winston-transport-browserconsole": "^1.0.5",
"ws": "^8.8.1", "ws": "^8.11.0",
"xterm-link-provider": "^1.3.1" "xterm-link-provider": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
@ -300,11 +298,11 @@
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
"@sentry/types": "^6.19.7", "@sentry/types": "^6.19.7",
"@swc/cli": "^0.1.57", "@swc/cli": "^0.1.57",
"@swc/core": "^1.2.249", "@swc/core": "^1.3.19",
"@swc/jest": "^0.2.22", "@swc/jest": "^0.2.23",
"@testing-library/dom": "^7.31.2", "@testing-library/dom": "^7.31.2",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5", "@testing-library/react": "^12.1.5",
@ -316,7 +314,7 @@
"@types/color": "^3.0.3", "@types/color": "^3.0.3",
"@types/command-line-args": "^5.2.0", "@types/command-line-args": "^5.2.0",
"@types/crypto-js": "^3.1.47", "@types/crypto-js": "^3.1.47",
"@types/dompurify": "^2.3.4", "@types/dompurify": "^2.4.0",
"@types/electron-devtools-installer": "^2.2.1", "@types/electron-devtools-installer": "^2.2.1",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/glob-to-regexp": "^0.4.1", "@types/glob-to-regexp": "^0.4.1",
@ -328,20 +326,19 @@
"@types/jest": "^28.1.6", "@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.14", "@types/jsdom": "^16.2.14",
"@types/lodash": "^4.14.184", "@types/lodash": "^4.14.190",
"@types/marked": "^4.0.7", "@types/marked": "^4.0.7",
"@types/md5-file": "^4.0.2", "@types/md5-file": "^4.0.2",
"@types/memorystream": "^0.3.0",
"@types/mini-css-extract-plugin": "^2.4.0", "@types/mini-css-extract-plugin": "^2.4.0",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^16.11.58", "@types/node": "^16.18.2",
"@types/node-fetch": "^2.6.2",
"@types/npm": "^2.0.32",
"@types/proper-lockfile": "^4.1.2", "@types/proper-lockfile": "^4.1.2",
"@types/randomcolor": "^0.5.6", "@types/randomcolor": "^0.5.6",
"@types/react": "^17.0.45", "@types/react": "^17.0.45",
"@types/react-beautiful-dnd": "^13.1.2", "@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.16", "@types/react-dom": "^17.0.16",
"@types/react-router": "^5.1.18", "@types/react-router": "^5.1.19",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/react-table": "^7.7.12", "@types/react-table": "^7.7.12",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
@ -349,10 +346,9 @@
"@types/readable-stream": "^2.3.13", "@types/readable-stream": "^2.3.13",
"@types/request": "^2.48.7", "@types/request": "^2.48.7",
"@types/request-promise-native": "^1.0.18", "@types/request-promise-native": "^1.0.18",
"@types/semver": "^7.3.12", "@types/semver": "^7.3.13",
"@types/sharp": "^0.30.5", "@types/sharp": "^0.31.0",
"@types/spdy": "^3.4.5", "@types/tar": "^6.1.3",
"@types/tar": "^4.0.5",
"@types/tar-stream": "^2.2.2", "@types/tar-stream": "^2.2.2",
"@types/tcp-port-used": "^1.0.1", "@types/tcp-port-used": "^1.0.1",
"@types/tempy": "^0.3.0", "@types/tempy": "^0.3.0",
@ -363,31 +359,32 @@
"@types/webpack-dev-server": "^4.7.2", "@types/webpack-dev-server": "^4.7.2",
"@types/webpack-env": "^1.18.0", "@types/webpack-env": "^1.18.0",
"@types/webpack-node-externals": "^2.5.3", "@types/webpack-node-externals": "^2.5.3",
"@typescript-eslint/eslint-plugin": "^5.36.2", "@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.36.2", "@typescript-eslint/parser": "^5.44.0",
"adr": "^1.4.1", "adr": "^1.4.3",
"ansi_up": "^5.1.0", "ansi_up": "^5.1.0",
"chalk": "^4.1.2",
"chart.js": "^2.9.4", "chart.js": "^2.9.4",
"circular-dependency-plugin": "^5.2.2", "circular-dependency-plugin": "^5.2.2",
"cli-progress": "^3.11.2", "cli-progress": "^3.11.2",
"color": "^3.2.1", "color": "^3.2.1",
"command-line-args": "^5.2.1", "command-line-args": "^5.2.1",
"concurrently": "^7.4.0", "concurrently": "^7.6.0",
"css-loader": "^6.7.1", "css-loader": "^6.7.2",
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dompurify": "^2.4.0", "dompurify": "^2.4.1",
"electron": "^19.0.16", "electron": "^19.1.7",
"electron-builder": "^23.3.3", "electron-builder": "^23.6.0",
"electron-notarize": "^0.3.0", "electron-notarize": "^0.3.0",
"esbuild": "^0.15.7", "esbuild": "^0.15.15",
"esbuild-loader": "^2.20.0", "esbuild-loader": "^2.20.0",
"eslint": "^8.23.0", "eslint": "^8.28.0",
"eslint-plugin-header": "^3.1.1", "eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "7.31.7", "eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"flex.box": "^3.4.4", "eslint-import-resolver-typescript": "^3.5.2",
"fork-ts-checker-webpack-plugin": "^6.5.2", "fork-ts-checker-webpack-plugin": "^6.5.2",
"gunzip-maybe": "^1.4.2", "gunzip-maybe": "^1.4.2",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
@ -397,44 +394,44 @@
"jest": "^28.1.3", "jest": "^28.1.3",
"jest-canvas-mock": "^2.3.1", "jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^28.1.3", "jest-environment-jsdom": "^28.1.3",
"jest-fetch-mock": "^3.0.3", "jest-mock-extended": "^2.0.9",
"jest-mock-extended": "^2.0.7",
"make-plural": "^6.2.2", "make-plural": "^6.2.2",
"mini-css-extract-plugin": "^2.6.1", "memorystream": "^0.3.1",
"mini-css-extract-plugin": "^2.7.0",
"mock-http": "^1.1.0", "mock-http": "^1.1.0",
"node-gyp": "^8.3.0", "node-gyp": "^8.3.0",
"node-loader": "^2.0.0", "node-loader": "^2.0.0",
"nodemon": "^2.0.19", "nodemon": "^2.0.20",
"playwright": "^1.25.2", "playwright": "^1.28.1",
"postcss": "^8.4.16", "postcss": "^8.4.19",
"postcss-loader": "^6.2.1", "postcss-loader": "^6.2.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"react-beautiful-dnd": "^13.1.1", "react-beautiful-dnd": "^13.1.1",
"react-refresh": "^0.14.0", "react-refresh": "^0.14.0",
"react-refresh-typescript": "^2.0.7", "react-refresh-typescript": "^2.0.7",
"react-router-dom": "^5.3.3", "react-router-dom": "^5.3.4",
"react-select": "^5.4.0", "react-select": "^5.6.1",
"react-select-event": "^5.5.1", "react-select-event": "^5.5.1",
"react-table": "^7.8.0", "react-table": "^7.8.0",
"react-window": "^1.8.7", "react-window": "^1.8.8",
"sass": "^1.54.9", "sass": "^1.56.1",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"sharp": "^0.31.0", "sharp": "^0.31.2",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"tailwindcss": "^3.1.8", "tailwindcss": "^3.2.4",
"tar-stream": "^2.2.0", "tar-stream": "^2.2.0",
"ts-loader": "^9.3.1", "ts-loader": "^9.4.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"type-fest": "^2.14.0", "type-fest": "^2.14.0",
"typed-emitter": "^1.4.0", "typed-emitter": "^1.4.0",
"typedoc": "0.23.14", "typedoc": "0.23.21",
"typedoc-plugin-markdown": "^3.13.1", "typedoc-plugin-markdown": "^3.13.6",
"typescript": "^4.8.2", "typescript": "^4.9.3",
"typescript-plugin-css-modules": "^3.4.0", "typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.74.0", "webpack": "^5.75.0",
"webpack-cli": "^4.9.2", "webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.11.0", "webpack-dev-server": "^4.11.1",
"webpack-node-externals": "^3.0.0", "webpack-node-externals": "^3.0.0",
"xterm": "^4.19.0", "xterm": "^4.19.0",
"xterm-addon-fit": "^0.5.0" "xterm-addon-fit": "^0.5.0"

View File

@ -8,7 +8,7 @@ import fse from "fs-extra";
import { basename } from "path"; import { basename } from "path";
import { createInterface } from "readline"; import { createInterface } from "readline";
import semver from "semver"; import semver from "semver";
import { inspect, promisify } from "util"; import { promisify } from "util";
const { const {
SemVer, SemVer,
@ -27,6 +27,10 @@ const options = commandLineArgs([
{ {
name: "preid", name: "preid",
}, },
{
name: "check-commits",
type: Boolean,
},
]); ]);
const validReleaseValues = [ const validReleaseValues = [
@ -79,10 +83,22 @@ if (basename(process.cwd()) === "scripts") {
console.error(errorMessages.wrongCwd); console.error(errorMessages.wrongCwd);
} }
const packageJson = await fse.readJson("./package.json");
const currentVersion = new SemVer((await fse.readJson("./package.json")).version); const currentVersion = new SemVer(packageJson.version);
console.log(`current version: ${currentVersion.format()}`); console.log(`current version: ${currentVersion.format()}`);
const newVersion = currentVersion.inc(options.type, options.preid);
const newVersionMilestone = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`;
const prBranch = `release/v${newVersion.format()}`;
await fse.writeJson("./package.json", { ...packageJson, version: newVersion.format() }, { spaces: 2 });
await exec(`git checkout -b ${prBranch}`);
await exec("git add package.json");
await exec(`git commit -sm "Release ${newVersion.format()}"`);
console.log(`new version: ${newVersion.format()}`);
console.log("fetching tags..."); console.log("fetching tags...");
await exec("git fetch --tags --force"); await exec("git fetch --tags --force");
@ -93,25 +109,6 @@ const [previousReleasedVersion] = actualTags
.sort((l, r) => semverRcompare(l, r)) .sort((l, r) => semverRcompare(l, r))
.filter(version => semverLte(version, currentVersion)); .filter(version => semverLte(version, currentVersion));
const npmVersionArgs = [
"npm",
"version",
options.type,
];
if (options.preid) {
npmVersionArgs.push(`--preid=${options.preid}`);
}
npmVersionArgs.push("--git-tag-version false");
await exec(npmVersionArgs.join(" "));
const newVersion = new SemVer((await fse.readJson("./package.json")).version);
const newVersionMilestone = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`;
console.log(`new version: ${newVersion.format()}`);
const getMergedPrsArgs = [ const getMergedPrsArgs = [
"gh", "gh",
"pr", "pr",
@ -146,6 +143,10 @@ interface GithubPrData {
title: string; title: string;
} }
interface ExtendedGithubPrData extends Omit<GithubPrData, "mergedAt"> {
mergedAt: Date;
}
console.log("retreiving last 500 PRs to create release PR body..."); console.log("retreiving last 500 PRs to create release PR body...");
const mergedPrs = JSON.parse((await exec(getMergedPrsArgs.join(" "), { encoding: "utf-8" })).stdout) as GithubPrData[]; const mergedPrs = JSON.parse((await exec(getMergedPrsArgs.join(" "), { encoding: "utf-8" })).stdout) as GithubPrData[];
const milestoneRelevantPrs = mergedPrs.filter(pr => pr.milestone?.title === newVersionMilestone); const milestoneRelevantPrs = mergedPrs.filter(pr => pr.milestone?.title === newVersionMilestone);
@ -159,7 +160,7 @@ const relaventPrs = relaventPrsQuery
.filter(query => query.stdout) .filter(query => query.stdout)
.map(query => query.pr) .map(query => query.pr)
.filter(pr => pr.labels.every(label => label.name !== "skip-changelog")) .filter(pr => pr.labels.every(label => label.name !== "skip-changelog"))
.map(pr => ({ ...pr, mergedAt: new Date(pr.mergedAt) })) .map(pr => ({ ...pr, mergedAt: new Date(pr.mergedAt) } as ExtendedGithubPrData))
.sort((left, right) => { .sort((left, right) => {
const leftAge = left.mergedAt.valueOf(); const leftAge = left.mergedAt.valueOf();
const rightAge = right.mergedAt.valueOf(); const rightAge = right.mergedAt.valueOf();
@ -175,75 +176,55 @@ const relaventPrs = relaventPrsQuery
return -1; return -1;
}); });
console.log(inspect(relaventPrs, false, null, true));
const enhancementPrLabelName = "enhancement"; const enhancementPrLabelName = "enhancement";
const bugfixPrLabelName = "bug"; const bugfixPrLabelName = "bug";
const enhancementPrs = relaventPrs.filter(pr => pr.labels.some(label => label.name === enhancementPrLabelName)); const isEnhancementPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === enhancementPrLabelName);
const bugfixPrs = relaventPrs.filter(pr => pr.labels.some(label => label.name === bugfixPrLabelName)); const isBugfixPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === bugfixPrLabelName);
const maintenencePrs = relaventPrs.filter(pr => pr.labels.every(label => label.name !== bugfixPrLabelName && label.name !== enhancementPrLabelName));
console.log("Found:"); const prLines = {
console.log(`${enhancementPrs.length} enhancement PRs`); enhancement: [] as string[],
console.log(`${bugfixPrs.length} bug fix PRs`); bugfix: [] as string[],
console.log(`${maintenencePrs.length} maintenence PRs`); maintenence: [] as string[],
};
const prBodyLines = [ function getPrEntry(pr: ExtendedGithubPrData) {
`## Changes since ${previousReleasedVersion}`,
"",
];
function getPrEntry(pr) {
return `- ${pr.title} (**[#${pr.number}](https://github.com/lensapp/lens/pull/${pr.number})**) https://github.com/${pr.author.login}`; return `- ${pr.title} (**[#${pr.number}](https://github.com/lensapp/lens/pull/${pr.number})**) https://github.com/${pr.author.login}`;
} }
if (enhancementPrs.length > 0) { const rl = createInterface(process.stdin);
prBodyLines.push(
"## 🚀 Features",
"",
...enhancementPrs.map(getPrEntry),
"",
);
}
if (bugfixPrs.length > 0) {
prBodyLines.push(
"## 🐛 Bug Fixes",
"",
...bugfixPrs.map(getPrEntry),
"",
);
}
if (maintenencePrs.length > 0) {
prBodyLines.push(
"## 🧰 Maintenance",
"",
...maintenencePrs.map(getPrEntry),
"",
);
}
const prBody = prBodyLines.join("\n");
const prBase = newVersion.patch === 0 const prBase = newVersion.patch === 0
? "master" ? "master"
: `release/v${newVersion.major}.${newVersion.minor}`; : `release/v${newVersion.major}.${newVersion.minor}`;
const createPrArgs = [
"pr",
"create",
"--base", prBase,
"--title", `release ${newVersion.format()}`,
"--label", "skip-changelog",
"--body-file", "-",
];
const rl = createInterface(process.stdin); function askQuestion(question: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
function _askQuestion() {
console.log(question);
if (prBase !== "master") { rl.once("line", (answer) => {
console.log("Cherry-picking commits to current branch"); const cleaned = answer.trim().toLowerCase();
for (const pr of relaventPrs) { if (cleaned === "y") {
resolve(true);
} else if (cleaned === "n") {
resolve(false);
} else {
_askQuestion();
}
});
}
_askQuestion();
});
}
async function handleRelaventPr(pr: ExtendedGithubPrData) {
if (options["check-commits"] && !(await askQuestion(`Would you like to use #${pr.number}: ${pr.title}? - Y/N`))) {
return;
}
if (prBase !== "master") {
try { try {
const promise = exec(`git cherry-pick ${pr.mergeCommit.oid}`); const promise = exec(`git cherry-pick ${pr.mergeCommit.oid}`);
@ -255,11 +236,72 @@ if (prBase !== "master") {
await promise; await promise;
} catch { } catch {
console.error(`Failed to cherry-pick ${pr.mergeCommit.oid}, please resolve conflicts and then press enter here:`); console.error(`Failed to cherry-pick ${pr.mergeCommit.oid}, please resolve conflicts and then press enter here:`);
await new Promise<void>(resolve => rl.on("line", () => resolve())); await new Promise<void>(resolve => rl.once("line", () => resolve()));
} }
} }
if (isEnhancementPr(pr)) {
prLines.enhancement.push(getPrEntry(pr));
} else if (isBugfixPr(pr)) {
prLines.bugfix.push(getPrEntry(pr));
} else {
prLines.maintenence.push(getPrEntry(pr));
}
} }
for (const pr of relaventPrs) {
await handleRelaventPr(pr);
}
rl.close();
const prBodyLines = [
`## Changes since ${previousReleasedVersion}`,
"",
...(
prLines.enhancement.length > 0
? [
"## 🚀 Features",
"",
...prLines.enhancement,
"",
]
: []
),
...(
prLines.bugfix.length > 0
? [
"## 🐛 Bug Fixes",
"",
...prLines.bugfix,
"",
]
: []
),
...(
prLines.maintenence.length > 0
? [
"## 🧰 Maintenance",
"",
...prLines.maintenence,
"",
]
: []
),
];
const prBody = prBodyLines.join("\n");
const createPrArgs = [
"pr",
"create",
"--base", prBase,
"--title", `Release ${newVersion.format()}`,
"--label", "skip-changelog",
"--label", "release",
"--body-file", "-",
];
await exec(`git push --set-upstream origin ${prBranch}`);
const createPrProcess = execFile("gh", createPrArgs); const createPrProcess = execFile("gh", createPrArgs);
createPrProcess.child.stdout?.pipe(process.stdout); createPrProcess.child.stdout?.pipe(process.stdout);

View File

@ -1,21 +0,0 @@
#!/bin/bash
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-f|--force)
FORCE="--force"
shift # past argument
;;
esac
done
if [[ `git branch --show-current` =~ ^release/v ]]
then
VERSION_STRING=$(cat package.json | jq '.version' -r | xargs printf "v%s")
git tag ${VERSION_STRING} ${FORCE}
git push ${GIT_REMOTE:-origin} ${VERSION_STRING} ${FORCE}
else
echo "You must be in a release branch"
fi

View File

@ -18,13 +18,13 @@ import { createClusterInjectionToken } from "../cluster/create-cluster-injection
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../vars/app-version.injectable";
import assert from "assert"; import assert from "assert";
import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable"; import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable";
import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable";
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
import fsInjectable from "../fs/fs.injectable"; import fsInjectable from "../fs/fs.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -372,7 +372,7 @@ users:
mockFs(mockOpts); mockFs(mockOpts);
mainDi.override(appVersionInjectable, () => "3.6.0"); mainDi.override(storeMigrationVersionInjectable, () => "3.6.0");
createCluster = mainDi.inject(createClusterInjectionToken); createCluster = mainDi.inject(createClusterInjectionToken);

View File

@ -1,27 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AppEvent } from "../app-event-bus/event-bus";
import { appEventBus } from "../app-event-bus/event-bus";
import { assert, Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
describe("event bus tests", () => {
describe("emit", () => {
it("emits an event", () => {
let event: AppEvent | undefined;
appEventBus.addListener((data) => {
event = data;
});
appEventBus.emit({ name: "foo", action: "bar" });
assert(event);
expect(event?.name).toBe("foo");
});
});
});

View File

@ -8,7 +8,6 @@ import mockFs from "mock-fs";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../vars/app-version.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../hotbars/store.injectable"; import hotbarStoreInjectable from "../hotbars/store.injectable";
import type { HotbarStore } from "../hotbars/store"; import type { HotbarStore } from "../hotbars/store";
@ -19,6 +18,7 @@ import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
import type { Logger } from "../logger"; import type { Logger } from "../logger";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity { function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return { return {
@ -348,7 +348,7 @@ describe("HotbarStore", () => {
mockFs(configurationToBeMigrated); mockFs(configurationToBeMigrated);
di.override(appVersionInjectable, () => "5.0.0-beta.10"); di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");
hotbarStore = di.inject(hotbarStoreInjectable); hotbarStore = di.inject(hotbarStoreInjectable);

View File

@ -1,99 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import https from "https";
import os from "os";
import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca";
import { dependencies, devDependencies } from "../../../package.json";
import assert from "assert";
const deps = { ...dependencies, ...devDependencies };
// Skip the test if mac-ca is not installed, or os is not darwin
(deps["mac-ca"] && os.platform().includes("darwin") ? describe: describe.skip)("inject CA for Mac", () => {
// for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => {
_ca = https.globalAgent.options.ca;
});
afterEach(() => {
https.globalAgent.options.ca = _ca;
});
/**
* The test to ensure using getMacRootCA + injectCAs injects CAs in the same way as using
* the auto injection (require('mac-ca'))
*/
it("should inject the same ca as mac-ca", async () => {
const osxCAs = await getMacRootCA();
injectCAs(osxCAs);
const injected = https.globalAgent.options.ca as (string | Buffer)[];
await import("mac-ca");
const injectedByMacCA = https.globalAgent.options.ca as (string | Buffer)[];
expect(new Set(injected)).toEqual(new Set(injectedByMacCA));
});
it("shouldn't included the expired DST Root CA X3 on Mac", async () => {
const osxCAs = await getMacRootCA();
injectCAs(osxCAs);
const injected = https.globalAgent.options.ca;
assert(injected);
expect(injected.includes(DSTRootCAX3)).toBeFalsy();
});
});
// Skip the test if win-ca is not installed, or os is not win32
(deps["win-ca"] && os.platform().includes("win32") ? describe: describe.skip)("inject CA for Windows", () => {
// for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => {
_ca = https.globalAgent.options.ca;
});
afterEach(() => {
https.globalAgent.options.ca = _ca;
});
/**
* The test to ensure using win-ca/api injects CAs in the same way as using
* the auto injection (require('win-ca').inject('+'))
*/
it("should inject the same ca as winca.inject('+')", async () => {
const winCAs = await getWinRootCA();
const wincaAPI = await import("win-ca/api");
wincaAPI.inject("+", winCAs);
const injected = https.globalAgent.options.ca as (string | Buffer)[];
const winca = await import("win-ca");
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
const injectedByWinCA = https.globalAgent.options.ca as (string | Buffer)[];
expect(new Set(injected)).toEqual(new Set(injectedByWinCA));
});
it("shouldn't included the expired DST Root CA X3 on Windows", async () => {
const winCAs = await getWinRootCA();
const wincaAPI = await import("win-ca/api");
wincaAPI.inject("true", winCAs);
const injected = https.globalAgent.options.ca as (string | Buffer)[];
expect(injected.includes(DSTRootCAX3)).toBeFalsy();
});
});

View File

@ -23,8 +23,6 @@ jest.mock("electron", () => ({
import type { UserStore } from "../user-store"; import type { UserStore } from "../user-store";
import { Console } from "console"; import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import userStoreInjectable from "../user-store/user-store.injectable"; import userStoreInjectable from "../user-store/user-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
@ -34,7 +32,9 @@ import { defaultThemeId } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable"; import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../vars/app-version.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import releaseChannelInjectable from "../vars/release-channel.injectable";
import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -42,7 +42,7 @@ describe("user store tests", () => {
let userStore: UserStore; let userStore: UserStore;
let di: DiContainer; let di: DiContainer;
beforeEach(() => { beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs(); mockFs();
@ -52,6 +52,12 @@ describe("user store tests", () => {
di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(userStoreInjectable); di.permitSideEffects(userStoreInjectable);
di.override(releaseChannelInjectable, () => ({
get: () => "latest" as const,
init: async () => {},
}));
await di.inject(defaultUpdateChannelInjectable).init();
di.unoverride(userStoreInjectable); di.unoverride(userStoreInjectable);
}); });
@ -64,6 +70,7 @@ describe("user store tests", () => {
mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }}); mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }});
userStore = di.inject(userStoreInjectable); userStore = di.inject(userStoreInjectable);
userStore.load();
}); });
it("allows setting and retrieving lastSeenAppVersion", () => { it("allows setting and retrieving lastSeenAppVersion", () => {
@ -86,13 +93,6 @@ describe("user store tests", () => {
userStore.resetTheme(); userStore.resetTheme();
expect(userStore.colorTheme).toBe(defaultThemeId); expect(userStore.colorTheme).toBe(defaultThemeId);
}); });
it("correctly calculates if the last seen version is an old release", () => {
expect(userStore.isNewVersion).toBe(true);
userStore.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format();
expect(userStore.isNewVersion).toBe(false);
});
}); });
describe("migrations", () => { describe("migrations", () => {
@ -125,9 +125,10 @@ describe("user store tests", () => {
}, },
}); });
di.override(appVersionInjectable, () => "10.0.0"); di.override(storeMigrationVersionInjectable, () => "10.0.0");
userStore = di.inject(userStoreInjectable); userStore = di.inject(userStoreInjectable);
userStore.load();
}); });
it("sets last seen app version to 0.0.0", () => { it("sets last seen app version to 0.0.0", () => {

View File

@ -3,12 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { appEventBus } from "./event-bus"; import { EventEmitter } from "../event-emitter";
import type { AppEvent } from "./event-bus";
const appEventBusInjectable = getInjectable({ const appEventBusInjectable = getInjectable({
id: "app-event-bus", id: "app-event-bus",
instantiate: () => appEventBus, instantiate: () => new EventEmitter<[AppEvent]>,
causesSideEffects: true,
decorable: false, decorable: false,
}); });

View File

@ -4,11 +4,18 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import appEventBusInjectable from "./app-event-bus.injectable"; import appEventBusInjectable from "./app-event-bus.injectable";
import type { AppEvent } from "./event-bus";
const emitEventInjectable = getInjectable({ export type EmitAppEvent = (event: AppEvent) => void;
id: "emit-event",
instantiate: (di) => di.inject(appEventBusInjectable).emit, const emitAppEventInjectable = getInjectable({
id: "emit-app-event",
instantiate: (di): EmitAppEvent => {
const bus = di.inject(appEventBusInjectable);
return (event) => bus.emit(event);
},
decorable: false, decorable: false,
}); });
export default emitEventInjectable; export default emitAppEventInjectable;

View File

@ -3,13 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { EventEmitter } from "../event-emitter"; /**
* Data for telemetry
*/
export interface AppEvent { export interface AppEvent {
name: string; name: string;
action: string; action: string;
destination?: string; destination?: string;
params?: Record<string, any>; params?: Record<string, any>;
} }
export const appEventBus = new EventEmitter<[AppEvent]>();

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AppPaths } from "./app-path-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type AppPathsChannel = RequestChannel<void, AppPaths>;
const appPathsChannelInjectable = getInjectable({
id: "app-paths-channel",
instantiate: (): AppPathsChannel => ({
id: "app-paths",
}),
injectionToken: messageChannelInjectionToken,
});
export default appPathsChannelInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AppPaths } from "./app-path-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
export type AppPathsChannel = RequestChannel<void, AppPaths>;
export const appPathsChannel: AppPathsChannel = {
id: "app-paths",
};

View File

@ -7,7 +7,6 @@ import { appPathsInjectionToken } from "./app-path-injection-token";
import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable"; import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable";
import type { PathName } from "./app-path-names"; import type { PathName } from "./app-path-names";
import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable"; import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable"; import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
@ -53,8 +52,6 @@ describe("app-paths", () => {
defaultAppPathsStub[key] = path; defaultAppPathsStub[key] = path;
}, },
); );
mainDi.override(appNameInjectable, () => "some-app-name");
}); });
}); });
@ -88,7 +85,7 @@ describe("app-paths", () => {
recent: "some-recent", recent: "some-recent",
temp: "some-temp", temp: "some-temp",
videos: "some-videos", videos: "some-videos",
userData: "some-app-data/some-app-name", userData: "some-app-data/some-product-name",
}); });
}); });
@ -111,7 +108,7 @@ describe("app-paths", () => {
recent: "some-recent", recent: "some-recent",
temp: "some-temp", temp: "some-temp",
videos: "some-videos", videos: "some-videos",
userData: "some-app-data/some-app-name", userData: "some-app-data/some-product-name",
}); });
}); });
}); });
@ -137,7 +134,7 @@ describe("app-paths", () => {
expect({ appData, userData }).toEqual({ expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data", appData: "some-integration-testing-app-data",
userData: `some-integration-testing-app-data/some-app-name`, userData: `some-integration-testing-app-data/some-product-name`,
}); });
}); });
@ -146,7 +143,7 @@ describe("app-paths", () => {
expect({ appData, userData }).toEqual({ expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data", appData: "some-integration-testing-app-data",
userData: "some-integration-testing-app-data/some-app-name", userData: "some-integration-testing-app-data/some-product-name",
}); });
}); });
}); });

View File

@ -4,16 +4,16 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; import joinPathsInjectable from "../../path/join-paths.injectable";
const directoryForBinariesInjectable = getInjectable({ const directoryForBinariesInjectable = getInjectable({
id: "directory-for-binaries", id: "directory-for-binaries",
instantiate: (di) => { instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable); const joinPaths = di.inject(joinPathsInjectable);
const directoryForUserData = di.inject(directoryForUserDataInjectable); const directoryForUserData = di.inject(directoryForUserDataInjectable);
return getAbsolutePath(directoryForUserData, "binaries"); return joinPaths(directoryForUserData, "binaries");
}, },
}); });

View File

@ -4,19 +4,16 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; import joinPathsInjectable from "../../path/join-paths.injectable";
const directoryForKubeConfigsInjectable = getInjectable({ const directoryForKubeConfigsInjectable = getInjectable({
id: "directory-for-kube-configs", id: "directory-for-kube-configs",
instantiate: (di) => { instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable); const joinPaths = di.inject(joinPathsInjectable);
const directoryForUserData = di.inject(directoryForUserDataInjectable); const directoryForUserData = di.inject(directoryForUserDataInjectable);
return getAbsolutePath( return joinPaths(directoryForUserData, "kubeconfigs");
directoryForUserData,
"kubeconfigs",
);
}, },
}); });

View File

@ -4,17 +4,16 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import directoryForBinariesInjectable from "../directory-for-binaries/directory-for-binaries.injectable"; import directoryForBinariesInjectable from "../directory-for-binaries/directory-for-binaries.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; import joinPathsInjectable from "../../path/join-paths.injectable";
const directoryForKubectlBinariesInjectable = getInjectable({ const directoryForKubectlBinariesInjectable = getInjectable({
id: "directory-for-kubectl-binaries", id: "directory-for-kubectl-binaries",
instantiate: (di) => { instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable); const joinPaths = di.inject(joinPathsInjectable);
const directoryForBinaries = di.inject(directoryForBinariesInjectable); const directoryForBinaries = di.inject(directoryForBinariesInjectable);
return joinPaths(directoryForBinaries, "kubectl");
return getAbsolutePath(directoryForBinaries, "kubectl");
}, },
}); });

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import appPathsInjectable from "./app-paths.injectable";
const directoryForLogsInjectable = getInjectable({
id: "directory-for-logs",
instantiate: (di) => di.inject(appPathsInjectable).logs,
});
export default directoryForLogsInjectable;

View File

@ -4,17 +4,16 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable"; import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable"; import joinPathsInjectable from "../../path/join-paths.injectable";
const getCustomKubeConfigDirectoryInjectable = getInjectable({ const getCustomKubeConfigDirectoryInjectable = getInjectable({
id: "get-custom-kube-config-directory", id: "get-custom-kube-config-directory",
instantiate: (di) => { instantiate: (di) => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable); const joinPaths = di.inject(joinPathsInjectable);
return (directoryName: string) => return (directoryName: string) => joinPaths(directoryForKubeConfigs, directoryName);
getAbsolutePath(directoryForKubeConfigs, directoryName);
}, },
}); });

View File

@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { MessageChannel } from "../../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../../utils/channel/message-channel-injection-token";
export type RestartAndInstallUpdateChannel = MessageChannel;
const restartAndInstallUpdateChannel = getInjectable({
id: "restart-and-install-update-channel",
instantiate: (): RestartAndInstallUpdateChannel => ({
id: "restart-and-install-update-channel",
}),
injectionToken: messageChannelInjectionToken,
});
export default restartAndInstallUpdateChannel;

View File

@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import appSemanticVersionInjectable from "../../vars/app-semantic-version.injectable";
import type { UpdateChannelId } from "../update-channels";
import { updateChannels } from "../update-channels";
const defaultUpdateChannelInjectable = getInjectable({
id: "default-update-channel",
instantiate: (di) => {
const appSemanticVersion = di.inject(appSemanticVersionInjectable);
const currentReleaseChannel = appSemanticVersion.prerelease[0]?.toString();
if (currentReleaseChannel in updateChannels) {
return updateChannels[currentReleaseChannel as UpdateChannelId];
}
return updateChannels.latest;
},
});
export default defaultUpdateChannelInjectable;

View File

@ -19,7 +19,7 @@ import { kebabCase } from "lodash";
import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "./vars/app-version.injectable"; import storeMigrationVersionInjectable from "./vars/store-migration-version.injectable";
export interface BaseStoreParams<T> extends ConfOptions<T> { export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: { syncOptions?: {
@ -60,7 +60,7 @@ export abstract class BaseStore<T extends object> extends Singleton {
this.storeConfig = getConfigurationFileModel({ this.storeConfig = getConfigurationFileModel({
projectName: "lens", projectName: "lens",
projectVersion: di.inject(appVersionInjectable), projectVersion: di.inject(storeMigrationVersionInjectable),
cwd: this.cwd(), cwd: this.cwd(),
...this.params, ...this.params,
}); });

View File

@ -3,9 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity"; import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import { productName } from "../vars"; import productNameInjectable from "../vars/product-name.injectable";
import { WeblinkStore } from "../weblink-store"; import { WeblinkStore } from "../weblink-store";
export type WebLinkStatusPhase = "available" | "unavailable"; export type WebLinkStatusPhase = "available" | "unavailable";
@ -30,6 +31,9 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
} }
onContextMenuOpen(context: CatalogEntityContextMenuContext) { onContextMenuOpen(context: CatalogEntityContextMenuContext) {
const di = getLegacyGlobalDiForExtensionApi();
const productName = di.inject(productNameInjectable);
if (this.metadata.source === "local") { if (this.metadata.source === "local") {
context.menuItems.push({ context.menuItems.push({
title: "Delete", title: "Delete",

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
const filteredCategoriesInjectable = getInjectable({
id: "filtered-categories",
instantiate: (di) => {
const registry = di.inject(catalogCategoryRegistryInjectable);
return computed(() => [...registry.filteredItems]);
},
});
export default filteredCategoriesInjectable;

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { globalAgent } from "https";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
// DST Root CA X3, which was expired on 9.30.2021
const DSTRootCAX3 = "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n";
function isCertActive(cert: string) {
const isExpired = typeof cert !== "string" || cert.includes(DSTRootCAX3);
return !isExpired;
}
const injectSystemCAsInjectable = getInjectable({
id: "inject-system-cas",
instantiate: (di) => {
const requestSystemCAs = di.inject(requestSystemCAsInjectionToken);
return async () => {
const certs = await requestSystemCAs();
if (certs.length === 0) {
// Leave the global option alone
return;
}
const cas = (() => {
if (Array.isArray(globalAgent.options.ca)) {
return globalAgent.options.ca;
}
if (globalAgent.options.ca) {
return [globalAgent.options.ca];
}
return [];
})();
for (const cert of certs) {
if (!isCertActive(cert)) {
continue;
}
if (!cas.includes(cert)) {
cas.push(cert);
}
}
globalAgent.options.ca = cas;
};
},
});
export default injectSystemCAsInjectable;

View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export const requestSystemCAsInjectionToken = getInjectionToken<() => Promise<string[]>>({
id: "request-system-cas-token",
});

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import execFileInjectable from "../fs/exec-file.injectable";
import loggerInjectable from "../logger.injectable";
import type { AsyncResult } from "../utils/async-result";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
const certSplitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: (di) => {
const execFile = di.inject(execFileInjectable);
const logger = di.inject(loggerInjectable);
const execSecurity = async (...args: string[]): Promise<AsyncResult<string[]>> => {
const result = await execFile("/usr/bin/security", args);
if (!result.callWasSuccessful) {
return {
callWasSuccessful: false,
error: result.error.stderr || result.error.message,
};
}
return {
callWasSuccessful: true,
response: result.response.split(certSplitPattern),
};
};
return async () => {
const [trustedResult, rootCAResult] = await Promise.all([
execSecurity("find-certificate", "-a", "-p"),
execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
]);
if (!trustedResult.callWasSuccessful) {
logger.warn(`[INJECT-CAS]: Error retreiving trusted CAs: ${trustedResult.error}`);
} else if (!rootCAResult.callWasSuccessful) {
logger.warn(`[INJECT-CAS]: Error retreiving root CAs: ${rootCAResult.error}`);
} else {
return [...new Set([...trustedResult.response, ...rootCAResult.response])];
}
return [];
};
},
causesSideEffects: true,
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: () => async () => [],
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: () => async () => [],
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import execFileInjectable from "../fs/exec-file.injectable";
import loggerInjectable from "../logger.injectable";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
const pemEncoding = (hexEncodedCert: String) => {
const certData = Buffer.from(hexEncodedCert, "hex").toString("base64");
const lines = ["-----BEGIN CERTIFICATE-----"];
for (let i = 0; i < certData.length; i += 64) {
lines.push(certData.substring(i, i + 64));
}
lines.push("-----END CERTIFICATE-----", "");
return lines.join("\r\n");
};
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: (di) => {
const wincaRootsExePath: string = __non_webpack_require__.resolve("win-ca/lib/roots.exe");
const execFile = di.inject(execFileInjectable);
const logger = di.inject(loggerInjectable);
return async () => {
/**
* This needs to be done manually because for some reason calling the api from "win-ca"
* directly fails to load "child_process" correctly on renderer
*/
const result = await execFile(wincaRootsExePath, {
maxBuffer: 128 * 1024 * 1024, // 128 MiB
});
if (!result.callWasSuccessful) {
logger.warn(`[INJECT-CAS]: Error retreiving CAs`, result.error);
return [];
}
return result
.response
.split("\r\n")
.filter(Boolean)
.map(pemEncoding);
};
},
causesSideEffects: true,
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;

View File

@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store"; import { ClusterStore } from "./cluster-store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable"; import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import emitAppEventInjectable from "../app-event-bus/emit-event.injectable";
const clusterStoreInjectable = getInjectable({ const clusterStoreInjectable = getInjectable({
id: "cluster-store", id: "cluster-store",
@ -16,6 +17,7 @@ const clusterStoreInjectable = getInjectable({
return ClusterStore.createInstance({ return ClusterStore.createInstance({
createCluster: di.inject(createClusterInjectionToken), createCluster: di.inject(createClusterInjectionToken),
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable), readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
emitAppEvent: di.inject(emitAppEventInjectable),
}); });
}, },

View File

@ -10,7 +10,6 @@ import { BaseStore } from "../base-store";
import { Cluster } from "../cluster/cluster"; import { Cluster } from "../cluster/cluster";
import migrations from "../../migrations/cluster-store"; import migrations from "../../migrations/cluster-store";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle } from "../ipc"; import { ipcMainHandle } from "../ipc";
import { disposer, toJS } from "../utils"; import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
@ -18,6 +17,7 @@ import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster"; import { clusterStates } from "../ipc/cluster";
import type { CreateCluster } from "../cluster/create-cluster-injection-token"; import type { CreateCluster } from "../cluster/create-cluster-injection-token";
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable"; import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
export interface ClusterStoreModel { export interface ClusterStoreModel {
clusters?: ClusterModel[]; clusters?: ClusterModel[];
@ -26,6 +26,7 @@ export interface ClusterStoreModel {
interface Dependencies { interface Dependencies {
createCluster: CreateCluster; createCluster: CreateCluster;
readClusterConfigSync: ReadClusterConfigSync; readClusterConfigSync: ReadClusterConfigSync;
emitAppEvent: EmitAppEvent;
} }
export class ClusterStore extends BaseStore<ClusterStoreModel> { export class ClusterStore extends BaseStore<ClusterStoreModel> {
@ -34,7 +35,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
protected disposer = disposer(); protected disposer = disposer();
constructor(private dependencies: Dependencies) { constructor(private readonly dependencies: Dependencies) {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -115,7 +116,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster { addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" }); this.dependencies.emitAppEvent({ name: "cluster", action: "add" });
const cluster = clusterOrModel instanceof Cluster const cluster = clusterOrModel instanceof Cluster
? clusterOrModel ? clusterOrModel

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterId } from "../cluster-types";
import type { Cluster } from "../cluster/cluster";
import clusterStoreInjectable from "./cluster-store.injectable";
export type GetClusterById = (id: ClusterId) => Cluster | undefined;
const getClusterByIdInjectable = getInjectable({
id: "get-cluster-by-id",
instantiate: (di): GetClusterById => {
const store = di.inject(clusterStoreInjectable);
return (id) => store.getById(id);
},
});
export default getClusterByIdInjectable;

View File

@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import Joi from "joi";
/** /**
* JSON serializable metadata type * JSON serializable metadata type
*/ */
@ -27,6 +29,37 @@ export type ClusterId = string;
*/ */
export type UpdateClusterModel = Omit<ClusterModel, "id">; export type UpdateClusterModel = Omit<ClusterModel, "id">;
/**
* A type validator for `UpdateClusterModel` so that only expected types are present
*/
export const updateClusterModelChecker = Joi.object<UpdateClusterModel>({
kubeConfigPath: Joi.string()
.required()
.min(1),
contextName: Joi.string()
.required()
.min(1),
workspace: Joi.string()
.optional(),
workspaces: Joi.array()
.items(Joi.string()),
preferences: Joi.object(),
metadata: Joi.object(),
accessibleNamespaces: Joi.array()
.items(Joi.string()),
labels: Joi.object().pattern(Joi.string(), Joi.string()),
});
/**
* A type validator for just the `id` fields of `ClusterModel`. The rest is
* covered by `updateClusterModelChecker`
*/
export const clusterModelIdChecker = Joi.object<Pick<ClusterModel, "id">>({
id: Joi.string()
.required()
.min(1),
});
/** /**
* The model for passing cluster data around, including to disk * The model for passing cluster data around, including to disk
*/ */
@ -162,13 +195,6 @@ export enum ClusterMetricsResourceType {
*/ */
export const initialNodeShellImage = "docker.io/alpine:3.13"; export const initialNodeShellImage = "docker.io/alpine:3.13";
/**
* The arguments for requesting to refresh a cluster's metadata
*/
export interface ClusterRefreshOptions {
refreshMetadata?: boolean;
}
/** /**
* The data representing a cluster's state, for passing between main and renderer * The data representing a cluster's state, for passing between main and renderer
*/ */

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac";
/**
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
* @param availableResources List of available resources in the cluster to resolve glob values fir api groups
* @returns list of allowed resources names
*/
export type RequestNamespaceResources = (namespace: string, availableResources: KubeApiResource[]) => Promise<string[]>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type AuthorizationNamespaceReview = (proxyConfig: KubeConfig) => RequestNamespaceResources;
interface Dependencies {
logger: Logger;
}
const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNamespaceReview => {
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (namespace, availableResources) => {
try {
const { body } = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectRulesReview",
spec: { namespace },
});
const resources = new Set<string>();
body.status?.resourceRules.forEach(resourceRule => {
if (!resourceRule.verbs.some(verb => ["*", "list"].includes(verb)) || !resourceRule.resources) {
return;
}
const apiGroups = resourceRule.apiGroups;
if (resourceRule.resources.length === 1 && resourceRule.resources[0] === "*" && apiGroups) {
if (apiGroups[0] === "*") {
availableResources.forEach(resource => resources.add(resource.apiName));
} else {
availableResources.forEach((apiResource)=> {
if (apiGroups.includes(apiResource.group || "")) {
resources.add(apiResource.apiName);
}
});
}
} else {
resourceRule.resources.forEach(resource => resources.add(resource));
}
});
return [...resources];
} catch (error) {
logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review: ${error}`, { namespace });
return [];
}
};
};
};
const authorizationNamespaceReviewInjectable = getInjectable({
id: "authorization-namespace-review",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return authorizationNamespaceReview({ logger });
},
});
export default authorizationNamespaceReviewInjectable;

View File

@ -5,42 +5,55 @@
import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node"; import { AuthorizationV1Api } from "@kubernetes/client-node";
import logger from "../logger";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
/**
* Requests the permissions for actions on the kube cluster
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
* @returns `true` if the actions described are allowed
*/
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>; export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
/** /**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/ */
export function authorizationReview(proxyConfig: KubeConfig): CanI { export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI;
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
/** interface Dependencies {
* Requests the permissions for actions on the kube cluster logger: Logger;
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
* @returns `true` if the actions described are allowed
*/
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
try {
const { body } = await api.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes },
});
return body.status?.allowed ?? false;
} catch (error) {
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
return false;
}
};
} }
const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => {
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
try {
const { body } = await api.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes },
});
return body.status?.allowed ?? false;
} catch (error) {
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
return false;
}
};
};
};
const authorizationReviewInjectable = getInjectable({ const authorizationReviewInjectable = getInjectable({
id: "authorization-review", id: "authorization-review",
instantiate: () => authorizationReview, instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return authorizationReview({ logger });
},
}); });
export default authorizationReviewInjectable; export default authorizationReviewInjectable;

View File

@ -9,14 +9,13 @@ import type { KubeConfig } from "@kubernetes/client-node";
import { HttpError } from "@kubernetes/client-node"; import { HttpError } from "@kubernetes/client-node";
import type { Kubectl } from "../../main/kubectl/kubectl"; import type { Kubectl } from "../../main/kubectl/kubectl";
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager"; import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
import { loadConfigFromFile } from "../kube-helpers";
import type { KubeApiResource, KubeResource } from "../rbac"; import type { KubeApiResource, KubeResource } from "../rbac";
import { apiResourceRecord, apiResources } from "../rbac"; import { apiResourceRecord, apiResources } from "../rbac";
import type { VersionDetector } from "../../main/cluster-detectors/version-detector"; import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import plimit from "p-limit"; import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types"; import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
import { disposer, isDefined, isRequestError, toJS } from "../utils"; import { disposer, isDefined, isRequestError, toJS } from "../utils";
import type { Response } from "request"; import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster"; import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
@ -25,6 +24,9 @@ import type { ListNamespaces } from "./list-namespaces.injectable";
import assert from "assert"; import assert from "assert";
import type { Logger } from "../logger"; import type { Logger } from "../logger";
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable"; import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable";
import type { RequestNamespaceResources } from "./authorization-namespace-review.injectable";
import type { RequestListApiResources } from "./list-api-resources.injectable";
export interface ClusterDependencies { export interface ClusterDependencies {
readonly directoryForKubeConfigs: string; readonly directoryForKubeConfigs: string;
@ -34,9 +36,12 @@ export interface ClusterDependencies {
createContextHandler: (cluster: Cluster) => ClusterContextHandler; createContextHandler: (cluster: Cluster) => ClusterContextHandler;
createKubectl: (clusterVersion: string) => Kubectl; createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI; createAuthorizationReview: (config: KubeConfig) => CanI;
createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources;
createListApiResources: (cluster: Cluster) => RequestListApiResources;
createListNamespaces: (config: KubeConfig) => ListNamespaces; createListNamespaces: (config: KubeConfig) => ListNamespaces;
createVersionDetector: (cluster: Cluster) => VersionDetector; createVersionDetector: (cluster: Cluster) => VersionDetector;
broadcastMessage: BroadcastMessage; broadcastMessage: BroadcastMessage;
loadConfigfromFile: LoadConfigfromFile;
} }
/** /**
@ -237,9 +242,16 @@ export class Cluster implements ClusterModel, ClusterState {
return this.preferences.defaultNamespace; return this.preferences.defaultNamespace;
} }
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel, configData: ClusterConfigData) { constructor(private readonly dependencies: ClusterDependencies, { id, ...model }: ClusterModel, configData: ClusterConfigData) {
makeObservable(this); makeObservable(this);
this.id = model.id;
const { error } = clusterModelIdChecker.validate({ id });
if (error) {
throw error;
}
this.id = id;
this.updateModel(model); this.updateModel(model);
this.apiUrl = configData.clusterServerUrl; this.apiUrl = configData.clusterServerUrl;
@ -261,6 +273,12 @@ export class Cluster implements ClusterModel, ClusterState {
@action updateModel(model: UpdateClusterModel) { @action updateModel(model: UpdateClusterModel) {
// Note: do not assign ID as that should never be updated // Note: do not assign ID as that should never be updated
const { error } = updateClusterModelChecker.validate(model, { allowUnknown: true });
if (error) {
throw error;
}
this.kubeConfigPath = model.kubeConfigPath; this.kubeConfigPath = model.kubeConfigPath;
this.contextName = model.contextName; this.contextName = model.contextName;
@ -295,7 +313,7 @@ export class Cluster implements ClusterModel, ClusterState {
protected bindEvents() { protected bindEvents() {
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes
this.eventsDisposer.push( this.eventsDisposer.push(
reaction(() => this.getState(), state => this.pushState(state)), reaction(() => this.getState(), state => this.pushState(state)),
@ -425,69 +443,71 @@ export class Cluster implements ClusterModel, ClusterState {
/** /**
* @internal * @internal
* @param opts refresh options
*/ */
@action @action
async refresh(opts: ClusterRefreshOptions = {}) { async refresh() {
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.refreshConnectionStatus(); await this.refreshConnectionStatus();
if (this.accessible) {
await this.refreshAccessibility();
if (opts.refreshMetadata) {
this.refreshMetadata();
}
}
this.pushState(); this.pushState();
} }
/** /**
* @internal * @internal
*/ */
@action @action
async refreshMetadata() { async refreshAccessibilityAndMetadata() {
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); await this.refreshAccessibility();
const metadata = await this.dependencies.detectorRegistry.detectForCluster(this); await this.refreshMetadata();
const existingMetadata = this.metadata;
this.metadata = Object.assign(existingMetadata, metadata);
} }
/** /**
* @internal * @internal
*/ */
private async refreshAccessibility(): Promise<void> { async refreshMetadata() {
const proxyConfig = await this.getProxyKubeconfig(); this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const canI = this.dependencies.createAuthorizationReview(proxyConfig); const metadata = await this.dependencies.detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata;
this.isAdmin = await canI({ this.metadata = Object.assign(existingMetadata, metadata);
namespace: "kube-system", }
resource: "*",
verb: "create", /**
}); * @internal
this.isGlobalWatchEnabled = await canI({ */
verb: "watch", private async refreshAccessibility(): Promise<void> {
resource: "*", this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta());
}); const proxyConfig = await this.getProxyKubeconfig();
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig); const canI = this.dependencies.createAuthorizationReview(proxyConfig);
this.allowedResources = await this.getAllowedResources(canI); const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig);
this.ready = true; const listApiResources = this.dependencies.createListApiResources(this);
}
this.isAdmin = await canI({
namespace: "kube-system",
resource: "*",
verb: "create",
});
this.isGlobalWatchEnabled = await canI({
verb: "watch",
resource: "*",
});
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
this.allowedResources = await this.getAllowedResources(listApiResources, requestNamespaceResources);
this.ready = true;
}
/** /**
* @internal * @internal
*/ */
@action @action
async refreshConnectionStatus() { async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus(); const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline; this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted; this.accessible = connectionStatus == ClusterStatus.AccessGranted;
} }
async getKubeconfig(): Promise<KubeConfig> { async getKubeconfig(): Promise<KubeConfig> {
const { config } = await loadConfigFromFile(this.kubeConfigPath); const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath);
return config; return config;
} }
@ -497,7 +517,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/ */
async getProxyKubeconfig(): Promise<KubeConfig> { async getProxyKubeconfig(): Promise<KubeConfig> {
const proxyKCPath = await this.getProxyKubeconfigPath(); const proxyKCPath = await this.getProxyKubeconfigPath();
const { config } = await loadConfigFromFile(proxyKCPath); const { config } = await this.dependencies.loadConfigfromFile(proxyKCPath);
return config; return config;
} }
@ -653,32 +673,48 @@ export class Cluster implements ClusterModel, ClusterState {
} }
} }
protected async getAllowedResources(canI: CanI) { protected async getAllowedResources(listApiResources:RequestListApiResources, requestNamespaceResources: RequestNamespaceResources) {
try { try {
if (!this.allowedNamespaces.length) { if (!this.allowedNamespaces.length) {
return []; return [];
} }
const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined);
const apiLimit = plimit(5); // 5 concurrent api requests
const requests = [];
for (const apiResource of resources) { const unknownResources = new Map<string, KubeApiResource>(apiResources.map(resource => ([resource.apiName, resource])));
requests.push(apiLimit(async () => {
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
if (!this.resourceAccessStatuses.get(apiResource)) {
const result = await canI({
resource: apiResource.apiName,
group: apiResource.group,
verb: "list",
namespace,
});
this.resourceAccessStatuses.set(apiResource, result); const availableResources = await listApiResources();
const availableResourcesNames = new Set(availableResources.map(apiResource => apiResource.apiName));
[...unknownResources.values()].map(unknownResource => {
if (!availableResourcesNames.has(unknownResource.apiName)) {
this.resourceAccessStatuses.set(unknownResource, false);
unknownResources.delete(unknownResource.apiName);
}
});
if (unknownResources.size > 0) {
const apiLimit = plimit(5); // 5 concurrent api requests
await Promise.all(this.allowedNamespaces.map(namespace => apiLimit(async () => {
if (unknownResources.size === 0) {
return;
}
const namespaceResources = await requestNamespaceResources(namespace, availableResources);
for (const resourceName of namespaceResources) {
const unknownResource = unknownResources.get(resourceName);
if (unknownResource) {
this.resourceAccessStatuses.set(unknownResource, true);
unknownResources.delete(resourceName);
} }
} }
})); })));
for (const forbiddenResource of unknownResources.values()) {
this.resourceAccessStatuses.set(forbiddenResource, false);
}
} }
await Promise.all(requests);
return apiResources return apiResources
.filter((resource) => this.resourceAccessStatuses.get(resource)) .filter((resource) => this.resourceAccessStatuses.get(resource))

View File

@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type {
V1APIGroupList,
V1APIResourceList,
V1APIVersions,
} from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { K8sRequest } from "../../main/k8s-request.injectable";
import k8SRequestInjectable from "../../main/k8s-request.injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource, KubeResource } from "../rbac";
import type { Cluster } from "./cluster";
import plimit from "p-limit";
export type RequestListApiResources = () => Promise<KubeApiResource[]>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type ListApiResources = (cluster: Cluster) => RequestListApiResources;
interface Dependencies {
logger: Logger;
k8sRequest: K8sRequest;
}
const listApiResources = ({ k8sRequest, logger }: Dependencies): ListApiResources => {
return (cluster) => {
const clusterRequest = (path: string) => k8sRequest(cluster, path);
const apiLimit = plimit(5);
return async () => {
const resources: KubeApiResource[] = [];
try {
const resourceListGroups:{ group:string;path:string }[] = [];
await Promise.all(
[
clusterRequest("/api").then((response:V1APIVersions)=>response.versions.forEach(version => resourceListGroups.push({ group:version, path:`/api/${version}` }))),
clusterRequest("/apis").then((response:V1APIGroupList) => response.groups.forEach(group => {
const preferredVersion = group.preferredVersion?.groupVersion;
if (preferredVersion) {
resourceListGroups.push({ group:group.name, path:`/apis/${preferredVersion}` });
}
})),
],
);
await Promise.all(
resourceListGroups.map(({ group, path }) => apiLimit(async () => {
const apiResources:V1APIResourceList = await clusterRequest(path);
if (apiResources.resources) {
resources.push(
...apiResources.resources.filter(resource => resource.verbs.includes("list")).map((resource) => ({
apiName: resource.name as KubeResource,
kind: resource.kind,
group,
})),
);
}
}),
),
);
} catch (error) {
logger.error(`[LIST-API-RESOURCES]: failed to list api resources: ${error}`);
}
return resources;
};
};
};
const listApiResourcesInjectable = getInjectable({
id: "list-api-resources",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
const logger = di.inject(loggerInjectable);
return listApiResources({ k8sRequest, logger });
},
});
export default listApiResourcesInjectable;

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../cluster-types";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export const clusterVisibilityChannel: MessageChannel<ClusterId | null> = {
id: "cluster-visibility",
};

View File

@ -4,16 +4,16 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getAbsolutePathInjectable from "../path/get-absolute-path.injectable"; import joinPathsInjectable from "../path/join-paths.injectable";
const directoryForLensLocalStorageInjectable = getInjectable({ const directoryForLensLocalStorageInjectable = getInjectable({
id: "directory-for-lens-local-storage", id: "directory-for-lens-local-storage",
instantiate: (di) => { instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable); const joinPaths = di.inject(joinPathsInjectable);
const directoryForUserData = di.inject(directoryForUserDataInjectable); const directoryForUserData = di.inject(directoryForUserDataInjectable);
return getAbsolutePath( return joinPaths(
directoryForUserData, directoryForUserData,
"lens-local-storage", "lens-local-storage",
); );

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import initializeSentryReportingWithInjectable from "./initialize-sentry-reporting.injectable";
export default getGlobalOverride(initializeSentryReportingWithInjectable, () => () => {});

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ElectronMainOptions } from "@sentry/electron/main";
import type { BrowserOptions } from "@sentry/electron/renderer";
import isProductionInjectable from "../vars/is-production.injectable";
import sentryDataSourceNameInjectable from "../vars/sentry-dsn-url.injectable";
import { Dedupe, Offline } from "@sentry/integrations";
import { inspect } from "util";
import userStoreInjectable from "../user-store/user-store.injectable";
export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void;
const mapProcessName = (type: "browser" | "renderer" | "worker") => type === "browser" ? "main" : type;
const initializeSentryReportingWithInjectable = getInjectable({
id: "initialize-sentry-reporting-with",
instantiate: (di): InitializeSentryReportingWith => {
const sentryDataSourceName = di.inject(sentryDataSourceNameInjectable);
const isProduction = di.inject(isProductionInjectable);
const userStore = di.inject(userStoreInjectable);
if (!sentryDataSourceName) {
return () => {};
}
return (initSentry) => initSentry({
beforeSend: (event) => {
if (userStore.allowErrorReporting) {
return event;
}
/**
* Directly write to stdout so that no other integrations capture this and create an infinite loop
*/
process.stdout.write(`🔒 [SENTRY-BEFORE-SEND-HOOK]: Sentry event is caught but not sent to server.`);
process.stdout.write("🔒 [SENTRY-BEFORE-SEND-HOOK]: === START OF SENTRY EVENT ===");
process.stdout.write(inspect(event, false, null, true));
process.stdout.write("🔒 [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: sentryDataSourceName,
integrations: [
new Dedupe(),
new Offline(),
],
initialScope: {
tags: {
"process": mapProcessName(process.type),
},
},
environment: isProduction ? "production" : "development",
});
},
causesSideEffects: true,
});
export default initializeSentryReportingWithInjectable;

View File

@ -29,7 +29,7 @@ export class EventEmitter<D extends [...any[]]> {
this.listeners.length = 0; this.listeners.length = 0;
} }
emit = (...data: D) => { emit(...data: D) {
for (const [callback, { once }] of this.listeners) { for (const [callback, { once }] of this.listeners) {
if (once) { if (once) {
this.removeListener(callback); this.removeListener(callback);
@ -39,5 +39,5 @@ export class EventEmitter<D extends [...any[]]> {
break; break;
} }
} }
}; }
} }

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { RequestInit, Response } from "node-fetch";
import type { AsyncResult } from "../utils/async-result";
import fetchInjectable from "./fetch.injectable";
export interface DownloadBinaryOptions {
signal?: AbortSignal | null | undefined;
}
export type DownloadBinary = (url: string, opts?: DownloadBinaryOptions) => Promise<AsyncResult<Buffer, string>>;
const downloadBinaryInjectable = getInjectable({
id: "download-binary",
instantiate: (di): DownloadBinary => {
const fetch = di.inject(fetchInjectable);
return async (url, opts) => {
let result: Response;
try {
// TODO: upgrade node-fetch once we switch to ESM
result = await fetch(url, opts as RequestInit);
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
if (result.status < 200 || 300 <= result.status) {
return {
callWasSuccessful: false,
error: result.statusText,
};
}
try {
return {
callWasSuccessful: true,
response: await result.buffer(),
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
},
});
export default downloadBinaryInjectable;

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { RequestInit, Response } from "node-fetch";
import type { AsyncResult } from "../utils/async-result";
import fetchInjectable from "./fetch.injectable";
export interface DownloadJsonOptions {
signal?: AbortSignal | null | undefined;
}
export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise<AsyncResult<unknown, string>>;
const downloadJsonInjectable = getInjectable({
id: "download-json",
instantiate: (di): DownloadJson => {
const fetch = di.inject(fetchInjectable);
return async (url, opts) => {
let result: Response;
try {
result = await fetch(url, opts as RequestInit);
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
if (result.status < 200 || 300 <= result.status) {
return {
callWasSuccessful: false,
error: result.statusText,
};
}
try {
return {
callWasSuccessful: true,
response: await result.json(),
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
},
});
export default downloadJsonInjectable;

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverrideForFunction } from "../test-utils/get-global-override-for-function";
import fetchInjectable from "./fetch.injectable";
export default getGlobalOverrideForFunction(fetchInjectable);

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type * as FetchModule from "node-fetch";
const { NodeFetch: { default: fetch }} = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule };
type Response = FetchModule.Response;
type RequestInit = FetchModule.RequestInit;
export type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
const fetchInjectable = getInjectable({
id: "fetch",
instantiate: (): Fetch => fetch,
causesSideEffects: true,
});
export default fetchInjectable;

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Creates an AbortController with an associated timeout
* @param timeout The number of milliseconds before this controller will auto abort
*/
export function withTimeout(timeout: number): AbortController {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
controller.signal.addEventListener("abort", () => clearTimeout(id));
return controller;
}

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type AppNavigationChannel = MessageChannel<string>;
const appNavigationChannelInjectable = getInjectable({
id: "app-navigation-channel",
instantiate: (): AppNavigationChannel => ({
id: IpcRendererNavigationEvents.NAVIGATE_IN_APP,
}),
injectionToken: messageChannelInjectionToken,
});
export default appNavigationChannelInjectable;

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export type AppNavigationChannel = MessageChannel<string>;
export const appNavigationChannel: AppNavigationChannel = {
id: IpcRendererNavigationEvents.NAVIGATE_IN_APP,
};

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type ClusterFrameNavigationChannel = MessageChannel<string>;
const clusterFrameNavigationChannelInjectable = getInjectable({
id: "cluster-frame-navigation-channel",
instantiate: (): ClusterFrameNavigationChannel => ({
id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
}),
injectionToken: messageChannelInjectionToken,
});
export default clusterFrameNavigationChannelInjectable;

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export type ClusterFrameNavigationChannel = MessageChannel<string>;
export const clusterFrameNavigationChannel: ClusterFrameNavigationChannel = {
id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
};

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const leasesRouteInjectable = getInjectable({
id: "leases",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "leases");
return {
path: "/leases",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
injectionToken: frontEndRouteInjectionToken,
});
export default leasesRouteInjectable;

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import leasesRouteInjectable from "./leases-route.injectable";
import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token";
const navigateToLeasesInjectable = getInjectable({
id: "navigate-to-leases",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(leasesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToLeasesInjectable;

View File

@ -3,18 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import appPreferencesRouteInjectable from "./app-preferences-route.injectable"; import runtimeClassesRouteInjectable from "./runtime-classes-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token"; import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token";
const navigateToAppPreferencesInjectable = getInjectable({ const navigateToRuntimeClassesInjectable = getInjectable({
id: "navigate-to-app-preferences", id: "navigate-to-runtime-classes",
instantiate: (di) => { instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken); const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(appPreferencesRouteInjectable); const route = di.inject(runtimeClassesRouteInjectable);
return () => navigateToRoute(route); return () => navigateToRoute(route);
}, },
}); });
export default navigateToAppPreferencesInjectable; export default navigateToRuntimeClassesInjectable;

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const runtimeClassesRouteInjectable = getInjectable({
id: "runtime-classes-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "runtimeclasses");
return {
path: "/runtimeclasses",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
injectionToken: frontEndRouteInjectionToken,
});
export default runtimeClassesRouteInjectable;

View File

@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const editorPreferencesRouteInjectable = getInjectable({
id: "editor-preferences-route",
instantiate: () => ({
path: "/preferences/editor",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default editorPreferencesRouteInjectable;

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