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

Merge remote-tracking branch 'origin/master' into eviction_api_support_for_pods

# Conflicts:
#	packages/core/src/common/k8s-api/__tests__/kube-api.test.ts
This commit is contained in:
Roman 2023-03-23 19:00:33 +04:00
commit 4b78e82dfb
351 changed files with 5848 additions and 2926 deletions

View File

@ -15,6 +15,8 @@ All releases will be made by creating a PR which bumps the version field in the
1. If you are making a patch release (or a prerelease for one) make sure you are on the `release/v<MAJOR>.<MINOR>` branch. 1. If you are making a patch release (or a prerelease for one) make sure you are on the `release/v<MAJOR>.<MINOR>` branch.
1. Run `npm run create-release-pr`. 1. Run `npm run create-release-pr`.
1. Pick the PRs that you want to include in this release using the keys listed. 1. Pick the PRs that you want to include in this release using the keys listed.
- If you are making a patch release this might include fixing up some cherry-picking of commits. These actions should be done in a separate terminal.
- If a package version is having a major version bump then `npm` will complain about `peerDependency` conflicts. These will have to be fixed up separately.
1. Once the PR is created, approved, and then merged the `Release Open Lens` workflow will create a tag and release for you. 1. Once the PR is created, approved, and then merged the `Release Open Lens` workflow will create a tag and release for you.
1. If you are making a major or minor release, create a `release/v<MAJOR>.<MINOR>` branch and push it to `origin` so that future patch releases can be made from it. 1. If you are making a major or minor release, create a `release/v<MAJOR>.<MINOR>` branch and push it to `origin` so that future patch releases can be made from it.
1. If you released a major or minor version, create a new patch milestone and move all bug issues to that milestone and all enhancement issues to the next minor milestone. 1. If you released a major or minor version, create a new patch milestone and move all bug issues to that milestone and all enhancement issues to the next minor milestone.

713
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
"clean:node_modules": "lerna clean -y && rimraf node_modules", "clean:node_modules": "lerna clean -y && rimraf node_modules",
"dev": "lerna run dev --stream --skip-nx-cache", "dev": "lerna run dev --stream --skip-nx-cache",
"lint": "lerna run lint --stream", "lint": "lerna run lint --stream",
"lint:fix": "lerna run lint:fix --stream",
"mkdocs:serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest", "mkdocs:serve-local": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -it -p 8000:8000 -v ${PWD}:/docs mkdocs-serve-local:latest",
"mkdocs:verify": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict", "mkdocs:verify": "docker build -t mkdocs-serve-local:latest mkdocs/ && docker run --rm -v ${PWD}:/docs mkdocs-serve-local:latest build --strict",
"test:unit": "lerna run --stream test:unit", "test:unit": "lerna run --stream test:unit",
@ -28,6 +29,11 @@
"precreate-release-pr": "cd packages/release-tool && npm run build", "precreate-release-pr": "cd packages/release-tool && npm run build",
"create-release-pr": "node packages/release-tool/dist/index.js" "create-release-pr": "node packages/release-tool/dist/index.js"
}, },
"overrides": {
"underscore": "^1.12.1",
"react": "^17",
"@types/react": "^17"
},
"devDependencies": { "devDependencies": {
"adr": "^1.4.3", "adr": "^1.4.3",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View File

@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript"
},
"target": "es2022"
}
}

View File

@ -0,0 +1,3 @@
# Description
The package exports tokens needed for external configuration of Cluster Settings page.

View File

@ -0,0 +1,31 @@
{
"name": "@k8slens/cluster-settings",
"version": "6.5.0-alpha.1",
"description": "Injection token exporter for cluster settings configuration",
"license": "MIT",
"private": false,
"mode": "production",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"clean": "rimraf dist/",
"generate-types": "tsc --d --declarationDir ./dist --declarationMap --emitDeclarationOnly",
"build": "npm run generate-types && swc ./src/index.ts -d ./dist",
"prepare:test": "npm run build"
},
"devDependencies": {
"@ogre-tools/injectable": "^15.1.2",
"@swc/cli": "^0.1.61",
"@swc/core": "^1.3.37",
"@types/node": "^16.18.11",
"@types/semver": "^7.3.13",
"rimraf": "^4.1.2"
}
}

View File

@ -0,0 +1,30 @@
import { getInjectionToken } from "@ogre-tools/injectable";
type ClusterPreferences = {
clusterName?: string;
icon?: string | null;
}
export interface ClusterIconMenuItem {
id: string;
title: string;
disabled?: (preferences: ClusterPreferences) => boolean;
onClick: (preferences: ClusterPreferences) => void;
}
export interface ClusterIconSettingComponentProps {
preferences: ClusterPreferences;
}
export interface ClusterIconSettingsComponent {
id: string;
Component: React.ComponentType<ClusterIconSettingComponentProps>;
}
export const clusterIconSettingsMenuInjectionToken = getInjectionToken<ClusterIconMenuItem>({
id: "cluster-icon-settings-menu-injection-token",
});
export const clusterIconSettingsComponentInjectionToken = getInjectionToken<ClusterIconSettingsComponent>({
id: "cluster-icon-settings-component-injection-token",
});

View File

@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist/",
"paths": {
"*": [
"node_modules/*",
"types/*"
]
},
},
"include": [
"src/**/*",
],
"exclude": [
"node_modules",
]
}

View File

@ -116,13 +116,11 @@
} }
} }
}, },
"resolutions": {
"@astronautlabs/jsonpath/underscore": "^1.12.1"
},
"dependencies": { "dependencies": {
"@astronautlabs/jsonpath": "^1.1.0", "@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1", "@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0", "@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1", "@k8slens/node-fetch": "^6.5.0-alpha.1",
"@kubernetes/client-node": "^0.18.1", "@kubernetes/client-node": "^0.18.1",
"@material-ui/styles": "^4.11.5", "@material-ui/styles": "^4.11.5",
@ -134,7 +132,6 @@
"@sentry/electron": "^3.0.8", "@sentry/electron": "^3.0.8",
"@sentry/integrations": "^6.19.3", "@sentry/integrations": "^6.19.3",
"@side/jest-runtime": "^1.1.0", "@side/jest-runtime": "^1.1.0",
"abort-controller": "^3.0.0",
"auto-bind": "^4.0.0", "auto-bind": "^4.0.0",
"await-lock": "^2.2.2", "await-lock": "^2.2.2",
"byline": "^5.0.0", "byline": "^5.0.0",
@ -193,6 +190,7 @@
}, },
"devDependencies": { "devDependencies": {
"@async-fn/jest": "1.6.4", "@async-fn/jest": "1.6.4",
"@k8slens/messaging-fake-bridge": "^1.0.0-alpha.1",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",
@ -223,7 +221,6 @@
"@types/marked": "^4.0.8", "@types/marked": "^4.0.8",
"@types/md5-file": "^4.0.2", "@types/md5-file": "^4.0.2",
"@types/memorystream": "^0.3.0", "@types/memorystream": "^0.3.0",
"@types/mini-css-extract-plugin": "^2.4.0",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^16.18.11", "@types/node": "^16.18.11",
"@types/proper-lockfile": "^4.1.2", "@types/proper-lockfile": "^4.1.2",
@ -240,7 +237,6 @@
"@types/semver": "^7.3.13", "@types/semver": "^7.3.13",
"@types/tar": "^6.1.4", "@types/tar": "^6.1.4",
"@types/tcp-port-used": "^1.0.1", "@types/tcp-port-used": "^1.0.1",
"@types/tempy": "^0.3.0",
"@types/triple-beam": "^1.3.2", "@types/triple-beam": "^1.3.2",
"@types/url-parse": "^1.4.8", "@types/url-parse": "^1.4.8",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
@ -262,7 +258,7 @@
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dompurify": "^2.4.4", "dompurify": "^2.4.4",
"electron": "^19.1.9", "electron": "^22.3.3",
"electron-builder": "^23.6.0", "electron-builder": "^23.6.0",
"esbuild": "^0.17.8", "esbuild": "^0.17.8",
"esbuild-loader": "^2.21.0", "esbuild-loader": "^2.21.0",
@ -328,7 +324,11 @@
"@k8slens/application": "^6.5.0-alpha.0", "@k8slens/application": "^6.5.0-alpha.0",
"@k8slens/application-for-electron-main": "^6.5.0-alpha.0", "@k8slens/application-for-electron-main": "^6.5.0-alpha.0",
"@k8slens/legacy-extensions": "^1.0.0-alpha.0", "@k8slens/legacy-extensions": "^1.0.0-alpha.0",
"@k8slens/messaging": "^1.0.0-alpha.1",
"@k8slens/messaging-for-main": "^1.0.0-alpha.1",
"@k8slens/messaging-for-renderer": "^1.0.0-alpha.1",
"@k8slens/run-many": "^1.0.0-alpha.1", "@k8slens/run-many": "^1.0.0-alpha.1",
"@k8slens/startable-stoppable": "^1.0.0-alpha.1",
"@k8slens/test-utils": "^1.0.0-alpha.1", "@k8slens/test-utils": "^1.0.0-alpha.1",
"@k8slens/utilities": "^1.0.0-alpha.1", "@k8slens/utilities": "^1.0.0-alpha.1",
"@types/byline": "^4.2.33", "@types/byline": "^4.2.33",

View File

@ -11,7 +11,7 @@ export const pathNames: PathName[] = [
"home", "home",
"appData", "appData",
"userData", "userData",
"cache", "sessionData",
"temp", "temp",
"exe", "exe",
"module", "module",

View File

@ -3,11 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { AppPaths } from "./app-path-injection-token"; import type { AppPaths } from "./app-path-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
export type AppPathsChannel = RequestChannel<void, AppPaths>; export const appPathsChannel = getRequestChannel<void, AppPaths>("app-paths");
export const appPathsChannel: AppPathsChannel = {
id: "app-paths",
};

View File

@ -21,7 +21,6 @@ describe("app-paths", () => {
const defaultAppPathsStub: AppPaths = { const defaultAppPathsStub: AppPaths = {
currentApp: "/some-current-app", currentApp: "/some-current-app",
appData: "/some-app-data", appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps", crashDumps: "/some-crash-dumps",
desktop: "/some-desktop", desktop: "/some-desktop",
documents: "/some-documents", documents: "/some-documents",
@ -36,6 +35,7 @@ describe("app-paths", () => {
temp: "/some-temp", temp: "/some-temp",
videos: "/some-videos", videos: "/some-videos",
userData: "/some-irrelevant-user-data", userData: "/some-irrelevant-user-data",
sessionData: "/some-irrelevant-user-data", // By default this points to userData
}; };
builder.beforeApplicationStart(({ mainDi }) => { builder.beforeApplicationStart(({ mainDi }) => {
@ -73,7 +73,6 @@ describe("app-paths", () => {
expect(actual).toEqual({ expect(actual).toEqual({
currentApp: "/some-current-app", currentApp: "/some-current-app",
appData: "/some-app-data", appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps", crashDumps: "/some-crash-dumps",
desktop: "/some-desktop", desktop: "/some-desktop",
documents: "/some-documents", documents: "/some-documents",
@ -88,6 +87,7 @@ describe("app-paths", () => {
temp: "/some-temp", temp: "/some-temp",
videos: "/some-videos", videos: "/some-videos",
userData: "/some-app-data/some-product-name", userData: "/some-app-data/some-product-name",
sessionData: "/some-app-data/some-product-name",
}); });
}); });
@ -97,7 +97,6 @@ describe("app-paths", () => {
expect(actual).toEqual({ expect(actual).toEqual({
currentApp: "/some-current-app", currentApp: "/some-current-app",
appData: "/some-app-data", appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps", crashDumps: "/some-crash-dumps",
desktop: "/some-desktop", desktop: "/some-desktop",
documents: "/some-documents", documents: "/some-documents",
@ -112,6 +111,7 @@ describe("app-paths", () => {
temp: "/some-temp", temp: "/some-temp",
videos: "/some-videos", videos: "/some-videos",
userData: "/some-app-data/some-product-name", userData: "/some-app-data/some-product-name",
sessionData: "/some-app-data/some-product-name",
}); });
}); });
}); });

View File

@ -15,7 +15,7 @@ import type { GetConfigurationFileModel } from "../get-configuration-file-model/
import type { Logger } from "../logger"; import type { Logger } from "../logger";
import type { PersistStateToConfig } from "./save-to-file"; import type { PersistStateToConfig } from "./save-to-file";
import type { GetBasenameOfPath } from "../path/get-basename.injectable"; import type { GetBasenameOfPath } from "../path/get-basename.injectable";
import type { EnlistMessageChannelListener } from "../utils/channel/enlist-message-channel-listener-injection-token"; import type { EnlistMessageChannelListener } from "@k8slens/messaging";
import { toJS } from "../utils"; import { toJS } from "../utils";
export interface BaseStoreParams<T> extends Omit<ConfOptions<T>, "migrations"> { export interface BaseStoreParams<T> extends Omit<ConfOptions<T>, "migrations"> {
@ -108,6 +108,7 @@ export abstract class BaseStore<T extends object> {
this.params.syncOptions, this.params.syncOptions,
), ),
this.dependencies.enlistMessageChannelListener({ this.dependencies.enlistMessageChannelListener({
id: this.displayName,
channel: { channel: {
id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`, id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`,
}, },

View File

@ -4,6 +4,6 @@
*/ */
import type { SelfSignedCert } from "selfsigned"; import type { SelfSignedCert } from "selfsigned";
import { getRequestChannel } from "../utils/channel/get-request-channel"; import { getRequestChannel } from "@k8slens/messaging";
export const lensProxyCertificateChannel = getRequestChannel<void, SelfSignedCert>("request-lens-proxy-certificate"); export const lensProxyCertificateChannel = getRequestChannel<void, SelfSignedCert>("request-lens-proxy-certificate");

View File

@ -16,7 +16,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync"; import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getBasenameOfPathInjectable from "../path/get-basename.injectable"; import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
const clusterStoreInjectable = getInjectable({ const clusterStoreInjectable = getInjectable({
id: "cluster-store", id: "cluster-store",

View File

@ -1,50 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../logger.injectable";
/**
* Requests the permissions for actions on the kube cluster
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
* @returns `true` if the actions described are allowed
*/
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type CreateAuthorizationReview = (proxyConfig: KubeConfig) => CanI;
const createAuthorizationReviewInjectable = getInjectable({
id: "authorization-review",
instantiate: (di): CreateAuthorizationReview => {
const logger = di.inject(loggerInjectable);
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
try {
const { body } = await api.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes },
});
return body.status?.allowed ?? false;
} catch (error) {
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
return false;
}
};
};
},
});
export default createAuthorizationReviewInjectable;

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { AuthorizationV1Api } from "@kubernetes/client-node";
import type { KubeConfig } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
export type CreateAuthorizationApi = (config: KubeConfig) => AuthorizationV1Api;
const createAuthorizationApiInjectable = getInjectable({
id: "create-authorization-api",
instantiate: (): CreateAuthorizationApi => (config) => config.makeApiClient(AuthorizationV1Api),
});
export default createAuthorizationApiInjectable;

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../logger.injectable";
/**
* Requests the permissions for actions on the kube cluster
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
* @returns `true` if the actions described are allowed
*/
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
export type CreateCanI = (api: AuthorizationV1Api) => CanI;
const createCanIInjectable = getInjectable({
id: "create-can-i",
instantiate: (di): CreateCanI => {
const logger = di.inject(loggerInjectable);
return (api) => async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
try {
const { body } = await api.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes },
});
return body.status?.allowed ?? false;
} catch (error) {
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
return false;
}
};
},
});
export default createCanIInjectable;

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
export type CreateCoreApi = (config: KubeConfig) => CoreV1Api;
const createCoreApiInjectable = getInjectable({
id: "create-core-api",
instantiate: (): CreateCoreApi => config => config.makeApiClient(CoreV1Api),
});
export default createCoreApiInjectable;

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac";
export type CanListResource = (resource: KubeApiResource) => boolean;
/**
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
*/
export type RequestNamespaceListPermissions = (namespace: string) => Promise<CanListResource>;
export type CreateRequestNamespaceListPermissions = (api: AuthorizationV1Api) => RequestNamespaceListPermissions;
const createRequestNamespaceListPermissionsInjectable = getInjectable({
id: "create-request-namespace-list-permissions",
instantiate: (di): CreateRequestNamespaceListPermissions => {
const logger = di.inject(loggerInjectable);
return (api) => async (namespace) => {
try {
const { body: { status }} = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectRulesReview",
spec: { namespace },
});
if (!status || status.incomplete) {
logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`);
return () => true;
}
const { resourceRules } = status;
return (resource) => (
resourceRules
.filter(({ apiGroups = ["*"] }) => apiGroups.includes("*") || apiGroups.includes(resource.group))
.filter(({ resources = ["*"] }) => resources.includes("*") || resources.includes(resource.apiName))
.some(({ verbs }) => verbs.includes("*") || verbs.includes("list"))
);
} catch (error) {
logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error });
return () => true;
}
};
},
});
export default createRequestNamespaceListPermissionsInjectable;

View File

@ -4,7 +4,7 @@
*/ */
import type { ClusterId } from "../cluster-types"; import type { ClusterId } from "../cluster-types";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; import type { MessageChannel } from "@k8slens/messaging";
export const currentClusterMessageChannel: MessageChannel<ClusterId> = { export const currentClusterMessageChannel: MessageChannel<ClusterId> = {
id: "current-visible-cluster", id: "current-visible-cluster",

View File

@ -2,27 +2,21 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { KubeConfig } from "@kubernetes/client-node"; import type { CoreV1Api } from "@kubernetes/client-node";
import { CoreV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { isDefined } from "@k8slens/utilities"; import { isDefined } from "@k8slens/utilities";
export type ListNamespaces = () => Promise<string[]>; export type ListNamespaces = () => Promise<string[]>;
export type CreateListNamespaces = (api: CoreV1Api) => ListNamespaces;
export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces;
const createListNamespacesInjectable = getInjectable({ const createListNamespacesInjectable = getInjectable({
id: "create-list-namespaces", id: "create-list-namespaces",
instantiate: (): CreateListNamespaces => (config) => { instantiate: (): CreateListNamespaces => (api) => async () => {
const coreApi = config.makeApiClient(CoreV1Api); const { body: { items }} = await api.listNamespace();
return async () => { return items
const { body: { items }} = await coreApi.listNamespace(); .map(ns => ns.metadata?.name)
.filter(isDefined);
return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
};
}, },
}); });

View File

@ -1,72 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac";
export type CanListResource = (resource: KubeApiResource) => boolean;
/**
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
*/
export type RequestNamespaceListPermissions = (namespace: string) => Promise<CanListResource>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type RequestNamespaceListPermissionsFor = (proxyConfig: KubeConfig) => RequestNamespaceListPermissions;
const requestNamespaceListPermissionsForInjectable = getInjectable({
id: "request-namespace-list-permissions-for",
instantiate: (di): RequestNamespaceListPermissionsFor => {
const logger = di.inject(loggerInjectable);
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (namespace) => {
try {
const { body: { status }} = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectRulesReview",
spec: { namespace },
});
if (!status || status.incomplete) {
logger.warn(`[AUTHORIZATION-NAMESPACE-REVIEW]: allowing all resources in namespace="${namespace}" due to incomplete SelfSubjectRulesReview: ${status?.evaluationError}`);
return () => true;
}
const { resourceRules } = status;
return (resource) => {
const rules = resourceRules.filter(({
apiGroups = ["*"],
resources = ["*"],
}) => {
const isAboutRelevantApiGroup = apiGroups.includes("*") || apiGroups.includes(resource.group);
const isAboutResource = resources.includes("*") || resources.includes(resource.apiName);
return isAboutRelevantApiGroup && isAboutResource;
});
return rules.some(({ verbs }) => verbs.includes("*") || verbs.includes("list"));
};
} catch (error) {
logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review`, { namespace, error });
return () => true;
}
};
};
},
});
export default requestNamespaceListPermissionsForInjectable;

View File

@ -3,334 +3,225 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { V1SubjectRulesReviewStatus } from "@kubernetes/client-node"; import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { AuthorizationV1Api, V1SubjectRulesReviewStatus } from "@kubernetes/client-node";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import type { IncomingMessage } from "http";
import { anyObject } from "jest-mock-extended";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable"; import { cast } from "../../test-utils/cast";
import requestNamespaceListPermissionsForInjectable from "./request-namespace-list-permissions.injectable"; import type { KubeApiResource } from "../rbac";
import type { RequestNamespaceListPermissions } from "./create-request-namespace-list-permissions.injectable";
import createRequestNamespaceListPermissionsInjectable from "./create-request-namespace-list-permissions.injectable";
const createStubProxyConfig = (statusResponse: Promise<{ body: { status: V1SubjectRulesReviewStatus }}>) => ({ interface TestCase {
makeApiClient: () => ({ description: string;
createSelfSubjectRulesReview: (): Promise<{ body: { status: V1SubjectRulesReviewStatus }}> => statusResponse, status: V1SubjectRulesReviewStatus;
}), expected: boolean;
}); }
describe("requestNamespaceListPermissions", () => { describe("requestNamespaceListPermissions", () => {
let di: DiContainer; let di: DiContainer;
let requestNamespaceListPermissions: RequestNamespaceListPermissionsFor; let createSelfSubjectRulesReviewMock: AsyncFnMock<AuthorizationV1Api["createSelfSubjectRulesReview"]>;
let requestNamespaceListPermissions: RequestNamespaceListPermissions;
beforeEach(() => { beforeEach(() => {
di = getDiForUnitTesting(); di = getDiForUnitTesting();
requestNamespaceListPermissions = di.inject(requestNamespaceListPermissionsForInjectable);
const createRequestNamespaceListPermissions = di.inject(createRequestNamespaceListPermissionsInjectable);
createSelfSubjectRulesReviewMock = asyncFn();
requestNamespaceListPermissions = createRequestNamespaceListPermissions(cast<AuthorizationV1Api>({
createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock,
}));
}); });
describe("when api returns incomplete data", () => { describe("when a request for list permissions in a namespace has been started", () => {
it("returns truthy function", async () => { let request: ReturnType<RequestNamespaceListPermissions>;
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: true,
resourceRules: [],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace"); beforeEach(() => {
request = requestNamespaceListPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
}); });
});
describe("when api rejects", () => { it("should request the creation of a SelfSubjectRulesReview", () => {
it("returns truthy function", async () => { expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( spec: {
new Promise((resolve, reject) => reject("unknown error")), namespace: "irrelevant-namespace",
) as any); },
}));
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
}); });
});
describe("when first resourceRule has all permissions for everything", () => { ([
it("return truthy function", async () => { {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( description: "incomplete data",
new Promise((resolve) => resolve({ status: {
body: { incomplete: true,
status: { resourceRules: [],
incomplete: false, nonResourceRules: [],
resourceRules: [ },
{ expected: true,
apiGroups: ["*"], },
verbs: ["*"], {
}, description: "first resourceRule has all permissions for everything",
{ status: {
apiGroups: ["*"], incomplete: false,
verbs: ["get"], resourceRules: [
}, {
], apiGroups: ["*"],
nonResourceRules: [], verbs: ["*"],
}, },
}, {
})), apiGroups: ["*"],
) as any); verbs: ["get"],
},
],
nonResourceRules: [],
},
expected: true,
},
{
description: "first resourceRule has list permissions for everything",
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["list"],
},
{
apiGroups: ["*"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
expected: true,
},
{
description: "first resourceRule has list permissions for asked resource",
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["some-api-group"],
resources: ["some-kind"],
verbs: ["list"],
},
{
apiGroups: ["*"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
expected: true,
},
{
description: "last resourceRule has all permissions for everything",
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["get"],
},
{
apiGroups: ["*"],
verbs: ["*"],
},
],
nonResourceRules: [],
},
expected: true,
},
{
description: "last resourceRule has list permissions for asked resource",
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["get"],
},
{
apiGroups: ["some-api-group"],
resources: ["some-kind"],
verbs: ["list"],
},
],
nonResourceRules: [],
},
expected: true,
},
{
description: "resourceRules has matching resource without list verb",
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["some-api-group"],
resources: ["some-kind"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
expected: false,
},
{
description: "resourceRules has no matching resource with list verb",
status: {
incomplete: false,
resourceRules: [
{
apiGroups: [""],
resources: ["services"],
verbs: ["list"],
},
],
nonResourceRules: [],
},
expected: false,
},
] as TestCase[]).forEach(({ description, status, expected }) => {
describe(`when api returns ${description}`, () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve({
body: {
status,
spec: {},
},
response: null as unknown as IncomingMessage,
});
});
const permissionCheck = await requestPermissions("irrelevant-namespace"); it(`allows the request to complete, and 'canListResource' will return ${expected}`, async () => {
const canListResource = await request;
expect(permissionCheck({ expect(canListResource(someKubeResource)).toBe(expected);
apiName: "pods", });
group: "", });
kind: "Pod",
namespaced: true,
})).toBeTruthy();
}); });
});
describe("when first resourceRule has list permissions for everything", () => { describe("when api rejects", () => {
it("return truthy function", async () => { beforeEach(async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig( await createSelfSubjectRulesReviewMock.reject(new Error("unknown error"));
new Promise((resolve) => resolve({ });
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["list"],
},
{
apiGroups: ["*"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace"); it("allows the request to complete, and 'canListResource' will return true", async () => {
const canListResource = await request;
expect(permissionCheck({ expect(canListResource(someKubeResource)).toBe(true);
apiName: "pods", });
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
});
});
describe("when first resourceRule has list permissions for asked resource", () => {
it("return truthy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
},
{
apiGroups: ["*"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
});
});
describe("when last resourceRule has all permissions for everything", () => {
it("return truthy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["get"],
},
{
apiGroups: ["*"],
verbs: ["*"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
});
});
describe("when last resourceRule has list permissions for everything", () => {
it("return truthy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["get"],
},
{
apiGroups: ["*"],
verbs: ["list"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
});
});
describe("when last resourceRule has list permissions for asked resource", () => {
it("return truthy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["get"],
},
{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
});
});
describe("when resourceRules has matching resource without list verb", () => {
it("return falsy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: [""],
resources: ["pods"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeFalsy();
});
});
describe("when resourceRules has no matching resource with list verb", () => {
it("return falsy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: false,
resourceRules: [
{
apiGroups: [""],
resources: ["services"],
verbs: ["list"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeFalsy();
}); });
}); });
}); });
const someKubeResource: KubeApiResource = {
apiName: "some-kind",
group: "some-api-group",
kind: "SomeKind",
namespaced: true,
};

View File

@ -4,7 +4,7 @@
*/ */
import type { ClusterId } from "../cluster-types"; import type { ClusterId } from "../cluster-types";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; import type { MessageChannel } from "@k8slens/messaging";
export const clusterVisibilityChannel: MessageChannel<ClusterId | null> = { export const clusterVisibilityChannel: MessageChannel<ClusterId | null> = {
id: "cluster-visibility", id: "cluster-visibility",

View File

@ -13,7 +13,7 @@ import userStoreInjectable from "../user-store/user-store.injectable";
export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void; export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void;
const mapProcessName = (type: "browser" | "renderer" | "worker") => type === "browser" ? "main" : type; const mapProcessName = (type: "browser" | "renderer" | "worker" | "utility") => type === "browser" ? "main" : type;
const initializeSentryReportingWithInjectable = getInjectable({ const initializeSentryReportingWithInjectable = getInjectable({
id: "initialize-sentry-reporting-with", id: "initialize-sentry-reporting-with",

View File

@ -3,13 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { formatDuration } from "@k8slens/utilities";
/** /**
* Creates an AbortController with an associated timeout * Creates an AbortController with an associated timeout
* @param timeout The number of milliseconds before this controller will auto abort * @param timeout The number of milliseconds before this controller will auto abort
*/ */
export function withTimeout(timeout: number): AbortController { export function withTimeout(timeout: number): AbortController {
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout); const id = setTimeout(() => controller.abort(`Operation timed out: timeout ${formatDuration(timeout)}`), timeout);
controller.signal.addEventListener("abort", () => clearTimeout(id)); controller.signal.addEventListener("abort", () => clearTimeout(id));

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { IpcRendererNavigationEvents } from "../ipc/navigation-events"; import { IpcRendererNavigationEvents } from "../ipc/navigation-events";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; import type { MessageChannel } from "@k8slens/messaging";
export type AppNavigationChannel = MessageChannel<string>; export type AppNavigationChannel = MessageChannel<string>;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { IpcRendererNavigationEvents } from "../ipc/navigation-events"; import { IpcRendererNavigationEvents } from "../ipc/navigation-events";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; import type { MessageChannel } from "@k8slens/messaging";
export type ClusterFrameNavigationChannel = MessageChannel<string>; export type ClusterFrameNavigationChannel = MessageChannel<string>;

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import copyInjectable from "./copy.injectable";
export default getGlobalOverride(copyInjectable, () => async () => {
throw new Error("tried to copy filepaths without override");
});

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import lstatInjectable from "./lstat.injectable";
export default getGlobalOverride(lstatInjectable, () => async () => {
throw new Error("tried to lstat a filepath without override");
});

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import readDirectoryInjectable from "./read-directory.injectable";
export default getGlobalOverride(readDirectoryInjectable, () => async () => {
throw new Error("tried to read a directory's content without override");
});

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import removePathInjectable from "./remove.injectable";
export default getGlobalOverride(removePathInjectable, () => async () => {
throw new Error("tried to remove path without override");
});

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import writeFileInjectable from "./write-file.injectable";
export default getGlobalOverride(writeFileInjectable, () => async () => {
throw new Error("tried to write file without override");
});

View File

@ -3,11 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { HelmRepo } from "./helm-repo"; import type { HelmRepo } from "./helm-repo";
import type { AsyncResult } from "@k8slens/utilities"; import type { Result } from "@k8slens/utilities";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
export type AddHelmRepositoryChannel = RequestChannel<HelmRepo, AsyncResult<void, string>>; export const addHelmRepositoryChannel = getRequestChannel<
HelmRepo,
export const addHelmRepositoryChannel: AddHelmRepositoryChannel = { Result<void, string>
id: "add-helm-repository-channel", >("add-helm-repository-channel");
};

View File

@ -4,10 +4,9 @@
*/ */
import type { HelmRepo } from "./helm-repo"; import type { HelmRepo } from "./helm-repo";
import type { AsyncResult } from "@k8slens/utilities"; import type { AsyncResult } from "@k8slens/utilities";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
export type GetActiveHelmRepositoriesChannel = RequestChannel<void, AsyncResult<HelmRepo[]>>; export const getActiveHelmRepositoriesChannel = getRequestChannel<
void,
export const getActiveHelmRepositoriesChannel: GetActiveHelmRepositoriesChannel = { AsyncResult<HelmRepo[]>
id: "get-helm-active-list-repositories", >("get-helm-active-list-repositories");
};

View File

@ -3,11 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { AsyncResult } from "@k8slens/utilities"; import type { AsyncResult } from "@k8slens/utilities";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
import type { HelmRepo } from "./helm-repo"; import type { HelmRepo } from "./helm-repo";
export type RemoveHelmRepositoryChannel = RequestChannel<HelmRepo, AsyncResult<void, string>>; export const removeHelmRepositoryChannel = getRequestChannel<
HelmRepo,
export const removeHelmRepositoryChannel: RemoveHelmRepositoryChannel = { AsyncResult<void, string>
id: "remove-helm-repository-channel", >("remove-helm-repository-channel");
};

View File

@ -14,7 +14,7 @@ import { hotbarStoreMigrationInjectionToken } from "./migrations-token";
import getBasenameOfPathInjectable from "../path/get-basename.injectable"; import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix"; import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync"; import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
const hotbarStoreInjectable = getInjectable({ const hotbarStoreInjectable = getInjectable({

View File

@ -4,6 +4,7 @@
*/ */
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable"; import clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable"; import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
@ -21,6 +22,9 @@ import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api"; import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api";
import { Cluster } from "../../cluster/cluster"; import { Cluster } from "../../cluster/cluster";
import { runInAction } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "../api-manager/crd-api-token";
import assert from "assert";
class TestApi extends KubeApi<KubeObject> { class TestApi extends KubeApi<KubeObject> {
protected async checkPreferredVersion() { protected async checkPreferredVersion() {
@ -117,4 +121,90 @@ describe("ApiManager", () => {
}); });
}); });
}); });
describe("given than a CRD has a default KubeApi registered for it", () => {
const apiBase = "/apis/aquasecurity.github.io/v1alpha1/vulnerabilityreports";
beforeEach(() => {
runInAction(() => {
di.register(getInjectable({
id: `default-kube-api-for-custom-resource-definition-${apiBase}`,
instantiate: (di) => {
const objectConstructor = class extends KubeObject {
static readonly kind = "VulnerabilityReport";
static readonly namespaced = true;
static readonly apiBase = apiBase;
};
return Object.assign(
new KubeApi({
logger: di.inject(loggerInjectable),
maybeKubeApi: di.inject(maybeKubeApiInjectable),
}, { objectConstructor }),
{
myField: 1,
},
);
},
injectionToken: customResourceDefinitionApiInjectionToken,
}));
});
});
it("can be retrieved from apiManager", () => {
expect(apiManager.getApi(apiBase)).toMatchObject({
myField: 1,
});
});
it("can have a default KubeObjectStore instance retrieved for it", () => {
expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore);
});
describe("given that an extension registers an api with the same apibase", () => {
beforeEach(() => {
void Object.assign(new ExternalKubeApi({
objectConstructor: KubeObject,
apiBase,
kind: "VulnerabilityReport",
}), {
myField: 2,
});
});
it("the extension's instance is retrievable instead from apiManager", () => {
expect(apiManager.getApi(apiBase)).toMatchObject({
myField: 2,
});
});
it("can have a default KubeObjectStore instance retrieved for it", () => {
expect(apiManager.getStore(apiBase)).toBeInstanceOf(KubeObjectStore);
});
describe("given that an extension registers a store for the same apibase", () => {
beforeEach(() => {
const api = apiManager.getApi(apiBase);
assert(api);
apiManager.registerStore(Object.assign(
new KubeObjectStore({
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
}, api),
{
someField: 2,
},
));
});
it("can gets the custom KubeObjectStore instance instead", () => {
expect(apiManager.getStore(apiBase)).toMatchObject({
someField: 2,
});
});
});
});
});
}); });

View File

@ -22,7 +22,6 @@ import { flushPromises } from "@k8slens/test-utils";
import createKubeJsonApiInjectable from "../create-kube-json-api.injectable"; import createKubeJsonApiInjectable from "../create-kube-json-api.injectable";
import type { IKubeWatchEvent } from "../kube-watch-event"; import type { IKubeWatchEvent } from "../kube-watch-event";
import type { KubeJsonApiDataFor, KubeStatusData } from "../kube-object"; import type { KubeJsonApiDataFor, KubeStatusData } from "../kube-object";
import AbortController from "abort-controller";
import setupAutoRegistrationInjectable import setupAutoRegistrationInjectable
from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable"; from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable";
import { import {

View File

@ -10,7 +10,8 @@ import { autorun, action, observable } from "mobx";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
import type { KubeObject, ObjectReference } from "../kube-object"; import type { KubeObject, ObjectReference } from "../kube-object";
import { parseKubeApi, createKubeApiURL } from "../kube-api-parse"; import { parseKubeApi, createKubeApiURL } from "../kube-api-parse";
import { iter } from "@k8slens/utilities"; import { getOrInsertWith, iter } from "@k8slens/utilities";
import type { CreateCustomResourceStore } from "./create-custom-resource-store.injectable";
export type RegisterableStore<Store> = Store extends KubeObjectStore<any, any, any> export type RegisterableStore<Store> = Store extends KubeObjectStore<any, any, any>
? Store ? Store
@ -26,13 +27,15 @@ export type FindApiCallback = (api: KubeApi<KubeObject>) => boolean;
interface Dependencies { interface Dependencies {
readonly apis: IComputedValue<KubeApi[]>; readonly apis: IComputedValue<KubeApi[]>;
readonly crdApis: IComputedValue<KubeApi[]>;
readonly stores: IComputedValue<KubeObjectStore[]>; readonly stores: IComputedValue<KubeObjectStore[]>;
createCustomResourceStore: CreateCustomResourceStore;
} }
export class ApiManager { export class ApiManager {
private readonly externalApis = observable.array<KubeApi>(); private readonly externalApis = observable.array<KubeApi>();
private readonly externalStores = observable.array<KubeObjectStore>(); private readonly externalStores = observable.array<KubeObjectStore>();
private readonly defaultCrdStores = observable.map<string, KubeObjectStore>();
private readonly apis = observable.map<string, KubeApi>(); private readonly apis = observable.map<string, KubeApi>();
constructor(private readonly dependencies: Dependencies) { constructor(private readonly dependencies: Dependencies) {
@ -56,6 +59,12 @@ export class ApiManager {
} }
} }
for (const crdApi of this.dependencies.crdApis.get()) {
if (!newState.has(crdApi.apiBase)) {
newState.set(crdApi.apiBase, crdApi);
}
}
this.apis.replace(newState); this.apis.replace(newState);
}); });
} }
@ -110,6 +119,16 @@ export class ApiManager {
this.externalStores.push(store); this.externalStores.push(store);
} }
private apiIsDefaultCrdApi(api: KubeApi): boolean {
for (const crdApi of this.dependencies.crdApis.get()) {
if (crdApi.apiBase === api.apiBase) {
return true;
}
}
return false;
}
getStore(api: string | undefined): KubeObjectStore | undefined; getStore(api: string | undefined): KubeObjectStore | undefined;
getStore<Api>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined; getStore<Api>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined;
/** /**
@ -130,9 +149,19 @@ export class ApiManager {
return undefined; return undefined;
} }
return iter.chain(this.dependencies.stores.get().values()) const defaultResult = iter.chain(this.dependencies.stores.get().values())
.concat(this.externalStores.values()) .concat(this.externalStores.values())
.find(store => store.api.apiBase === api.apiBase); .find(store => store.api.apiBase === api.apiBase);
if (defaultResult) {
return defaultResult;
}
if (this.apiIsDefaultCrdApi(api)) {
return getOrInsertWith(this.defaultCrdStores, api.apiBase, () => this.dependencies.createCustomResourceStore(api));
}
return undefined;
} }
lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string { lookupApiLink(ref: ObjectReference, parentObject?: KubeObject): string {

View File

@ -5,11 +5,9 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import EventEmitter from "events"; import EventEmitter from "events";
import type TypedEventEmitter from "typed-emitter"; import type TypedEventEmitter from "typed-emitter";
import type { CustomResourceDefinition } from "../endpoints";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
export interface LegacyAutoRegistration { export interface LegacyAutoRegistration {
customResourceDefinition: (crd: CustomResourceDefinition) => void;
kubeApi: (api: KubeApi<any, any>) => void; kubeApi: (api: KubeApi<any, any>) => void;
} }

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { KubeApi } from "../kube-api";
export const customResourceDefinitionApiInjectionToken = getInjectionToken<KubeApi>({
id: "custom-resource-definition-api-token",
});

View File

@ -0,0 +1,27 @@
/**
* 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 clusterFrameContextForNamespacedResourcesInjectable from "../../../renderer/cluster-frame-context/for-namespaced-resources.injectable";
import loggerInjectable from "../../logger.injectable";
import type { KubeApi } from "../kube-api";
import type { KubeObject } from "../kube-object";
import type { KubeObjectStoreDependencies } from "../kube-object.store";
import { CustomResourceStore } from "./resource.store";
export type CreateCustomResourceStore = <K extends KubeObject>(api: KubeApi<K>) => CustomResourceStore<K>;
const createCustomResourceStoreInjectable = getInjectable({
id: "create-custom-resource-store",
instantiate: (di): CreateCustomResourceStore => {
const deps: KubeObjectStoreDependencies = {
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
};
return (api) => new CustomResourceStore(deps, api);
},
});
export default createCustomResourceStoreInjectable;

View File

@ -9,6 +9,8 @@ import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-f
import { kubeObjectStoreInjectionToken } from "./kube-object-store-token"; import { kubeObjectStoreInjectionToken } from "./kube-object-store-token";
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token"; import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
import { computed } from "mobx"; import { computed } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "./crd-api-token";
import createCustomResourceStoreInjectable from "./create-custom-resource-store.injectable";
const apiManagerInjectable = getInjectable({ const apiManagerInjectable = getInjectable({
id: "api-manager", id: "api-manager",
@ -23,6 +25,10 @@ const apiManagerInjectable = getInjectable({
stores: storesAndApisCanBeCreated stores: storesAndApisCanBeCreated
? computedInjectMany(kubeObjectStoreInjectionToken) ? computedInjectMany(kubeObjectStoreInjectionToken)
: computed(() => []), : computed(() => []),
crdApis: storesAndApisCanBeCreated
? computedInjectMany(customResourceDefinitionApiInjectionToken)
: computed(() => []),
createCustomResourceStore: di.inject(createCustomResourceStoreInjectable),
}); });
}, },
}); });

View File

@ -20,7 +20,6 @@ import type { Patch } from "rfc6902";
import assert from "assert"; import assert from "assert";
import type { PartialDeep } from "type-fest"; import type { PartialDeep } from "type-fest";
import type { Logger } from "../logger"; import type { Logger } from "../logger";
import type AbortController from "abort-controller";
import { matches } from "lodash/fp"; import { matches } from "lodash/fp";
import { makeObservable, observable } from "mobx"; import { makeObservable, observable } from "mobx";

View File

@ -17,7 +17,6 @@ import type { Patch } from "rfc6902";
import type { Logger } from "../logger"; import type { Logger } from "../logger";
import assert from "assert"; import assert from "assert";
import type { PartialDeep } from "type-fest"; import type { PartialDeep } from "type-fest";
import AbortController from "abort-controller";
import type { ClusterContext } from "../../renderer/cluster-frame-context/cluster-frame-context"; import type { ClusterContext } from "../../renderer/cluster-frame-context/cluster-frame-context";
import autoBind from "auto-bind"; import autoBind from "auto-bind";
@ -89,7 +88,7 @@ export interface KubeObjectStoreDependencies {
readonly logger: Logger; readonly logger: Logger;
} }
export abstract class KubeObjectStore< export class KubeObjectStore<
K extends KubeObject = KubeObject, K extends KubeObject = KubeObject,
A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>, A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>,
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>, D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,

View File

@ -4,11 +4,9 @@
*/ */
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import type { Asyncify } from "type-fest";
import type { RequestChannelHandler } from "../../main/utils/channel/channel-listeners/listener-tokens";
import type { ClusterId } from "../cluster-types"; import type { ClusterId } from "../cluster-types";
import type { AsyncResult } from "@k8slens/utilities"; import type { AsyncResult, Result } from "@k8slens/utilities";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
export interface KubectlApplyAllArgs { export interface KubectlApplyAllArgs {
clusterId: ClusterId; clusterId: ClusterId;
@ -16,11 +14,12 @@ export interface KubectlApplyAllArgs {
extraArgs: string[]; extraArgs: string[];
} }
export const kubectlApplyAllChannel: RequestChannel<KubectlApplyAllArgs, AsyncResult<string, string>> = { export const kubectlApplyAllChannel = getRequestChannel<
id: "kubectl-apply-all", KubectlApplyAllArgs,
}; Result<string, string>
>("kubectl-apply-all");
export type KubectlApplyAll = Asyncify<RequestChannelHandler<typeof kubectlApplyAllChannel>>; export type KubectlApplyAll = (req: KubectlApplyAllArgs) => AsyncResult<string, string>;
export const kubectlApplyAllInjectionToken = getInjectionToken<KubectlApplyAll>({ export const kubectlApplyAllInjectionToken = getInjectionToken<KubectlApplyAll>({
id: "kubectl-apply-all", id: "kubectl-apply-all",
@ -32,11 +31,12 @@ export interface KubectlDeleteAllArgs {
extraArgs: string[]; extraArgs: string[];
} }
export const kubectlDeleteAllChannel: RequestChannel<KubectlDeleteAllArgs, AsyncResult<string, string>> = { export const kubectlDeleteAllChannel = getRequestChannel<
id: "kubectl-delete-all", KubectlDeleteAllArgs,
}; Result<string, string>
>("kubectl-delete-all");
export type KubectlDeleteAll = Asyncify<RequestChannelHandler<typeof kubectlDeleteAllChannel>>; export type KubectlDeleteAll = (req: KubectlDeleteAllArgs) => AsyncResult<string, string>;
export const kubectlDeleteAllInjectionToken = getInjectionToken<KubectlDeleteAll>({ export const kubectlDeleteAllInjectionToken = getInjectionToken<KubectlDeleteAll>({
id: "kubectl-delete-all", id: "kubectl-delete-all",

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token"; import type { MessageChannel } from "@k8slens/messaging";
export type RootFrameHasRenderedChannel = MessageChannel<void>; export type RootFrameHasRenderedChannel = MessageChannel<void>;

View File

@ -16,7 +16,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync"; import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file"; import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getBasenameOfPathInjectable from "../path/get-basename.injectable"; import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import userStorePreferenceDescriptorsInjectable from "./preference-descriptors.injectable"; import userStorePreferenceDescriptorsInjectable from "./preference-descriptors.injectable";
const userStoreInjectable = getInjectable({ const userStoreInjectable = getInjectable({

View File

@ -1,245 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { SendMessageToChannel } from "./message-to-channel-injection-token";
import { sendMessageToChannelInjectionToken } from "./message-to-channel-injection-token";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { MessageChannel } from "./message-channel-listener-injection-token";
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
import type { RequestFromChannel } from "./request-from-channel-injection-token";
import { requestFromChannelInjectionToken } from "./request-from-channel-injection-token";
import type { RequestChannel } from "./request-channel-listener-injection-token";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "@k8slens/test-utils";
import { runInAction } from "mobx";
import type { RequestChannelHandler } from "../../../main/utils/channel/channel-listeners/listener-tokens";
import {
getRequestChannelListenerInjectable,
requestChannelListenerInjectionToken,
} from "../../../main/utils/channel/channel-listeners/listener-tokens";
type TestMessageChannel = MessageChannel<string>;
type TestRequestChannel = RequestChannel<string, string>;
describe("channel", () => {
describe("messaging from main to renderer, given listener for channel in a window and application has started", () => {
let messageListenerInWindowMock: jest.Mock;
let mainDi: DiContainer;
let messageToChannel: SendMessageToChannel;
let builder: ApplicationBuilder;
beforeEach(async () => {
builder = getApplicationBuilder();
messageListenerInWindowMock = jest.fn();
const testChannelListenerInTestWindowInjectable = getInjectable({
id: "test-channel-listener-in-test-window",
instantiate: () => ({
channel: testMessageChannel,
handler: messageListenerInWindowMock,
}),
injectionToken: messageChannelListenerInjectionToken,
});
builder.beforeWindowStart(({ windowDi }) => {
runInAction(() => {
windowDi.register(testChannelListenerInTestWindowInjectable);
});
});
mainDi = builder.mainDi;
await builder.startHidden();
messageToChannel = mainDi.inject(sendMessageToChannelInjectionToken);
});
describe("given window is started", () => {
let someWindowFake: LensWindow;
beforeEach(async () => {
someWindowFake = builder.applicationWindow.create("some-window");
await someWindowFake.start();
});
it("when sending message, triggers listener in window", () => {
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInWindowMock).toHaveBeenCalledWith("some-message");
});
it("given window is hidden, when sending message, does not trigger listener in window", () => {
someWindowFake.close();
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInWindowMock).not.toHaveBeenCalled();
});
});
it("given multiple started windows, when sending message, triggers listeners in all windows", async () => {
const someWindowFake = builder.applicationWindow.create("some-window");
const someOtherWindowFake = builder.applicationWindow.create("some-other-window");
await someWindowFake.start();
await someOtherWindowFake.start();
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInWindowMock.mock.calls).toEqual([
["some-message"],
["some-message"],
]);
});
});
describe("messaging from renderer to main, given listener for channel in a main and application has started", () => {
let messageListenerInMainMock: jest.Mock;
let messageToChannel: SendMessageToChannel;
beforeEach(async () => {
const applicationBuilder = getApplicationBuilder();
messageListenerInMainMock = jest.fn();
const testChannelListenerInMainInjectable = getInjectable({
id: "test-channel-listener-in-main",
instantiate: () => ({
channel: testMessageChannel,
handler: messageListenerInMainMock,
}),
injectionToken: messageChannelListenerInjectionToken,
});
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
});
});
await applicationBuilder.render();
const windowDi = applicationBuilder.applicationWindow.only.di;
messageToChannel = windowDi.inject(sendMessageToChannelInjectionToken);
});
it("when sending message, triggers listener in main", () => {
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInMainMock).toHaveBeenCalledWith("some-message");
});
});
describe("requesting from main in renderer, given listener for channel in a main and application has started", () => {
let requestListenerInMainMock: AsyncFnMock<RequestChannelHandler<TestRequestChannel>>;
let requestFromChannel: RequestFromChannel;
beforeEach(async () => {
const applicationBuilder = getApplicationBuilder();
requestListenerInMainMock = asyncFn();
const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({
channel: testRequestChannel,
handler: () => requestListenerInMainMock,
});
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
});
});
await applicationBuilder.render();
const windowDi = applicationBuilder.applicationWindow.only.di;
requestFromChannel = windowDi.inject(
requestFromChannelInjectionToken,
);
});
describe("when requesting from channel", () => {
let actualPromise: Promise<string>;
beforeEach(() => {
actualPromise = requestFromChannel(testRequestChannel, "some-request");
});
it("triggers listener in main", () => {
expect(requestListenerInMainMock).toHaveBeenCalledWith("some-request");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when main resolves with response, resolves with response", async () => {
await requestListenerInMainMock.resolve("some-response");
const actual = await actualPromise;
expect(actual).toBe("some-response");
});
});
});
it("when registering multiple handlers for the same channel, throws", async () => {
const applicationBuilder = getApplicationBuilder();
const someChannelListenerInjectable = getInjectable({
id: "some-channel-listener",
instantiate: () => ({
channel: testRequestChannel,
handler: () => () => "irrelevant",
}),
injectionToken: requestChannelListenerInjectionToken,
});
const someOtherChannelListenerInjectable = getInjectable({
id: "some-other-channel-listener",
instantiate: () => ({
channel: testRequestChannel,
handler: () => () => "irrelevant",
}),
injectionToken: requestChannelListenerInjectionToken,
});
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
runInAction(() => {
mainDi.register(someChannelListenerInjectable);
mainDi.register(someOtherChannelListenerInjectable);
});
});
await expect(applicationBuilder.render()).rejects.toThrow('Tried to register a multiple channel handlers for "some-request-channel-id", only one handler is supported for a request channel.');
});
});
const testMessageChannel: TestMessageChannel = {
id: "some-message-channel-id",
};
const testRequestChannel: TestRequestChannel = {
id: "some-request-channel-id",
};

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 { getInjectionToken } from "@ogre-tools/injectable";
import type { Disposer } from "@k8slens/utilities";
import type { MessageChannel, MessageChannelListener } from "./message-channel-listener-injection-token";
export type EnlistMessageChannelListener = (listener: MessageChannelListener<MessageChannel<unknown>>) => Disposer;
export const enlistMessageChannelListenerInjectionToken = getInjectionToken<EnlistMessageChannelListener>({
id: "enlist-message-channel-listener",
});

View File

@ -1,9 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestChannel } from "./request-channel-listener-injection-token";
export const getRequestChannel = <Request, Response>(id: string): RequestChannel<Request, Response> => ({
id,
});

View File

@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getStartableStoppable } from "../get-startable-stoppable";
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token";
import { disposer } from "@k8slens/utilities";
const listeningOnMessageChannelsInjectable = getInjectable({
id: "listening-on-message-channels",
instantiate: (di) => {
const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken);
return getStartableStoppable("listening-on-channels", () => (
disposer(messageChannelListeners.map(enlistMessageChannelListener))
));
},
});
export default listeningOnMessageChannelsInjectable;

View File

@ -1,51 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainerForInjection } from "@ogre-tools/injectable";
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
export interface MessageChannel<Message> {
id: string;
_messageSignature?: Message; // only used to mark `Message` as used
}
export type MessageChannelHandler<Channel> = Channel extends MessageChannel<infer Message>
? (message: Message) => void
: never;
export interface MessageChannelListener<Channel> {
channel: Channel;
handler: MessageChannelHandler<Channel>;
}
export const messageChannelListenerInjectionToken = getInjectionToken<MessageChannelListener<MessageChannel<unknown>>>(
{
id: "message-channel-listener",
},
);
export interface GetMessageChannelListenerInfo<
Channel extends MessageChannel<Message>,
Message,
> {
id: string;
channel: Channel;
handler: (di: DiContainerForInjection) => MessageChannelHandler<Channel>;
causesSideEffects?: boolean;
}
export function getMessageChannelListenerInjectable<
Channel extends MessageChannel<Message>,
Message,
>(info: GetMessageChannelListenerInfo<Channel, Message>) {
return getInjectable({
id: `${info.channel.id}-listener-${info.id}`,
instantiate: (di) => ({
channel: info.channel,
handler: info.handler(di),
}),
injectionToken: messageChannelListenerInjectionToken,
causesSideEffects: info.causesSideEffects,
});
}

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface RequestChannel<Request, Response> {
id: string;
_requestSignature?: Request; // used only to mark `Request` as "used"
_responseSignature?: Response; // used only to mark `Response` as "used"
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { iter } from "@k8slens/utilities";
import type { DiContainerForInjection, Injectable } from "@ogre-tools/injectable";
// Register new injectables and deregister removed injectables by id
export const injectableDifferencingRegistratorWith = (di: DiContainerForInjection) => (
(rawCurrent: Injectable<any, any, any>[], rawPrevious: Injectable<any, any, any>[] = []) => {
const current = new Map(rawCurrent.map(inj => [inj.id, inj]));
const previous = new Map(rawPrevious.map(inj => [inj.id, inj]));
const toAdd = iter.chain(current.entries())
.filter(([id]) => !previous.has(id))
.collect(entries => new Map(entries));
const toRemove = iter.chain(previous.entries())
.filter(([id]) => !current.has(id))
.collect(entries => new Map(entries));
di.deregister(...toRemove.values());
di.register(...toAdd.values());
}
);

View File

@ -2,10 +2,8 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { RequestChannel } from "../channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
export type ResolveSystemProxyChannel = RequestChannel<string, string>; export const resolveSystemProxyChannel = getRequestChannel<string, string>(
"resolve-system-proxy-channel",
export const resolveSystemProxyChannel: ResolveSystemProxyChannel = { );
id: "resolve-system-proxy-channel",
};

View File

@ -3,13 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { syncBoxChannel } from "./channels"; import { syncBoxChannel } from "./channels";
import { getMessageChannelListenerInjectable } from "../channel/message-channel-listener-injection-token"; import { getMessageChannelListenerInjectable } from "@k8slens/messaging";
import syncBoxStateInjectable from "./sync-box-state.injectable"; import syncBoxStateInjectable from "./sync-box-state.injectable";
const syncBoxChannelListenerInjectable = getMessageChannelListenerInjectable({ const syncBoxChannelListenerInjectable = getMessageChannelListenerInjectable({
id: "init", id: "init",
channel: syncBoxChannel, channel: syncBoxChannel,
handler: (di) => ({ id, value }) => di.inject(syncBoxStateInjectable, id).set(value), getHandler: (di) => ({ id, value }) => di.inject(syncBoxStateInjectable, id).set(value),
}); });
export default syncBoxChannelListenerInjectable; export default syncBoxChannelListenerInjectable;

View File

@ -2,20 +2,12 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { MessageChannel } from "../channel/message-channel-listener-injection-token"; import { getMessageChannel, getRequestChannel } from "@k8slens/messaging";
import type { RequestChannel } from "../channel/request-channel-listener-injection-token";
export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>; export const syncBoxChannel =
getMessageChannel<{ id: string; value: any }>("sync-box-channel");
export const syncBoxChannel: SyncBoxChannel = { export const syncBoxInitialValueChannel = getRequestChannel<
id: "sync-box-channel",
};
export type SyncBoxInitialValueChannel = RequestChannel<
void, void,
{ id: string; value: any }[] { id: string; value: any }[]
>; >("sync-box-initial-value-channel");
export const syncBoxInitialValueChannel: SyncBoxInitialValueChannel = {
id: "sync-box-initial-value-channel",
};

View File

@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import type { IObservableValue } from "mobx"; import type { IObservableValue } from "mobx";
import { computed } from "mobx"; import { computed } from "mobx";
import { syncBoxChannel } from "./channels"; import { syncBoxChannel } from "./channels";
import { sendMessageToChannelInjectionToken } from "../channel/message-to-channel-injection-token"; import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
import syncBoxStateInjectable from "./sync-box-state.injectable"; import syncBoxStateInjectable from "./sync-box-state.injectable";
import type { SyncBox } from "./sync-box-injection-token"; import type { SyncBox } from "./sync-box-injection-token";
import { toJS } from "../toJS"; import { toJS } from "../toJS";

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { MessageChannelHandler } from "../channel/message-channel-listener-injection-token";
import type { SyncBoxChannel } from "./channels";
import syncBoxStateInjectable from "./sync-box-state.injectable";
const syncBoxChannelHandlerInjectable = getInjectable({
id: "sync-box-channel-handler",
instantiate: (di): MessageChannelHandler<SyncBoxChannel> => {
const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
return ({ id, value }) => getSyncBoxState(id)?.set(value);
},
});
export default syncBoxChannelHandlerInjectable;

View File

@ -7,7 +7,7 @@ import { getInjectionToken } from "@ogre-tools/injectable";
import { SemVer } from "semver"; import { SemVer } from "semver";
import type { InitializableState } from "../initializable-state/create"; import type { InitializableState } from "../initializable-state/create";
import { createInitializableState } from "../initializable-state/create"; import { createInitializableState } from "../initializable-state/create";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token"; import type { RequestChannel } from "@k8slens/messaging";
export const buildVersionInjectionToken = getInjectionToken<InitializableState<string>>({ export const buildVersionInjectionToken = getInjectionToken<InitializableState<string>>({
id: "build-version-token", id: "build-version-token",

View File

@ -11,7 +11,7 @@ import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
import getBasenameOfPathInjectable from "../path/get-basename.injectable"; import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import { weblinkStoreMigrationInjectionToken } from "./migration-token"; import { weblinkStoreMigrationInjectionToken } from "./migration-token";
import { WeblinkStore } from "./weblink-store"; import { WeblinkStore } from "./weblink-store";

View File

@ -2,29 +2,18 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { difference, find, map } from "lodash";
import { reaction, runInAction } from "mobx"; import { reaction, runInAction } from "mobx";
import { disposer } from "@k8slens/utilities"; import { disposer } from "@k8slens/utilities";
import type { LensExtension } from "../../lens-extension"; import type { LensExtension } from "../../lens-extension";
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token"; import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper";
export interface Extension { export interface Extension {
register: () => void; register: () => void;
deregister: () => void; deregister: () => void;
} }
const idsToInjectables = (ids: string[], injectables: Injectable<any, any, any>[]) => ids.map(id => {
const injectable = find(injectables, { id });
if (!injectable) {
throw new Error(`Injectable ${id} not found`);
}
return injectable;
});
const extensionInjectable = getInjectable({ const extensionInjectable = getInjectable({
id: "extension", id: "extension",
@ -35,36 +24,27 @@ const extensionInjectable = getInjectable({
instantiate: (childDi) => { instantiate: (childDi) => {
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken); const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
const reactionDisposer = disposer(); const reactionDisposer = disposer();
const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(childDi);
return { return {
register: () => { register: () => {
extensionRegistrators.forEach((getInjectablesOfExtension) => { for (const extensionRegistrator of extensionRegistrators) {
const injectables = getInjectablesOfExtension(instance); const injectables = extensionRegistrator(instance);
reactionDisposer.push( if (Array.isArray(injectables)) {
// injectables is either an array or a computed array, in which case runInAction(() => {
// we need to update the registered injectables with a reaction every time they change injectableDifferencingRegistrator(injectables);
reaction( });
() => Array.isArray(injectables) ? injectables : injectables.get(), } else {
(currentInjectables, previousInjectables = []) => { reactionDisposer.push(reaction(
// Register new injectables and deregister removed injectables by id () => injectables.get(),
const currentIds = map(currentInjectables, "id"); injectableDifferencingRegistrator,
const previousIds = map(previousInjectables, "id"); {
const idsToAdd = difference(currentIds, previousIds);
const idsToRemove = previousIds.filter(previousId => !currentIds.includes(previousId));
if (idsToRemove.length > 0) {
childDi.deregister(...idsToInjectables(idsToRemove, previousInjectables));
}
if (idsToAdd.length > 0) {
childDi.register(...idsToInjectables(idsToAdd, currentInjectables));
}
}, {
fireImmediately: true, fireImmediately: true,
}, },
)); ));
}); }
}
}, },
deregister: () => { deregister: () => {

View File

@ -12,7 +12,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../../../common/base-
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../common/base-store/disable-sync"; import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../common/base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file"; import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file";
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable"; import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable"; import ensureHashedDirectoryForExtensionInjectable from "./ensure-hashed-directory-for-extension.injectable";
import { registeredExtensionsInjectable } from "./registered-extensions.injectable"; import { registeredExtensionsInjectable } from "./registered-extensions.injectable";

View File

@ -20,7 +20,7 @@ import { baseStoreIpcChannelPrefixesInjectionToken } from "../common/base-store/
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../common/base-store/disable-sync"; import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../common/base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../common/base-store/save-to-file"; import { persistStateToConfigInjectionToken } from "../common/base-store/save-to-file";
import getBasenameOfPathInjectable from "../common/path/get-basename.injectable"; import getBasenameOfPathInjectable from "../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../common/utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
export interface ExtensionStoreParams<T extends object> extends BaseStoreParams<T> { export interface ExtensionStoreParams<T extends object> extends BaseStoreParams<T> {
migrations?: Migrations<T>; migrations?: Migrations<T>;

View File

@ -10,7 +10,7 @@ import { persistStateToConfigInjectionToken } from "../../common/base-store/save
import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable"; import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable"; import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../common/utils/channel/enlist-message-channel-listener-injection-token"; import { enlistMessageChannelListenerInjectionToken } from "@k8slens/messaging";
import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable"; import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable";
import { ExtensionsStore } from "./extensions-store"; import { ExtensionsStore } from "./extensions-store";

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { autorun } from "mobx"; import { autorun } from "mobx";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; import { getStartableStoppable } from "@k8slens/startable-stoppable";
import populateApplicationMenuInjectable from "./populate-application-menu.injectable"; import populateApplicationMenuInjectable from "./populate-application-menu.injectable";
import applicationMenuItemCompositeInjectable from "./application-menu-item-composite.injectable"; import applicationMenuItemCompositeInjectable from "./application-menu-item-composite.injectable";

View File

@ -28,7 +28,7 @@ const checkForUpdatesMenuItemInjectable = getInjectable({
id: "check-for-updates", id: "check-for-updates",
parentId: isMac ? "mac" : "help", parentId: isMac ? "mac" : "help",
orderNumber: isMac ? 20 : 50, orderNumber: isMac ? 20 : 50,
label: "Check for updates", label: "Check for Updates...",
isShown: updatingIsEnabled, isShown: updatingIsEnabled,
onClick: () => { onClick: () => {

View File

@ -146,7 +146,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates indicates that checking is happening", () => { it("name of tray item for checking updates indicates that checking is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Checking for updates..."); ).toBe("Checking for Updates...");
}); });
it("user cannot install update yet", () => { it("user cannot install update yet", () => {
@ -177,7 +177,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates no longer indicates that checking is happening", () => { it("name of tray item for checking updates no longer indicates that checking is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates"); ).toBe("Check for Updates...");
}); });
it("renders", () => { it("renders", () => {
@ -241,7 +241,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates no longer indicates that downloading is happening", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates"); ).toBe("Check for Updates...");
}); });
it("renders", () => { it("renders", () => {
@ -269,7 +269,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates no longer indicates that downloading is happening", () => { it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
expect( expect(
builder.tray.get("check-for-updates")?.label, builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates"); ).toBe("Check for Updates...");
}); });
it("renders", () => { it("renders", () => {

View File

@ -47,10 +47,10 @@ const checkForUpdatesTrayItemInjectable = getInjectable({
} }
if (checkingForUpdatesState.value.get()) { if (checkingForUpdatesState.value.get()) {
return "Checking for updates..."; return "Checking for Updates...";
} }
return "Check for updates"; return "Check for Updates...";
}), }),
enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()), enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()),

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { getStartableStoppable } from "../../../../../common/utils/get-startable-stoppable"; import { getStartableStoppable } from "@k8slens/startable-stoppable";
import processCheckingForUpdatesInjectable from "../../../main/process-checking-for-updates.injectable"; import processCheckingForUpdatesInjectable from "../../../main/process-checking-for-updates.injectable";
import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable"; import withOrphanPromiseInjectable from "../../../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { MessageChannel } from "../../../common/utils/channel/message-channel-listener-injection-token"; import type { MessageChannel } from "@k8slens/messaging";
export type RestartAndInstallUpdateChannel = MessageChannel<void>; export type RestartAndInstallUpdateChannel = MessageChannel<void>;

View File

@ -3,13 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { restartAndInstallUpdateChannel } from "../../common/restart-and-install-update-channel"; import { restartAndInstallUpdateChannel } from "../../common/restart-and-install-update-channel";
import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; import { getMessageChannelListenerInjectable } from "@k8slens/messaging";
import quitAndInstallUpdateInjectable from "../quit-and-install-update.injectable"; import quitAndInstallUpdateInjectable from "../quit-and-install-update.injectable";
const restartAndInstallUpdateListenerInjectable = getMessageChannelListenerInjectable({ const restartAndInstallUpdateListenerInjectable = getMessageChannelListenerInjectable({
id: "restart", id: "restart",
channel: restartAndInstallUpdateChannel, channel: restartAndInstallUpdateChannel,
handler: (di) => di.inject(quitAndInstallUpdateInjectable), getHandler: (di) => di.inject(quitAndInstallUpdateInjectable),
}); });
export default restartAndInstallUpdateListenerInjectable; export default restartAndInstallUpdateListenerInjectable;

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { autorun } from "mobx"; import { autorun } from "mobx";
import { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable"; import { getStartableStoppable } from "@k8slens/startable-stoppable";
import setUpdateOnQuitInjectable from "../../../../main/electron-app/features/set-update-on-quit.injectable"; import setUpdateOnQuitInjectable from "../../../../main/electron-app/features/set-update-on-quit.injectable";
import selectedUpdateChannelInjectable from "../../common/selected-update-channel/selected-update-channel.injectable"; import selectedUpdateChannelInjectable from "../../common/selected-update-channel/selected-update-channel.injectable";
import type { ReleaseChannel, UpdateChannel } from "../../common/update-channels"; import type { ReleaseChannel, UpdateChannel } from "../../common/update-channels";

View File

@ -4,13 +4,13 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { restartAndInstallUpdateChannel } from "../common/restart-and-install-update-channel"; import { restartAndInstallUpdateChannel } from "../common/restart-and-install-update-channel";
import messageToChannelInjectable from "../../../renderer/utils/channel/message-to-channel.injectable"; import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
const restartAndInstallUpdateInjectable = getInjectable({ const restartAndInstallUpdateInjectable = getInjectable({
id: "restart-and-install-update", id: "restart-and-install-update",
instantiate: (di) => { instantiate: (di) => {
const messageToChannel = di.inject(messageToChannelInjectable); const messageToChannel = di.inject(sendMessageToChannelInjectionToken);
return () => { return () => {
messageToChannel(restartAndInstallUpdateChannel); messageToChannel(restartAndInstallUpdateChannel);

View File

@ -490,10 +490,10 @@ exports[`entity running technical tests when navigated to catalog renders 1`] =
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 33px; width: 100%;" style="height: 36px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -1206,10 +1206,10 @@ exports[`entity running technical tests when navigated to catalog when details p
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 33px; width: 100%;" style="height: 36px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"

View File

@ -739,10 +739,10 @@ exports[`opening catalog entity details panel when navigated to the catalog rend
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 99px; width: 100%;" style="height: 108px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -815,7 +815,7 @@ exports[`opening catalog entity details panel when navigated to the catalog rend
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 33px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 36px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -888,7 +888,7 @@ exports[`opening catalog entity details panel when navigated to the catalog rend
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 66px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 72px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -1560,10 +1560,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 99px; width: 100%;" style="height: 108px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -1636,7 +1636,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 33px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 36px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -1709,7 +1709,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 66px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 72px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -2413,10 +2413,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 99px; width: 100%;" style="height: 108px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -2489,7 +2489,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 33px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 36px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -2562,7 +2562,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 66px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 72px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -3266,10 +3266,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 99px; width: 100%;" style="height: 108px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -3342,7 +3342,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 33px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 36px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -3415,7 +3415,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 66px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 72px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -4370,10 +4370,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 99px; width: 100%;" style="height: 108px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -4446,7 +4446,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 33px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 36px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -4519,7 +4519,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 66px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 72px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -5211,10 +5211,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 99px; width: 100%;" style="height: 108px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -5287,7 +5287,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 33px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 36px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -5360,7 +5360,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 66px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 72px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -6052,10 +6052,10 @@ exports[`opening catalog entity details panel when navigated to the catalog when
style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;" style="position: relative; height: 420000px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
> >
<div <div
style="height: 99px; width: 100%;" style="height: 108px; width: 100%;"
> >
<div <div
style="position: absolute; left: 0px; top: 0px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 0px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -6128,7 +6128,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 33px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 36px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"
@ -6201,7 +6201,7 @@ exports[`opening catalog entity details panel when navigated to the catalog when
</div> </div>
</div> </div>
<div <div
style="position: absolute; left: 0px; top: 66px; height: 33px; width: 100%;" style="position: absolute; left: 0px; top: 72px; height: 36px; width: 100%;"
> >
<div <div
class="TableRow nowrap" class="TableRow nowrap"

View File

@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getRequestChannel } from "@k8slens/messaging";
import { getRequestChannel } from "../../../common/utils/channel/get-request-channel";
export const casChannel = getRequestChannel<void, string[]>("certificate-authorities"); export const casChannel = getRequestChannel<void, string[]>("certificate-authorities");

View File

@ -8,6 +8,7 @@ import { casChannel } from "../common/channel";
import certificateAuthoritiesChannelListenerInjectable from "./channel-handler.injectable"; import certificateAuthoritiesChannelListenerInjectable from "./channel-handler.injectable";
export default getGlobalOverride(certificateAuthoritiesChannelListenerInjectable, () => ({ export default getGlobalOverride(certificateAuthoritiesChannelListenerInjectable, () => ({
id: "certificate-authorities-channel-listener",
channel: casChannel, channel: casChannel,
handler: () => [], handler: () => [],
})); }));

View File

@ -2,14 +2,15 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import { casChannel } from "../common/channel"; import { casChannel } from "../common/channel";
import { globalAgent } from "https"; import { globalAgent } from "https";
import { isString } from "@k8slens/utilities"; import { isString } from "@k8slens/utilities";
const certificateAuthoritiesChannelListenerInjectable = getRequestChannelListenerInjectable({ const certificateAuthoritiesChannelListenerInjectable = getRequestChannelListenerInjectable({
id: "certificate-authorities-channel-listener",
channel: casChannel, channel: casChannel,
handler: () => () => { getHandler: () => () => {
if (Array.isArray(globalAgent.options.ca)) { if (Array.isArray(globalAgent.options.ca)) {
return globalAgent.options.ca.filter(isString); return globalAgent.options.ca.filter(isString);
} }

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { requestFromChannelInjectionToken } from "../../../common/utils/channel/request-from-channel-injection-token"; import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { casChannel } from "../common/channel"; import { casChannel } from "../common/channel";
import { requestSystemCAsInjectionToken } from "../common/request-system-cas-token"; import { requestSystemCAsInjectionToken } from "../common/request-system-cas-token";

View File

@ -4,7 +4,7 @@
*/ */
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import { getRequestChannel } from "../../../../common/utils/channel/get-request-channel"; import { getRequestChannel } from "@k8slens/messaging";
export interface ActivateCluster { export interface ActivateCluster {
clusterId: ClusterId; clusterId: ClusterId;

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectionToken } from "@ogre-tools/injectable"; import { getInjectionToken } from "@ogre-tools/injectable";
import type { ChannelRequester } from "../../../../common/utils/channel/request-from-channel-injection-token"; import type { ChannelRequester } from "@k8slens/messaging";
import type { activateClusterChannel, deactivateClusterChannel } from "./channels"; import type { activateClusterChannel, deactivateClusterChannel } from "./channels";
export type RequestClusterActivation = ChannelRequester<typeof activateClusterChannel>; export type RequestClusterActivation = ChannelRequester<typeof activateClusterChannel>;

View File

@ -2,13 +2,14 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import { activateClusterChannel } from "../common/channels"; import { activateClusterChannel } from "../common/channels";
import requestClusterActivationInjectable from "./request-activation.injectable"; import requestClusterActivationInjectable from "./request-activation.injectable";
const activateClusterRequestChannelListenerInjectable = getRequestChannelListenerInjectable({ const activateClusterRequestChannelListenerInjectable = getRequestChannelListenerInjectable({
id: "activate-cluster-request-channel-listener",
channel: activateClusterChannel, channel: activateClusterChannel,
handler: (di) => di.inject(requestClusterActivationInjectable), getHandler: (di) => di.inject(requestClusterActivationInjectable),
}); });
export default activateClusterRequestChannelListenerInjectable; export default activateClusterRequestChannelListenerInjectable;

View File

@ -2,13 +2,14 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import { deactivateClusterChannel } from "../common/channels"; import { deactivateClusterChannel } from "../common/channels";
import requestClusterDeactivationInjectable from "./request-deactivation.injectable"; import requestClusterDeactivationInjectable from "./request-deactivation.injectable";
const clusterDeactivationRequestChannelListenerInjectable = getRequestChannelListenerInjectable({ const clusterDeactivationRequestChannelListenerInjectable = getRequestChannelListenerInjectable({
id: "cluster-deactivation-request-channel-listener",
channel: deactivateClusterChannel, channel: deactivateClusterChannel,
handler: (di) => di.inject(requestClusterDeactivationInjectable), getHandler: (di) => di.inject(requestClusterDeactivationInjectable),
}); });
export default clusterDeactivationRequestChannelListenerInjectable; export default clusterDeactivationRequestChannelListenerInjectable;

View File

@ -3,14 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { activateClusterChannel } from "../common/channels"; import { activateClusterChannel } from "../common/channels";
import { requestClusterActivationInjectionToken } from "../common/request-token"; import { requestClusterActivationInjectionToken } from "../common/request-token";
const requestClusterActivationInjectable = getInjectable({ const requestClusterActivationInjectable = getInjectable({
id: "request-cluster-activation", id: "request-cluster-activation",
instantiate: (di) => { instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectable); const requestFromChannel = di.inject(requestFromChannelInjectionToken);
return (req) => requestFromChannel(activateClusterChannel, req); return (req) => requestFromChannel(activateClusterChannel, req);
}, },

View File

@ -3,14 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { deactivateClusterChannel } from "../common/channels"; import { deactivateClusterChannel } from "../common/channels";
import { requestClusterDeactivationInjectionToken } from "../common/request-token"; import { requestClusterDeactivationInjectionToken } from "../common/request-token";
const requestClusterDeactivationInjectable = getInjectable({ const requestClusterDeactivationInjectable = getInjectable({
id: "request-cluster-deactivation", id: "request-cluster-deactivation",
instantiate: (di) => { instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectable); const requestFromChannel = di.inject(requestFromChannelInjectionToken);
return (clusterId) => requestFromChannel(deactivateClusterChannel, clusterId); return (clusterId) => requestFromChannel(deactivateClusterChannel, clusterId);
}, },

View File

@ -3,10 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
export type ClearClusterAsDeletingChannel = RequestChannel<ClusterId, void>; export const clearClusterAsDeletingChannel = getRequestChannel<ClusterId, void>(
"clear-cluster-as-deleting",
export const clearClusterAsDeletingChannel: ClearClusterAsDeletingChannel = { );
id: "clear-cluster-as-deleting",
};

View File

@ -3,10 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; import type { RequestChannel } from "@k8slens/messaging";
import { getRequestChannel } from "@k8slens/messaging";
export type DeleteClusterChannel = RequestChannel<ClusterId, void>; export type DeleteClusterChannel = RequestChannel<ClusterId, void>;
export const deleteClusterChannel: DeleteClusterChannel = { export const deleteClusterChannel = getRequestChannel<ClusterId, void>(
id: "delete-cluster", "delete-cluster",
}; );

View File

@ -3,10 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; import { getRequestChannel } from "@k8slens/messaging";
export type SetClusterAsDeletingChannel = RequestChannel<ClusterId, void>; export const setClusterAsDeletingChannel = getRequestChannel<ClusterId, void>(
"set-cluster-as-deleting",
export const setClusterAsDeletingChannel: SetClusterAsDeletingChannel = { );
id: "set-cluster-as-deleting",
};

View File

@ -3,12 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel"; import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel";
const clearClusterAsDeletingChannelListenerInjectable = getRequestChannelListenerInjectable({ const clearClusterAsDeletingChannelListenerInjectable = getRequestChannelListenerInjectable({
id: "clear-cluster-as-deleting-channel-listener",
channel: clearClusterAsDeletingChannel, channel: clearClusterAsDeletingChannel,
handler: (di) => { getHandler: (di) => {
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return (clusterId) => { return (clusterId) => {

View File

@ -10,12 +10,13 @@ import removePathInjectable from "../../../../common/fs/remove.injectable";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import clusterConnectionInjectable from "../../../../main/cluster/cluster-connection.injectable"; import clusterConnectionInjectable from "../../../../main/cluster/cluster-connection.injectable";
import { noop } from "@k8slens/utilities"; import { noop } from "@k8slens/utilities";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import { deleteClusterChannel } from "../common/delete-channel"; import { deleteClusterChannel } from "../common/delete-channel";
const deleteClusterChannelListenerInjectable = getRequestChannelListenerInjectable({ const deleteClusterChannelListenerInjectable = getRequestChannelListenerInjectable({
id: "delete-cluster-channel-listener",
channel: deleteClusterChannel, channel: deleteClusterChannel,
handler: (di) => { getHandler: (di) => {
const emitAppEvent = di.inject(emitAppEventInjectable); const emitAppEvent = di.inject(emitAppEventInjectable);
const clusterStore = di.inject(clusterStoreInjectable); const clusterStore = di.inject(clusterStoreInjectable);
const clusterFrames = di.inject(clusterFramesInjectable); const clusterFrames = di.inject(clusterFramesInjectable);

View File

@ -3,12 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable"; import clustersThatAreBeingDeletedInjectable from "../../../../main/cluster/are-being-deleted.injectable";
import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel"; import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel";
const setClusterAsDeletingChannelHandlerInjectable = getRequestChannelListenerInjectable({ const setClusterAsDeletingChannelHandlerInjectable = getRequestChannelListenerInjectable({
id: "set-cluster-as-deleting-channel-handler",
channel: setClusterAsDeletingChannel, channel: setClusterAsDeletingChannel,
handler: (di) => { getHandler: (di) => {
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable); const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return (clusterId) => { return (clusterId) => {

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel"; import { clearClusterAsDeletingChannel } from "../common/clear-as-deleting-channel";
export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise<void>; export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise<void>;
@ -12,7 +12,7 @@ export type RequestClearClusterAsDeleting = (clusterId: ClusterId) => Promise<vo
const requestClearClusterAsDeletingInjectable = getInjectable({ const requestClearClusterAsDeletingInjectable = getInjectable({
id: "request-clear-cluster-as-deleting", id: "request-clear-cluster-as-deleting",
instantiate: (di): RequestClearClusterAsDeleting => { instantiate: (di): RequestClearClusterAsDeleting => {
const requestChannel = di.inject(requestFromChannelInjectable); const requestChannel = di.inject(requestFromChannelInjectionToken);
return (clusterId) => requestChannel(clearClusterAsDeletingChannel, clusterId); return (clusterId) => requestChannel(clearClusterAsDeletingChannel, clusterId);
}, },

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { deleteClusterChannel } from "../common/delete-channel"; import { deleteClusterChannel } from "../common/delete-channel";
export type RequestDeleteCluster = (clusterId: ClusterId) => Promise<void>; export type RequestDeleteCluster = (clusterId: ClusterId) => Promise<void>;
@ -12,7 +12,7 @@ export type RequestDeleteCluster = (clusterId: ClusterId) => Promise<void>;
const requestDeleteClusterInjectable = getInjectable({ const requestDeleteClusterInjectable = getInjectable({
id: "request-delete-cluster", id: "request-delete-cluster",
instantiate: (di): RequestDeleteCluster => { instantiate: (di): RequestDeleteCluster => {
const requestChannel = di.inject(requestFromChannelInjectable); const requestChannel = di.inject(requestFromChannelInjectionToken);
return (clusterId) => requestChannel(deleteClusterChannel, clusterId); return (clusterId) => requestChannel(deleteClusterChannel, clusterId);
}, },

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterId } from "../../../../common/cluster-types"; import type { ClusterId } from "../../../../common/cluster-types";
import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel"; import { setClusterAsDeletingChannel } from "../common/set-as-deleting-channel";
export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise<void>; export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise<void>;
@ -12,7 +12,7 @@ export type RequestSetClusterAsDeleting = (clusterId: ClusterId) => Promise<void
const requestSetClusterAsDeletingInjectable = getInjectable({ const requestSetClusterAsDeletingInjectable = getInjectable({
id: "request-set-cluster-as-deleting", id: "request-set-cluster-as-deleting",
instantiate: (di): RequestSetClusterAsDeleting => { instantiate: (di): RequestSetClusterAsDeleting => {
const requestChannel = di.inject(requestFromChannelInjectable); const requestChannel = di.inject(requestFromChannelInjectionToken);
return (clusterId) => requestChannel(setClusterAsDeletingChannel, clusterId); return (clusterId) => requestChannel(setClusterAsDeletingChannel, clusterId);
}, },

View File

@ -0,0 +1,637 @@
/**
* 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 { AuthorizationV1Api, CoreV1Api, V1APIGroupList, V1APIVersions, V1NamespaceList, V1SelfSubjectAccessReview, V1SelfSubjectRulesReview } from "@kubernetes/client-node";
import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable";
import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import type { PartialDeep } from "type-fest";
import { anyObject } from "jest-mock-extended";
import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable";
import type { K8sRequest } from "../../main/k8s-request.injectable";
import k8sRequestInjectable from "../../main/k8s-request.injectable";
import type { DetectClusterMetadata } from "../../main/cluster-detectors/detect-cluster-metadata.injectable";
import detectClusterMetadataInjectable from "../../main/cluster-detectors/detect-cluster-metadata.injectable";
import type { ClusterConnection } from "../../main/cluster/cluster-connection.injectable";
import clusterConnectionInjectable from "../../main/cluster/cluster-connection.injectable";
import type { KubeAuthProxy } from "../../main/kube-auth-proxy/create-kube-auth-proxy.injectable";
import createKubeAuthProxyInjectable from "../../main/kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { Mocked } from "../../test-utils/mock-interface";
import { flushPromises } from "@k8slens/test-utils";
describe("Refresh Cluster Accessibility Technical Tests", () => {
let builder: ApplicationBuilder;
let createSelfSubjectRulesReviewMock: AsyncFnMock<AuthorizationV1Api["createSelfSubjectRulesReview"]>;
let createSelfSubjectAccessReviewMock: AsyncFnMock<AuthorizationV1Api["createSelfSubjectAccessReview"]>;
let listNamespaceMock: AsyncFnMock<CoreV1Api["listNamespace"]>;
let k8sRequestMock: AsyncFnMock<K8sRequest>;
let detectClusterMetadataMock: AsyncFnMock<DetectClusterMetadata>;
let kubeAuthProxyMock: Mocked<KubeAuthProxy>;
beforeEach(async () => {
builder = getApplicationBuilder();
const mainDi = builder.mainDi;
mainDi.override(broadcastMessageInjectable, () => async () => {});
kubeAuthProxyMock = {
apiPrefix: "/some-api-prefix",
port: 0,
exit: jest.fn(),
run: asyncFn(),
};
mainDi.override(createKubeAuthProxyInjectable, () => () => kubeAuthProxyMock);
detectClusterMetadataMock = asyncFn();
mainDi.override(detectClusterMetadataInjectable, () => detectClusterMetadataMock);
k8sRequestMock = asyncFn();
mainDi.override(k8sRequestInjectable, () => k8sRequestMock);
createSelfSubjectRulesReviewMock = asyncFn();
createSelfSubjectAccessReviewMock = asyncFn();
mainDi.override(createAuthorizationApiInjectable, () => () => ({
createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock,
createSelfSubjectAccessReview: createSelfSubjectAccessReviewMock,
} as any));
listNamespaceMock = asyncFn();
mainDi.override(createCoreApiInjectable, () => () => ({
listNamespace: listNamespaceMock,
} as any));
await builder.render();
});
describe("given a cluster with no configured preferences", () => {
let cluster: Cluster;
let clusterConnection: ClusterConnection;
let refreshPromise: Promise<void>;
beforeEach(async () => {
const mainDi = builder.mainDi;
const clusterStore = mainDi.inject(clusterStoreInjectable);
const writeJsonFile = mainDi.inject(writeJsonFileInjectable);
await writeJsonFile("/some-kube-config-path", {
apiVersion: "v1",
kind: "Config",
clusters: [{
name: "some-cluster-name",
cluster: {
server: "https://localhost:8989",
},
}],
users: [{
name: "some-user-name",
}],
contexts: [{
name: "some-cluster-context",
context: {
user: "some-user-name",
cluster: "some-cluster-name",
},
}],
});
clusterStore.addCluster({
contextName: "some-cluster-context",
id: "some-cluster-id",
kubeConfigPath: "/some-kube-config-path",
});
cluster = clusterStore.getById("some-cluster-id") ?? (() => { throw new Error("missing cluster"); })();
clusterConnection = mainDi.inject(clusterConnectionInjectable, cluster);
refreshPromise = clusterConnection.refreshAccessibilityAndMetadata();
});
it("starts kubeAuthProxy", () => {
expect(kubeAuthProxyMock.run).toBeCalled();
});
describe("when kubeAuthProxy has started running and its port is found", () => {
beforeEach(async () => {
kubeAuthProxyMock.port = 1235;
await kubeAuthProxyMock.run.resolve();
await flushPromises();
});
it("requests if cluster has admin permissions", async () => {
expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({
spec: {
namespace: "kube-system",
resource: "*",
verb: "create",
},
}));
});
describe.each([ true, false ])("when cluster admin request resolves to %p", (isAdmin) => {
beforeEach(async () => {
await createSelfSubjectAccessReviewMock.resolve({
body: {
status: {
allowed: isAdmin,
},
} as PartialDeep<V1SelfSubjectAccessReview>,
} as any);
});
it("requests if cluster has global watch permissions", () => {
expect(createSelfSubjectAccessReviewMock).toBeCalledWith(anyObject({
spec: {
verb: "watch",
resource: "*",
},
}));
});
describe.each([ true, false ])("when cluster global watch request resolves with %p", (globalWatch) => {
beforeEach(async () => {
await createSelfSubjectAccessReviewMock.resolve({
body: {
status: {
allowed: globalWatch,
},
} as PartialDeep<V1SelfSubjectAccessReview>,
} as any);
});
it("requests namespaces", () => {
expect(listNamespaceMock).toBeCalled();
});
describe("when list namespaces resolves", () => {
beforeEach(async () => {
await listNamespaceMock.resolve(listNamespaceResponse);
});
it("requests core api versions", () => {
expect(k8sRequestMock).toBeCalledWith(
anyObject({ id: "some-cluster-id" }),
"/api",
);
});
describe("when core api versions request resolves", () => {
beforeEach(async () => {
await k8sRequestMock.resolve({
serverAddressByClientCIDRs: [],
versions: [
"v1",
],
} as V1APIVersions);
});
it("requests non-core api resource kinds", () => {
expect(k8sRequestMock).toBeCalledWith(
anyObject({ id: "some-cluster-id" }),
"/apis",
);
});
describe("when non-core api resource kinds request resolves", () => {
beforeEach(async () => {
await k8sRequestMock.resolve(nonCoreApiResponse);
});
it("requests specific resource kinds in core", () => {
expect(k8sRequestMock).toBeCalledWith(
anyObject({ id: "some-cluster-id" }),
"/api/v1",
);
});
describe("when core specific resource kinds request resolves", () => {
beforeEach(async () => {
await k8sRequestMock.resolve(coreApiKindsResponse);
});
it("requests specific resources kinds from the first non-core response", () => {
expect(k8sRequestMock).toBeCalledWith(
anyObject({ id: "some-cluster-id" }),
"/apis/node.k8s.io/v1",
);
});
describe("when first specific resource kinds request resolves", () => {
beforeEach(async () => {
await k8sRequestMock.resolve(nodeK8sIoKindsResponse);
});
it("requests specific resources kinds from the second non-core response", () => {
expect(k8sRequestMock).toBeCalledWith(
anyObject({ id: "some-cluster-id" }),
"/apis/discovery.k8s.io/v1",
);
});
describe("when second specific resource kinds request resolves", () => {
beforeEach(async () => {
await k8sRequestMock.resolve(discoveryK8sIoKindsResponse);
});
it("requests namespace list permissions for 'default' namespace", () => {
expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
spec: {
namespace: "default",
},
}));
});
describe("when the permissions are incomplete", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(defaultIncompletePermissions);
});
it("requests namespace list permissions for 'my-namespace' namespace", () => {
expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
spec: {
namespace: "my-namespace",
},
}));
});
describe("when the permissions request for 'my-namespace' resolves as empty", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(emptyPermissions);
});
it("requests cluster metadata", () => {
expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" }));
});
describe("when cluster metadata request resolves", () => {
beforeEach(async () => {
await detectClusterMetadataMock.resolve({});
});
it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => {
await refreshPromise;
});
it("should have the cluster displaying 'pods'", () => {
expect(cluster.resourcesToShow.has("pods")).toBe(true);
});
it("should have the cluster displaying 'namespaces'", () => {
expect(cluster.resourcesToShow.has("namespaces")).toBe(true);
});
});
});
describe.skip("when the permissions are incomplete", () => {});
describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {});
describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {});
});
describe("when the permissions resolve to an empty list", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(emptyPermissions);
});
it("requests namespace list permissions for 'my-namespace' namespace", () => {
expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
spec: {
namespace: "my-namespace",
},
}));
});
describe("when the permissions request for 'my-namespace' resolves as empty", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(emptyPermissions);
});
it("requests cluster metadata", () => {
expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" }));
});
describe("when cluster metadata request resolves", () => {
beforeEach(async () => {
await detectClusterMetadataMock.resolve({});
});
it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => {
await refreshPromise;
});
it("should have the cluster displaying 'pods'", () => {
expect(cluster.resourcesToShow.has("pods")).toBe(false);
});
it("should have the cluster not displaying 'namespaces'", () => {
expect(cluster.resourcesToShow.has("namespaces")).toBe(false);
});
});
});
describe.skip("when the permissions are incomplete", () => {});
describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {});
describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {});
});
describe("when the permissions resolve to a single entry with 'list' verb", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(defaultSingleListPermissions);
});
it("requests namespace list permissions for 'my-namespace' namespace", () => {
expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
spec: {
namespace: "my-namespace",
},
}));
});
describe("when the permissions request for 'my-namespace' resolves as empty", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(emptyPermissions);
});
it("requests cluster metadata", () => {
expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" }));
});
describe("when cluster metadata request resolves", () => {
beforeEach(async () => {
await detectClusterMetadataMock.resolve({});
});
it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => {
await refreshPromise;
});
it("should have the cluster displaying 'pods'", () => {
expect(cluster.resourcesToShow.has("pods")).toBe(true);
});
it("should have the cluster not displaying 'namespaces'", () => {
expect(cluster.resourcesToShow.has("namespaces")).toBe(false);
});
});
});
describe.skip("when the permissions are incomplete", () => {});
describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {});
describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {});
});
describe("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(defaultMultipleListPermissions);
});
it("requests namespace list permissions for 'my-namespace' namespace", () => {
expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
spec: {
namespace: "my-namespace",
},
}));
});
describe("when the permissions request for 'my-namespace' resolves as empty", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.resolve(emptyPermissions);
});
it("requests cluster metadata", () => {
expect(detectClusterMetadataMock).toBeCalledWith(anyObject({ id: "some-cluster-id" }));
});
describe("when cluster metadata request resolves", () => {
beforeEach(async () => {
await detectClusterMetadataMock.resolve({});
});
it("allows the call to refreshAccessibilityAndMetadata to resolve", async () => {
await refreshPromise;
});
it("should have the cluster displaying 'pods'", () => {
expect(cluster.resourcesToShow.has("pods")).toBe(true);
});
it("should have the cluster not displaying 'namespaces'", () => {
expect(cluster.resourcesToShow.has("namespaces")).toBe(false);
});
});
});
describe.skip("when the permissions are incomplete", () => {});
describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {});
describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {});
});
});
describe.skip("when second specific resource kinds rejects", () => {});
});
});
describe.skip("when first specific resource kinds rejects", () => {});
});
});
});
});
});
});
});
});
const nonCoreApiResponse = {
groups: [
{
name: "node.k8s.io",
versions: [
{
groupVersion: "node.k8s.io/v1",
version: "v1",
},
],
preferredVersion: {
groupVersion: "node.k8s.io/v1",
version: "v1",
},
},
{
name: "discovery.k8s.io",
versions: [
{
groupVersion: "discovery.k8s.io/v1",
version: "v1",
},
],
preferredVersion: {
groupVersion: "discovery.k8s.io/v1",
version: "v1",
},
},
],
} as V1APIGroupList;
const listNamespaceResponse = {
body: {
items: [
{
metadata: {
name: "default",
},
},
{
metadata: {
name: "my-namespace",
},
},
],
} as PartialDeep<V1NamespaceList>,
} as Awaited<ReturnType<CoreV1Api["listNamespace"]>>;
const coreApiKindsResponse = {
kind: "APIResourceList",
groupVersion: "v1",
resources: [
{
name: "namespaces",
singularName: "",
namespaced: false,
kind: "Namespace",
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"],
shortNames: ["ns"],
storageVersionHash: "Q3oi5N2YM8M=",
},
{
name: "pods",
singularName: "",
namespaced: true,
kind: "Pod",
verbs: [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch",
],
shortNames: ["po"],
categories: ["all"],
storageVersionHash: "xPOwRZ+Yhw8=",
},
{
name: "pods/attach",
singularName: "",
namespaced: true,
kind: "PodAttachOptions",
verbs: ["create", "get"],
},
],
};
const nodeK8sIoKindsResponse = {
kind: "APIResourceList",
apiVersion: "v1",
groupVersion: "node.k8s.io/v1",
resources: [
{
name: "runtimeclasses",
singularName: "",
namespaced: false,
kind: "RuntimeClass",
verbs: [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch",
],
storageVersionHash: "WQTu1GL3T2Q=",
},
],
};
const discoveryK8sIoKindsResponse = {
kind: "APIResourceList",
apiVersion: "v1",
groupVersion: "discovery.k8s.io/v1",
resources: [
{
name: "endpointslices",
singularName: "",
namespaced: true,
kind: "EndpointSlice",
verbs: [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch",
],
storageVersionHash: "Nx3SIv6I0mE=",
},
],
};
type CreateSelfSubjectRulesReviewRes = Awaited<ReturnType<AuthorizationV1Api["createSelfSubjectRulesReview"]>>;
const defaultIncompletePermissions = {
body: {
status: {
incomplete: true,
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const emptyPermissions = {
body: {
status: {
resourceRules: [],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const defaultSingleListPermissions = {
body: {
status: {
resourceRules: [{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
}],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const defaultMultipleListPermissions = {
body: {
status: {
resourceRules: [
{
apiGroups: [""],
resources: ["pods"],
verbs: ["get"],
},
{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
},
],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;

View File

@ -4,8 +4,7 @@
*/ */
import type { ClusterId, ClusterState } from "../../../../common/cluster-types"; import type { ClusterId, ClusterState } from "../../../../common/cluster-types";
import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; import type { MessageChannel, RequestChannel } from "@k8slens/messaging";
import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token";
export interface ClusterStateSync { export interface ClusterStateSync {
clusterId: ClusterId; clusterId: ClusterId;

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