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

Add behavioural tests to cover bug fix

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-03-05 18:51:51 -05:00
parent a63f737adb
commit 786fa39c33
19 changed files with 880 additions and 251 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,63 @@
/**
* 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) => {
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 createRequestNamespaceListPermissionsInjectable;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,652 @@
/**
* 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 createContextHandlerInjectable from "../../main/context-handler/create-context-handler.injectable";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import url from "url";
import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable";
import type { PartialDeep } from "type-fest";
import { anyObject } from "jest-mock-extended";
import { flushPromises } from "../../common/test-utils/flush-promises";
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 { delay } from "../../common/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>;
beforeEach(async () => {
builder = getApplicationBuilder({
fakeTime: false,
});
const mainDi = builder.mainDi;
mainDi.override(broadcastMessageInjectable, () => async () => {});
detectClusterMetadataMock = asyncFn();
mainDi.override(detectClusterMetadataInjectable, () => detectClusterMetadataMock);
k8sRequestMock = asyncFn();
mainDi.override(k8sRequestInjectable, () => k8sRequestMock);
mainDi.override(createContextHandlerInjectable, () => (cluster) => {
const clusterUrl = url.parse(cluster.apiUrl);
return {
clusterUrl,
ensureServer: async () => {},
};
});
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 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"); })();
refreshPromise = cluster.refreshAccessibilityAndMetadata();
// NOTE: I don't know why these are all are required to get the tests to pass
await flushPromises();
await delay(50);
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.shouldShowResource({
apiName: "pods",
group: "",
})).toBe(true);
});
it("should have the cluster displaying 'namespaces'", () => {
expect(cluster.shouldShowResource({
apiName: "namespaces",
group: "",
})).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.shouldShowResource({
apiName: "pods",
group: "",
})).toBe(false);
});
it("should have the cluster not displaying 'namespaces'", () => {
expect(cluster.shouldShowResource({
apiName: "namespaces",
group: "",
})).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.shouldShowResource({
apiName: "pods",
group: "",
})).toBe(true);
});
it("should have the cluster not displaying 'namespaces'", () => {
expect(cluster.shouldShowResource({
apiName: "namespaces",
group: "",
})).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.shouldShowResource({
apiName: "pods",
group: "",
})).toBe(true);
});
it("should have the cluster not displaying 'namespaces'", () => {
expect(cluster.shouldShowResource({
apiName: "namespaces",
group: "",
})).toBe(false);
});
});
});
describe.skip("when the permissions are incomplete", () => {});
describe.skip("when the permissions resolve to a single entry with 'list' verb", () => {});
describe.skip("when the permissions resolve to multiple entries with the 'list' verb not on the first entry", () => {});
});
});
describe.skip("when second specific resource kinds rejects", () => {});
});
});
describe.skip("when first specific resource kinds rejects", () => {});
});
});
});
});
});
});
});
const nonCoreApiResponse = {
groups: [
{
name: "node.k8s.io",
versions: [
{
groupVersion: "node.k8s.io/v1",
version: "v1",
},
],
preferredVersion: {
groupVersion: "node.k8s.io/v1",
version: "v1",
},
},
{
name: "discovery.k8s.io",
versions: [
{
groupVersion: "discovery.k8s.io/v1",
version: "v1",
},
],
preferredVersion: {
groupVersion: "discovery.k8s.io/v1",
version: "v1",
},
},
],
} as V1APIGroupList;
const listNamespaceResponse = {
body: {
items: [
{
metadata: {
name: "default",
},
},
{
metadata: {
name: "my-namespace",
},
},
],
} as PartialDeep<V1NamespaceList>,
} as Awaited<ReturnType<CoreV1Api["listNamespace"]>>;
const coreApiKindsResponse = {
kind: "APIResourceList",
groupVersion: "v1",
resources: [
{
name: "namespaces",
singularName: "",
namespaced: false,
kind: "Namespace",
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"],
shortNames: ["ns"],
storageVersionHash: "Q3oi5N2YM8M=",
},
{
name: "pods",
singularName: "",
namespaced: true,
kind: "Pod",
verbs: [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch",
],
shortNames: ["po"],
categories: ["all"],
storageVersionHash: "xPOwRZ+Yhw8=",
},
{
name: "pods/attach",
singularName: "",
namespaced: true,
kind: "PodAttachOptions",
verbs: ["create", "get"],
},
],
};
const nodeK8sIoKindsResponse = {
kind: "APIResourceList",
apiVersion: "v1",
groupVersion: "node.k8s.io/v1",
resources: [
{
name: "runtimeclasses",
singularName: "",
namespaced: false,
kind: "RuntimeClass",
verbs: [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch",
],
storageVersionHash: "WQTu1GL3T2Q=",
},
],
};
const discoveryK8sIoKindsResponse = {
kind: "APIResourceList",
apiVersion: "v1",
groupVersion: "discovery.k8s.io/v1",
resources: [
{
name: "endpointslices",
singularName: "",
namespaced: true,
kind: "EndpointSlice",
verbs: [
"create",
"delete",
"deletecollection",
"get",
"list",
"patch",
"update",
"watch",
],
storageVersionHash: "Nx3SIv6I0mE=",
},
],
};
type CreateSelfSubjectRulesReviewRes = Awaited<ReturnType<AuthorizationV1Api["createSelfSubjectRulesReview"]>>;
const defaultIncompletePermissions = {
body: {
status: {
incomplete: true,
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const emptyPermissions = {
body: {
status: {
resourceRules: [],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const defaultSingleListPermissions = {
body: {
status: {
resourceRules: [{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
}],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;
const defaultMultipleListPermissions = {
body: {
status: {
resourceRules: [
{
apiGroups: [""],
resources: ["pods"],
verbs: ["get"],
},
{
apiGroups: [""],
resources: ["pods"],
verbs: ["list"],
},
],
},
} as PartialDeep<V1SelfSubjectRulesReview>,
} as CreateSelfSubjectRulesReviewRes;

View File

@ -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(),

View File

@ -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,
), ),

View File

@ -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 "./request-api-versions";
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}`,

View File

@ -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",
}); });

View File

@ -5,33 +5,36 @@
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 "./request-api-versions";
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 {
try { request: async (cluster) => {
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions; try {
const { versions } = await k8sRequest(cluster, "/api") as V1APIVersions;
return { return {
callWasSuccessful: true, callWasSuccessful: true,
response: versions.map(version => ({ response: versions.map(version => ({
group: "", group: "",
path: `/api/${version}`, path: `/api/${version}`,
})), })),
}; };
} catch (error) { } catch (error) {
return { return {
callWasSuccessful: false, callWasSuccessful: false,
error: error as Error, error: error as Error,
}; };
} }
},
orderNumber: 10,
}; };
}, },
injectionToken: requestApiVersionsInjectionToken, injectionToken: apiVersionsRequesterInjectionToken,
}); });
export default requestCoreApiVersionsInjectable; export default requestCoreApiVersionsInjectable;

View File

@ -6,35 +6,40 @@ 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 "./request-api-versions";
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 {
try { request: async (cluster) => {
const { groups } = await k8sRequest(cluster, "/apis") as V1APIGroupList; try {
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: group.name, group.versions.map((version) => ({
path: `/apis/${version.groupVersion}`, group: group.name,
}))) path: `/apis/${version.groupVersion}`,
.collect(v => [...v]), })),
}; )
} catch (error) { .collect((v) => [...v]),
return { };
callWasSuccessful: false, } catch (error) {
error: error as Error, return {
}; callWasSuccessful: false,
} error: error as Error,
};
}
},
orderNumber: 20,
}; };
}, },
injectionToken: requestApiVersionsInjectionToken, injectionToken: apiVersionsRequesterInjectionToken,
}); });
export default requestNonCoreApiVersionsInjectable; export default requestNonCoreApiVersionsInjectable;