mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Add behavioural tests for Cluster Menu K8s Resources in Sidebar menu not being shown (#7280)
* Add behavioural tests to cover bug fix Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove previous fix to fix last test Signed-off-by: Sebastian Malton <sebastian@malton.name> * More consistent impl of flushPromises Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fixup tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove ContextHandler test (dead code) Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix PrometheusHandler describe text Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix type errors Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add useful case test-utils helper Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rename file to match token Signed-off-by: Sebastian Malton <sebastian@malton.name> * Cleanup tests to fix type errors and use tables Signed-off-by: Sebastian Malton <sebastian@malton.name> --------- Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
5409324236
commit
8a80607d85
@ -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;
|
||||||
42
packages/core/src/common/cluster/create-can-i.injectable.ts
Normal file
42
packages/core/src/common/cluster/create-can-i.injectable.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AuthorizationV1Api, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import loggerInjectable from "../logger.injectable";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the permissions for actions on the kube cluster
|
||||||
|
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
|
||||||
|
* @returns `true` if the actions described are allowed
|
||||||
|
*/
|
||||||
|
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
|
||||||
|
|
||||||
|
export type CreateCanI = (api: AuthorizationV1Api) => CanI;
|
||||||
|
|
||||||
|
const createCanIInjectable = getInjectable({
|
||||||
|
id: "create-can-i",
|
||||||
|
instantiate: (di): CreateCanI => {
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
|
||||||
|
return (api) => async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const { body } = await api.createSelfSubjectAccessReview({
|
||||||
|
apiVersion: "authorization.k8s.io/v1",
|
||||||
|
kind: "SelfSubjectAccessReview",
|
||||||
|
spec: { resourceAttributes },
|
||||||
|
});
|
||||||
|
|
||||||
|
return body.status?.allowed ?? false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createCanIInjectable;
|
||||||
@ -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.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import type { CoreV1Api } from "@kubernetes/client-node";
|
||||||
import { CoreV1Api } from "@kubernetes/client-node";
|
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { isDefined } from "@k8slens/utilities";
|
import { isDefined } from "@k8slens/utilities";
|
||||||
|
|
||||||
export type ListNamespaces = () => Promise<string[]>;
|
export type ListNamespaces = () => Promise<string[]>;
|
||||||
|
export type CreateListNamespaces = (api: CoreV1Api) => ListNamespaces;
|
||||||
export type CreateListNamespaces = (config: KubeConfig) => ListNamespaces;
|
|
||||||
|
|
||||||
const createListNamespacesInjectable = getInjectable({
|
const createListNamespacesInjectable = getInjectable({
|
||||||
id: "create-list-namespaces",
|
id: "create-list-namespaces",
|
||||||
instantiate: (): CreateListNamespaces => (config) => {
|
instantiate: (): CreateListNamespaces => (api) => async () => {
|
||||||
const coreApi = config.makeApiClient(CoreV1Api);
|
const { body: { items }} = await api.listNamespace();
|
||||||
|
|
||||||
return async () => {
|
|
||||||
const { body: { items }} = await coreApi.listNamespace();
|
|
||||||
|
|
||||||
return items
|
return items
|
||||||
.map(ns => ns.metadata?.name)
|
.map(ns => ns.metadata?.name)
|
||||||
.filter(isDefined);
|
.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,74 +3,68 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { V1SubjectRulesReviewStatus } from "@kubernetes/client-node";
|
import type { AsyncFnMock } from "@async-fn/jest";
|
||||||
|
import asyncFn from "@async-fn/jest";
|
||||||
|
import type { AuthorizationV1Api, V1SubjectRulesReviewStatus } from "@kubernetes/client-node";
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
|
import type { IncomingMessage } from "http";
|
||||||
|
import { anyObject } from "jest-mock-extended";
|
||||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||||
import type { RequestNamespaceListPermissionsFor } from "./request-namespace-list-permissions.injectable";
|
import { cast } from "../../test-utils/cast";
|
||||||
import requestNamespaceListPermissionsForInjectable from "./request-namespace-list-permissions.injectable";
|
import type { KubeApiResource } from "../rbac";
|
||||||
|
import type { RequestNamespaceListPermissions } from "./create-request-namespace-list-permissions.injectable";
|
||||||
|
import createRequestNamespaceListPermissionsInjectable from "./create-request-namespace-list-permissions.injectable";
|
||||||
|
|
||||||
const createStubProxyConfig = (statusResponse: Promise<{ body: { status: V1SubjectRulesReviewStatus }}>) => ({
|
interface TestCase {
|
||||||
makeApiClient: () => ({
|
description: string;
|
||||||
createSelfSubjectRulesReview: (): Promise<{ body: { status: V1SubjectRulesReviewStatus }}> => statusResponse,
|
status: V1SubjectRulesReviewStatus;
|
||||||
}),
|
expected: boolean;
|
||||||
});
|
}
|
||||||
|
|
||||||
describe("requestNamespaceListPermissions", () => {
|
describe("requestNamespaceListPermissions", () => {
|
||||||
let di: DiContainer;
|
let di: DiContainer;
|
||||||
let requestNamespaceListPermissions: RequestNamespaceListPermissionsFor;
|
let createSelfSubjectRulesReviewMock: AsyncFnMock<AuthorizationV1Api["createSelfSubjectRulesReview"]>;
|
||||||
|
let requestNamespaceListPermissions: RequestNamespaceListPermissions;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
di = getDiForUnitTesting();
|
di = getDiForUnitTesting();
|
||||||
requestNamespaceListPermissions = di.inject(requestNamespaceListPermissionsForInjectable);
|
|
||||||
|
const createRequestNamespaceListPermissions = di.inject(createRequestNamespaceListPermissionsInjectable);
|
||||||
|
|
||||||
|
createSelfSubjectRulesReviewMock = asyncFn();
|
||||||
|
|
||||||
|
requestNamespaceListPermissions = createRequestNamespaceListPermissions(cast<AuthorizationV1Api>({
|
||||||
|
createSelfSubjectRulesReview: createSelfSubjectRulesReviewMock,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when api returns incomplete data", () => {
|
describe("when a request for list permissions in a namespace has been started", () => {
|
||||||
it("returns truthy function", async () => {
|
let request: ReturnType<RequestNamespaceListPermissions>;
|
||||||
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
|
|
||||||
new Promise((resolve) => resolve({
|
beforeEach(() => {
|
||||||
body: {
|
request = requestNamespaceListPermissions("irrelevant-namespace");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should request the creation of a SelfSubjectRulesReview", () => {
|
||||||
|
expect(createSelfSubjectRulesReviewMock).toBeCalledWith(anyObject({
|
||||||
|
spec: {
|
||||||
|
namespace: "irrelevant-namespace",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
([
|
||||||
|
{
|
||||||
|
description: "incomplete data",
|
||||||
status: {
|
status: {
|
||||||
incomplete: true,
|
incomplete: true,
|
||||||
resourceRules: [],
|
resourceRules: [],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: true,
|
||||||
},
|
},
|
||||||
})),
|
{
|
||||||
) as any);
|
description: "first resourceRule has all permissions for everything",
|
||||||
|
|
||||||
const permissionCheck = await requestPermissions("irrelevant-namespace");
|
|
||||||
|
|
||||||
expect(permissionCheck({
|
|
||||||
apiName: "pods",
|
|
||||||
group: "",
|
|
||||||
kind: "Pod",
|
|
||||||
namespaced: true,
|
|
||||||
})).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when first resourceRule has all permissions for everything", () => {
|
|
||||||
it("return truthy function", async () => {
|
|
||||||
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
|
|
||||||
new Promise((resolve) => resolve({
|
|
||||||
body: {
|
|
||||||
status: {
|
status: {
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resourceRules: [
|
resourceRules: [
|
||||||
@ -85,26 +79,10 @@ describe("requestNamespaceListPermissions", () => {
|
|||||||
],
|
],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: true,
|
||||||
},
|
},
|
||||||
})),
|
{
|
||||||
) as any);
|
description: "first resourceRule has list permissions for everything",
|
||||||
|
|
||||||
const permissionCheck = await requestPermissions("irrelevant-namespace");
|
|
||||||
|
|
||||||
expect(permissionCheck({
|
|
||||||
apiName: "pods",
|
|
||||||
group: "",
|
|
||||||
kind: "Pod",
|
|
||||||
namespaced: true,
|
|
||||||
})).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when first resourceRule has list permissions for everything", () => {
|
|
||||||
it("return truthy function", async () => {
|
|
||||||
const requestPermissions = requestNamespaceListPermissions(createStubProxyConfig(
|
|
||||||
new Promise((resolve) => resolve({
|
|
||||||
body: {
|
|
||||||
status: {
|
status: {
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resourceRules: [
|
resourceRules: [
|
||||||
@ -119,32 +97,16 @@ describe("requestNamespaceListPermissions", () => {
|
|||||||
],
|
],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: true,
|
||||||
},
|
},
|
||||||
})),
|
{
|
||||||
) as any);
|
description: "first resourceRule has list permissions for asked resource",
|
||||||
|
|
||||||
const permissionCheck = await requestPermissions("irrelevant-namespace");
|
|
||||||
|
|
||||||
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: {
|
status: {
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resourceRules: [
|
resourceRules: [
|
||||||
{
|
{
|
||||||
apiGroups: [""],
|
apiGroups: ["some-api-group"],
|
||||||
resources: ["pods"],
|
resources: ["some-kind"],
|
||||||
verbs: ["list"],
|
verbs: ["list"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -154,26 +116,10 @@ describe("requestNamespaceListPermissions", () => {
|
|||||||
],
|
],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: true,
|
||||||
},
|
},
|
||||||
})),
|
{
|
||||||
) as any);
|
description: "last resourceRule has all permissions for everything",
|
||||||
|
|
||||||
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: {
|
status: {
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resourceRules: [
|
resourceRules: [
|
||||||
@ -188,26 +134,10 @@ describe("requestNamespaceListPermissions", () => {
|
|||||||
],
|
],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: true,
|
||||||
},
|
},
|
||||||
})),
|
{
|
||||||
) as any);
|
description: "last resourceRule has list permissions for asked resource",
|
||||||
|
|
||||||
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: {
|
status: {
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resourceRules: [
|
resourceRules: [
|
||||||
@ -216,98 +146,32 @@ describe("requestNamespaceListPermissions", () => {
|
|||||||
verbs: ["get"],
|
verbs: ["get"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
apiGroups: ["*"],
|
apiGroups: ["some-api-group"],
|
||||||
|
resources: ["some-kind"],
|
||||||
verbs: ["list"],
|
verbs: ["list"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: true,
|
||||||
},
|
},
|
||||||
})),
|
{
|
||||||
) as any);
|
description: "resourceRules has matching resource without list verb",
|
||||||
|
|
||||||
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: {
|
status: {
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resourceRules: [
|
resourceRules: [
|
||||||
{
|
{
|
||||||
apiGroups: ["*"],
|
apiGroups: ["some-api-group"],
|
||||||
verbs: ["get"],
|
resources: ["some-kind"],
|
||||||
},
|
|
||||||
{
|
|
||||||
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"],
|
verbs: ["get"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: false,
|
||||||
},
|
},
|
||||||
})),
|
{
|
||||||
) as any);
|
description: "resourceRules has no matching resource with list verb",
|
||||||
|
|
||||||
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: {
|
status: {
|
||||||
incomplete: false,
|
incomplete: false,
|
||||||
resourceRules: [
|
resourceRules: [
|
||||||
@ -319,18 +183,45 @@ describe("requestNamespaceListPermissions", () => {
|
|||||||
],
|
],
|
||||||
nonResourceRules: [],
|
nonResourceRules: [],
|
||||||
},
|
},
|
||||||
|
expected: false,
|
||||||
},
|
},
|
||||||
})),
|
] as TestCase[]).forEach(({ description, status, expected }) => {
|
||||||
) as any);
|
describe(`when api returns ${description}`, () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createSelfSubjectRulesReviewMock.resolve({
|
||||||
|
body: {
|
||||||
|
status,
|
||||||
|
spec: {},
|
||||||
|
},
|
||||||
|
response: null as unknown as IncomingMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const permissionCheck = await requestPermissions("irrelevant-namespace");
|
it(`allows the request to complete, and 'canListResource' will return ${expected}`, async () => {
|
||||||
|
const canListResource = await request;
|
||||||
|
|
||||||
expect(permissionCheck({
|
expect(canListResource(someKubeResource)).toBe(expected);
|
||||||
apiName: "pods",
|
});
|
||||||
group: "",
|
});
|
||||||
kind: "Pod",
|
});
|
||||||
|
|
||||||
|
describe("when api rejects", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await createSelfSubjectRulesReviewMock.reject(new Error("unknown error"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows the request to complete, and 'canListResource' will return true", async () => {
|
||||||
|
const canListResource = await request;
|
||||||
|
|
||||||
|
expect(canListResource(someKubeResource)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const someKubeResource: KubeApiResource = {
|
||||||
|
apiName: "some-kind",
|
||||||
|
group: "some-api-group",
|
||||||
|
kind: "SomeKind",
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
})).toBeFalsy();
|
};
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@ -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");
|
|
||||||
});
|
|
||||||
@ -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;
|
||||||
@ -5,10 +5,6 @@
|
|||||||
import { Cluster } from "../../common/cluster/cluster";
|
import { Cluster } from "../../common/cluster/cluster";
|
||||||
import { Kubectl } from "../kubectl/kubectl";
|
import { Kubectl } from "../kubectl/kubectl";
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
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 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 directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
|
||||||
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.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 kubeconfigManagerInjectable from "../kubeconfig-manager/kubeconfig-manager.injectable";
|
||||||
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
import type { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
|
||||||
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.injectable";
|
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", () => {
|
describe("create clusters", () => {
|
||||||
let cluster: Cluster;
|
let cluster: Cluster;
|
||||||
@ -34,8 +34,8 @@ describe("create clusters", () => {
|
|||||||
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
di.override(normalizedPlatformInjectable, () => "darwin");
|
||||||
di.override(broadcastConnectionUpdateInjectable, () => async () => {});
|
di.override(broadcastConnectionUpdateInjectable, () => async () => {});
|
||||||
di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
di.override(createCanIInjectable, () => () => () => Promise.resolve(true));
|
||||||
di.override(requestNamespaceListPermissionsForInjectable, () => () => async () => () => true);
|
di.override(createRequestNamespaceListPermissionsInjectable, () => () => async () => () => true);
|
||||||
di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||||
di.override(prometheusHandlerInjectable, () => ({
|
di.override(prometheusHandlerInjectable, () => ({
|
||||||
getPrometheusDetails: jest.fn(),
|
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 waitUntilPortIsUsedInjectable from "../kube-auth-proxy/wait-until-port-is-used/wait-until-port-is-used.injectable";
|
||||||
import { Cluster } from "../../common/cluster/cluster";
|
import { Cluster } from "../../common/cluster/cluster";
|
||||||
import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy";
|
|
||||||
import type { ChildProcess } from "child_process";
|
import type { ChildProcess } from "child_process";
|
||||||
import { Kubectl } from "../kubectl/kubectl";
|
import { Kubectl } from "../kubectl/kubectl";
|
||||||
import type { DeepMockProxy } from "jest-mock-extended";
|
import type { DeepMockProxy } from "jest-mock-extended";
|
||||||
@ -13,6 +12,7 @@ import { mockDeep, mock } from "jest-mock-extended";
|
|||||||
import type { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
import { EventEmitter } from "stream";
|
import { EventEmitter } from "stream";
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
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 createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||||
import spawnInjectable from "../child-process/spawn.injectable";
|
import spawnInjectable from "../child-process/spawn.injectable";
|
||||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.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";
|
const clusterServerUrl = "https://192.168.64.3:8443";
|
||||||
|
|
||||||
describe("kube auth proxy tests", () => {
|
describe("kube auth proxy tests", () => {
|
||||||
let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
|
let createKubeAuthProxy: CreateKubeAuthProxy;
|
||||||
let spawnMock: jest.Mock;
|
let spawnMock: jest.Mock;
|
||||||
let waitUntilPortIsUsedMock: jest.Mock;
|
let waitUntilPortIsUsedMock: jest.Mock;
|
||||||
let broadcastMessageMock: jest.Mock;
|
let broadcastMessageMock: jest.Mock;
|
||||||
|
|||||||
@ -43,7 +43,7 @@ const createTestPrometheusProvider = (kind: string, alwaysFail: ServiceResult):
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ContextHandler", () => {
|
describe("PrometheusHandler", () => {
|
||||||
let di: DiContainer;
|
let di: DiContainer;
|
||||||
let cluster: Cluster;
|
let cluster: Cluster;
|
||||||
|
|
||||||
|
|||||||
@ -15,8 +15,11 @@ export interface ClusterData {
|
|||||||
readonly id: string;
|
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",
|
id: "request-api-versions-token",
|
||||||
});
|
});
|
||||||
@ -6,10 +6,7 @@
|
|||||||
import { type KubeConfig, HttpError } from "@kubernetes/client-node";
|
import { type KubeConfig, HttpError } from "@kubernetes/client-node";
|
||||||
import { reaction, comparer, runInAction } from "mobx";
|
import { reaction, comparer, runInAction } from "mobx";
|
||||||
import { ClusterStatus } from "../../common/cluster-types";
|
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 { 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 type { BroadcastMessage } from "../../common/ipc/broadcast-message.injectable";
|
||||||
import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster";
|
import { clusterListNamespaceForbiddenChannel } from "../../common/ipc/cluster";
|
||||||
import type { Logger } from "../../common/logger";
|
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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
|
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
|
||||||
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.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 createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||||
import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable";
|
import kubeAuthProxyServerInjectable from "./kube-auth-proxy-server.injectable";
|
||||||
import loadProxyKubeconfigInjectable from "./load-proxy-kubeconfig.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 prometheusHandlerInjectable from "./prometheus-handler/prometheus-handler.injectable";
|
||||||
import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable";
|
import removeProxyKubeconfigInjectable from "./remove-proxy-kubeconfig.injectable";
|
||||||
import requestApiResourcesInjectable from "./request-api-resources.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 { DetectClusterMetadata } from "../cluster-detectors/detect-cluster-metadata.injectable";
|
||||||
import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token";
|
import type { FallibleOnlyClusterMetadataDetector } from "../cluster-detectors/token";
|
||||||
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
|
import clusterVersionDetectorInjectable from "../cluster-detectors/cluster-version-detector.injectable";
|
||||||
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
|
import detectClusterMetadataInjectable from "../cluster-detectors/detect-cluster-metadata.injectable";
|
||||||
import { replaceObservableObject } from "../../common/utils/replace-observable-object";
|
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 {
|
interface Dependencies {
|
||||||
readonly logger: Logger;
|
readonly logger: Logger;
|
||||||
readonly prometheusHandler: ClusterPrometheusHandler;
|
readonly prometheusHandler: ClusterPrometheusHandler;
|
||||||
readonly kubeAuthProxyServer: KubeAuthProxyServer;
|
readonly kubeAuthProxyServer: KubeAuthProxyServer;
|
||||||
readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector;
|
readonly clusterVersionDetector: FallibleOnlyClusterMetadataDetector;
|
||||||
createAuthorizationReview: CreateAuthorizationReview;
|
createCanI: CreateCanI;
|
||||||
requestApiResources: RequestApiResources;
|
requestApiResources: RequestApiResources;
|
||||||
requestNamespaceListPermissionsFor: RequestNamespaceListPermissionsFor;
|
createRequestNamespaceListPermissions: CreateRequestNamespaceListPermissions;
|
||||||
|
createAuthorizationApi: CreateAuthorizationApi;
|
||||||
|
createCoreApi: CreateCoreApi;
|
||||||
createListNamespaces: CreateListNamespaces;
|
createListNamespaces: CreateListNamespaces;
|
||||||
detectClusterMetadata: DetectClusterMetadata;
|
detectClusterMetadata: DetectClusterMetadata;
|
||||||
broadcastMessage: BroadcastMessage;
|
broadcastMessage: BroadcastMessage;
|
||||||
@ -224,8 +230,9 @@ class ClusterConnection {
|
|||||||
private async refreshAccessibility(): Promise<void> {
|
private async refreshAccessibility(): Promise<void> {
|
||||||
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta());
|
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.cluster.getMeta());
|
||||||
const proxyConfig = await this.dependencies.loadProxyKubeconfig();
|
const proxyConfig = await this.dependencies.loadProxyKubeconfig();
|
||||||
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
|
const api = this.dependencies.createAuthorizationApi(proxyConfig);
|
||||||
const requestNamespaceListPermissions = this.dependencies.requestNamespaceListPermissionsFor(proxyConfig);
|
const canI = this.dependencies.createCanI(api);
|
||||||
|
const requestNamespaceListPermissions = this.dependencies.createRequestNamespaceListPermissions(api);
|
||||||
|
|
||||||
const isAdmin = await canI({
|
const isAdmin = await canI({
|
||||||
namespace: "kube-system",
|
namespace: "kube-system",
|
||||||
@ -360,7 +367,8 @@ class ClusterConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const listNamespaces = this.dependencies.createListNamespaces(proxyConfig);
|
const api = this.dependencies.createCoreApi(proxyConfig);
|
||||||
|
const listNamespaces = this.dependencies.createListNamespaces(api);
|
||||||
|
|
||||||
return await listNamespaces();
|
return await listNamespaces();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -403,13 +411,15 @@ const clusterConnectionInjectable = getInjectable({
|
|||||||
prometheusHandler: di.inject(prometheusHandlerInjectable, cluster),
|
prometheusHandler: di.inject(prometheusHandlerInjectable, cluster),
|
||||||
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
|
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
|
||||||
broadcastMessage: di.inject(broadcastMessageInjectable),
|
broadcastMessage: di.inject(broadcastMessageInjectable),
|
||||||
createAuthorizationReview: di.inject(createAuthorizationReviewInjectable),
|
|
||||||
createListNamespaces: di.inject(createListNamespacesInjectable),
|
createListNamespaces: di.inject(createListNamespacesInjectable),
|
||||||
detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
|
detectClusterMetadata: di.inject(detectClusterMetadataInjectable),
|
||||||
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
|
loadProxyKubeconfig: di.inject(loadProxyKubeconfigInjectable, cluster),
|
||||||
removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster),
|
removeProxyKubeconfig: di.inject(removeProxyKubeconfigInjectable, cluster),
|
||||||
requestApiResources: di.inject(requestApiResourcesInjectable),
|
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,
|
cluster,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
|||||||
import type { Cluster } from "../../common/cluster/cluster";
|
import type { Cluster } from "../../common/cluster/cluster";
|
||||||
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
|
||||||
import kubeAuthProxyCertificateInjectable from "../kube-auth-proxy/kube-auth-proxy-certificate.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 {
|
export interface KubeAuthProxyServer {
|
||||||
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;
|
getApiTarget(isLongRunningRequest?: boolean): Promise<ServerOptions>;
|
||||||
|
|||||||
@ -7,11 +7,12 @@ import { getInjectable } from "@ogre-tools/injectable";
|
|||||||
import loggerInjectable from "../../common/logger.injectable";
|
import loggerInjectable from "../../common/logger.injectable";
|
||||||
import type { KubeApiResource } from "../../common/rbac";
|
import type { KubeApiResource } from "../../common/rbac";
|
||||||
import type { Cluster } from "../../common/cluster/cluster";
|
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 { backoffCaller, withConcurrencyLimit } from "@k8slens/utilities";
|
||||||
import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable";
|
import requestKubeApiResourcesForInjectable from "./request-kube-api-resources-for.injectable";
|
||||||
import type { AsyncResult } from "@k8slens/utilities";
|
import type { AsyncResult } from "@k8slens/utilities";
|
||||||
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
|
import broadcastConnectionUpdateInjectable from "./broadcast-connection-update.injectable";
|
||||||
|
import { byOrderNumber } from "../../common/utils/composable-responsibilities/orderable/orderable";
|
||||||
|
|
||||||
export type RequestApiResources = (cluster: Cluster) => AsyncResult<KubeApiResource[], Error>;
|
export type RequestApiResources = (cluster: Cluster) => AsyncResult<KubeApiResource[], Error>;
|
||||||
|
|
||||||
@ -24,7 +25,8 @@ const requestApiResourcesInjectable = getInjectable({
|
|||||||
id: "request-api-resources",
|
id: "request-api-resources",
|
||||||
instantiate: (di): RequestApiResources => {
|
instantiate: (di): RequestApiResources => {
|
||||||
const logger = di.inject(loggerInjectable);
|
const logger = di.inject(loggerInjectable);
|
||||||
const apiVersionRequesters = di.injectMany(requestApiVersionsInjectionToken);
|
const apiVersionRequesters = di.injectMany(apiVersionsRequesterInjectionToken)
|
||||||
|
.sort(byOrderNumber);
|
||||||
const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable);
|
const requestKubeApiResourcesFor = di.inject(requestKubeApiResourcesForInjectable);
|
||||||
|
|
||||||
return async (...args) => {
|
return async (...args) => {
|
||||||
@ -35,7 +37,7 @@ const requestApiResourcesInjectable = getInjectable({
|
|||||||
const groupLists: KubeResourceListGroup[] = [];
|
const groupLists: KubeResourceListGroup[] = [];
|
||||||
|
|
||||||
for (const apiVersionRequester of apiVersionRequesters) {
|
for (const apiVersionRequester of apiVersionRequesters) {
|
||||||
const result = await backoffCaller(() => apiVersionRequester(cluster), {
|
const result = await backoffCaller(() => apiVersionRequester.request(cluster), {
|
||||||
onIntermediateError: (error, attempt) => {
|
onIntermediateError: (error, attempt) => {
|
||||||
broadcastConnectionUpdate({
|
broadcastConnectionUpdate({
|
||||||
message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`,
|
message: `Failed to list kube API resource kinds, attempt ${attempt}: ${error}`,
|
||||||
|
|||||||
@ -5,14 +5,15 @@
|
|||||||
import type { V1APIVersions } from "@kubernetes/client-node";
|
import type { V1APIVersions } from "@kubernetes/client-node";
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import k8sRequestInjectable from "../k8s-request.injectable";
|
import k8sRequestInjectable from "../k8s-request.injectable";
|
||||||
import { requestApiVersionsInjectionToken } from "./request-api-versions";
|
import { apiVersionsRequesterInjectionToken } from "./api-versions-requester";
|
||||||
|
|
||||||
const requestCoreApiVersionsInjectable = getInjectable({
|
const requestCoreApiVersionsInjectable = getInjectable({
|
||||||
id: "request-core-api-versions",
|
id: "request-core-api-versions",
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const k8sRequest = di.inject(k8sRequestInjectable);
|
const k8sRequest = di.inject(k8sRequestInjectable);
|
||||||
|
|
||||||
return async (cluster) => {
|
return {
|
||||||
|
request: async (cluster) => {
|
||||||
try {
|
try {
|
||||||
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions;
|
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions;
|
||||||
|
|
||||||
@ -29,9 +30,11 @@ const requestCoreApiVersionsInjectable = getInjectable({
|
|||||||
error: error as Error,
|
error: error as Error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
orderNumber: 10,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
injectionToken: requestApiVersionsInjectionToken,
|
injectionToken: apiVersionsRequesterInjectionToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default requestCoreApiVersionsInjectable;
|
export default requestCoreApiVersionsInjectable;
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import type { Cluster } from "../../common/cluster/cluster";
|
|||||||
import type { KubeApiResource } from "../../common/rbac";
|
import type { KubeApiResource } from "../../common/rbac";
|
||||||
import type { AsyncResult } from "@k8slens/utilities";
|
import type { AsyncResult } from "@k8slens/utilities";
|
||||||
import k8sRequestInjectable from "../k8s-request.injectable";
|
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>;
|
export type RequestKubeApiResources = (grouping: KubeResourceListGroup) => AsyncResult<KubeApiResource[], Error>;
|
||||||
|
|
||||||
|
|||||||
@ -6,25 +6,28 @@ import type { V1APIGroupList } from "@kubernetes/client-node";
|
|||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import { iter } from "@k8slens/utilities";
|
import { iter } from "@k8slens/utilities";
|
||||||
import k8sRequestInjectable from "../k8s-request.injectable";
|
import k8sRequestInjectable from "../k8s-request.injectable";
|
||||||
import { requestApiVersionsInjectionToken } from "./request-api-versions";
|
import { apiVersionsRequesterInjectionToken } from "./api-versions-requester";
|
||||||
|
|
||||||
const requestNonCoreApiVersionsInjectable = getInjectable({
|
const requestNonCoreApiVersionsInjectable = getInjectable({
|
||||||
id: "request-non-core-api-versions",
|
id: "request-non-core-api-versions",
|
||||||
instantiate: (di) => {
|
instantiate: (di) => {
|
||||||
const k8sRequest = di.inject(k8sRequestInjectable);
|
const k8sRequest = di.inject(k8sRequestInjectable);
|
||||||
|
|
||||||
return async (cluster) => {
|
return {
|
||||||
|
request: async (cluster) => {
|
||||||
try {
|
try {
|
||||||
const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList;
|
const { groups } = (await k8sRequest(cluster, "/apis")) as V1APIGroupList;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
callWasSuccessful: true,
|
callWasSuccessful: true,
|
||||||
response: iter.chain(groups.values())
|
response: iter.chain(groups.values())
|
||||||
.flatMap(group => group.versions.map(version => ({
|
.flatMap((group) =>
|
||||||
|
group.versions.map((version) => ({
|
||||||
group: group.name,
|
group: group.name,
|
||||||
path: `/apis/${version.groupVersion}`,
|
path: `/apis/${version.groupVersion}`,
|
||||||
})))
|
})),
|
||||||
.collect(v => [...v]),
|
)
|
||||||
|
.collect((v) => [...v]),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
@ -32,9 +35,11 @@ const requestNonCoreApiVersionsInjectable = getInjectable({
|
|||||||
error: error as Error,
|
error: error as Error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
orderNumber: 20,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
injectionToken: requestApiVersionsInjectionToken,
|
injectionToken: apiVersionsRequesterInjectionToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default requestNonCoreApiVersionsInjectable;
|
export default requestNonCoreApiVersionsInjectable;
|
||||||
|
|||||||
@ -10,13 +10,13 @@ import type { DiContainer } from "@ogre-tools/injectable";
|
|||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
import type { K8sRequest } from "../k8s-request.injectable";
|
import type { K8sRequest } from "../k8s-request.injectable";
|
||||||
import k8sRequestInjectable 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";
|
import requestNonCoreApiVersionsInjectable from "./request-non-core-api-versions.injectable";
|
||||||
|
|
||||||
describe("requestNonCoreApiVersions", () => {
|
describe("requestNonCoreApiVersions", () => {
|
||||||
let di: DiContainer;
|
let di: DiContainer;
|
||||||
let k8sRequestMock: AsyncFnMock<K8sRequest>;
|
let k8sRequestMock: AsyncFnMock<K8sRequest>;
|
||||||
let requestNonCoreApiVersions: RequestApiVersions;
|
let requestNonCoreApiVersions: ApiVersionsRequester;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
di = getDiForUnitTesting();
|
di = getDiForUnitTesting();
|
||||||
@ -28,10 +28,10 @@ describe("requestNonCoreApiVersions", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when called", () => {
|
describe("when called", () => {
|
||||||
let versionsRequest: ReturnType<RequestApiVersions>;
|
let versionsRequest: ReturnType<ApiVersionsRequester["request"]>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
versionsRequest = requestNonCoreApiVersions({ id: "some-cluster-id" });
|
versionsRequest = requestNonCoreApiVersions.request({ id: "some-cluster-id" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should request all api groups", () => {
|
it("should request all api groups", () => {
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import type { KubeAuthProxyDependencies } from "./kube-auth-proxy";
|
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 type { Cluster } from "../../common/cluster/cluster";
|
||||||
import spawnInjectable from "../child-process/spawn.injectable";
|
import spawnInjectable from "../child-process/spawn.injectable";
|
||||||
import kubeAuthProxyCertificateInjectable from "./kube-auth-proxy-certificate.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 getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||||
import broadcastConnectionUpdateInjectable from "../cluster/broadcast-connection-update.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;
|
export type CreateKubeAuthProxy = (cluster: Cluster, env: NodeJS.ProcessEnv) => KubeAuthProxy;
|
||||||
|
|
||||||
const createKubeAuthProxyInjectable = getInjectable({
|
const createKubeAuthProxyInjectable = getInjectable({
|
||||||
@ -33,7 +40,7 @@ const createKubeAuthProxyInjectable = getInjectable({
|
|||||||
return (cluster, env) => {
|
return (cluster, env) => {
|
||||||
const clusterUrl = new URL(cluster.apiUrl.get());
|
const clusterUrl = new URL(cluster.apiUrl.get());
|
||||||
|
|
||||||
return new KubeAuthProxy({
|
return new KubeAuthProxyImpl({
|
||||||
...dependencies,
|
...dependencies,
|
||||||
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
|
proxyCert: di.inject(kubeAuthProxyCertificateInjectable, clusterUrl.hostname),
|
||||||
broadcastConnectionUpdate: di.inject(broadcastConnectionUpdateInjectable, cluster),
|
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 { WaitUntilPortIsUsed } from "./wait-until-port-is-used/wait-until-port-is-used.injectable";
|
||||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||||
import type { BroadcastConnectionUpdate } from "../cluster/broadcast-connection-update.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 startingServeMatcher = "starting to serve on (?<address>.+)";
|
||||||
const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
|
const startingServeRegex = Object.assign(TypedRegEx(startingServeMatcher, "i"), {
|
||||||
@ -33,7 +34,7 @@ export interface KubeAuthProxyDependencies {
|
|||||||
broadcastConnectionUpdate: BroadcastConnectionUpdate;
|
broadcastConnectionUpdate: BroadcastConnectionUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubeAuthProxy {
|
export class KubeAuthProxyImpl implements KubeAuthProxy {
|
||||||
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
|
public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`;
|
||||||
|
|
||||||
public get port(): number {
|
public get port(): number {
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export class KubeconfigManager {
|
|||||||
|
|
||||||
return this.tempFilePath = await this.createProxyKubeconfig();
|
return this.tempFilePath = await this.createProxyKubeconfig();
|
||||||
} catch (error) {
|
} 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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
packages/core/src/test-utils/cast.ts
Normal file
6
packages/core/src/test-utils/cast.ts
Normal 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;
|
||||||
17
packages/core/src/test-utils/mock-interface.ts
Normal file
17
packages/core/src/test-utils/mock-interface.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { 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,6 +2,9 @@
|
|||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { setImmediate } from "timers";
|
import { setImmediate, setTimeout } from "timers/promises";
|
||||||
|
|
||||||
export const flushPromises = () => new Promise(setImmediate);
|
export const flushPromises = async () => {
|
||||||
|
await setImmediate();
|
||||||
|
await setTimeout(5);
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user