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

Merge branch 'master' into refactor-download-logs-tests

This commit is contained in:
Alex Andreev 2022-10-03 15:56:53 +04:00
commit d38d8deaf9
308 changed files with 12641 additions and 10817 deletions

View File

@ -28,3 +28,7 @@ See [Development](https://docs.k8slens.dev/latest/contributing/development/) pag
## Contributing
See [Contributing](https://docs.k8slens.dev/latest/contributing/) page.
## License
See [License](LICENSE).

View File

@ -194,7 +194,7 @@ async function main() {
}, multiBar),
];
if (normalizedPlatform === "darwin") {
if (normalizedPlatform !== "windows") {
downloaders.push(
new LensK8sProxyDownloader({
version: packageInfo.config.k8sProxyVersion,

View File

@ -16,7 +16,6 @@
"dist/**/*"
],
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"npm": "^8.5.3"
"@k8slens/extensions": "file:../../src/extensions/npm/extensions"
}
}

View File

@ -19,7 +19,6 @@
],
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"npm": "^8.5.3",
"semver": "^7.3.2"
}
}

View File

@ -17,7 +17,6 @@
],
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"npm": "^8.5.3"
"@k8slens/extensions": "file:../../src/extensions/npm/extensions"
}
}

View File

@ -17,7 +17,6 @@
],
"dependencies": {},
"devDependencies": {
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
"npm": "^8.5.3"
"@k8slens/extensions": "file:../../src/extensions/npm/extensions"
}
}

View File

@ -11,7 +11,6 @@
*/
import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils";
import { isWindows } from "../../src/common/vars";
describe("preferences page tests", () => {
let window: Page, cleanup: () => Promise<void>;
@ -34,8 +33,7 @@ describe("preferences page tests", () => {
await cleanup();
}, 10*60*1000);
// skip on windows due to suspected playwright issue with Electron 14
utils.itIf(!isWindows)('shows "preferences" and can navigate through the tabs', async () => {
it('shows "preferences" and can navigate through the tabs', async () => {
const pages = [
{
id: "application",

View File

@ -5,7 +5,6 @@
import type { ElectronApplication, Page } from "playwright";
import * as utils from "../helpers/utils";
import { isWindows } from "../../src/common/vars";
describe("Lens command palette", () => {
let window: Page, cleanup: () => Promise<void>, app: ElectronApplication;
@ -20,8 +19,7 @@ describe("Lens command palette", () => {
}, 10*60*1000);
describe("menu", () => {
// skip on windows due to suspected playwright issue with Electron 14
utils.itIf(!isWindows)("opens command dialog from menu", async () => {
it("opens command dialog from menu", async () => {
await app.evaluate(async ({ app }) => {
await app.applicationMenu
?.getMenuItemById("view")

View File

@ -3,7 +3,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "6.0.0",
"version": "6.1.0",
"main": "static/build/main.js",
"copyright": "© 2022 OpenLens Authors",
"license": "MIT",
@ -56,7 +56,8 @@
"bundledKubectlVersion": "1.23.3",
"bundledHelmVersion": "3.7.2",
"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": {
"node": ">=16 <17"
@ -216,14 +217,14 @@
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.0",
"@hapi/subtext": "^7.0.4",
"@kubernetes/client-node": "^0.17.0",
"@kubernetes/client-node": "^0.17.1",
"@material-ui/styles": "^4.11.5",
"@ogre-tools/fp": "9.0.3",
"@ogre-tools/injectable": "9.0.3",
"@ogre-tools/injectable-extension-for-auto-registration": "9.0.3",
"@ogre-tools/injectable-extension-for-mobx": "9.0.3",
"@ogre-tools/injectable-react": "9.0.3",
"@sentry/electron": "^3.0.7",
"@ogre-tools/fp": "10.1.0",
"@ogre-tools/injectable": "10.1.0",
"@ogre-tools/injectable-extension-for-auto-registration": "10.1.0",
"@ogre-tools/injectable-extension-for-mobx": "10.1.0",
"@ogre-tools/injectable-react": "10.1.0",
"@sentry/electron": "^3.0.8",
"@sentry/integrations": "^6.19.3",
"@side/jest-runtime": "^1.0.1",
"@types/circular-dependency-plugin": "5.0.5",
@ -251,11 +252,11 @@
"jsdom": "^16.7.0",
"lodash": "^4.17.15",
"mac-ca": "^1.0.6",
"marked": "^4.0.19",
"marked": "^4.1.0",
"md5-file": "^5.0.0",
"mobx": "^6.6.1",
"mobx": "^6.6.2",
"mobx-observable-history": "^2.0.3",
"mobx-react": "^7.5.2",
"mobx-react": "^7.5.3",
"mobx-utils": "^6.0.4",
"mock-fs": "^5.1.4",
"moment": "^2.29.4",
@ -264,7 +265,7 @@
"monaco-editor-webpack-plugin": "^5.0.0",
"node-fetch": "^2.6.7",
"node-pty": "0.10.1",
"npm": "^6.14.17",
"npm": "^8.19.2",
"p-limit": "^3.1.0",
"path-to-regexp": "^6.2.0",
"proper-lockfile": "^4.1.2",
@ -272,12 +273,12 @@
"react-dom": "^17.0.2",
"react-material-ui-carousel": "^2.3.11",
"react-router": "^5.2.0",
"react-virtualized-auto-sizer": "^1.0.6",
"react-virtualized-auto-sizer": "^1.0.7",
"readable-stream": "^3.6.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.9",
"rfc6902": "^4.0.2",
"selfsigned": "^2.0.1",
"selfsigned": "^2.1.1",
"semver": "^7.3.7",
"shell-env": "^3.0.1",
"spdy": "^4.0.2",
@ -288,7 +289,7 @@
"url-parse": "^1.5.10",
"uuid": "^8.3.2",
"win-ca": "^3.5.0",
"winston": "^3.8.1",
"winston": "^3.8.2",
"winston-console-format": "^1.0.8",
"winston-transport-browserconsole": "^1.0.5",
"ws": "^8.8.1",
@ -302,7 +303,7 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@sentry/types": "^6.19.7",
"@swc/cli": "^0.1.57",
"@swc/core": "^1.2.242",
"@swc/core": "^1.3.1",
"@swc/jest": "^0.2.22",
"@testing-library/dom": "^7.31.2",
"@testing-library/jest-dom": "^5.16.5",
@ -327,12 +328,12 @@
"@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5",
"@types/jsdom": "^16.2.14",
"@types/lodash": "^4.14.184",
"@types/marked": "^4.0.6",
"@types/lodash": "^4.14.185",
"@types/marked": "^4.0.7",
"@types/md5-file": "^4.0.2",
"@types/mini-css-extract-plugin": "^2.4.0",
"@types/mock-fs": "^4.13.1",
"@types/node": "^16.11.55",
"@types/node": "^16.11.59",
"@types/node-fetch": "^2.6.2",
"@types/npm": "^2.0.32",
"@types/proper-lockfile": "^4.1.2",
@ -362,28 +363,28 @@
"@types/webpack-dev-server": "^4.7.2",
"@types/webpack-env": "^1.18.0",
"@types/webpack-node-externals": "^2.5.3",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.35.1",
"adr": "^1.4.1",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"adr": "^1.4.2",
"ansi_up": "^5.1.0",
"chart.js": "^2.9.4",
"circular-dependency-plugin": "^5.2.2",
"cli-progress": "^3.11.2",
"color": "^3.2.1",
"command-line-args": "^5.2.1",
"concurrently": "^7.3.0",
"concurrently": "^7.4.0",
"css-loader": "^6.7.1",
"deepdash": "^5.3.9",
"dompurify": "^2.4.0",
"electron": "^19.0.13",
"electron": "^19.0.17",
"electron-builder": "^23.3.3",
"electron-notarize": "^0.3.0",
"esbuild": "^0.15.6",
"esbuild-loader": "^2.19.0",
"eslint": "^8.23.0",
"esbuild": "^0.15.7",
"esbuild-loader": "^2.20.0",
"eslint": "^8.23.1",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "7.30.1",
"eslint-plugin-react": "7.31.8",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "^2.0.0",
"flex.box": "^3.4.4",
@ -397,18 +398,19 @@
"jest-canvas-mock": "^2.3.1",
"jest-environment-jsdom": "^28.1.3",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^2.0.7",
"jest-mock-extended": "^2.0.9",
"make-plural": "^6.2.2",
"mini-css-extract-plugin": "^2.6.1",
"mock-http": "^1.1.0",
"node-gyp": "^8.3.0",
"node-loader": "^2.0.0",
"nodemon": "^2.0.19",
"playwright": "^1.25.1",
"playwright": "^1.25.2",
"postcss": "^8.4.16",
"postcss-loader": "^6.2.1",
"query-string": "^7.1.1",
"randomcolor": "^0.6.2",
"react-beautiful-dnd": "^13.1.0",
"react-beautiful-dnd": "^13.1.1",
"react-refresh": "^0.14.0",
"react-refresh-typescript": "^2.0.7",
"react-router-dom": "^5.3.3",
@ -416,9 +418,9 @@
"react-select-event": "^5.5.1",
"react-table": "^7.8.0",
"react-window": "^1.8.7",
"sass": "^1.54.6",
"sass": "^1.54.9",
"sass-loader": "^12.6.0",
"sharp": "^0.30.7",
"sharp": "^0.31.0",
"style-loader": "^3.3.1",
"tailwindcss": "^3.1.8",
"tar-stream": "^2.2.0",
@ -426,13 +428,13 @@
"ts-node": "^10.9.1",
"type-fest": "^2.14.0",
"typed-emitter": "^1.4.0",
"typedoc": "0.23.11",
"typedoc": "0.23.14",
"typedoc-plugin-markdown": "^3.13.1",
"typescript": "^4.7.4",
"typescript": "^4.8.3",
"typescript-plugin-css-modules": "^3.4.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.10.1",
"webpack-dev-server": "^4.11.0",
"webpack-node-externals": "^3.0.0",
"xterm": "^4.19.0",
"xterm-addon-fit": "^0.5.0"

View File

@ -8,7 +8,7 @@ import fse from "fs-extra";
import { basename } from "path";
import { createInterface } from "readline";
import semver from "semver";
import { inspect, promisify } from "util";
import { promisify } from "util";
const {
SemVer,
@ -27,6 +27,10 @@ const options = commandLineArgs([
{
name: "preid",
},
{
name: "check-commits",
type: Boolean,
},
]);
const validReleaseValues = [
@ -79,10 +83,22 @@ if (basename(process.cwd()) === "scripts") {
console.error(errorMessages.wrongCwd);
}
const currentVersion = new SemVer((await fse.readJson("./package.json")).version);
const packageJson = await fse.readJson("./package.json");
const currentVersion = new SemVer(packageJson.version);
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...");
await exec("git fetch --tags --force");
@ -93,25 +109,6 @@ const [previousReleasedVersion] = actualTags
.sort((l, r) => semverRcompare(l, r))
.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 = [
"gh",
"pr",
@ -146,6 +143,10 @@ interface GithubPrData {
title: string;
}
interface ExtendedGithubPrData extends Omit<GithubPrData, "mergedAt"> {
mergedAt: Date;
}
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 milestoneRelevantPrs = mergedPrs.filter(pr => pr.milestone?.title === newVersionMilestone);
@ -159,7 +160,7 @@ const relaventPrs = relaventPrsQuery
.filter(query => query.stdout)
.map(query => query.pr)
.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) => {
const leftAge = left.mergedAt.valueOf();
const rightAge = right.mergedAt.valueOf();
@ -175,75 +176,55 @@ const relaventPrs = relaventPrsQuery
return -1;
});
console.log(inspect(relaventPrs, false, null, true));
const enhancementPrLabelName = "enhancement";
const bugfixPrLabelName = "bug";
const enhancementPrs = relaventPrs.filter(pr => pr.labels.some(label => label.name === enhancementPrLabelName));
const bugfixPrs = relaventPrs.filter(pr => pr.labels.some(label => label.name === bugfixPrLabelName));
const maintenencePrs = relaventPrs.filter(pr => pr.labels.every(label => label.name !== bugfixPrLabelName && label.name !== enhancementPrLabelName));
const isEnhancementPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === enhancementPrLabelName);
const isBugfixPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === bugfixPrLabelName);
console.log("Found:");
console.log(`${enhancementPrs.length} enhancement PRs`);
console.log(`${bugfixPrs.length} bug fix PRs`);
console.log(`${maintenencePrs.length} maintenence PRs`);
const prLines = {
enhancement: [] as string[],
bugfix: [] as string[],
maintenence: [] as string[],
};
const prBodyLines = [
`## Changes since ${previousReleasedVersion}`,
"",
];
function getPrEntry(pr) {
function getPrEntry(pr: ExtendedGithubPrData) {
return `- ${pr.title} (**[#${pr.number}](https://github.com/lensapp/lens/pull/${pr.number})**) https://github.com/${pr.author.login}`;
}
if (enhancementPrs.length > 0) {
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 rl = createInterface(process.stdin);
const prBase = newVersion.patch === 0
? "master"
: `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") {
console.log("Cherry-picking commits to current branch");
rl.once("line", (answer) => {
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 {
const promise = exec(`git cherry-pick ${pr.mergeCommit.oid}`);
@ -255,11 +236,71 @@ if (prBase !== "master") {
await promise;
} catch {
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",
"--body-file", "-",
];
await exec(`git push --set-upstream origin ${prBranch}`);
const createPrProcess = execFile("gh", createPrArgs);
createPrProcess.child.stdout?.pipe(process.stdout);

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

View File

@ -8,7 +8,6 @@ import mockFs from "mock-fs";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
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 hotbarStoreInjectable from "../hotbars/store.injectable";
import type { HotbarStore } from "../hotbars/store";
@ -19,6 +18,7 @@ import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-
import loggerInjectable from "../logger.injectable";
import type { Logger } from "../logger";
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 {
return {
@ -348,7 +348,7 @@ describe("HotbarStore", () => {
mockFs(configurationToBeMigrated);
di.override(appVersionInjectable, () => "5.0.0-beta.10");
di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");
hotbarStore = di.inject(hotbarStoreInjectable);

View File

@ -23,8 +23,6 @@ jest.mock("electron", () => ({
import type { UserStore } from "../user-store";
import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
import { stdout, stderr } from "process";
import userStoreInjectable from "../user-store/user-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
@ -34,7 +32,9 @@ import { defaultThemeId } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
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 "../application-update/selected-update-channel/default-update-channel.injectable";
console = new Console(stdout, stderr);
@ -42,7 +42,7 @@ describe("user store tests", () => {
let userStore: UserStore;
let di: DiContainer;
beforeEach(() => {
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
@ -52,6 +52,12 @@ describe("user store tests", () => {
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(userStoreInjectable);
di.override(releaseChannelInjectable, () => ({
get: () => "latest" as const,
init: async () => {},
}));
await di.inject(defaultUpdateChannelInjectable).init();
di.unoverride(userStoreInjectable);
});
@ -64,6 +70,7 @@ describe("user store tests", () => {
mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }});
userStore = di.inject(userStoreInjectable);
userStore.load();
});
it("allows setting and retrieving lastSeenAppVersion", () => {
@ -86,13 +93,6 @@ describe("user store tests", () => {
userStore.resetTheme();
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", () => {
@ -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.load();
});
it("sets last seen app version to 0.0.0", () => {

View File

@ -0,0 +1,8 @@
/**
* 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 emitEventInjectable from "./emit-event.injectable";
export default getGlobalOverride(emitEventInjectable, () => () => {});

View File

@ -9,6 +9,7 @@ const appEventBusInjectable = getInjectable({
id: "app-event-bus",
instantiate: () => appEventBus,
causesSideEffects: true,
decorable: false,
});
export default appEventBusInjectable;

View File

@ -8,6 +8,7 @@ import appEventBusInjectable from "./app-event-bus.injectable";
const emitEventInjectable = getInjectable({
id: "emit-event",
instantiate: (di) => di.inject(appEventBusInjectable).emit,
decorable: false,
});
export default emitEventInjectable;

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 type { PathName } from "./app-path-names";
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 type { ApplicationBuilder } 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;
},
);
mainDi.override(appNameInjectable, () => "some-app-name");
});
});
@ -88,7 +85,7 @@ describe("app-paths", () => {
recent: "some-recent",
temp: "some-temp",
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",
temp: "some-temp",
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({
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({
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

@ -2,24 +2,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 appSemanticVersionInjectable from "../../vars/app-semantic-version.injectable";
import type { UpdateChannelId } from "../update-channels";
import { createInitializableState } from "../../initializable-state/create";
import releaseChannelInjectable from "../../vars/release-channel.injectable";
import { updateChannels } from "../update-channels";
const defaultUpdateChannelInjectable = getInjectable({
const defaultUpdateChannelInjectable = createInitializableState({
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;
},
init: (di) => updateChannels[di.inject(releaseChannelInjectable).get()],
});
export default defaultUpdateChannelInjectable;

View File

@ -5,13 +5,13 @@
import { getInjectable } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import { action, computed, observable } from "mobx";
import type { UpdateChannel, UpdateChannelId } from "../update-channels";
import type { UpdateChannel, ReleaseChannel } from "../update-channels";
import { updateChannels } from "../update-channels";
import defaultUpdateChannelInjectable from "./default-update-channel.injectable";
export interface SelectedUpdateChannel {
value: IComputedValue<UpdateChannel>;
setValue: (channelId?: UpdateChannelId) => void;
setValue: (channelId?: ReleaseChannel) => void;
}
const selectedUpdateChannelInjectable = getInjectable({
@ -19,16 +19,16 @@ const selectedUpdateChannelInjectable = getInjectable({
instantiate: (di): SelectedUpdateChannel => {
const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable);
const state = observable.box(defaultUpdateChannel);
const state = observable.box<UpdateChannel>();
return {
value: computed(() => state.get()),
value: computed(() => state.get() ?? defaultUpdateChannel.get()),
setValue: action((channelId) => {
const targetUpdateChannel =
channelId && updateChannels[channelId]
? updateChannels[channelId]
: defaultUpdateChannel;
: defaultUpdateChannel.get();
state.set(targetUpdateChannel);
}),

View File

@ -4,7 +4,7 @@
*/
export type UpdateChannelId = "alpha" | "beta" | "latest";
export type ReleaseChannel = "alpha" | "beta" | "latest";
const latestChannel: UpdateChannel = {
id: "latest",
@ -24,14 +24,14 @@ const alphaChannel: UpdateChannel = {
moreStableUpdateChannel: betaChannel,
};
export const updateChannels: Record<UpdateChannelId, UpdateChannel> = {
export const updateChannels = {
latest: latestChannel,
beta: betaChannel,
alpha: alphaChannel,
};
export interface UpdateChannel {
readonly id: UpdateChannelId;
readonly id: ReleaseChannel;
readonly label: string;
readonly moreStableUpdateChannel: UpdateChannel | null;
}

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 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 appVersionInjectable from "./vars/app-version.injectable";
import storeMigrationVersionInjectable from "./vars/store-migration-version.injectable";
export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: {
@ -31,7 +31,7 @@ export interface BaseStoreParams<T> extends ConfOptions<T> {
/**
* Note: T should only contain base JSON serializable types.
*/
export abstract class BaseStore<T> extends Singleton {
export abstract class BaseStore<T extends object> extends Singleton {
protected storeConfig?: Config<T>;
protected syncDisposers: Disposer[] = [];
@ -59,10 +59,10 @@ export abstract class BaseStore<T> extends Singleton {
const getConfigurationFileModel = di.inject(getConfigurationFileModelInjectable);
this.storeConfig = getConfigurationFileModel({
...this.params,
projectName: "lens",
projectVersion: di.inject(appVersionInjectable),
projectVersion: di.inject(storeMigrationVersionInjectable),
cwd: this.cwd(),
...this.params,
});
const res: any = this.fromStore(this.storeConfig.store);

View File

@ -12,7 +12,7 @@ describe("kubernetesClusterCategory", () => {
let kubernetesClusterCategory: KubernetesClusterCategory;
beforeEach(() => {
const di = getDiForUnitTesting();
const di = getDiForUnitTesting({ doGeneralOverrides: true });
kubernetesClusterCategory = di.inject(kubernetesClusterCategoryInjectable);
});

View File

@ -3,9 +3,10 @@
* 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 { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import { productName } from "../vars";
import productNameInjectable from "../vars/product-name.injectable";
import { WeblinkStore } from "../weblink-store";
export type WebLinkStatusPhase = "available" | "unavailable";
@ -30,6 +31,9 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
}
onContextMenuOpen(context: CatalogEntityContextMenuContext) {
const di = getLegacyGlobalDiForExtensionApi();
const productName = di.inject(productNameInjectable);
if (this.metadata.source === "local") {
context.menuItems.push({
title: "Delete",

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

@ -16,8 +16,7 @@ const navigateToHelmReleasesInjectable = getInjectable({
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(helmReleasesRouteInjectable);
return (parameters) =>
navigateToRoute(route, { parameters });
return (parameters) => navigateToRoute(route, { parameters });
},
});

View File

@ -0,0 +1,26 @@
/**
* 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 welcomeRouteConfigInjectable from "./welcome-route-config.injectable";
import { frontEndRouteInjectionToken } from "../../front-end-route-injection-token";
const defaultWelcomeRouteInjectable = getInjectable({
id: "default-welcome-route",
instantiate: (di) => {
const welcomeRoute = di.inject(welcomeRouteConfigInjectable);
return {
path: "/welcome",
clusterFrame: false,
isEnabled: computed(() => welcomeRoute === "/welcome"),
};
},
injectionToken: frontEndRouteInjectionToken,
});
export default defaultWelcomeRouteInjectable;

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 applicationInformationInjectable from "../../../vars/application-information.injectable";
const welcomeRouteConfigInjectable = getInjectable({
id: "welcome-route-config",
instantiate: (di) => di.inject(applicationInformationInjectable).config.welcomeRoute,
});
export default welcomeRouteConfigInjectable;

View File

@ -4,16 +4,21 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import welcomeRouteConfigInjectable from "./welcome-route-config.injectable";
import { frontEndRouteInjectionToken } from "../../front-end-route-injection-token";
const welcomeRouteInjectable = getInjectable({
id: "welcome-route",
instantiate: () => ({
path: "/welcome",
clusterFrame: false,
isEnabled: computed(() => true),
}),
instantiate: (di) => {
const welcomeRoute = di.inject(welcomeRouteConfigInjectable);
return {
path: welcomeRoute,
clusterFrame: false,
isEnabled: computed(() => true),
};
},
injectionToken: frontEndRouteInjectionToken,
});

View File

@ -5,7 +5,7 @@
import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
import { frontEndRouteInjectionToken } from "./front-end-route-injection-token";
import { filter, map, matches } from "lodash/fp";
import { filter, map } from "lodash/fp";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../cluster-store/cluster-store";
import { pipeline } from "@ogre-tools/fp";
@ -27,9 +27,11 @@ describe("verify-that-all-routes-have-component", () => {
routes,
map(
(route) => ({
path: route.path,
routeComponent: routeComponents.find(matches({ route })),
(currentRoute) => ({
path: currentRoute.path,
routeComponent: routeComponents.find(({ route }) => (
route.path === currentRoute.path
&& route.clusterFrame === currentRoute.clusterFrame)),
}),
),

View File

@ -3,20 +3,23 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ExecFileOptions } from "child_process";
import { execFile } from "child_process";
import { promisify } from "util";
export type ExecFile = (filePath: string, args: string[]) => Promise<string>;
export type ExecFile = (filePath: string, args: string[], options: ExecFileOptions) => Promise<string>;
const execFileInjectable = getInjectable({
id: "exec-file",
instantiate: (): ExecFile => async (filePath, args) => {
instantiate: (): ExecFile => {
const asyncExecFile = promisify(execFile);
const result = await asyncExecFile(filePath, args);
return async (filePath, args, options) => {
const result = await asyncExecFile(filePath, args, options);
return result.stdout;
return result.stdout;
};
},
causesSideEffects: true,

View File

@ -8,7 +8,7 @@ import type { BaseStoreParams } from "../base-store";
const getConfigurationFileModelInjectable = getInjectable({
id: "get-configuration-file-model",
instantiate: () => <ConfigurationContent>(content: BaseStoreParams<ConfigurationContent>) => new Config(content),
instantiate: () => <T extends object>(content: BaseStoreParams<T>) => new Config(content),
causesSideEffects: true,
});

View File

@ -0,0 +1,77 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { DiContainer, Injectable } from "@ogre-tools/injectable";
import { runInAction } from "mobx";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { InitializableState } from "./create";
import { createInitializableState } from "./create";
describe("InitializableState tests", () => {
let di: DiContainer;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
});
describe("when created", () => {
let stateInjectable: Injectable<InitializableState<number>, unknown, void>;
let initMock: AsyncFnMock<() => number>;
beforeEach(() => {
initMock = asyncFn();
stateInjectable = createInitializableState({
id: "my-state",
init: initMock,
});
runInAction(() => {
di.register(stateInjectable);
});
});
describe("when injected", () => {
let state: InitializableState<number>;
beforeEach(() => {
state = di.inject(stateInjectable);
});
it("when get is called, throw", () => {
expect(() => state.get()).toThrowError("InitializableState(my-state) has not been initialized yet");
});
describe("when init is called", () => {
beforeEach(() => {
state.init();
});
it("should call provided initialization function", () => {
expect(initMock).toBeCalled();
});
it("when get is called, throw", () => {
expect(() => state.get()).toThrowError("InitializableState(my-state) has not finished initializing");
});
describe("when initialization resolves", () => {
beforeEach(async () => {
await initMock.resolve(42);
});
it("when get is called, returns value", () => {
expect(state.get()).toBe(42);
});
it("when init is called again, throws", async () => {
await expect(() => state.init()).rejects.toThrow("Cannot initialize InitializableState(my-state) more than once");
});
});
});
});
});
});

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainerForInjection, Injectable, InjectionToken } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
export interface CreateInitializableStateArgs<T> {
id: string;
init: (di: DiContainerForInjection) => Promise<T> | T;
injectionToken?: InjectionToken<InitializableState<T>, void>;
}
export interface InitializableState<T> {
get: () => T;
init: () => Promise<void>;
}
type InitializableStateValue<T> =
| { set: false }
| { set: true; value: T } ;
export function createInitializableState<T>(args: CreateInitializableStateArgs<T>): Injectable<InitializableState<T>, unknown, void> {
const { id, init, injectionToken } = args;
return getInjectable({
id,
instantiate: (di) => {
let box: InitializableStateValue<T> = {
set: false,
};
let initCalled = false;
return {
init: async () => {
if (initCalled) {
throw new Error(`Cannot initialize InitializableState(${id}) more than once`);
}
initCalled = true;
box = {
set: true,
value: await init(di),
};
},
get: () => {
if (!initCalled) {
throw new Error(`InitializableState(${id}) has not been initialized yet`);
}
if (box.set === false) {
throw new Error(`InitializableState(${id}) has not finished initializing`);
}
return box.value;
},
};
},
injectionToken,
});
}

View File

@ -4,7 +4,7 @@
*/
import assert from "assert";
import type { PodContainer, PodContainerStatus } from "../endpoints";
import type { Container, PodContainerStatus } from "../endpoints";
import { Pod } from "../endpoints";
interface GetDummyPodOptions {
@ -22,8 +22,8 @@ function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
initRunning = 0,
} = rawOpts;
const containers: PodContainer[] = [];
const initContainers: PodContainer[] = [];
const containers: Container[] = [];
const initContainers: Container[] = [];
const containerStatuses: PodContainerStatus[] = [];
const initContainerStatuses: PodContainerStatus[] = [];
const pod = new Pod({
@ -58,7 +58,7 @@ function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
containers.push({
image: "dummy",
imagePullPolicy: "dummy",
imagePullPolicy: "Always",
name,
});
containerStatuses.push({
@ -80,7 +80,7 @@ function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
containers.push({
image: "dummy",
imagePullPolicy: "dummy",
imagePullPolicy: "Always",
name,
});
containerStatuses.push({
@ -105,7 +105,7 @@ function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
initContainers.push({
image: "dummy",
imagePullPolicy: "dummy",
imagePullPolicy: "Always",
name,
});
initContainerStatuses.push({
@ -127,7 +127,7 @@ function getDummyPod(rawOpts: GetDummyPodOptions = {}): Pod {
initContainers.push({
image: "dummy",
imagePullPolicy: "dummy",
imagePullPolicy: "Always",
name,
});
initContainerStatuses.push({
@ -169,7 +169,7 @@ describe("Pods", () => {
function getNamedContainer(name: string) {
return {
image: "dummy",
imagePullPolicy: "dummy",
imagePullPolicy: "Always",
name,
};
}

View File

@ -41,3 +41,4 @@ export * from "./service-account.api";
export * from "./stateful-set.api";
export * from "./storage-class.api";
export * from "./legacy-globals";
export * from "./types";

View File

@ -6,7 +6,8 @@
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions } from "../kube-api";
import { KubeApi } from "../kube-api";
import { metricsApi } from "./metrics.api";
import type { PodContainer, PodMetricData, PodSpec } from "./pod.api";
import type { PodMetricData, PodSpec } from "./pod.api";
import type { Container } from "./types/container";
import type { KubeObjectStatus, LabelSelector, NamespaceScopedMetadata } from "../kube-object";
import { KubeObject } from "../kube-object";
@ -23,7 +24,7 @@ export interface JobSpec {
};
spec: PodSpec;
};
containers?: PodContainer[];
containers?: Container[];
restartPolicy?: string;
terminationGracePeriodSeconds?: number;
dnsPolicy?: string;

View File

@ -7,7 +7,6 @@
import moment from "moment";
import { apiBase } from "../index";
import type { IMetricsQuery } from "../../../main/routes/metrics/metrics-query";
export interface MetricData {
status: string;
@ -55,28 +54,33 @@ export interface IResourceMetrics<T extends MetricData> {
networkTransmit: T;
}
async function getMetrics(query: string, reqParams?: IMetricsReqParams): Promise<MetricData>;
async function getMetrics(query: string[], reqParams?: IMetricsReqParams): Promise<MetricData[]>;
async function getMetrics<MetricNames extends string>(query: Record<MetricNames, Partial<Record<string, string>>>, reqParams?: IMetricsReqParams): Promise<Record<MetricNames, MetricData>>;
async function getMetrics(query: string | string[] | Partial<Record<string, Partial<Record<string, string>>>>, reqParams: IMetricsReqParams = {}): Promise<MetricData | MetricData[] | Partial<Record<string, MetricData>>> {
const { range = 3600, step = 60, namespace } = reqParams;
let { start, end } = reqParams;
if (!start && !end) {
const timeNow = Date.now() / 1000;
const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes
start = now - range;
end = now;
}
return apiBase.post("/metrics", {
data: query,
query: {
start, end, step,
"kubernetes_namespace": namespace,
},
});
}
export const metricsApi = {
async getMetrics<T = IMetricsQuery>(query: T, reqParams: IMetricsReqParams = {}): Promise<T extends object ? { [K in keyof T]: MetricData } : MetricData> {
const { range = 3600, step = 60, namespace } = reqParams;
let { start, end } = reqParams;
if (!start && !end) {
const timeNow = Date.now() / 1000;
const now = moment.unix(timeNow).startOf("minute").unix(); // round date to minutes
start = now - range;
end = now;
}
return apiBase.post("/metrics", {
data: query,
query: {
start, end, step,
"kubernetes_namespace": namespace,
},
});
},
getMetrics,
async getMetricProviders(): Promise<MetricProviderInfo[]> {
return apiBase.get("/metrics/providers");
},

View File

@ -8,11 +8,15 @@ import { metricsApi } from "./metrics.api";
import type { DerivedKubeApiOptions, IgnoredKubeApiOptions, ResourceDescriptor } from "../kube-api";
import { KubeApi } from "../kube-api";
import type { RequireExactlyOne } from "type-fest";
import type { KubeObjectMetadata, LocalObjectReference, Affinity, Toleration, LabelSelector, NamespaceScopedMetadata } from "../kube-object";
import type { KubeObjectMetadata, LocalObjectReference, Affinity, Toleration, NamespaceScopedMetadata } from "../kube-object";
import type { SecretReference } from "./secret.api";
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claim.api";
import { KubeObject } from "../kube-object";
import { isDefined } from "../../utils";
import type { PodSecurityContext } from "./types/pod-security-context";
import type { Probe } from "./types/probe";
import type { Container } from "./types/container";
import type { ObjectFieldSelector, ResourceFieldSelector } from "./types";
export class PodApi extends KubeApi<Pod> {
constructor(opts: DerivedKubeApiOptions & IgnoredKubeApiOptions = {}) {
@ -83,93 +87,6 @@ export enum PodStatusPhase {
EVICTED = "Evicted",
}
export interface ContainerPort {
containerPort: number;
hostIP?: string;
hostPort?: number;
name?: string;
protocol?: "UDP" | "TCP" | "SCTP";
}
export interface VolumeMount {
name: string;
readOnly?: boolean;
mountPath: string;
mountPropagation?: string;
subPath?: string;
subPathExpr?: string;
}
export interface PodContainer extends Partial<Record<PodContainerProbe, IContainerProbe>> {
name: string;
image: string;
command?: string[];
args?: string[];
ports?: ContainerPort[];
resources?: {
limits?: {
cpu: string;
memory: string;
};
requests?: {
cpu: string;
memory: string;
};
};
terminationMessagePath?: string;
terminationMessagePolicy?: string;
env?: {
name: string;
value?: string;
valueFrom?: {
fieldRef?: {
apiVersion: string;
fieldPath: string;
};
secretKeyRef?: {
key: string;
name: string;
};
configMapKeyRef?: {
key: string;
name: string;
};
};
}[];
envFrom?: {
configMapRef?: LocalObjectReference;
secretRef?: LocalObjectReference;
}[];
volumeMounts?: VolumeMount[];
imagePullPolicy?: string;
}
export type PodContainerProbe = "livenessProbe" | "readinessProbe" | "startupProbe";
interface IContainerProbe {
httpGet?: {
path?: string;
/**
* either a port number or an IANA_SVC_NAME string referring to a port defined in the container
*/
port: number | string;
scheme: string;
host?: string;
};
exec?: {
command: string[];
};
tcpSocket?: {
port: number;
};
initialDelaySeconds?: number;
timeoutSeconds?: number;
periodSeconds?: number;
successThreshold?: number;
failureThreshold?: number;
}
export interface ContainerStateRunning {
startedAt: string;
}
@ -459,17 +376,6 @@ export interface ConfigMapProjection {
optional?: boolean;
}
export interface ObjectFieldSelector {
fieldPath: string;
apiVersion?: string;
}
export interface ResourceFieldSelector {
resource: string;
containerName?: string;
divisor?: string;
}
export interface DownwardAPIVolumeFile {
path: string;
fieldRef?: ObjectFieldSelector;
@ -674,43 +580,11 @@ export interface HostAlias {
hostnames: string[];
}
export interface SELinuxOptions {
level?: string;
role?: string;
type?: string;
user?: string;
}
export interface SeccompProfile {
localhostProfile?: string;
type: string;
}
export interface Sysctl {
name: string;
value: string;
}
export interface WindowsSecurityContextOptions {
labelSelector?: LabelSelector;
maxSkew: number;
topologyKey: string;
whenUnsatisfiable: string;
}
export interface PodSecurityContext {
fsGroup?: number;
fsGroupChangePolicy?: string;
runAsGroup?: number;
runAsNonRoot?: boolean;
runAsUser?: number;
seLinuxOptions?: SELinuxOptions;
seccompProfile?: SeccompProfile;
supplementalGroups?: number[];
sysctls?: Sysctl;
windowsOptions?: WindowsSecurityContextOptions;
}
export interface TopologySpreadConstraint {
}
@ -719,7 +593,7 @@ export interface PodSpec {
activeDeadlineSeconds?: number;
affinity?: Affinity;
automountServiceAccountToken?: boolean;
containers?: PodContainer[];
containers?: Container[];
dnsPolicy?: string;
enableServiceLinks?: boolean;
ephemeralContainers?: unknown[];
@ -729,7 +603,7 @@ export interface PodSpec {
hostNetwork?: boolean;
hostPID?: boolean;
imagePullSecrets?: LocalObjectReference[];
initContainers?: PodContainer[];
initContainers?: Container[];
nodeName?: string;
nodeSelector?: Partial<Record<string, string>>;
overhead?: Partial<Record<string, string>>;
@ -931,44 +805,45 @@ export class Pod extends KubeObject<
return this.getStatusPhase() !== "Running";
}
getLivenessProbe(container: PodContainer) {
return this.getProbe(container, "livenessProbe");
getLivenessProbe(container: Container) {
return this.getProbe(container, container.livenessProbe);
}
getReadinessProbe(container: PodContainer) {
return this.getProbe(container, "readinessProbe");
getReadinessProbe(container: Container) {
return this.getProbe(container, container.readinessProbe);
}
getStartupProbe(container: PodContainer) {
return this.getProbe(container, "startupProbe");
getStartupProbe(container: Container) {
return this.getProbe(container, container.startupProbe);
}
private getProbe(container: PodContainer, field: PodContainerProbe): string[] {
const probe: string[] = [];
const probeData = container[field];
private getProbe(container: Container, probe: Probe | undefined): string[] {
const probeItems: string[] = [];
if (!probeData) {
return probe;
if (!probe) {
return probeItems;
}
const {
httpGet, exec, tcpSocket,
httpGet,
exec,
tcpSocket,
initialDelaySeconds = 0,
timeoutSeconds = 0,
periodSeconds = 0,
successThreshold = 0,
failureThreshold = 0,
} = probeData;
} = probe;
// HTTP Request
if (httpGet) {
const { path = "", port, host = "", scheme } = httpGet;
const { path = "", port, host = "", scheme = "HTTP" } = httpGet;
const resolvedPort = typeof port === "number"
? port
// Try and find the port number associated witht the name or fallback to the name itself
: container.ports?.find(containerPort => containerPort.name === port)?.containerPort || port;
probe.push(
probeItems.push(
"http-get",
`${scheme.toLowerCase()}://${host}:${resolvedPort}${path}`,
);
@ -976,15 +851,15 @@ export class Pod extends KubeObject<
// Command
if (exec?.command) {
probe.push(`exec [${exec.command.join(" ")}]`);
probeItems.push(`exec [${exec.command.join(" ")}]`);
}
// TCP Probe
if (tcpSocket?.port) {
probe.push(`tcp-socket :${tcpSocket.port}`);
probeItems.push(`tcp-socket :${tcpSocket.port}`);
}
probe.push(
probeItems.push(
`delay=${initialDelaySeconds}s`,
`timeout=${timeoutSeconds}s`,
`period=${periodSeconds}s`,
@ -992,7 +867,7 @@ export class Pod extends KubeObject<
`#failure=${failureThreshold}`,
);
return probe;
return probeItems;
}
getNodeName(): string | undefined {

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Adds and removes POSIX capabilities from running containers.
*/
export interface Capabilities {
/**
* Added capabilities
*/
add?: string[];
/**
* Removed capabilities
*/
drop?: string[];
}

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.
*/
export interface ContainerPort {
containerPort: number;
hostIP?: string;
hostPort?: number;
name?: string;
protocol?: "UDP" | "TCP" | "SCTP";
}

View File

@ -0,0 +1,176 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Lifecycle } from "./lifecycle";
import type { ResourceRequirements } from "./resource-requirements";
import type { SecurityContext } from "./security-context";
import type { Probe } from "./probe";
import type { VolumeDevice } from "./volume-device";
import type { VolumeMount } from "./volume-mount";
import type { ContainerPort } from "./container-port";
import type { EnvFromSource } from "./env-from-source";
import type { EnvVar } from "./env-var";
/**
* A single application container that you want to run within a pod.
*/
export interface Container {
/**
* Arguments to the entrypoint. The docker image's CMD is used if this is not provided. Variable
* references `$(VAR_NAME)` are expanded using the container's environment.
*
* If a variable cannot be resolved, the reference in the input string will be unchanged.
* Double `$$` are reduced to a single `$`, which allows for escaping the `$(VAR_NAME)` syntax:
* i.e. `"$$(VAR_NAME)"` will produce the string literal `"$(VAR_NAME)`".
*
* Escaped references will never be expanded, regardless of whether the variable exists or not.
* Cannot be updated.
*
* More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell
*/
args?: string[];
/**
* Entrypoint array. Not executed within a shell. The docker image's ENTRYPOINT is used if this
* is not provided. Variable references `$(VAR_NAME)` are expanded using the container's
* environment.
*
* If a variable cannot be resolved, the reference in the input string will be unchanged.
* Double `$$` are reduced to a single `$`, which allows for escaping the `$(VAR_NAME)` syntax:
* i.e. `"$$(VAR_NAME)"` will produce the string literal `"$(VAR_NAME)`".
*
* Escaped references will never be expanded, regardless of whether the variable exists or not.
* Cannot be updated.
*
* More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell
*/
command?: string[];
/**
* List of environment variables to set in the container. Cannot be updated.
*/
env?: EnvVar[];
/**
* List of sources to populate environment variables in the container. The keys defined within a
* source must be a C_IDENTIFIER. All invalid keys will be reported as an event when the
* container is starting.
*
* When a key exists in multiple sources, the value associated with the last source will take
* precedence. Values defined by an Env with a duplicate key will take precedence. Cannot be
* updated.
*/
envFrom?: EnvFromSource[];
/**
* Docker image name.
*
* More info: https://kubernetes.io/docs/concepts/containers/images
*/
image?: string;
/**
* Image pull policy. Defaults to `"Always"` if :latest tag is specified, or `"IfNotPresent"`
* otherwise. Cannot be updated.
*
* More info: https://kubernetes.io/docs/concepts/containers/images#updating-images
*/
imagePullPolicy?: "Always" | "Never" | "IfNotPresent";
lifecycle?: Lifecycle;
livenessProbe?: Probe;
/**
* Name of the container specified as a DNS_LABEL. Each container in a pod must have a unique
* name. Cannot be updated.
*/
name: string;
/**
* List of ports to expose from the container. Exposing a port here gives the system additional
* information about the network connections a container uses, but is primarily informational.
* Not specifying a port here DOES NOT prevent that port from being exposed. Any port which is
* listening on the default `"0.0.0.0"` address inside a container will be accessible from the
* network. Cannot be updated.
*/
ports?: ContainerPort[];
readinessProbe?: Probe;
resources?: ResourceRequirements;
securityContext?: SecurityContext;
startupProbe?: Probe;
/**
* Whether this container should allocate a buffer for stdin in the container runtime. If this is
* not set, reads from stdin in the container will always result in EOF.
*
* @default false
*/
stdin?: boolean;
/**
* Whether the container runtime should close the stdin channel after it has been opened by a
* single attach. When stdin is true the stdin stream will remain open across multiple attach
* sessions.
*
* If stdinOnce is set to true, stdin is opened on container start, is empty until the first
* client attaches to stdin, and then remains open and accepts data until the client disconnects,
* at which time stdin is closed and remains closed until the container is restarted.
*
* If this flag is false, a container processes that reads from stdin will never receive an EOF.
*
* @default false
*/
stdinOnce?: boolean;
/**
* Path at which the file to which the container's termination message will be written
* is mounted into the container's filesystem. Message written is intended to be brief final
* status, such as an assertion failure message.
*
* Will be truncated by the node if greater than 4096 bytes.
* The total message length across all containers will be limited to 12kb. Cannot be updated.
*
* @default "/dev/termination-log"
*/
terminationMessagePath?: string;
/**
* Indicate how the termination message should be populated.
*
* - `File`: will use the contents of {@link terminationMessagePath} to populate the container
* status message on both success and failure.
*
* - `FallbackToLogsOnError`: will use the last chunk of container log output if the
* termination message file is empty and the container exited with an error.
*
* The log output is limited to 2048 bytes or 80 lines, whichever is smaller. Cannot be updated.
*
* @default "File"
*/
terminationMessagePolicy?: "File" | "FallbackToLogsOnError";
/**
* Whether this container should allocate a TTY for itself, also requires 'stdin' to be true.
*
* @default false
*/
tty?: boolean;
/**
* volumeDevices is the list of block devices to be used by the container.
*/
volumeDevices?: VolumeDevice[];
/**
* Pod volumes to mount into the container's filesystem. Cannot be updated.
*/
volumeMounts?: VolumeMount[];
/**
* Container's working directory. If not specified, the container runtime's default will be used,
* which might be configured in the container image. Cannot be updated.
*/
workingDir?: string;
}

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 type { EnvSource } from "./env-source";
export interface EnvFromSource {
configMapRef?: EnvSource;
/**
* An identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER.
*/
prefix?: string;
secretRef?: EnvSource;
}

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 { LocalObjectReference } from "../../kube-object";
export interface EnvSource extends LocalObjectReference {
/**
* Whether the object must be defined
*/
optional?: boolean;
}

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.
*/
export interface EnvVarKeySelector {
key: string;
name?: string;
optional?: boolean;
}

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { EnvVarKeySelector } from "./env-var-key-selector";
import type { ObjectFieldSelector } from "./object-field-selector";
import type { ResourceFieldSelector } from "./resource-field-selector";
export interface EnvVarSource {
configMapKeyRef?: EnvVarKeySelector;
fieldRef?: ObjectFieldSelector;
resourceFieldRef?: ResourceFieldSelector;
secretKeyRef?: EnvVarKeySelector;
}

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 type { EnvVarSource } from "./env-var-source";
export interface EnvVar {
name: string;
value?: string;
valueFrom?: EnvVarSource;
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* ExecAction describes a "run in container" action.
*/
export interface ExecAction {
/**
* Command is the command line to execute inside the container, the working directory for the
* command is root ('\\') in the container's filesystem. The command is simply exec'd, it is not
* run inside a shell, so traditional shell instructions ('|', etc) won't work. To use a shell,
* you need to explicitly call out to that shell.
*
* Exit status of 0 is treated as live/healthy and non-zero is unhealthy.
*/
command?: string[];
}

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.
*/
import type { ExecAction } from "./exec-action";
import type { HttpGetAction } from "./http-get-action";
import type { TcpSocketAction } from "./tcp-socket-action";
/**
* Handler defines a specific action that should be taken.
*/
export interface Handler {
exec?: ExecAction;
httpGet?: HttpGetAction;
tcpSocket?: TcpSocketAction;
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { HttpHeader } from "./http-header";
/**
* An action based on HTTP Get requests.
*/
export interface HttpGetAction {
/**
* Host name to connect to, defaults to the pod IP. You probably want to set \"Host\" in httpHeaders instead.
*/
host?: string;
/**
* Custom headers to set in the request. HTTP allows repeated headers.
*/
httpHeaders?: HttpHeader[];
/**
* Path to access on the HTTP server.
*/
path?: string;
/**
* The PORT to request from.
*/
port: string | number;
/**
* Scheme to use for connecting to the host.
*
* @default "HTTP"
*/
scheme?: string;
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A custom header to be used in HTTP probes and get actions
*/
export interface HttpHeader {
/**
* Field name
*/
name: string;
/**
* The value of the field
*/
value: string;
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./aggregation-rule";
export * from "./capabilities";
export * from "./container";
export * from "./container-port";
export * from "./env-from-source";
export * from "./env-source";
export * from "./env-var-key-selector";
export * from "./env-var-source";
export * from "./env-var";
export * from "./exec-action";
export * from "./handler";
export * from "./http-get-action";
export * from "./http-header";
export * from "./job-template-spec";
export * from "./lifecycle";
export * from "./object-field-selector";
export * from "./persistent-volume-claim-template-spec";
export * from "./pod-security-context";
export * from "./pod-template-spec";
export * from "./policy-rule";
export * from "./probe";
export * from "./resource-field-selector";
export * from "./resource-requirements";
export * from "./role-ref";
export * from "./se-linux-options";
export * from "./seccomp-profile";
export * from "./subject";
export * from "./tcp-socket-action";
export * from "./volume-device";
export * from "./volume-mount";
export * from "./windows-security-context-options";

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.
*/
import type { Handler } from "./handler";
/**
* Lifecycle describes actions that the management system should take in response to container
* lifecycle events. For the PostStart and PreStop lifecycle handlers, management of the container
* blocks until the action is complete, unless the container process fails, in which case the
* handler is aborted.
*/
export interface Lifecycle {
postStart?: Handler;
preStop?: Handler;
}

View File

@ -2,6 +2,8 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type IMetricsQuery = string | string[] | {
[metricName: string]: string;
};
export interface ObjectFieldSelector {
apiVersion?: string;
fieldPath: string;
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { SeLinuxOptions } from "./se-linux-options";
import type { SeccompProfile } from "./seccomp-profile";
import type { WindowsSecurityContextOptions } from "./windows-security-context-options";
import type { Sysctl } from "../pod.api";
export interface PodSecurityContext {
fsGroup?: number;
fsGroupChangePolicy?: string;
runAsGroup?: number;
runAsNonRoot?: boolean;
runAsUser?: number;
seLinuxOptions?: SeLinuxOptions;
seccompProfile?: SeccompProfile;
supplementalGroups?: number[];
sysctls?: Sysctl;
windowsOptions?: WindowsSecurityContextOptions;
}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ExecAction } from "./exec-action";
import type { HttpGetAction } from "./http-get-action";
import type { TcpSocketAction } from "./tcp-socket-action";
/**
* Describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic.
*/
export interface Probe {
exec?: ExecAction;
/**
* Minimum consecutive failures for the probe to be considered failed after having succeeded.
*
* @default 3
* @minimum 1
*/
failureThreshold?: number;
httpGet?: HttpGetAction;
/**
* Duration after the container has started before liveness probes are initiated.
*
* More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
*/
initialDelaySeconds?: number;
/**
* How often to perform the probe.
*
* @default 10
* @minimum 1
*/
periodSeconds?: number;
/**
* Minimum consecutive successes for the probe to be considered successful after having failed.
*
* Must be 1 for liveness and startup.
*
* @default 1
* @minimum 1
*/
successThreshold?: number;
tcpSocket?: TcpSocketAction;
/**
* Duration the pod needs to terminate gracefully upon probe failure.
*
* The grace period is the duration in seconds after the processes running in the pod are sent a
* termination signal and the time when the processes are forcibly halted with a kill signal.
*
* Set this value longer than the expected cleanup time for your process.
*
* If this value is not set, the pod's terminationGracePeriodSeconds will be used. Otherwise,
* this value overrides the value provided by the pod spec. Value must be non-negative integer.
* The value zero indicates stop immediately via the kill signal (no opportunity to shut down).
*
* This is a beta field and requires enabling ProbeTerminationGracePeriod feature gate.
*
* @minimum 1
*/
terminationGracePeriodSeconds?: number;
/**
* Duration after which the probe times out.
*
* More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes
*
* @default 1
* @minimum 1
*/
timeoutSeconds?: number;
}

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.
*/
export interface ResourceFieldSelector {
containerName?: string;
divisor?: string;
resource: string;
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* SELinuxOptions are the labels to be applied to the container
*/
export interface SeLinuxOptions {
/**
* The SELinux `level` label that applies to the container.
*/
level?: string;
/**
* The SELinux `role` label that applies to the container.
*/
role?: string;
/**
* The SELinux `type` label that applies to the container.
*/
type?: string;
/**
* The SELinux `user` label that applies to the container.
*/
user?: string;
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Defines a pod's or a container's seccomp profile settings. Only one profile source may be set.
*/
export interface SeccompProfile {
/**
* Indicates a profile defined in a file on the node should be used. The profile must be
* preconfigured on the node to work. Must be a descending path, relative to the kubelet's
* configured seccomp profile location. Must only be set if type is "Localhost".
*/
localhostProfile?: string;
/**
* Indicates which kind of seccomp profile will be applied.
*
* Options:
*
* | Value | Description |
* |--|--|
* | `Localhost` | A profile defined in a file on the node should be used. |
* | `RuntimeDefault` | The container runtime default profile should be used. |
* | `Unconfined` | No profile should be applied. |
*/
type: "Localhost" | "RuntimeDefault" | "Unconfined";
}

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 type { Capabilities } from "./capabilities";
import type { SeLinuxOptions } from "./se-linux-options";
import type { SeccompProfile } from "./seccomp-profile";
import type { WindowsSecurityContextOptions } from "./windows-security-context-options";
/**
* SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.
*/
export interface SecurityContext {
/**
* AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN
*/
allowPrivilegeEscalation?: boolean;
capabilities?: Capabilities;
/**
* Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false.
*/
privileged?: boolean;
/**
* procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled.
*/
procMount?: string;
/**
* Whether this container has a read-only root filesystem. Default is false.
*/
readOnlyRootFilesystem?: boolean;
/**
* The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
*/
runAsGroup?: number;
/**
* Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
*/
runAsNonRoot?: boolean;
/**
* The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
*/
runAsUser?: number;
seLinuxOptions?: SeLinuxOptions;
seccompProfile?: SeccompProfile;
windowsOptions?: WindowsSecurityContextOptions;
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* An action based on opening a socket
*/
export interface TcpSocketAction {
/**
* Host name to connect to, defaults to the pod IP.
*/
host?: string;
/**
* Port to connect to
*/
port: number | string;
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A mapping of a raw block device within a container.
*/
export interface VolumeDevice {
/**
* The path inside of the container that the device will be mapped to.
*/
devicePath: string;
/**
* Must match the name of a persistentVolumeClaim in the pod
*/
name: string;
}

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.
*/
export interface VolumeMount {
name: string;
readOnly?: boolean;
mountPath: string;
mountPropagation?: string;
subPath?: string;
subPathExpr?: string;
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Windows-specific options and credentials.
*/
export interface WindowsSecurityContextOptions {
/**
* The location of the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa)
* inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
*/
gmsaCredentialSpec?: string;
/**
* The name of the GMSA credential spec to use.
*/
gmsaCredentialSpecName?: string;
/**
* Determines if a container should be run as a 'Host Process' container.
*
* This field is alpha-level and will only be honored by components that enable the
* WindowsHostProcessContainers feature flag.
*
* Setting this field without the feature flag will result in errors when validating the Pod.
*
* All of a Pod's containers must have the same effective HostProcess value (it is not allowed to
* have a mix of HostProcess containers and non-HostProcess containers).
*
* In addition, if HostProcess is true then HostNetwork must also be set to true.
*/
hostProcess?: boolean;
/**
* The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
*/
runAsUserName?: string;
}

View File

@ -11,8 +11,9 @@ import logger from "../../main/logger";
import { app } from "electron";
import { ClusterStore } from "../cluster-store/cluster-store";
import yaml from "js-yaml";
import { productName } from "../vars";
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import productNameInjectable from "../vars/product-name.injectable";
export class ResourceStack {
constructor(protected cluster: KubernetesCluster, protected name: string) {}
@ -97,6 +98,8 @@ export class ResourceStack {
protected async renderTemplates(folderPath: string, templateContext: any): Promise<string[]> {
const resources: string[] = [];
const di = getLegacyGlobalDiForExtensionApi();
const productName = di.inject(productNameInjectable);
logger.info(`[RESOURCE-STACK]: render templates from ${folderPath}`);
const files = await fse.readdir(folderPath);

View File

@ -113,7 +113,17 @@ export abstract class LensProtocolRouter {
}
// if no exact match pick the one that is the most specific
return matches.sort(([a], [b]) => compareMatches(a, b))[0] ?? null;
return matches.sort(([a], [b]) => {
if (a.path === "/") {
return 1;
}
if (b.path === "/") {
return -1;
}
return countBy(b.path)["/"] - countBy(a.path)["/"];
})[0] ?? null;
}
/**
@ -265,21 +275,3 @@ export abstract class LensProtocolRouter {
this.internalRoutes.delete(urlSchema);
}
}
/**
* a comparison function for `array.sort(...)`. Sort order should be most path
* parts to least path parts.
* @param a the left side to compare
* @param b the right side to compare
*/
function compareMatches<T>(a: match<T>, b: match<T>): number {
if (a.path === "/") {
return 1;
}
if (b.path === "/") {
return -1;
}
return countBy(b.path)["/"] - countBy(a.path)["/"];
}

View File

@ -1,67 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Dedupe, Offline } from "@sentry/integrations";
import { sentryDsn, isProduction } from "./vars";
import { UserStore } from "./user-store";
import { inspect } from "util";
import type { BrowserOptions } from "@sentry/electron/renderer";
import type { ElectronMainOptions } from "@sentry/electron/main";
/**
* "Translate" 'browser' to 'main' as Lens developer more familiar with the term 'main'
*/
function mapProcessName(processType: string) {
if (processType === "browser") {
return "main";
}
return processType;
}
/**
* Initialize Sentry for the current process so to send errors for debugging.
*/
export function initializeSentryReporting(init: (opts: BrowserOptions | ElectronMainOptions) => void) {
const processName = mapProcessName(process.type);
if (!sentryDsn) {
return; // do nothing if not configured to avoid uncaught error in dev mode
}
init({
beforeSend: (event) => {
// default to false, in case instance of UserStore is not created (yet)
const allowErrorReporting = UserStore.getInstance(false)?.allowErrorReporting ?? false;
if (allowErrorReporting) {
return event;
}
/**
* Directly write to stdout so that no other integrations capture this and create an infinite loop
*/
process.stdout.write(`🔒 [SENTRY-BEFORE-SEND-HOOK]: allowErrorReporting: ${allowErrorReporting}. 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: sentryDsn,
integrations: [
new Dedupe(),
new Offline(),
],
initialScope: {
tags: {
"process": processName,
},
},
environment: isProduction ? "production" : "development",
});
}

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 userStoreFileNameMigrationInjectable from "./file-name-migration.injectable";
export default getGlobalOverride(userStoreFileNameMigrationInjectable, () => async () => {});

View File

@ -9,27 +9,32 @@ import directoryForUserDataInjectable from "../app-paths/directory-for-user-data
import { isErrnoException } from "../utils";
import { getInjectable } from "@ogre-tools/injectable";
export type UserStoreFileNameMigration = () => Promise<void>;
const userStoreFileNameMigrationInjectable = getInjectable({
id: "user-store-file-name-migration",
instantiate: (di) => {
instantiate: (di): UserStoreFileNameMigration => {
const userDataPath = di.inject(directoryForUserDataInjectable);
const configJsonPath = path.join(userDataPath, "config.json");
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
try {
fse.moveSync(configJsonPath, lensUserStoreJsonPath);
} catch (error) {
if (error instanceof Error && error.message === "dest already exists.") {
fse.removeSync(configJsonPath);
} else if (isErrnoException(error) && error.code === "ENOENT" && error.path === configJsonPath) {
// (No such file or directory)
return; // file already moved
} else {
// pass other errors along
throw error;
return async () => {
try {
await fse.move(configJsonPath, lensUserStoreJsonPath);
} catch (error) {
if (error instanceof Error && error.message === "dest already exists.") {
await fse.remove(configJsonPath);
} else if (isErrnoException(error) && error.code === "ENOENT" && error.path === configJsonPath) {
// (No such file or directory)
return; // file already moved
} else {
// pass other errors along
throw error;
}
}
}
};
},
causesSideEffects: true,
});
export default userStoreFileNameMigrationInjectable;

View File

@ -3,8 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ipcMain } from "electron";
import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable";
import { UserStore } from "./user-store";
import selectedUpdateChannelInjectable from "../application-update/selected-update-channel/selected-update-channel.injectable";
@ -14,10 +12,6 @@ const userStoreInjectable = getInjectable({
instantiate: (di) => {
UserStore.resetInstance();
if (ipcMain) {
di.inject(userStoreFileNameMigrationInjectable);
}
return UserStore.createInstance({
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
});

View File

@ -4,19 +4,16 @@
*/
import { app } from "electron";
import semver from "semver";
import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import { BaseStore } from "../base-store";
import migrations from "../../migrations/user-store";
import { getAppVersion } from "../utils/app-version";
import { kubeConfigDefaultPath } from "../kube-helpers";
import { appEventBus } from "../app-event-bus/event-bus";
import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import logger from "../../main/logger";
import type { SelectedUpdateChannel } from "../application-update/selected-update-channel/selected-update-channel.injectable";
import type { UpdateChannelId } from "../application-update/update-channels";
import type { ReleaseChannel } from "../application-update/update-channels";
export interface UserStoreModel {
lastSeenAppVersion: string;
@ -24,7 +21,7 @@ export interface UserStoreModel {
}
interface Dependencies {
selectedUpdateChannel: SelectedUpdateChannel;
readonly selectedUpdateChannel: SelectedUpdateChannel;
}
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
@ -37,7 +34,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
});
makeObservable(this);
this.load();
}
@observable lastSeenAppVersion = "0.0.0";
@ -98,10 +94,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
*/
@observable syncKubeconfigEntries!: StoreType<typeof DESCRIPTORS["syncKubeconfigEntries"]>;
@computed get isNewVersion() {
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
}
@computed get resolvedShell(): string | undefined {
return this.shell || process.env.SHELL || process.env.PTYSHELL;
}
@ -151,12 +143,6 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined);
}
@action
saveLastSeenAppVersion() {
appEventBus.emit({ name: "app", action: "whats-new-seen" });
this.lastSeenAppVersion = getAppVersion();
}
@action
protected fromStore({ lastSeenAppVersion, preferences }: Partial<UserStoreModel> = {}) {
logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences });
@ -180,7 +166,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
// TODO: Switch to action-based saving instead saving stores by reaction
if (preferences?.updateChannel) {
this.dependencies.selectedUpdateChannel.setValue(preferences?.updateChannel as UpdateChannelId);
this.dependencies.selectedUpdateChannel.setValue(preferences?.updateChannel as ReleaseChannel);
}
}

View File

@ -4,15 +4,6 @@
*/
import requestPromise from "request-promise-native";
import packageInfo from "../../../package.json";
export function getAppVersion(): string {
return packageInfo.version;
}
export function getBundledKubectlVersion(): string {
return packageInfo.config.bundledKubectlVersion;
}
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
const response = await requestPromise({

View File

@ -18,6 +18,7 @@ import { requestChannelListenerInjectionToken } from "./request-channel-listener
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "../../test-utils/get-promise-status";
import { runInAction } from "mobx";
type TestMessageChannel = MessageChannel<string>;
type TestRequestChannel = RequestChannel<string, string>;
@ -47,12 +48,16 @@ describe("channel", () => {
});
builder.beforeApplicationStart((mainDi) => {
mainDi.register(testMessageChannelInjectable);
runInAction(() => {
mainDi.register(testMessageChannelInjectable);
});
});
builder.beforeWindowStart((windowDi) => {
windowDi.register(testChannelListenerInTestWindowInjectable);
windowDi.register(testMessageChannelInjectable);
runInAction(() => {
windowDi.register(testChannelListenerInTestWindowInjectable);
windowDi.register(testMessageChannelInjectable);
});
});
mainDi = builder.mainDi;
@ -126,12 +131,16 @@ describe("channel", () => {
});
applicationBuilder.beforeApplicationStart((mainDi) => {
mainDi.register(testChannelListenerInMainInjectable);
mainDi.register(testMessageChannelInjectable);
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
mainDi.register(testMessageChannelInjectable);
});
});
applicationBuilder.beforeWindowStart((windowDi) => {
windowDi.register(testMessageChannelInjectable);
runInAction(() => {
windowDi.register(testMessageChannelInjectable);
});
});
await applicationBuilder.render();
@ -172,12 +181,16 @@ describe("channel", () => {
});
applicationBuilder.beforeApplicationStart((mainDi) => {
mainDi.register(testChannelListenerInMainInjectable);
mainDi.register(testRequestChannelInjectable);
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
mainDi.register(testRequestChannelInjectable);
});
});
applicationBuilder.beforeWindowStart((windowDi) => {
windowDi.register(testRequestChannelInjectable);
runInAction(() => {
windowDi.register(testRequestChannelInjectable);
});
});
await applicationBuilder.render();

View File

@ -4,6 +4,7 @@
*/
import { Readable } from "readable-stream";
import type { ReadableStreamDefaultReadResult } from "stream/web";
import type { TypedArray } from "type-fest";
/**

View File

@ -27,7 +27,7 @@ export class Singleton {
* @param args The constructor arguments for the child class
* @returns An instance of the child class
*/
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
static createInstance<T extends Singleton, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
if (!Singleton.instances.has(this)) {
if (Singleton.creating.length > 0) {
throw new TypeError(`Cannot create a second singleton (${this.name}) while creating a first (${Singleton.creating})`);

View File

@ -18,11 +18,15 @@ describe("sync-box", () => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(mainDi => {
mainDi.register(someInjectable);
runInAction(() => {
mainDi.register(someInjectable);
});
});
applicationBuilder.beforeWindowStart((windowDi) => {
windowDi.register(someInjectable);
runInAction(() => {
windowDi.register(someInjectable);
});
});
});

View File

@ -19,7 +19,7 @@ describe("with-error-logging", () => {
let decorated: (a: string, b: string) => number | undefined;
beforeEach(() => {
const di = getDiForUnitTesting();
const di = getDiForUnitTesting({ doGeneralOverrides: true });
loggerStub = {
error: jest.fn(),
@ -119,7 +119,7 @@ describe("with-error-logging", () => {
let toBeDecorated: AsyncFnMock<typeof decorated>;
beforeEach(() => {
const di = getDiForUnitTesting();
const di = getDiForUnitTesting({ doGeneralOverrides: true });
loggerStub = {
error: jest.fn(),

View File

@ -5,7 +5,6 @@
// App's common configuration for any process (main, renderer, build pipeline, etc.)
import path from "path";
import packageInfo from "../../package.json";
import type { ThemeId } from "../renderer/themes/store";
import { lazyInitialized } from "./utils/lazy-initialized";
@ -25,7 +24,6 @@ export const isWindows = process.platform === "win32";
export const isLinux = process.platform === "linux";
export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase());
export const isSnap = !!process.env.SNAP;
/**
* @deprecated Switch to using isTestEnvInjectable
@ -42,13 +40,6 @@ export const isProduction = process.env.NODE_ENV === "production";
*/
export const isDevelopment = !isTestEnv && !isProduction;
export const productName = packageInfo.productName;
/**
* @deprecated Switch to using appNameInjectable
*/
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
export const publicPath = "/build/" as string;
export const defaultThemeId: ThemeId = "lens-dark";
export const defaultFontSize = 12;
@ -139,6 +130,3 @@ export const lensBlogWeblinkId = "lens-blog-link";
export const kubernetesDocumentationWeblinkId = "kubernetes-documentation-link";
export const docsUrl = "https://docs.k8slens.dev/main" as string;
export const sentryDsn = packageInfo.config?.sentryDsn ?? "";
export const contentSecurityPolicy = packageInfo.config?.contentSecurityPolicy ?? "";

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
import isDevelopmentInjectable from "./is-development.injectable";
import productNameInjectable from "./product-name.injectable";
const appNameInjectable = getInjectable({
@ -15,8 +15,6 @@ const appNameInjectable = getInjectable({
return `${productName}${isDevelopment ? "Dev" : ""}`;
},
causesSideEffects: true,
});
export default appNameInjectable;

View File

@ -1,14 +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 { SemVer } from "semver";
import appVersionInjectable from "./app-version.injectable";
const appSemanticVersionInjectable = getInjectable({
id: "app-semantic-version",
instantiate: (di) => new SemVer(di.inject(appVersionInjectable)),
});
export default appSemanticVersionInjectable;

View File

@ -1,13 +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 packageJsonInjectable from "./package-json.injectable";
const appVersionInjectable = getInjectable({
id: "app-version",
instantiate: (di) => di.inject(packageJsonInjectable).version,
});
export default appVersionInjectable;

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 applicationInformationInjectable from "./application-information.injectable";
const applicationCopyrightInjectable = getInjectable({
id: "application-copyright",
instantiate: (di) => di.inject(applicationInformationInjectable).copyright,
});
export default applicationCopyrightInjectable;

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 applicationInformationInjectable from "./application-information.injectable";
const applicationDescriptionInjectable = getInjectable({
id: "application-description",
instantiate: (di) => di.inject(applicationInformationInjectable).description,
});
export default applicationDescriptionInjectable;

View File

@ -0,0 +1,23 @@
/**
* 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 applicationInformationInjectable from "./application-information.injectable";
export default getGlobalOverride(applicationInformationInjectable, () => ({
productName: "some-product-name",
version: "6.0.0",
build: {},
config: {
k8sProxyVersion: "0.2.1",
bundledKubectlVersion: "1.23.3",
bundledHelmVersion: "3.7.2",
sentryDsn: "",
contentSecurityPolicy: "script-src 'unsafe-eval' 'self'; frame-src http://*.localhost:*/; img-src * data:",
welcomeRoute: "/welcome",
},
copyright: "some-copyright-information",
description: "some-descriptive-text",
}));

View File

@ -0,0 +1,22 @@
/**
* 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 packageJson from "../../../package.json";
export type ApplicationInformation = Pick<typeof packageJson, "version" | "config" | "productName" | "copyright" | "description"> & {
build: Partial<typeof packageJson["build"]> & { publish?: unknown[] };
};
const applicationInformationInjectable = getInjectable({
id: "application-information",
instantiate: (): ApplicationInformation => {
const { version, config, productName, build, copyright, description } = packageJson;
return { version, config, productName, build, copyright, description };
},
causesSideEffects: true,
});
export default applicationInformationInjectable;

View File

@ -0,0 +1,30 @@
/**
* 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";
import { SemVer } from "semver";
import type { InitializableState } from "../initializable-state/create";
import { createInitializableState } from "../initializable-state/create";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
export const buildVersionInjectionToken = getInjectionToken<InitializableState<string>>({
id: "build-version-token",
});
export const buildVersionChannel: RequestChannel<void, string> = {
id: "build-version",
};
const buildSemanticVersionInjectable = createInitializableState({
id: "build-semantic-version",
init: (di) => {
const buildVersion = di.inject(buildVersionInjectionToken);
return new SemVer(buildVersion.get());
},
});
export default buildSemanticVersionInjectable;

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 applicationInformationInjectable from "./application-information.injectable";
const bundledKubectlVersionInjectable = getInjectable({
id: "bundled-kubectl-version",
instantiate: (di) => di.inject(applicationInformationInjectable).config.bundledKubectlVersion,
});
export default bundledKubectlVersionInjectable;

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 applicationInformationInjectable from "./application-information.injectable";
const contentSecurityPolicyInjectable = getInjectable({
id: "content-security-policy",
instantiate: (di) => di.inject(applicationInformationInjectable).config.contentSecurityPolicy,
});
export default contentSecurityPolicyInjectable;

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 { SemVer } from "semver";
import applicationInformationInjectable from "./application-information.injectable";
const extensionApiVersionInjectable = getInjectable({
id: "extension-api-version",
instantiate: (di) => {
const { major, minor, patch } = new SemVer(di.inject(applicationInformationInjectable).version);
return `${major}.${minor}.${patch}`;
},
});
export default extensionApiVersionInjectable;

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 isSnapPackageInjectable from "./is-snap-package.injectable";
export default getGlobalOverride(isSnapPackageInjectable, () => false);

View File

@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import packageJson from "../../../package.json";
const packageJsonInjectable = getInjectable({
id: "package-json",
instantiate: () => packageJson,
const isSnapPackageInjectable = getInjectable({
id: "is-snap",
instantiate: () => Boolean(process.env.SNAP),
causesSideEffects: true,
});
export default packageJsonInjectable;
export default isSnapPackageInjectable;

View File

@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import packageInfo from "../../../../package.json";
import applicationInformationInjectable from "./application-information.injectable";
const productNameInjectable = getInjectable({
id: "product-name",
instantiate: () => packageInfo.productName,
causesSideEffects: true,
instantiate: (di) => di.inject(applicationInformationInjectable).productName,
});
export default productNameInjectable;

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ReleaseChannel } from "../application-update/update-channels";
import { createInitializableState } from "../initializable-state/create";
import buildSemanticVersionInjectable from "./build-semantic-version.injectable";
const releaseChannelInjectable = createInitializableState({
id: "release-channel",
init: (di): ReleaseChannel => {
const buildSemanticVersion = di.inject(buildSemanticVersionInjectable);
const currentReleaseChannel = buildSemanticVersion.get().prerelease[0];
switch (currentReleaseChannel) {
case "latest":
case "beta":
case "alpha":
return currentReleaseChannel;
default:
return "latest";
}
},
});
export default releaseChannelInjectable;

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 applicationInformationInjectable from "./application-information.injectable";
const sentryDataSourceNameInjectable = getInjectable({
id: "sentry-data-source-name",
instantiate: (di) => di.inject(applicationInformationInjectable).config.sentryDsn,
});
export default sentryDataSourceNameInjectable;

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 applicationInformationInjectable from "./application-information.injectable";
const storeMigrationVersionInjectable = getInjectable({
id: "store-migration-version",
instantiate: (di) => di.inject(applicationInformationInjectable).version,
});
export default storeMigrationVersionInjectable;

View File

@ -3,65 +3,53 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import assert from "assert";
import semver from "semver";
import { isCompatibleExtension } from "../extension-discovery/is-compatible-extension/is-compatible-extension";
import type { LensExtensionManifest } from "../lens-extension";
describe("Extension/App versions compatibility checks", () => {
it("is compatible with exact version matching", () => {
expect(isCompatible({ extLensEngineVersion: "5.5.0", appVersion: "5.5.0" })).toBeTruthy();
expect(isCompatible({ extLensEngineVersion: "5.5.0", extensionApiVersion: "5.5.0" })).toBeTruthy();
});
it("is compatible with upper %PATCH versions of base app", () => {
expect(isCompatible({ extLensEngineVersion: "5.5.0", appVersion: "5.5.5" })).toBeTruthy();
expect(isCompatible({ extLensEngineVersion: "5.5.0", extensionApiVersion: "5.5.5" })).toBeTruthy();
});
it("is compatible with higher %MINOR version of base app", () => {
expect(isCompatible({ extLensEngineVersion: "5.5.0", appVersion: "5.6.0" })).toBeTruthy();
expect(isCompatible({ extLensEngineVersion: "5.5.0", extensionApiVersion: "5.6.0" })).toBeTruthy();
});
it("is not compatible with higher %MAJOR version of base app", () => {
expect(isCompatible({ extLensEngineVersion: "5.6.0", appVersion: "6.0.0" })).toBeFalsy(); // extension for lens@5 not compatible with lens@6
expect(isCompatible({ extLensEngineVersion: "6.0.0", appVersion: "5.6.0" })).toBeFalsy();
});
it("is compatible with lensEngine with prerelease", () => {
expect(isCompatible({
extLensEngineVersion: "^5.4.0-alpha.0",
appVersion: "5.5.0-alpha.0",
})).toBeTruthy();
expect(isCompatible({ extLensEngineVersion: "5.6.0", extensionApiVersion: "6.0.0" })).toBeFalsy(); // extension for lens@5 not compatible with lens@6
expect(isCompatible({ extLensEngineVersion: "6.0.0", extensionApiVersion: "5.6.0" })).toBeFalsy();
});
it("supports short version format for manifest.engines.lens", () => {
expect(isCompatible({ extLensEngineVersion: "5.5", appVersion: "5.5.1" })).toBeTruthy();
expect(isCompatible({ extLensEngineVersion: "5.5", extensionApiVersion: "5.5.1" })).toBeTruthy();
});
it("throws for incorrect or not supported version format", () => {
expect(() => isCompatible({
extLensEngineVersion: ">=2.0",
appVersion: "2.0",
extensionApiVersion: "2.0",
})).toThrow(/Invalid format/i);
expect(() => isCompatible({
extLensEngineVersion: "~2.0",
appVersion: "2.0",
extensionApiVersion: "2.0",
})).toThrow(/Invalid format/i);
expect(() => isCompatible({
extLensEngineVersion: "*",
appVersion: "1.0",
extensionApiVersion: "1.0",
})).toThrow(/Invalid format/i);
});
});
function isCompatible({ extLensEngineVersion = "^1.0", appVersion = "1.0" } = {}): boolean {
const appSemVer = semver.coerce(appVersion);
function isCompatible({ extLensEngineVersion = "^1.0", extensionApiVersion = "1.0" } = {}): boolean {
const extensionManifestMock = getExtensionManifestMock(extLensEngineVersion);
assert(appSemVer);
return isCompatibleExtension({ appSemVer })(extensionManifestMock);
return isCompatibleExtension({ extensionApiVersion })(extensionManifestMock);
}
function getExtensionManifestMock(lensEngine = "1.0"): LensExtensionManifest {

View File

@ -9,7 +9,7 @@ import type { Injectable } from "@ogre-tools/injectable";
* @deprecated use asLegacyGlobalForExtensionApi instead, and use proper implementations instead of "modifications".
*/
export const asLegacyGlobalObjectForExtensionApiWithModifications = <
InjectableInstance extends InjectionTokenInstance,
InjectableInstance extends InjectionTokenInstance & object,
InjectionTokenInstance,
ModificationObject extends object,
>(

View File

@ -3,14 +3,65 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getAppVersion } from "../../common/utils";
import appNameInjectable from "../../common/vars/app-name.injectable";
import isLinuxInjectable from "../../common/vars/is-linux.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable";
import isSnapPackageInjectable from "../../common/vars/is-snap-package.injectable";
import isWindowsInjectable from "../../common/vars/is-windows.injectable";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { getLegacyGlobalDiForExtensionApi } from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import getEnabledExtensionsInjectable from "./get-enabled-extensions/get-enabled-extensions.injectable";
import * as Preferences from "./user-preferences";
import type { UserPreferenceExtensionItems } from "./user-preferences";
import { Preferences } from "./user-preferences";
import { slackUrl, issuesTrackerUrl } from "../../common/vars";
import { buildVersionInjectionToken } from "../../common/vars/build-semantic-version.injectable";
export const version = getAppVersion();
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
export interface AppExtensionItems {
readonly Preferences: UserPreferenceExtensionItems;
readonly version: string;
readonly appName: string;
readonly slackUrl: string;
readonly issuesTrackerUrl: string;
readonly isSnap: boolean;
readonly isWindows: boolean;
readonly isMac: boolean;
readonly isLinux: boolean;
getEnabledExtensions: () => string[];
}
export const getEnabledExtensions = asLegacyGlobalFunctionForExtensionApi(getEnabledExtensionsInjectable);
export const App: AppExtensionItems = {
Preferences,
getEnabledExtensions: asLegacyGlobalFunctionForExtensionApi(getEnabledExtensionsInjectable),
get version() {
const di = getLegacyGlobalDiForExtensionApi();
export { Preferences };
return di.inject(buildVersionInjectionToken).get();
},
get appName() {
const di = getLegacyGlobalDiForExtensionApi();
return di.inject(appNameInjectable);
},
get isSnap() {
const di = getLegacyGlobalDiForExtensionApi();
return di.inject(isSnapPackageInjectable);
},
get isWindows() {
const di = getLegacyGlobalDiForExtensionApi();
return di.inject(isWindowsInjectable);
},
get isMac() {
const di = getLegacyGlobalDiForExtensionApi();
return di.inject(isMacInjectable);
},
get isLinux() {
const di = getLegacyGlobalDiForExtensionApi();
return di.inject(isLinuxInjectable);
},
slackUrl,
issuesTrackerUrl,
};

View File

@ -4,10 +4,10 @@
*/
// APIs
import * as App from "./app";
import { App } from "./app";
import * as EventBus from "./event-bus";
import * as Store from "./stores";
import * as Util from "./utils";
import { Util } from "./utils";
import * as Catalog from "./catalog";
import * as Types from "./types";
import * as Proxy from "./proxy";

View File

@ -35,6 +35,7 @@ export {
} from "../../common/k8s-api/kube-object";
export {
KubeJsonApi,
type KubeJsonApiData,
} from "../../common/k8s-api/kube-json-api";
@ -47,7 +48,7 @@ export {
} from "../../common/k8s-api/kube-object.store";
export {
type PodContainer as IPodContainer,
type Container as IPodContainer,
type PodContainerStatus as IPodContainerStatus,
Pod,
PodApi as PodsApi,

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