Merge branch 'master' into no-updates-available-popup

This commit is contained in:
Gabriel Accettola authored and GitHub committed 2023-03-23 12:40:00 +01:00
commit cf47e8b38c
76 files changed
+1756 -1305

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/swcrc",
"jsc": {
"parser": {
"syntax": "typescript"
},
"target": "es2022"
}
}
+3
View File
@@ -0,0 +1,3 @@
# Description
The package exports tokens needed for external configuration of Cluster Settings page.
+31
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"
}
}
+30
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",
});
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist/",
"paths": {
"*": [
"node_modules/*",
"types/*"
]
},
},
"include": [
"src/**/*",
],
"exclude": [
"node_modules",
]
}
+2 -1
View File
@@ -120,6 +120,7 @@
"@astronautlabs/jsonpath": "^1.1.0",
"@hapi/call": "^9.0.1",
"@hapi/subtext": "^7.1.0",
"@k8slens/cluster-settings": "^6.5.0-alpha.1",
"@k8slens/node-fetch": "^6.5.0-alpha.1",
"@kubernetes/client-node": "^0.18.1",
"@material-ui/styles": "^4.11.5",
@@ -257,7 +258,7 @@
"css-loader": "^6.7.3",
"deepdash": "^5.3.9",
"dompurify": "^2.4.4",
"electron": "^19.1.9",
"electron": "^22.3.3",
"electron-builder": "^23.6.0",
"esbuild": "^0.17.8",
"esbuild-loader": "^2.21.0",
@@ -11,7 +11,7 @@ export const pathNames: PathName[] = [
"home",
"appData",
"userData",
"cache",
"sessionData",
"temp",
"exe",
"module",
@@ -21,7 +21,6 @@ describe("app-paths", () => {
const defaultAppPathsStub: AppPaths = {
currentApp: "/some-current-app",
appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps",
desktop: "/some-desktop",
documents: "/some-documents",
@@ -36,6 +35,7 @@ describe("app-paths", () => {
temp: "/some-temp",
videos: "/some-videos",
userData: "/some-irrelevant-user-data",
sessionData: "/some-irrelevant-user-data", // By default this points to userData
};
builder.beforeApplicationStart(({ mainDi }) => {
@@ -73,7 +73,6 @@ describe("app-paths", () => {
expect(actual).toEqual({
currentApp: "/some-current-app",
appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps",
desktop: "/some-desktop",
documents: "/some-documents",
@@ -88,6 +87,7 @@ describe("app-paths", () => {
temp: "/some-temp",
videos: "/some-videos",
userData: "/some-app-data/some-product-name",
sessionData: "/some-app-data/some-product-name",
});
});
@@ -97,7 +97,6 @@ describe("app-paths", () => {
expect(actual).toEqual({
currentApp: "/some-current-app",
appData: "/some-app-data",
cache: "/some-cache",
crashDumps: "/some-crash-dumps",
desktop: "/some-desktop",
documents: "/some-documents",
@@ -112,6 +111,7 @@ describe("app-paths", () => {
temp: "/some-temp",
videos: "/some-videos",
userData: "/some-app-data/some-product-name",
sessionData: "/some-app-data/some-product-name",
});
});
});
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -2,27 +2,21 @@
* 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 type { CoreV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import { isDefined } from "@k8slens/utilities";
export type ListNamespaces = () => Promise<string[]>;
export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces;
export type CreateListNamespaces = (api: CoreV1Api) => ListNamespaces;
const createListNamespacesInjectable = getInjectable({
id: "create-list-namespaces",
instantiate: (): CreateListNamespaces => (config) => {
const coreApi = config.makeApiClient(CoreV1Api);
instantiate: (): CreateListNamespaces => (api) => async () => {
const { body: { items }} = await api.listNamespace();
return async () => {
const { body: { items }} = await coreApi.listNamespace();
return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
};
return items
.map(ns => ns.metadata?.name)
.filter(isDefined);
},
});
@@ -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;
@@ -3,334 +3,225 @@
* 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 { IncomingMessage } from "http";
import { anyObject } from "jest-mock-extended";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable";
import requestNamespaceListPermissionsForInjectable from "./request-namespace-list-permissions.injectable";
import { cast } from "../../test-utils/cast";
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 }}>) => ({
makeApiClient: () => ({
createSelfSubjectRulesReview: (): Promise<{ body: { status: V1SubjectRulesReviewStatus }}> => statusResponse,
}),
});
interface TestCase {
description: string;
status: V1SubjectRulesReviewStatus;
expected: boolean;
}
describe("requestNamespaceListPermissions", () => {
let di: DiContainer;
let requestNamespaceListPermissions: RequestNamespaceListPermissionsFor;
let createSelfSubjectRulesReviewMock: AsyncFnMock<AuthorizationV1Api["createSelfSubjectRulesReview"]>;
let requestNamespaceListPermissions: RequestNamespaceListPermissions;
beforeEach(() => {
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", () => {
it("returns truthy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve) => resolve({
body: {
status: {
incomplete: true,
resourceRules: [],
nonResourceRules: [],
},
},
})),
) as any);
describe("when a request for list permissions in a namespace has been started", () => {
let request: ReturnType<RequestNamespaceListPermissions>;
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
beforeEach(() => {
request = requestNamespaceListPermissions("irrelevant-namespace");
});
});
describe("when api rejects", () => {
it("returns truthy function", async () => {
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
new Promise((resolve, reject) => reject("unknown error")),
) as any);
const permissionCheck = await requestPermissions("irrelevant-namespace");
expect(permissionCheck({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
it("should request the creation of a SelfSubjectRulesReview", () => {
expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
spec: {
namespace: "irrelevant-namespace",
},
}));
});
});
describe("when first 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: ["*"],
},
{
apiGroups: ["*"],
verbs: ["get"],
},
],
nonResourceRules: [],
([
{
description: "incomplete data",
status: {
incomplete: true,
resourceRules: [],
nonResourceRules: [],
},
expected: true,
},
{
description: "first resourceRule has all permissions for everything",
status: {
incomplete: false,
resourceRules: [
{
apiGroups: ["*"],
verbs: ["*"],
},
},
})),
) as any);
{
apiGroups: ["*"],
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({
apiName: "pods",
group: "",
kind: "Pod",
namespaced: true,
})).toBeTruthy();
expect(canListResource(someKubeResource)).toBe(expected);
});
});
});
});
describe("when first 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: ["list"],
},
{
apiGroups: ["*"],
verbs: ["get"],
},
],
nonResourceRules: [],
},
},
})),
) as any);
describe("when api rejects", () => {
beforeEach(async () => {
await createSelfSubjectRulesReviewMock.reject(new Error("unknown error"));
});
const permissionCheck = await requestPermissions("irrelevant-namespace");
it("allows the request to complete, and 'canListResource' will return true", async () => {
const canListResource = await request;
expect(permissionCheck({
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();
expect(canListResource(someKubeResource)).toBe(true);
});
});
});
});
const someKubeResource: KubeApiResource = {
apiName: "some-kind",
group: "some-api-group",
kind: "SomeKind",
namespaced: true,
};
@@ -13,7 +13,7 @@ import userStoreInjectable from "../user-store/user-store.injectable";
export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void;
const mapProcessName = (type: "browser" | "renderer" | "worker") => type === "browser" ? "main" : type;
const mapProcessName = (type: "browser" | "renderer" | "worker" | "utility") => type === "browser" ? "main" : type;
const initializeSentryReportingWithInjectable = getInjectable({
id: "initialize-sentry-reporting-with",
@@ -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");
});
@@ -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");
});
@@ -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");
});
@@ -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");
});
@@ -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");
});
@@ -4,6 +4,7 @@
*/
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 hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
@@ -21,6 +22,9 @@ import maybeKubeApiInjectable from "../maybe-kube-api.injectable";
// eslint-disable-next-line no-restricted-imports
import { KubeApi as ExternalKubeApi } from "../../../extensions/common-api/k8s-api";
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> {
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,
});
});
});
});
});
});
@@ -10,7 +10,8 @@ import { autorun, action, observable } from "mobx";
import type { KubeApi } from "../kube-api";
import type { KubeObject, ObjectReference } from "../kube-object";
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>
? Store
@@ -26,13 +27,15 @@ export type FindApiCallback = (api: KubeApi<KubeObject>) => boolean;
interface Dependencies {
readonly apis: IComputedValue<KubeApi[]>;
readonly crdApis: IComputedValue<KubeApi[]>;
readonly stores: IComputedValue<KubeObjectStore[]>;
createCustomResourceStore: CreateCustomResourceStore;
}
export class ApiManager {
private readonly externalApis = observable.array<KubeApi>();
private readonly externalStores = observable.array<KubeObjectStore>();
private readonly defaultCrdStores = observable.map<string, KubeObjectStore>();
private readonly apis = observable.map<string, KubeApi>();
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);
});
}
@@ -110,6 +119,16 @@ export class ApiManager {
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>(api: RegisterableApi<Api>): KubeObjectStoreFrom<Api> | undefined;
/**
@@ -130,9 +149,19 @@ export class ApiManager {
return undefined;
}
return iter.chain(this.dependencies.stores.get().values())
const defaultResult = iter.chain(this.dependencies.stores.get().values())
.concat(this.externalStores.values())
.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 {
@@ -5,11 +5,9 @@
import { getInjectable } from "@ogre-tools/injectable";
import EventEmitter from "events";
import type TypedEventEmitter from "typed-emitter";
import type { CustomResourceDefinition } from "../endpoints";
import type { KubeApi } from "../kube-api";
export interface LegacyAutoRegistration {
customResourceDefinition: (crd: CustomResourceDefinition) => void;
kubeApi: (api: KubeApi<any, any>) => void;
}
@@ -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",
});
@@ -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;
@@ -9,6 +9,8 @@ import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-f
import { kubeObjectStoreInjectionToken } from "./kube-object-store-token";
import { kubeApiInjectionToken } from "../kube-api/kube-api-injection-token";
import { computed } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "./crd-api-token";
import createCustomResourceStoreInjectable from "./create-custom-resource-store.injectable";
const apiManagerInjectable = getInjectable({
id: "api-manager",
@@ -23,6 +25,10 @@ const apiManagerInjectable = getInjectable({
stores: storesAndApisCanBeCreated
? computedInjectMany(kubeObjectStoreInjectionToken)
: computed(() => []),
crdApis: storesAndApisCanBeCreated
? computedInjectMany(customResourceDefinitionApiInjectionToken)
: computed(() => []),
createCustomResourceStore: di.inject(createCustomResourceStoreInjectable),
});
},
});
@@ -88,7 +88,7 @@ export interface KubeObjectStoreDependencies {
readonly logger: Logger;
}
export abstract class KubeObjectStore<
export class KubeObjectStore<
K extends KubeObject = KubeObject,
A extends KubeApi<K, D> = KubeApi<K, KubeJsonApiDataFor<K>>,
D extends KubeJsonApiDataFor<K> = KubeApiDataFrom<K, A>,
@@ -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());
}
);
@@ -2,29 +2,18 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* 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 { difference, find, map } from "lodash";
import { reaction, runInAction } from "mobx";
import { disposer } from "@k8slens/utilities";
import type { LensExtension } from "../../lens-extension";
import { extensionRegistratorInjectionToken } from "../extension-registrator-injection-token";
import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper";
export interface Extension {
register: () => 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({
id: "extension",
@@ -35,36 +24,27 @@ const extensionInjectable = getInjectable({
instantiate: (childDi) => {
const extensionRegistrators = childDi.injectMany(extensionRegistratorInjectionToken);
const reactionDisposer = disposer();
const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(childDi);
return {
register: () => {
extensionRegistrators.forEach((getInjectablesOfExtension) => {
const injectables = getInjectablesOfExtension(instance);
for (const extensionRegistrator of extensionRegistrators) {
const injectables = extensionRegistrator(instance);
reactionDisposer.push(
// injectables is either an array or a computed array, in which case
// we need to update the registered injectables with a reaction every time they change
reaction(
() => Array.isArray(injectables) ? injectables : injectables.get(),
(currentInjectables, previousInjectables = []) => {
// Register new injectables and deregister removed injectables by id
const currentIds = map(currentInjectables, "id");
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));
}
}, {
if (Array.isArray(injectables)) {
runInAction(() => {
injectableDifferencingRegistrator(injectables);
});
} else {
reactionDisposer.push(reaction(
() => injectables.get(),
injectableDifferencingRegistrator,
{
fireImmediately: true,
},
));
});
}
}
},
deregister: () => {
@@ -26,7 +26,7 @@ const checkForUpdatesMenuItemInjectable = getInjectable({
id: "check-for-updates",
parentId: isMac ? "mac" : "help",
orderNumber: isMac ? 20 : 50,
label: "Check for updates",
label: "Check for Updates...",
isShown: updatingIsEnabled,
onClick: async () => {
@@ -146,7 +146,7 @@ describe("installing update using tray", () => {
it("name of tray item for checking updates indicates that checking is happening", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Checking for updates...");
).toBe("Checking for Updates...");
});
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", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates");
).toBe("Check for Updates...");
});
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", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates");
).toBe("Check for Updates...");
});
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", () => {
expect(
builder.tray.get("check-for-updates")?.label,
).toBe("Check for updates");
).toBe("Check for Updates...");
});
it("renders", () => {
@@ -49,10 +49,10 @@ const checkForUpdatesTrayItemInjectable = getInjectable({
}
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()),
@@ -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;
@@ -31,8 +31,7 @@ describe("extension special characters in page registrations", () => {
describe("when navigating to route with ID having special characters", () => {
beforeEach(() => {
const testExtension =
builder.extensions.get("some-extension-id").applicationWindows.only;
const testExtension = builder.extensions.get("some-extension-id").applicationWindows.only;
testExtension.navigate("/some-page-id/");
});
@@ -5,10 +5,6 @@
import { Cluster } from "../../common/cluster/cluster";
import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
@@ -19,6 +15,10 @@ import clusterConnectionInjectable from "../cluster/cluster-connection.injectabl
import kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
import createCanIInjectable from "../../common/cluster/create-can-i.injectable";
import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable";
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
describe("create clusters", () => {
let cluster: Cluster;
@@ -34,8 +34,8 @@ describe("create clusters", () => {
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin");
di.override(broadcastConnectionUpdateInjectable, () => async () => {});
di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true);
di.override(createCanIInjectable, () => () => () => Promise.resolve(true));
di.override(createRequestNamespaceListPermissionsInjectable, () => () => async () => () => true);
di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(prometheusHandlerInjectable, () => ({
getPrometheusDetails: jest.fn(),
@@ -1,203 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import { Cluster } from "../../common/cluster/cluster";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { PrometheusProvider } from "../prometheus/provider";
import { prometheusProviderInjectionToken } from "../prometheus/provider";
import { runInAction } from "mobx";
import prometheusHandlerInjectable from "../cluster/prometheus-handler/prometheus-handler.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import loadProxyKubeconfigInjectable from "../cluster/load-proxy-kubeconfig.injectable";
import type { KubeConfig } from "@kubernetes/client-node";
enum ServiceResult {
Success,
Failure,
}
const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult): PrometheusProvider => ({
kind,
name: "TestProvider1",
isConfigurable: false,
getQuery: () => {
throw new Error("getQuery is not implemented.");
},
getPrometheusService: async () => {
switch (alwaysFail) {
case ServiceResult.Success:
return {
kind,
namespace: "default",
port: 7000,
service: "",
};
case ServiceResult.Failure:
throw new Error("does fail");
}
},
});
describe("ContextHandler", () => {
let di: DiContainer;
let cluster: Cluster;
beforeEach(() => {
di = getDiForUnitTesting();
di.override(loadProxyKubeconfigInjectable, () => async () => ({
makeApiClient: () => ({} as any),
} as Partial<KubeConfig>));
di.override(createKubeAuthProxyInjectable, () => () => ({
run: async () => {},
} as KubeAuthProxy));
di.override(directoryForTempInjectable, () => "/some-directory-for-tmp");
di.inject(lensProxyPortInjectable).set(9968);
cluster = new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-kubeconfig-path",
}, {
clusterServerUrl: "https://some-website.com",
});
});
describe("getPrometheusService", () => {
it.each([
[0],
[1],
[2],
[3],
])("should throw after %d failure(s)", async (failures) => {
runInAction(() => {
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
});
expect(() => di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails()).rejects.toThrowError();
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => {
runInAction(() => {
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
for (let i = 0; i < successes; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === `id_failure_${failures}`);
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => {
runInAction(() => {
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
for (let i = 0; i < successes; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === "id_failure_0");
});
it.each([
[1, 0],
[1, 1],
[1, 2],
[1, 3],
[2, 0],
[2, 1],
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => {
const beforeSuccesses = Math.floor(successes / 2);
runInAction(() => {
for (let i = 0; i < beforeSuccesses; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
for (let i = 0; i < failures; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-failure-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_failure_${i}`, ServiceResult.Failure),
}));
}
for (let i = beforeSuccesses; i < successes; i += 1) {
di.register(getInjectable({
id: `test-prometheus-provider-success-${i}`,
injectionToken: prometheusProviderInjectionToken,
instantiate: () => createTestPrometheusProvider(`id_success_${i}`, ServiceResult.Success),
}));
}
});
const details = await di.inject(prometheusHandlerInjectable, cluster).getPrometheusDetails();
expect(details.provider.kind === "id_success_0");
});
});
});
@@ -5,7 +5,6 @@
import waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable";
import { Cluster } from "../../common/cluster/cluster";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { ChildProcess } from "child_process";
import { Kubectl } from "../kubectl/kubectl";
import type { DeepMockProxy } from "jest-mock-extended";
@@ -13,6 +12,7 @@ import { mockDeep, mock } from "jest-mock-extended";
import type { Readable } from "stream";
import { EventEmitter } from "stream";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { CreateKubeAuthProxy, KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import spawnInjectable from "../child-process/spawn.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
@@ -29,7 +29,7 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab
const clusterServerUrl = "https://192.168.64.3:8443";
describe("kube auth proxy tests", () => {
let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
let createKubeAuthProxy: CreateKubeAuthProxy;
let spawnMock: jest.Mock;
let waitUntilPortIsUsedMock: jest.Mock;
let broadcastMessageMock: jest.Mock;
@@ -43,7 +43,7 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult):
},
});
describe("ContextHandler", () => {
describe("PrometheusHandler", () => {
let di: DiContainer;
let cluster: Cluster;
@@ -34,6 +34,7 @@ const setupAppPathsInjectable = getInjectable({
const appDataPath = getElectronAppPath("appData");
setElectronAppPath("userData", joinPaths(appDataPath, appName));
setElectronAppPath("sessionData", getElectronAppPath("userData"));
const appPaths = pipeline(
pathNames,
@@ -15,8 +15,11 @@ export interface ClusterData {
readonly id: string;
}
export type RequestApiVersions = (cluster: ClusterData) => AsyncResult<KubeResourceListGroup[], Error>;
export interface ApiVersionsRequester {
request(cluster: ClusterData): AsyncResult<KubeResourceListGroup[], Error>;
readonly orderNumber: number;
}
export const requestApiVersionsInjectionToken = getInjectionToken<RequestApiVersions>({
export const apiVersionsRequesterInjectionToken = getInjectionToken<ApiVersionsRequester>({
id: "request-api-versions-token",
});
@@ -6,10 +6,7 @@
import { type KubeConfig, HttpError } from "@kubernetes/client-node";
import { reaction, comparer, runInAction } from "mobx";
import { ClusterStatus } from "../../common/cluster-types";
import type { CreateAuthorizationReview } from "../../common/cluster/authorization-review.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import type { CreateListNamespaces } from "../../common/cluster/list-namespaces.injectable";
import type { RequestNamespaceListPermissionsFor, RequestNamespaceListPermissions } from "../../common/cluster/request-namespace-list-permissions.injectable";
import type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable";
import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster";
import type { Logger } from "../../common/logger";
@@ -25,7 +22,6 @@ import type { RequestApiResources } from "./request-api-resources.injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable";
import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.injectable";
@@ -33,21 +29,31 @@ import loggerInjectable from "../../common/logger.injectable";
import prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable";
import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable";
import requestApiResourcesInjectable from "./request-api-resources.injectable";
import requestNamespaceListPermissionsForInjectable from "../../common/cluster/request-namespace-list-permissions.injectable";
import type { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable";
import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token";
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
import { replaceObservableObject } from "../../common/utils/replace-observable-object";
import type { CreateAuthorizationApi } from "../../common/cluster/create-authorization-api.injectable";
import type { CreateCanI } from "../../common/cluster/create-can-i.injectable";
import type { CreateRequestNamespaceListPermissions, RequestNamespaceListPermissions } from "../../common/cluster/create-request-namespace-list-permissions.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import createAuthorizationApiInjectable from "../../common/cluster/create-authorization-api.injectable";
import createCanIInjectable from "../../common/cluster/create-can-i.injectable";
import createRequestNamespaceListPermissionsInjectable from "../../common/cluster/create-request-namespace-list-permissions.injectable";
import type { CreateCoreApi } from "../../common/cluster/create-core-api.injectable";
import createCoreApiInjectable from "../../common/cluster/create-core-api.injectable";
interface Dependencies {
readonly logger: Logger;
readonly prometheusHandler: ClusterPrometheusHandler;
readonly kubeAuthProxyServer: KubeAuthProxyServer;
readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector;
createAuthorizationReview: CreateAuthorizationReview;
createCanI: CreateCanI;
requestApiResources: RequestApiResources;
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor;
createRequestNamespaceListPermissions: CreateRequestNamespaceListPermissions;
createAuthorizationApi: CreateAuthorizationApi;
createCoreApi: CreateCoreApi;
createListNamespaces: CreateListNamespaces;
detectClusterMetadata: DetectClusterMetadata;
broadcastMessage: BroadcastMessage;
@@ -224,8 +230,9 @@ class ClusterConnection {
private async refreshAccessibility(): Promise<void> {
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta());
const proxyConfig = await this.dependencies.loadProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig);
const api = this.dependencies.createAuthorizationApi(proxyConfig);
const canI = this.dependencies.createCanI(api);
const requestNamespaceListPermissions = this.dependencies.createRequestNamespaceListPermissions(api);
const isAdmin = await canI({
namespace: "kube-system",
@@ -360,7 +367,8 @@ class ClusterConnection {
}
try {
const listNamespaces = this.dependencies.createListNamespaces(proxyConfig);
const api = this.dependencies.createCoreApi(proxyConfig);
const listNamespaces = this.dependencies.createListNamespaces(api);
return await listNamespaces();
} catch (error) {
@@ -403,13 +411,15 @@ const clusterConnectionInjectable = getInjectable({
prometheusHandler: di.inject(prometheusHandlerInjectable, cluster),
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
broadcastMessage: di.inject(broadcastMessageInjectable),
createAuthorizationReview: di.inject(createAuthorizationReviewInjectable),
createListNamespaces: di.inject(createListNamespacesInjectable),
detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster),
requestApiResources: di.inject(requestApiResourcesInjectable),
requestNamespaceListPermissionsFor: di.inject(requestNamespaceListPermissionsForInjectable),
createAuthorizationApi: di.inject(createAuthorizationApiInjectable),
createCoreApi: di.inject(createCoreApiInjectable),
createCanI: di.inject(createCanIInjectable),
createRequestNamespaceListPermissions: di.inject(createRequestNamespaceListPermissionsInjectable),
},
cluster,
),
@@ -8,7 +8,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Cluster } from "../../common/cluster/cluster";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.injectable";
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
import type { KubeAuthProxy } from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
export interface KubeAuthProxyServer {
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;
@@ -7,11 +7,12 @@ import { getInjectable } from "@ogre-tools/injectable";
import loggerInjectable from "../../common/logger.injectable";
import type { KubeApiResource } from "../../common/rbac";
import type { Cluster } from "../../common/cluster/cluster";
import { requestApiVersionsInjectionToken } from "./request-api-versions";
import { apiVersionsRequesterInjectionToken } from "./api-versions-requester";
import { backoffCaller, withConcurrencyLimit } from "@k8slens/utilities";
import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable";
import type { AsyncResult } from "@k8slens/utilities";
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
import { byOrderNumber } from "../../common/utils/composable-responsibilities/orderable/orderable";
export type RequestApiResources = (cluster: Cluster) => AsyncResult<KubeApiResource[], Error>;
@@ -24,7 +25,8 @@ const requestApiResourcesInjectable = getInjectable({
id: "request-api-resources",
instantiate: (di): RequestApiResources => {
const logger = di.inject(loggerInjectable);
const apiVersionRequesters = di.injectMany(requestApiVersionsInjectionToken);
const apiVersionRequesters = di.injectMany(apiVersionsRequesterInjectionToken)
.sort(byOrderNumber);
const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable);
return async (...args) => {
@@ -35,7 +37,7 @@ const requestApiResourcesInjectable = getInjectable({
const groupLists: KubeResourceListGroup[] = [];
for (const apiVersionRequester of apiVersionRequesters) {
const result = await backoffCaller(() => apiVersionRequester(cluster), {
const result = await backoffCaller(() => apiVersionRequester.request(cluster), {
onIntermediateError: (error, attempt) => {
broadcastConnectionUpdate({
message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`,
@@ -5,33 +5,36 @@
import type { V1APIVersions } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import k8sRequestInjectable from "../k8s-request.injectable";
import { requestApiVersionsInjectionToken } from "./request-api-versions";
import { apiVersionsRequesterInjectionToken } from "./api-versions-requester";
const requestCoreApiVersionsInjectable = getInjectable({
id: "request-core-api-versions",
instantiate: (di) => {
const k8sRequest = di.inject(k8sRequestInjectable);
return async (cluster) => {
try {
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions;
return {
request: async (cluster) => {
try {
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions;
return {
callWasSuccessful: true,
response: versions.map(version => ({
group: "",
path: `/api/${version}`,
})),
};
} catch (error) {
return {
callWasSuccessful: false,
error: error as Error,
};
}
return {
callWasSuccessful: true,
response: versions.map(version => ({
group: "",
path: `/api/${version}`,
})),
};
} catch (error) {
return {
callWasSuccessful: false,
error: error as Error,
};
}
},
orderNumber: 10,
};
},
injectionToken: requestApiVersionsInjectionToken,
injectionToken: apiVersionsRequesterInjectionToken,
});
export default requestCoreApiVersionsInjectable;
@@ -8,7 +8,7 @@ import type { Cluster } from "../../common/cluster/cluster";
import type { KubeApiResource } from "../../common/rbac";
import type { AsyncResult } from "@k8slens/utilities";
import k8sRequestInjectable from "../k8s-request.injectable";
import type { KubeResourceListGroup } from "./request-api-versions";
import type { KubeResourceListGroup } from "./api-versions-requester";
export type RequestKubeApiResources = (grouping: KubeResourceListGroup) => AsyncResult<KubeApiResource[], Error>;
@@ -6,35 +6,40 @@ import type { V1APIGroupList } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import { iter } from "@k8slens/utilities";
import k8sRequestInjectable from "../k8s-request.injectable";
import { requestApiVersionsInjectionToken } from "./request-api-versions";
import { apiVersionsRequesterInjectionToken } from "./api-versions-requester";
const requestNonCoreApiVersionsInjectable = getInjectable({
id: "request-non-core-api-versions",
instantiate: (di) => {
const k8sRequest = di.inject(k8sRequestInjectable);
return async (cluster) => {
try {
const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList;
return {
request: async (cluster) => {
try {
const { groups } = (await k8sRequest(cluster, "/apis")) as V1APIGroupList;
return {
callWasSuccessful: true,
response: iter.chain(groups.values())
.flatMap(group => group.versions.map(version => ({
group: group.name,
path: `/apis/${version.groupVersion}`,
})))
.collect(v => [...v]),
};
} catch (error) {
return {
callWasSuccessful: false,
error: error as Error,
};
}
return {
callWasSuccessful: true,
response: iter.chain(groups.values())
.flatMap((group) =>
group.versions.map((version) => ({
group: group.name,
path: `/apis/${version.groupVersion}`,
})),
)
.collect((v) => [...v]),
};
} catch (error) {
return {
callWasSuccessful: false,
error: error as Error,
};
}
},
orderNumber: 20,
};
},
injectionToken: requestApiVersionsInjectionToken,
injectionToken: apiVersionsRequesterInjectionToken,
});
export default requestNonCoreApiVersionsInjectable;
@@ -10,13 +10,13 @@ import type { DiContainer } from "@ogre-tools/injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { K8sRequest } from "../k8s-request.injectable";
import k8sRequestInjectable from "../k8s-request.injectable";
import type { RequestApiVersions } from "./request-api-versions";
import type { ApiVersionsRequester } from "./api-versions-requester";
import requestNonCoreApiVersionsInjectable from "./request-non-core-api-versions.injectable";
describe("requestNonCoreApiVersions", () => {
let di: DiContainer;
let k8sRequestMock: AsyncFnMock<K8sRequest>;
let requestNonCoreApiVersions: RequestApiVersions;
let requestNonCoreApiVersions: ApiVersionsRequester;
beforeEach(() => {
di = getDiForUnitTesting();
@@ -28,10 +28,10 @@ describe("requestNonCoreApiVersions", () => {
});
describe("when called", () => {
let versionsRequest: ReturnType<RequestApiVersions>;
let versionsRequest: ReturnType<ApiVersionsRequester["request"]>;
beforeEach(() => {
versionsRequest = requestNonCoreApiVersions({ id: "some-cluster-id" });
versionsRequest = requestNonCoreApiVersions.request({ id: "some-cluster-id" });
});
it("should request all api groups", () => {
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeAuthProxyDependencies } from "./kube-auth-proxy";
import { KubeAuthProxy } from "./kube-auth-proxy";
import { KubeAuthProxyImpl } from "./kube-auth-proxy";
import type { Cluster } from "../../common/cluster/cluster";
import spawnInjectable from "../child-process/spawn.injectable";
import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.injectable";
@@ -15,6 +15,13 @@ import getPortFromStreamInjectable from "../utils/get-port-from-stream.injectabl
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
export interface KubeAuthProxy {
readonly apiPrefix: string;
readonly port: number;
run: () => Promise<void>;
exit: () => void;
}
export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy;
const createKubeAuthProxyInjectable = getInjectable({
@@ -33,7 +40,7 @@ const createKubeAuthProxyInjectable = getInjectable({
return (cluster, env) => {
const clusterUrl = new URL(cluster.apiUrl.get());
return new KubeAuthProxy({
return new KubeAuthProxyImpl({
...dependencies,
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
@@ -16,6 +16,7 @@ import type { Logger } from "../../common/logger";
import type { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable";
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
import type { BroadcastConnectionUpdate } from "../cluster/broadcast-connection-update.injectable";
import type { KubeAuthProxy } from "./create-kube-auth-proxy.injectable";
const startingServeMatcher = "starting to serve on (?<address>.+)";
const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
@@ -33,7 +34,7 @@ export interface KubeAuthProxyDependencies {
broadcastConnectionUpdate: BroadcastConnectionUpdate;
}
export class KubeAuthProxy {
export class KubeAuthProxyImpl implements KubeAuthProxy {
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
public get port(): number {
@@ -85,7 +85,7 @@ export class KubeconfigManager {
return this.tempFilePath = await this.createProxyKubeconfig();
} catch (error) {
throw new Error(`Failed to creat temp config for auth-proxy: ${error}`);
throw new Error(`Failed to create temp config for auth-proxy: ${error}`);
}
}
@@ -8,7 +8,11 @@ import { BrowserWindow } from "electron";
const resolveSystemProxyWindowInjectable = getInjectable({
id: "resolve-system-proxy-window",
instantiate: () => {
return new BrowserWindow({ show: false, paintWhenInitiallyHidden: false });
const window = new BrowserWindow({ show: false });
window.hide();
return window;
},
causesSideEffects: true,
});
@@ -0,0 +1,53 @@
/**
* 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 { reaction } from "mobx";
import { customResourceDefinitionApiInjectionToken } from "../../../common/k8s-api/api-manager/crd-api-token";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import { KubeApi } from "../../../common/k8s-api/kube-api";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { injectableDifferencingRegistratorWith } from "../../../common/utils/registrator-helper";
import customResourceDefinitionStoreInjectable from "../../components/+custom-resources/definition.store.injectable";
import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens";
const setupAutoCrdApiCreationsInjectable = getInjectable({
id: "setup-auto-crd-api-creations",
instantiate: (di) => ({
run: () => {
const customResourceDefinitionStore = di.inject(customResourceDefinitionStoreInjectable);
const injectableDifferencingRegistrator = injectableDifferencingRegistratorWith(di);
reaction(
() => customResourceDefinitionStore.getItems().map(toCrdApiInjectable),
injectableDifferencingRegistrator,
{
fireImmediately: true,
},
);
},
}),
injectionToken: beforeClusterFrameStartsSecondInjectionToken,
});
export default setupAutoCrdApiCreationsInjectable;
const toCrdApiInjectable = (crd: CustomResourceDefinition) => getInjectable({
id: `default-kube-api-for-custom-resource-definition-${crd.getResourceApiBase()}`,
instantiate: (di) => {
const objectConstructor = class extends KubeObject {
static readonly kind = crd.getResourceKind();
static readonly namespaced = crd.isNamespaced();
static readonly apiBase = crd.getResourceApiBase();
};
return new KubeApi({
logger: di.inject(loggerInjectable),
maybeKubeApi: di.inject(maybeKubeApiInjectable),
}, { objectConstructor });
},
injectionToken: customResourceDefinitionApiInjectionToken,
});
@@ -5,71 +5,22 @@
import { getInjectable } from "@ogre-tools/injectable";
import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable";
import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable";
import { CustomResourceStore } from "../../../common/k8s-api/api-manager/resource.store";
import type { CustomResourceDefinition } from "../../../common/k8s-api/endpoints";
import type { KubeApiDependencies } from "../../../common/k8s-api/kube-api";
import { KubeApi } from "../../../common/k8s-api/kube-api";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import type { KubeApi } from "../../../common/k8s-api/kube-api";
import { beforeClusterFrameStartsSecondInjectionToken } from "../tokens";
import type { KubeObjectStoreDependencies } from "../../../common/k8s-api/kube-object.store";
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import maybeKubeApiInjectable from "../../../common/k8s-api/maybe-kube-api.injectable";
const setupAutoRegistrationInjectable = getInjectable({
id: "setup-auto-registration",
instantiate: (di) => ({
run: () => {
const autoRegistrationEmitter = di.inject(autoRegistrationEmitterInjectable);
const beforeApiManagerInitializationCrds: CustomResourceDefinition[] = [];
const beforeApiManagerInitializationApis: KubeApi[] = [];
const kubeApiDependencies: KubeApiDependencies = {
logger: di.inject(loggerInjectable),
maybeKubeApi: di.inject(maybeKubeApiInjectable),
};
const kubeObjectStoreDependencies: KubeObjectStoreDependencies = {
context: di.inject(clusterFrameContextForNamespacedResourcesInjectable),
logger: di.inject(loggerInjectable),
};
let initialized = false;
const autoInitCustomResourceStore = (crd: CustomResourceDefinition) => {
const objectConstructor = class extends KubeObject {
static readonly kind = crd.getResourceKind();
static readonly namespaced = crd.isNamespaced();
static readonly apiBase = crd.getResourceApiBase();
};
const api = (() => {
const rawApi = apiManager.getApi(objectConstructor.apiBase);
if (rawApi) {
return rawApi;
}
const api = new KubeApi(kubeApiDependencies, { objectConstructor });
apiManager.registerApi(api);
return api;
})();
if (!apiManager.getStore(api)) {
apiManager.registerStore(new CustomResourceStore(kubeObjectStoreDependencies, api));
}
};
const autoInitKubeApi = (api: KubeApi) => {
apiManager.registerApi(api);
};
autoRegistrationEmitter
.on("customResourceDefinition", (crd) => {
if (initialized) {
autoInitCustomResourceStore(crd);
} else {
beforeApiManagerInitializationCrds.push(crd);
}
})
.on("kubeApi", (api) => {
if (initialized) {
autoInitKubeApi(api);
@@ -81,7 +32,6 @@ const setupAutoRegistrationInjectable = getInjectable({
// NOTE: this MUST happen after the event emitter listeners are registered
const apiManager = di.inject(apiManagerInjectable);
beforeApiManagerInitializationCrds.forEach(autoInitCustomResourceStore);
beforeApiManagerInitializationApis.forEach(autoInitKubeApi);
initialized = true;
},
@@ -4,8 +4,13 @@
*/
import type { RenderResult } from "@testing-library/react";
import React from "react";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Cluster } from "../../../common/cluster/cluster";
import { HorizontalPodAutoscaler, HpaMetricType } from "../../../common/k8s-api/endpoints";
import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
import type { DiRender } from "../test-utils/renderFor";
import { renderFor } from "../test-utils/renderFor";
import { HpaDetails } from "./hpa-details";
@@ -41,6 +46,17 @@ describe("<HpaDetails/>", () => {
beforeEach(() => {
const di = getDiForUnitTesting();
di.override(directoryForUserDataInjectable, () => "/some-user-store-path");
di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
di.override(hostedClusterInjectable, () => new Cluster({
contextName: "some-context-name",
id: "some-cluster-id",
kubeConfigPath: "/some-path-to-a-kubeconfig",
}, {
clusterServerUrl: "https://localhost:8080",
}));
render = renderFor(di);
});
@@ -4,7 +4,6 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import assert from "assert";
import autoRegistrationEmitterInjectable from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable";
import { kubeObjectStoreInjectionToken } from "../../../common/k8s-api/api-manager/kube-object-store-token";
import customResourceDefinitionApiInjectable from "../../../common/k8s-api/endpoints/custom-resource-definition.api.injectable";
import loggerInjectable from "../../../common/logger.injectable";
@@ -20,7 +19,6 @@ const customResourceDefinitionStoreInjectable = getInjectable({
const api = di.inject(customResourceDefinitionApiInjectable);
return new CustomResourceDefinitionStore({
autoRegistration: di.inject(autoRegistrationEmitterInjectable),
context: di.inject(clusterFrameContextForClusterScopedResourcesInjectable),
logger: di.inject(loggerInjectable),
}, api);
@@ -3,37 +3,22 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { computed, reaction, makeObservable } from "mobx";
import { computed, makeObservable } from "mobx";
import type { KubeObjectStoreDependencies, KubeObjectStoreOptions } from "../../../common/k8s-api/kube-object.store";
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import type { CustomResourceDefinition, CustomResourceDefinitionApi } from "../../../common/k8s-api/endpoints/custom-resource-definition.api";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import type TypedEventEmitter from "typed-emitter";
import type { LegacyAutoRegistration } from "../../../common/k8s-api/api-manager/auto-registration-emitter.injectable";
import autoBind from "auto-bind";
export interface CustomResourceDefinitionStoreDependencies extends KubeObjectStoreDependencies {
readonly autoRegistration: TypedEventEmitter<LegacyAutoRegistration>;
}
export class CustomResourceDefinitionStore extends KubeObjectStore<CustomResourceDefinition, CustomResourceDefinitionApi> {
constructor(
protected readonly dependencies: CustomResourceDefinitionStoreDependencies,
dependencies: KubeObjectStoreDependencies,
api: CustomResourceDefinitionApi,
opts?: KubeObjectStoreOptions,
) {
super(dependencies, api, opts);
makeObservable(this);
autoBind(this);
reaction(
() => this.getItems(),
crds => {
for (const crd of crds) {
this.dependencies.autoRegistration.emit("customResourceDefinition", crd);
}
},
);
}
protected sortItems(items: CustomResourceDefinition[]) {
@@ -51,3 +51,55 @@ exports[`Icon settings given no external registrations for cluster settings menu
</div>
</body>
`;
exports[`Icon settings given no registrations for cluster settings component injection token renders 1`] = `
<body>
<div>
<div>
<div
class="file-loader flex flex-row items-center"
>
<div
class="mr-5"
>
<div
class="FilePicker"
>
<label
class="flex gaps align-center"
for="file-upload"
>
<div
class="Avatar rounded"
style="width: 53px; height: 53px; background: rgb(9, 124, 92);"
>
skc
</div>
</label>
<input
accept="image/*"
id="file-upload"
name="FilePicker"
type="file"
/>
</div>
</div>
<i
class="Icon material interactive focusable"
data-testid="icon-for-menu-actions-for-cluster-icon-settings-for-some-entity-id"
id="menu-actions-for-cluster-icon-settings-for-some-entity-id"
tabindex="0"
>
<span
class="icon"
data-icon-name="more_horiz"
>
more_horiz
</span>
</i>
</div>
</div>
</div>
</body>
`;
@@ -2,8 +2,7 @@
* 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 { RenderResult } from "@testing-library/react";
import React from "react";
import { KubernetesCluster } from "../../../../common/catalog-entities";
@@ -13,13 +12,18 @@ import { renderFor } from "../../test-utils/renderFor";
import { ClusterIconSetting } from "../icon-settings";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { clusterIconSettingsMenuInjectionToken } from "../cluster-settings-menu-injection-token";
import type { ClusterIconSettingComponentProps } from "@k8slens/cluster-settings";
import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings";
import { runInAction } from "mobx";
import { getInjectable, type DiContainer } from "@ogre-tools/injectable";
const cluster = new Cluster({
contextName: "some-context",
id: "some-id",
kubeConfigPath: "/some/path/to/kubeconfig",
preferences: {
clusterName: "some-cluster-name",
},
}, {
clusterServerUrl: "https://localhost:9999",
});
@@ -53,6 +57,29 @@ const newMenuItem = getInjectable({
injectionToken: clusterIconSettingsMenuInjectionToken,
});
function CustomSettingsComponent(props: ClusterIconSettingComponentProps) {
return (
<div data-testid="my-react-component">
<span>Test React Component</span>
<span>
Cluster
{props.preferences.clusterName}
</span>
</div>
);
}
const newSettingsReactComponent = getInjectable({
id: "cluster-icon-settings-react-component",
instantiate: () => ({
id: "test-react-component",
Component: CustomSettingsComponent,
}),
injectionToken: clusterIconSettingsComponentInjectionToken,
});
describe("Icon settings", () => {
let rendered: RenderResult;
let di: DiContainer;
@@ -98,4 +125,30 @@ describe("Icon settings", () => {
expect(rendered.getByText("Hello World")).toBeInTheDocument();
});
});
describe("given no registrations for cluster settings component injection token", () => {
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not have any external components", async () => {
expect(rendered.queryByTestId("test-react-component")).not.toBeInTheDocument();
});
});
describe("given registration for cluster settings component injection token", () => {
beforeEach(() => {
runInAction(() => {
di.register(newSettingsReactComponent);
});
});
it("renders external component", async () => {
expect(rendered.queryByTestId("my-react-component")).toBeInTheDocument();
});
it("external component has cluster preferences in props", async () => {
expect(rendered.getByText(/some-cluster-name/)).toBeInTheDocument();
});
});
});
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token";
import { clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings";
const clusterIconSettingsMenuClearItem = getInjectable({
id: "cluster-icon-settings-menu-clear-item",
@@ -1,17 +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 { ClusterPreferences } from "../../../common/cluster-types";
export interface ClusterIconMenuItem {
id: string;
title: string;
disabled?: (preferences: ClusterPreferences) => boolean;
onClick: (preferences: ClusterPreferences) => void;
}
export const clusterIconSettingsMenuInjectionToken = getInjectionToken<ClusterIconMenuItem>({
id: "cluster-icon-settings-menu-injection-token",
});
@@ -15,8 +15,8 @@ import { FilePicker, OverSizeLimitStyle } from "../file-picker";
import { MenuActions, MenuItem } from "../menu";
import type { ShowNotification } from "../notifications";
import showErrorNotificationInjectable from "../notifications/show-error-notification.injectable";
import type { ClusterIconMenuItem } from "./cluster-settings-menu-injection-token";
import { clusterIconSettingsMenuInjectionToken } from "./cluster-settings-menu-injection-token";
import { clusterIconSettingsComponentInjectionToken, clusterIconSettingsMenuInjectionToken } from "@k8slens/cluster-settings";
import type { ClusterIconMenuItem, ClusterIconSettingsComponent } from "@k8slens/cluster-settings";
export interface ClusterIconSettingProps {
cluster: Cluster;
@@ -25,6 +25,7 @@ export interface ClusterIconSettingProps {
interface Dependencies {
menuItems: IComputedValue<ClusterIconMenuItem[]>;
settingComponents: IComputedValue<ClusterIconSettingsComponent[]>;
showErrorNotification: ShowNotification;
}
@@ -95,6 +96,14 @@ const NonInjectedClusterIconSetting = observer((props: ClusterIconSettingProps &
)}
</MenuActions>
</div>
{props.settingComponents.get().map(item => {
return (
<item.Component
key={item.id}
preferences={cluster.preferences}
/>
);
})}
</div>
);
});
@@ -106,6 +115,7 @@ export const ClusterIconSetting = withInjectables<Dependencies, ClusterIconSetti
return {
...props,
menuItems: computedInjectMany(clusterIconSettingsMenuInjectionToken),
settingComponents: computedInjectMany(clusterIconSettingsComponentInjectionToken),
showErrorNotification: di.inject(showErrorNotificationInjectable),
};
},
@@ -53,7 +53,7 @@ import { applicationWindowInjectionToken } from "../../../main/start-main-applic
import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { FakeExtensionOptions } from "./get-extension-fake";
import { getExtensionFakeForMain, getExtensionFakeForRenderer } from "./get-extension-fake";
import { getMainExtensionFakeWith, getRendererExtensionFakeWith } from "./get-extension-fake";
import namespaceApiInjectable from "../../../common/k8s-api/endpoints/namespace.api.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import { getOverrideFsWithFakes } from "../../../test-utils/override-fs-with-fakes";
@@ -594,49 +594,28 @@ export const getApplicationBuilder = () => {
},
enable: (...extensions) => {
builder.afterWindowStart(({ windowDi }) => {
const rendererExtensionInstances = extensions.map((options) =>
getExtensionFakeForRenderer(
windowDi,
options.id,
options.name,
options.rendererOptions || {},
),
);
builder.afterWindowStart(action(({ windowDi }) => {
extensions
.map(getRendererExtensionFakeWith(windowDi))
.forEach(enableExtensionFor(windowDi, rendererExtensionsStateInjectable));
}));
rendererExtensionInstances.forEach(
enableExtensionFor(windowDi, rendererExtensionsStateInjectable),
);
});
builder.afterApplicationStart(({ mainDi }) => {
const mainExtensionInstances = extensions.map((extension) =>
getExtensionFakeForMain(mainDi, extension.id, extension.name, extension.mainOptions || {}),
);
runInAction(() => {
mainExtensionInstances.forEach(
enableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
});
});
builder.afterApplicationStart(action(({ mainDi }) => {
extensions
.map(getMainExtensionFakeWith(mainDi))
.forEach(enableExtensionFor(mainDi, mainExtensionsStateInjectable));
}));
},
disable: (...extensions) => {
builder.afterWindowStart(({ windowDi }) => {
extensions
.map((ext) => ext.id)
.forEach(
disableExtensionFor(windowDi, rendererExtensionsStateInjectable),
);
.forEach(disableExtensionFor(windowDi, rendererExtensionsStateInjectable));
});
builder.afterApplicationStart(({ mainDi }) => {
extensions
.map((ext) => ext.id)
.forEach(
disableExtensionFor(mainDi, mainExtensionsStateInjectable),
);
.forEach(disableExtensionFor(mainDi, mainExtensionsStateInjectable));
});
},
},
@@ -835,49 +814,29 @@ const selectOptionFor = (builder: ApplicationBuilder, menuId: string) => (labelT
userEvent.click(option);
};
const enableExtensionFor = (
di: DiContainer,
stateInjectable: Injectable<ObservableMap<string, any>, any, any>,
) => {
function enableExtensionFor(di: DiContainer, stateInjectable: Injectable<ObservableMap<string, any>, any, any>) {
const extensionState = di.inject(stateInjectable);
const getExtension = (extension: LensExtension) =>
di.inject(extensionInjectable, extension);
return (instance: LensExtension) => {
const extension = di.inject(extensionInjectable, instance);
return (extensionInstance: LensExtension) => {
const extension = getExtension(extensionInstance);
runInAction(() => {
extension.register();
extensionState.set(extensionInstance.id, extensionInstance);
});
extension.register();
extensionState.set(instance.id, instance);
};
};
}
const disableExtensionFor =
(
di: DiContainer,
stateInjectable: Injectable<ObservableMap<string, any>, unknown, void>,
) =>
(id: string) => {
const getExtension = (extension: LensExtension) =>
di.inject(extensionInjectable, extension);
function disableExtensionFor(di: DiContainer, stateInjectable: Injectable<ObservableMap<string, any>, unknown, void>) {
return (extension: FakeExtensionOptions) => {
const extensionsState = di.inject(stateInjectable);
const instance = extensionsState.get(extension.id);
const extensionsState = di.inject(stateInjectable);
if (!instance) {
throw new Error(`Tried to disable extension with ID "${extension.id}", but it wasn't enabled`);
}
const instance = extensionsState.get(id);
const injectable = di.inject(extensionInjectable, instance);
if (!instance) {
throw new Error(
`Tried to disable extension with ID "${id}", but it wasn't enabled`,
);
}
const injectable = getExtension(instance);
runInAction(() => {
injectable.deregister();
extensionsState.delete(id);
});
};
injectable.deregister();
extensionsState.delete(extension.id);
};
}
@@ -27,7 +27,7 @@ export interface FakeExtensionOptions {
mainOptions?: Partial<LensMainExtension>;
}
export const getExtensionFakeForMain = (di: DiContainer, id: string, name: string, options: Partial<LensMainExtension>) => {
export const getMainExtensionFakeWith = (di: DiContainer) => ({ id, name, mainOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionMain({
id,
absolutePath: "irrelevant",
@@ -44,7 +44,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin
manifestPath: "irrelevant",
});
Object.assign(instance, options);
Object.assign(instance, mainOptions);
(instance as Writable<LensMainExtension>)[lensExtensionDependencies] = {
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
@@ -56,7 +56,7 @@ export const getExtensionFakeForMain = (di: DiContainer, id: string, name: strin
return instance;
};
export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: string, options: Partial<LensRendererExtension>) => {
export const getRendererExtensionFakeWith = (di: DiContainer) => ({ id, name, rendererOptions = {}}: FakeExtensionOptions) => {
const instance = new TestExtensionRenderer({
id,
absolutePath: "irrelevant",
@@ -73,7 +73,7 @@ export const getExtensionFakeForRenderer = (di: DiContainer, id: string, name: s
manifestPath: "irrelevant",
});
Object.assign(instance, options);
Object.assign(instance, rendererOptions);
(instance as Writable<LensRendererExtension>)[lensExtensionDependencies] = {
categoryRegistry: di.inject(catalogCategoryRegistryInjectable),
+6
View File
@@ -0,0 +1,6 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const cast = <T>(data: Partial<T>): T => data as T;
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncFnMock } from "@async-fn/jest";
type GetMockedType<T> =
T extends (...args: any[]) => Promise<any>
? AsyncFnMock<T>
: T extends (...args: any[]) => any
? jest.MockedFunction<T>
: T;
export type Mocked<T extends object> = {
-readonly [P in keyof T]: GetMockedType<T[P]>;
};
+2 -2
View File
@@ -96,7 +96,7 @@
},
"build": {
"npmRebuild": false,
"electronVersion": "19.1.9",
"electronVersion": "22.3.3",
"generateUpdatesFilesForAllChannels": true,
"files": [
"static/**/*",
@@ -251,7 +251,7 @@
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.7.2",
"electron": "^19.1.9",
"electron": "^22.3.3",
"electron-builder": "^23.6.0",
"electron-notarize": "^0.3.0",
"esbuild-loader": "^2.20.0",
@@ -35,7 +35,7 @@
"@k8slens/feature-core": "^6.5.0-alpha.0",
"@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
"electron": "^19.1.9"
"electron": "^22.3.3"
},
"devDependencies": {
"@async-fn/jest": "^1.6.4",
@@ -9,7 +9,7 @@ export interface MessageChannel<Message> {
export type ExtraData = { processId: number; frameId: number };
export type MessageChannelHandler<Channel> = Channel extends MessageChannel<infer Message>
? (message: Message, data: ExtraData) => void
? (message: Message, data?: ExtraData) => void
: never;
export interface MessageChannelListener<Channel> {
@@ -38,7 +38,7 @@
"@k8slens/messaging": "^1.0.0-alpha.1",
"@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
"electron": "^19.1.8",
"electron": "^22.3.3",
"lodash": "^4.17.21"
},
"devDependencies": {
@@ -39,7 +39,7 @@
"@k8slens/startable-stoppable": "^1.0.0-alpha.1",
"@ogre-tools/injectable": "^15.1.2",
"@ogre-tools/injectable-extension-for-auto-registration": "^15.1.2",
"electron": "^19.1.8",
"electron": "^22.3.3",
"lodash": "^4.17.21"
},
"devDependencies": {
@@ -10,7 +10,7 @@ const enlistMessageChannelListenerInjectable = getInjectable({
const ipcRenderer = di.inject(ipcRendererInjectable);
return ({ channel, handler }) => {
const nativeCallback = (_: IpcRendererEvent, message: unknown) => {
const nativeCallback = (event: IpcRendererEvent, message: unknown) => {
handler(message);
};
@@ -2,6 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { setImmediate } from "timers";
import { setImmediate, setTimeout } from "timers/promises";
export const flushPromises = () => new Promise(setImmediate);
export const flushPromises = async () => {
await setImmediate();
await setTimeout(5);
};