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

Fix ResourceApplier to not use sync methods within an async context

- Add cleanup for apply all
- Cleanup files in kubectlCmdAll even on success
- Make ResourceApplier injectable
- Write some basic tests
- Remove only use of logger.silly

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-07-07 17:19:40 -04:00
parent fc770b4b44
commit 695f660387
30 changed files with 695 additions and 237 deletions

View File

@ -94,10 +94,12 @@ describe("cluster-store", () => {
mainDi.permitSideEffects(getConfigurationFileModelInjectable); mainDi.permitSideEffects(getConfigurationFileModelInjectable);
mainDi.permitSideEffects(appVersionInjectable); mainDi.permitSideEffects(appVersionInjectable);
mainDi.permitSideEffects(clusterStoreInjectable);
mainDi.permitSideEffects(fsInjectable);
mainDi.unoverride(clusterStoreInjectable); mainDi.unoverride(clusterStoreInjectable);
mainDi.permitSideEffects(clusterStoreInjectable);
mainDi.unoverride(fsInjectable);
mainDi.permitSideEffects(fsInjectable);
}); });
afterEach(() => { afterEach(() => {

View File

@ -95,7 +95,6 @@ describe("HotbarStore", () => {
debug: jest.fn(), debug: jest.fn(),
error: jest.fn(), error: jest.fn(),
info: jest.fn(), info: jest.fn(),
silly: jest.fn(),
}; };
di.override(loggerInjectable, () => loggerMock); di.override(loggerInjectable, () => loggerMock);

View File

@ -13,7 +13,7 @@ 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 * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/ */
export function authorizationReview(proxyConfig: KubeConfig): CanI { function createAuthorizationReview(proxyConfig: KubeConfig): CanI {
const api = proxyConfig.makeApiClient(AuthorizationV1Api); const api = proxyConfig.makeApiClient(AuthorizationV1Api);
/** /**
@ -38,9 +38,9 @@ export function authorizationReview(proxyConfig: KubeConfig): CanI {
}; };
} }
const authorizationReviewInjectable = getInjectable({ const createAuthorizationReviewInjectable = getInjectable({
id: "authorization-review", id: "create-authorization-review",
instantiate: () => authorizationReview, instantiate: () => createAuthorizationReview,
}); });
export default authorizationReviewInjectable; export default createAuthorizationReviewInjectable;

View File

@ -581,7 +581,6 @@ export class Cluster implements ClusterModel, ClusterState {
* @param state cluster state * @param state cluster state
*/ */
pushState(state = this.getState()) { pushState(state = this.getState()) {
this.dependencies.logger.silly(`[CLUSTER]: push-state`, state);
broadcastMessage("cluster:state", this.id, state); broadcastMessage("cluster:state", this.id, state);
} }

View File

@ -9,7 +9,7 @@ import { isDefined } from "../utils";
export type ListNamespaces = () => Promise<string[]>; export type ListNamespaces = () => Promise<string[]>;
export function listNamespaces(config: KubeConfig): ListNamespaces { function createListNamespaces(config: KubeConfig): ListNamespaces {
const coreApi = config.makeApiClient(CoreV1Api); const coreApi = config.makeApiClient(CoreV1Api);
return async () => { return async () => {
@ -21,9 +21,9 @@ export function listNamespaces(config: KubeConfig): ListNamespaces {
}; };
} }
const listNamespacesInjectable = getInjectable({ const createListNamespacesInjectable = getInjectable({
id: "list-namespaces", id: "list-namespaces",
instantiate: () => listNamespaces, instantiate: () => createListNamespaces,
}); });
export default listNamespacesInjectable; export default createListNamespacesInjectable;

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
export type RemoveDir = (path: string) => Promise<void>;
const removeDirInjectable = getInjectable({
instantiate: (di): RemoveDir => di.inject(fsInjectable).remove,
id: "remove-dir",
});
export default removeDirInjectable;

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import tempy from "tempy";
export interface TempDirOptions {
prefix?: string;
}
export type TempDir = (opts?: TempDirOptions) => string;
const tempDirInjectable = getInjectable({
id: "temp-dir",
instantiate: (): TempDir => opts => tempy.directory(opts),
});
export default tempDirInjectable;

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import tempy from "tempy";
import { getInjectable } from "@ogre-tools/injectable";
import type { MergeExclusive } from "type-fest";
export type TempFileOptions = MergeExclusive<{
name?: string;
}, {
extension?: string;
}>;
export type TempFile = (opts?: TempFileOptions) => string;
const tempFileInjectable = getInjectable({
id: "temp-file",
instantiate: (): TempFile => (opts) => tempy.file(opts),
});
export default tempFileInjectable;

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 { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
export type Unlink = (path: string) => Promise<void>;
const unlinkInjectable = getInjectable({
instantiate: (di): Unlink => di.inject(fsInjectable).unlink,
id: "unlink",
});
export default unlinkInjectable;

View File

@ -6,13 +6,15 @@ import { getInjectable } from "@ogre-tools/injectable";
import path from "path"; import path from "path";
import fsInjectable from "./fs.injectable"; import fsInjectable from "./fs.injectable";
export type WriteFile = (filePath: string, data: string | Buffer) => Promise<void>;
const writeFileInjectable = getInjectable({ const writeFileInjectable = getInjectable({
id: "write-file", id: "write-file",
instantiate: (di) => { instantiate: (di): WriteFile => {
const { writeFile, ensureDir } = di.inject(fsInjectable); const { writeFile, ensureDir } = di.inject(fsInjectable);
return async (filePath: string, content: string | Buffer) => { return async (filePath, content) => {
await ensureDir(path.dirname(filePath), { mode: 0o755 }); await ensureDir(path.dirname(filePath), { mode: 0o755 });
await writeFile(filePath, content, { await writeFile(filePath, content, {

View File

@ -5,7 +5,6 @@
import fse from "fs-extra"; import fse from "fs-extra";
import path from "path"; import path from "path";
import hb from "handlebars"; import hb from "handlebars";
import { ResourceApplier } from "../../main/resource-applier";
import type { KubernetesCluster } from "../catalog-entities"; import type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { app } from "electron"; import { app } from "electron";
@ -13,6 +12,10 @@ import { ClusterStore } from "../cluster-store/cluster-store";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { productName } from "../vars"; import { productName } from "../vars";
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc"; import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";
import { asLegacyGlobalFunctionForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import createK8sResourceApplierInjectable from "../../main/k8s/resource-applier/create.injectable";
const createK8sResourceApplier = asLegacyGlobalFunctionForExtensionApi(createK8sResourceApplierInjectable);
export class ResourceStack { export class ResourceStack {
constructor(protected cluster: KubernetesCluster, protected name: string) {} constructor(protected cluster: KubernetesCluster, protected name: string) {}
@ -51,7 +54,7 @@ export class ResourceStack {
kubectlArgs = this.appendKubectlArgs(kubectlArgs); kubectlArgs = this.appendKubectlArgs(kubectlArgs);
if (app) { if (app) {
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs); return await createK8sResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
} else { } else {
const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs); const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs);
@ -75,7 +78,7 @@ export class ResourceStack {
kubectlArgs = this.appendKubectlArgs(kubectlArgs); kubectlArgs = this.appendKubectlArgs(kubectlArgs);
if (app) { if (app) {
return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs); return await createK8sResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
} else { } else {
const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs); const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs);

View File

@ -15,7 +15,6 @@ export interface Logger {
error: (message: string, ...args: any) => void; error: (message: string, ...args: any) => void;
debug: (message: string, ...args: any) => void; debug: (message: string, ...args: any) => void;
warn: (message: string, ...args: any) => void; warn: (message: string, ...args: any) => void;
silly: (message: string, ...args: any) => void;
} }
const logLevel = process.env.LOG_LEVEL const logLevel = process.env.LOG_LEVEL

View File

@ -13,8 +13,6 @@ import { Kubectl } from "../kubectl/kubectl";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token"; import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { ClusterContextHandler } from "../context-handler/context-handler"; import type { ClusterContextHandler } from "../context-handler/context-handler";
import { parse } from "url"; import { parse } from "url";
@ -23,6 +21,10 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import { readFileSync } from "fs-extra";
import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
@ -41,8 +43,8 @@ describe("create clusters", () => {
di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin"); di.override(normalizedPlatformInjectable, () => "darwin");
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(createContextHandlerInjectable, () => (cluster) => ({ di.override(createContextHandlerInjectable, () => (cluster) => ({
restartServer: jest.fn(), restartServer: jest.fn(),
stopServer: jest.fn(), stopServer: jest.fn(),
@ -54,6 +56,7 @@ describe("create clusters", () => {
setupPrometheus: jest.fn(), setupPrometheus: jest.fn(),
ensureServer: jest.fn(), ensureServer: jest.fn(),
} as ClusterContextHandler)); } as ClusterContextHandler));
di.override(readFileSyncInjectable, () => readFileSync); // TODO: don't bypass injectables
createCluster = di.inject(createClusterInjectionToken); createCluster = di.inject(createClusterInjectionToken);

View File

@ -16,7 +16,6 @@ jest.mock("winston", () => ({
splat: jest.fn(), splat: jest.fn(),
}, },
createLogger: jest.fn().mockReturnValue({ createLogger: jest.fn().mockReturnValue({
silly: jest.fn(),
debug: jest.fn(), debug: jest.fn(),
log: jest.fn(), log: jest.fn(),
info: jest.fn(), info: jest.fn(),
@ -60,6 +59,8 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable";
import { readFileSync } from "fs";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -116,8 +117,8 @@ describe("kube auth proxy tests", () => {
mockFs(mockMinikubeConfig); mockFs(mockMinikubeConfig);
di.override(readFileSyncInjectable, () => readFileSync); // TODO: don't bypass injectables
createCluster = di.inject(createClusterInjectionToken); createCluster = di.inject(createClusterInjectionToken);
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
}); });

View File

@ -23,6 +23,7 @@ import directoryForUserDataInjectable from "../../common/app-paths/directory-for
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable";
import readFileSyncInjectable from "../../common/fs/read-file-sync.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
@ -48,7 +49,6 @@ describe("kubeconfig manager tests", () => {
debug: jest.fn(), debug: jest.fn(),
error: jest.fn(), error: jest.fn(),
info: jest.fn(), info: jest.fn(),
silly: jest.fn(),
}; };
di.override(loggerInjectable, () => loggerMock); di.override(loggerInjectable, () => loggerMock);
@ -89,6 +89,7 @@ describe("kubeconfig manager tests", () => {
ensureServer: jest.fn(), ensureServer: jest.fn(),
})); }));
di.override(readFileSyncInjectable, () => fse.readFileSync); // TODO: don't bypass injectables
const createCluster = di.inject(createClusterInjectionToken); const createCluster = di.inject(createClusterInjectionToken);
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable); createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);

View File

@ -55,7 +55,9 @@ describe("kubeconfig-sync.source tests", () => {
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin"); di.override(normalizedPlatformInjectable, () => "darwin");
di.unoverride(fsInjectable);
di.permitSideEffects(fsInjectable); di.permitSideEffects(fsInjectable);
di.unoverride(clusterStoreInjectable); di.unoverride(clusterStoreInjectable);
di.permitSideEffects(clusterStoreInjectable); di.permitSideEffects(clusterStoreInjectable);
di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(getConfigurationFileModelInjectable);

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 { getInjectable } from "@ogre-tools/injectable";
import { execFile } from "child_process";
import { promisify } from "util";
export type ExecFile = typeof execFile["__promisify__"];
const execFileInjectable = getInjectable({
id: "exec-file",
instantiate: (): ExecFile => promisify(execFile),
});
export default execFileInjectable;

View File

@ -10,8 +10,8 @@ import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kube
import createKubectlInjectable from "../kubectl/create-kubectl.injectable"; import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; import createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable"; import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable"; import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
@ -25,8 +25,8 @@ const createClusterInjectable = getInjectable({
createKubeconfigManager: di.inject(createKubeconfigManagerInjectable), createKubeconfigManager: di.inject(createKubeconfigManagerInjectable),
createKubectl: di.inject(createKubectlInjectable), createKubectl: di.inject(createKubectlInjectable),
createContextHandler: di.inject(createContextHandlerInjectable), createContextHandler: di.inject(createContextHandlerInjectable),
createAuthorizationReview: di.inject(authorizationReviewInjectable), createAuthorizationReview: di.inject(createAuthorizationReviewInjectable),
createListNamespaces: di.inject(listNamespacesInjectable), createListNamespaces: di.inject(createListNamespacesInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),
detectorRegistry: di.inject(detectorRegistryInjectable), detectorRegistry: di.inject(detectorRegistryInjectable),
createVersionDetector: di.inject(createVersionDetectorInjectable), createVersionDetector: di.inject(createVersionDetectorInjectable),

View File

@ -14,17 +14,14 @@ import { onLoadOfApplicationInjectionToken } from "../../../start-main-applicati
import operatingSystemThemeInjectable from "../../../theme/operating-system-theme.injectable"; import operatingSystemThemeInjectable from "../../../theme/operating-system-theme.injectable";
import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable"; import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable";
import askUserForFilePathsInjectable from "../../../ipc/ask-user-for-file-paths.injectable"; import askUserForFilePathsInjectable from "../../../ipc/ask-user-for-file-paths.injectable";
import createK8sResourceApplierInjectable from "../../../k8s/resource-applier/create.injectable";
const setupIpcMainHandlersInjectable = getInjectable({ const setupIpcMainHandlersInjectable = getInjectable({
id: "setup-ipc-main-handlers", id: "setup-ipc-main-handlers",
instantiate: (di) => { instantiate: (di) => {
const logger = di.inject(loggerInjectable); const logger = di.inject(loggerInjectable);
const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable);
const directoryForLensLocalStorage = di.inject(
directoryForLensLocalStorageInjectable,
);
const clusterManager = di.inject(clusterManagerInjectable); const clusterManager = di.inject(clusterManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable); const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable); const getAbsolutePath = di.inject(getAbsolutePathInjectable);
@ -32,6 +29,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
const clusterStore = di.inject(clusterStoreInjectable); const clusterStore = di.inject(clusterStoreInjectable);
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable); const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
const askUserForFilePaths = di.inject(askUserForFilePathsInjectable); const askUserForFilePaths = di.inject(askUserForFilePathsInjectable);
const createK8sResourceApplier = di.inject(createK8sResourceApplierInjectable);
return { return {
run: () => { run: () => {
@ -46,6 +44,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
clusterStore, clusterStore,
operatingSystemTheme, operatingSystemTheme,
askUserForFilePaths, askUserForFilePaths,
createK8sResourceApplier,
}); });
}, },
}; };

View File

@ -13,7 +13,6 @@ import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from
import type { CatalogEntityRegistry } from "../../../catalog"; import type { CatalogEntityRegistry } from "../../../catalog";
import { pushCatalogToRenderer } from "../../../catalog-pusher"; import { pushCatalogToRenderer } from "../../../catalog-pusher";
import type { ClusterManager } from "../../../cluster-manager"; import type { ClusterManager } from "../../../cluster-manager";
import { ResourceApplier } from "../../../resource-applier";
import { remove } from "fs-extra"; import { remove } from "fs-extra";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable"; import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable";
@ -24,6 +23,7 @@ import { openFilePickingDialogChannel } from "../../../../common/ipc/dialog";
import { getNativeThemeChannel } from "../../../../common/ipc/native-theme"; import { getNativeThemeChannel } from "../../../../common/ipc/native-theme";
import type { Theme } from "../../../theme/operating-system-theme-state.injectable"; import type { Theme } from "../../../theme/operating-system-theme-state.injectable";
import type { AskUserForFilePaths } from "../../../ipc/ask-user-for-file-paths.injectable"; import type { AskUserForFilePaths } from "../../../ipc/ask-user-for-file-paths.injectable";
import type { CreateK8sResourceApplier } from "../../../k8s/resource-applier/create.injectable";
interface Dependencies { interface Dependencies {
directoryForLensLocalStorage: string; directoryForLensLocalStorage: string;
@ -34,9 +34,20 @@ interface Dependencies {
clusterStore: ClusterStore; clusterStore: ClusterStore;
operatingSystemTheme: IComputedValue<Theme>; operatingSystemTheme: IComputedValue<Theme>;
askUserForFilePaths: AskUserForFilePaths; askUserForFilePaths: AskUserForFilePaths;
createK8sResourceApplier: CreateK8sResourceApplier;
} }
export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath, clusterManager, catalogEntityRegistry, clusterStore, operatingSystemTheme, askUserForFilePaths }: Dependencies) => { export function setupIpcMainHandlers({
applicationMenuItems,
directoryForLensLocalStorage,
getAbsolutePath,
clusterManager,
catalogEntityRegistry,
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
createK8sResourceApplier,
}: Dependencies) {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance() return ClusterStore.getInstance()
.getById(clusterId) .getById(clusterId)
@ -113,10 +124,8 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) { if (cluster) {
const applier = new ResourceApplier(cluster);
try { try {
const stdout = await applier.kubectlApplyAll(resources, extraArgs); const stdout = await createK8sResourceApplier(cluster).kubectlApplyAll(resources, extraArgs);
return { stdout }; return { stdout };
} catch (error: any) { } catch (error: any) {
@ -132,10 +141,8 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc
const cluster = ClusterStore.getInstance().getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) { if (cluster) {
const applier = new ResourceApplier(cluster);
try { try {
const stdout = await applier.kubectlDeleteAll(resources, extraArgs); const stdout = await createK8sResourceApplier(cluster).kubectlDeleteAll(resources, extraArgs);
return { stdout }; return { stdout };
} catch (error: any) { } catch (error: any) {
@ -172,4 +179,4 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc
}); });
clusterStore.provideInitialFromMain(); clusterStore.provideInitialFromMain();
}; }

View File

@ -0,0 +1,250 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import type { ChildProcess } from "child_process";
import path from "path";
import directoryForKubeConfigsInjectable from "../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import createAuthorizationReviewInjectable from "../../../../common/cluster/authorization-review.injectable";
import createListNamespacesInjectable from "../../../../common/cluster/list-namespaces.injectable";
import readFileSyncInjectable from "../../../../common/fs/read-file-sync.injectable";
import type { RemoveDir } from "../../../../common/fs/remove.injectable";
import removeDirInjectable from "../../../../common/fs/remove.injectable";
import tempDirInjectable from "../../../../common/fs/temp-dir.injectable";
import tempFileInjectable from "../../../../common/fs/temp-file.injectable";
import type { Unlink } from "../../../../common/fs/unlink.injectable";
import unlinkInjectable from "../../../../common/fs/unlink.injectable";
import type { WriteFile } from "../../../../common/fs/write-file.injectable";
import writeFileInjectable from "../../../../common/fs/write-file.injectable";
import { expectInSetOnce } from "../../../../test-utils/expects";
import type { ExecFile } from "../../../child-process/exec-file.injectable";
import execFileInjectable from "../../../child-process/exec-file.injectable";
import createContextHandlerInjectable from "../../../context-handler/create-context-handler.injectable";
import createClusterInjectable from "../../../create-cluster/create-cluster.injectable";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import createKubeconfigManagerInjectable from "../../../kubeconfig-manager/create-kubeconfig-manager.injectable";
import type { KubeconfigManager } from "../../../kubeconfig-manager/kubeconfig-manager";
import createKubectlInjectable from "../../../kubectl/create-kubectl.injectable";
import type { Kubectl } from "../../../kubectl/kubectl";
import type { K8sResourceApplier } from "../applier";
import createK8sResourceApplierInjectable from "../create.injectable";
describe("ResourceApplier", () => {
let di: DiContainer;
let writeFile: jest.MockedFunction<WriteFile>;
let execFile: jest.MockedFunction<ExecFile>;
let unlink: jest.MockedFunction<Unlink>;
let removeDir: jest.MockedFunction<RemoveDir>;
let resourceApplier: K8sResourceApplier;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(createKubectlInjectable, () => jest.fn().mockImplementation((): Partial<Kubectl> => ({
ensureKubectl: jest.fn(),
getPath: () => Promise.resolve("some-path"),
})));
di.override(directoryForKubeConfigsInjectable, () => "some/path");
di.override(createKubeconfigManagerInjectable, () => jest.fn().mockImplementation((): Partial<KubeconfigManager> => ({
getPath: () => Promise.resolve("some-proxy-path"),
})));
di.override(createContextHandlerInjectable, () => jest.fn());
di.override(createAuthorizationReviewInjectable, () => jest.fn());
di.override(createListNamespacesInjectable, () => jest.fn());
di.override(writeFileInjectable, () => writeFile = jest.fn());
di.override(execFileInjectable, () => execFile = jest.fn());
di.override(unlinkInjectable, () => unlink = jest.fn());
di.override(removeDirInjectable, () => removeDir = jest.fn());
di.override(tempDirInjectable, () => jest.fn().mockImplementation(() => "some/temp/dir"));
di.override(tempFileInjectable, () => jest.fn().mockImplementation(() => "some/temp/file"));
di.override(readFileSyncInjectable, () => jest.fn().mockImplementation(() => {
return JSON.stringify({
clusters: [{
name: "some-cluster",
cluster: {
server: "some-server-url",
},
}],
users: [{
name: "some-user",
}],
contexts: [{
name: "some-context",
context: {
user: "some-user",
cluster: "some-cluster",
},
}],
});
}));
const createK8sResourceApplier = di.inject(createK8sResourceApplierInjectable);
const createCluster = di.inject(createClusterInjectable);
resourceApplier = createK8sResourceApplier(createCluster({
contextName: "some-context",
id: "some-id",
kubeConfigPath: "some/path/config",
}));
});
describe(".apply()", () => {
it("should call unlink, if writeFile rejects", async () => {
writeFile.mockImplementation(() => {
throw new Error("irrelavent");
});
await expect(resourceApplier.apply({})).rejects.toBeTruthy();
expect(unlink).toBeCalledWith("some/temp/file");
});
it("should call unlink, if execFile rejects", async () => {
execFile.mockImplementation(() => {
throw new Error("irrelavent");
});
await expect(resourceApplier.apply({})).rejects.toBeTruthy();
expect(unlink).toBeCalledWith("some/temp/file");
});
it("should call unlink, if everything passes", async () => {
execFile.mockImplementation(() => Object.assign(
Promise.resolve({
stdout: "I am some output",
stderr: "",
}),
{
child: {} as ChildProcess,
},
));
await expect(resourceApplier.apply({})).resolves.toBeTruthy();
expect(unlink).toBeCalledWith("some/temp/file");
});
it("should return the stdout of execFile", async () => {
execFile.mockImplementation(() => Object.assign(
Promise.resolve({
stdout: "I am some output",
stderr: "",
}),
{
child: {} as ChildProcess,
},
));
expect(await resourceApplier.apply({})).toBe("I am some output");
});
it("should build up a correct set of arguments", async () => {
execFile.mockImplementation((path, args) => {
expect(args).toEqual([
"apply",
"--kubeconfig",
"some-proxy-path",
"-o",
"json",
"-f",
"some/temp/file",
]);
return Object.assign(
Promise.resolve({
stdout: "I am some output",
stderr: "",
}),
{
child: {} as ChildProcess,
},
);
});
await resourceApplier.apply({});
});
});
describe(".kubectlApplyAll()", () => {
it("should call removeDir, if any writeFile rejects", async () => {
let count = 0;
writeFile.mockImplementation(async () => {
count += 1;
if (count === 2) {
throw new Error("irrelavent");
}
});
await expect(resourceApplier.kubectlApplyAll(["foo", "bar"])).rejects.toBeTruthy();
expect(removeDir).toBeCalledWith("some/temp/dir");
});
it("should call removeDir, if any execFile rejects", async () => {
execFile.mockImplementation(() => {
throw new Error("irrelavent");
});
await expect(resourceApplier.kubectlApplyAll(["foo", "bar"])).rejects.toBeTruthy();
expect(removeDir).toBeCalledWith("some/temp/dir");
});
it("should call writeFile for each resource", async () => {
const resources = new Set(["foo", "bar"]);
const onlyOnce = expectInSetOnce(resources);
writeFile.mockImplementation(async (filePath, contents) => {
if (path.sep === "/") {
expect(filePath).toMatch(/^some\/temp\/dir\/[0-9]+.yaml$/);
} else {
expect(filePath).toMatch(/^some\\temp\\dir\\[0-9]+.yaml$/);
}
onlyOnce(contents);
});
execFile.mockImplementation(() => {
return Object.assign(
Promise.resolve({
stdout: "I am some output",
stderr: "",
}),
{
child: {} as ChildProcess,
},
);
});
await expect(resourceApplier.kubectlApplyAll([...resources])).resolves.toBeTruthy();
expect(removeDir).toBeCalledWith("some/temp/dir");
onlyOnce.allSatisfied();
});
it("should use resonable arguments", async () => {
execFile.mockImplementation((path, args) => {
expect(args).toEqual([
"apply",
"--kubeconfig",
"some-proxy-path",
"-o",
"json",
"-f",
"some/temp/dir",
]);
return Object.assign(
Promise.resolve({
stdout: "I am some output",
stderr: "",
}),
{
child: {} as ChildProcess,
},
);
});
await expect(resourceApplier.kubectlApplyAll(["foo", "bar"])).resolves.toBeTruthy();
expect(removeDir).toBeCalledWith("some/temp/dir");
});
});
});

View File

@ -0,0 +1,179 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Cluster } from "../../../common/cluster/cluster";
import type { KubernetesObject } from "@kubernetes/client-node";
import * as yaml from "js-yaml";
import path from "path";
import { appEventBus } from "../../../common/app-event-bus/event-bus";
import type { Patch } from "rfc6902";
import type { ExecFile } from "../../child-process/exec-file.injectable";
import type { WriteFile } from "../../../common/fs/write-file.injectable";
import type { Unlink } from "../../../common/fs/unlink.injectable";
import type { RemoveDir } from "../../../common/fs/remove.injectable";
import type { PartialDeep } from "type-fest";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
import type { Logger } from "../../../common/logger";
import type { TempFile } from "../../../common/fs/temp-file.injectable";
import type { TempDir } from "../../../common/fs/temp-dir.injectable";
import { hasTypedProperty, isObject, isString } from "../../../common/utils";
export interface ResourceApplierDependencies {
execFile: ExecFile;
writeFile: WriteFile;
unlink: Unlink;
removeDir: RemoveDir;
tempFile: TempFile;
tempDir: TempDir;
readonly logger: Logger;
}
export interface K8sResourceApplier {
patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string>;
apply(resource: PartialDeep<KubeObject>): Promise<string>;
/**
* @deprecated This function is only really for KubeObject's
*/
apply(resource: any): Promise<string>;
kubectlApplyAll(resources: string[], extraArgs?: string[]): Promise<string>;
kubectlDeleteAll(resources: string[], extraArgs?: string[]): Promise<string>;
}
export class ResourceApplier implements K8sResourceApplier {
constructor(protected readonly dependencies: ResourceApplierDependencies, protected readonly cluster: Cluster) {}
/**
* Patch a kube resource's manifest, throwing any error that occurs.
* @param name The name of the kube resource
* @param kind The kind of the kube resource
* @param patch The list of JSON operations
* @param ns The optional namespace of the kube resource
*/
async patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string> {
appEventBus.emit({ name: "resource", action: "patch" });
const kubectl = await this.cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const args = [
"--kubeconfig", proxyKubeconfigPath,
"patch",
kind,
name,
];
if (ns) {
args.push("--namespace", ns);
}
args.push(
"--type", "json",
"--patch", JSON.stringify(patch),
"-o", "json",
);
try {
const { stdout } = await this.dependencies.execFile(kubectlPath, args);
return stdout;
} catch (error) {
if (isObject(error) && hasTypedProperty(error, "stderr", isString)) {
throw error.stderr;
}
throw String(error);
}
}
async apply(resource: KubernetesObject | any): Promise<string> {
appEventBus.emit({ name: "resource", action: "apply" });
const content = yaml.dump(sanitizeObject(resource));
const kubectl = await this.cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const fileName = this.dependencies.tempFile({ name: "resource.yaml" });
const args = [
"apply",
"--kubeconfig", proxyKubeconfigPath,
"-o", "json",
"-f", fileName,
];
this.dependencies.logger.debug(`[RESOURCE-APPLIER]: shooting manifests with: ${kubectlPath}`, { args });
const execEnv = { ...process.env };
const httpsProxy = this.cluster.preferences?.httpsProxy;
if (httpsProxy) {
execEnv.HTTPS_PROXY = httpsProxy;
}
try {
await this.dependencies.writeFile(fileName, content);
const { stdout } = await this.dependencies.execFile(kubectlPath, args, { env: execEnv });
return stdout;
} catch (error) {
if (isObject(error) && hasTypedProperty(error, "stderr", isString)) {
throw error.stderr;
}
throw String(error);
} finally {
await this.dependencies.unlink(fileName);
}
}
public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise<string> {
return this.kubectlCmdAll("apply", resources, extraArgs);
}
public async kubectlDeleteAll(resources: string[], extraArgs: string[] = []): Promise<string> {
return this.kubectlCmdAll("delete", resources, extraArgs);
}
protected async kubectlCmdAll(subCmd: string, resources: string[], args: string[]): Promise<string> {
const kubectl = await this.cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const tmpDir = this.dependencies.tempDir();
try {
await Promise.all(
resources.map((resource, index) => this.dependencies.writeFile(path.join(tmpDir, `${index}.yaml`), resource)),
);
args.unshift(
subCmd,
"--kubeconfig", proxyKubeconfigPath,
);
args.push("-f", tmpDir);
this.dependencies.logger.info(`[RESOURCE-APPLIER] Executing ${kubectlPath}`, { args });
const { stdout } = await this.dependencies.execFile(kubectlPath, args);
return stdout;
} catch (error) {
this.dependencies.logger.error(`[RESOURCE-APPLIER] cmd errored: ${error}`);
throw String(error).split(`.yaml": `)[1] ?? error;
} finally {
await this.dependencies.removeDir(tmpDir);
}
}
}
function sanitizeObject(resource: KubernetesObject | any) {
const cleaned = JSON.parse(JSON.stringify(resource));
delete cleaned.status;
delete cleaned.metadata?.resourceVersion;
delete cleaned.metadata?.annotations?.["kubectl.kubernetes.io/last-applied-configuration"];
return cleaned;
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import removeDirInjectable from "../../../common/fs/remove.injectable";
import tempDirInjectable from "../../../common/fs/temp-dir.injectable";
import tempFileInjectable from "../../../common/fs/temp-file.injectable";
import unlinkInjectable from "../../../common/fs/unlink.injectable";
import writeFileInjectable from "../../../common/fs/write-file.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import execFileInjectable from "../../child-process/exec-file.injectable";
import type { K8sResourceApplier, ResourceApplierDependencies } from "./applier";
import { ResourceApplier } from "./applier";
export type CreateK8sResourceApplier = (cluster: Cluster) => K8sResourceApplier;
const createK8sResourceApplierInjectable = getInjectable({
id: "create-k8s-resource-applier",
instantiate: (di): CreateK8sResourceApplier => {
const deps: ResourceApplierDependencies = {
execFile: di.inject(execFileInjectable),
removeDir: di.inject(removeDirInjectable),
unlink: di.inject(unlinkInjectable),
writeFile: di.inject(writeFileInjectable),
logger: di.inject(loggerInjectable),
tempDir: di.inject(tempDirInjectable),
tempFile: di.inject(tempFileInjectable),
};
return (cluster) => new ResourceApplier(deps, cluster);
},
});
export default createK8sResourceApplierInjectable;

View File

@ -1,165 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Cluster } from "../common/cluster/cluster";
import type { KubernetesObject } from "@kubernetes/client-node";
import { exec } from "child_process";
import fs from "fs-extra";
import * as yaml from "js-yaml";
import path from "path";
import tempy from "tempy";
import logger from "./logger";
import { appEventBus } from "../common/app-event-bus/event-bus";
import { isChildProcessError } from "../common/utils";
import type { Patch } from "rfc6902";
import { promiseExecFile } from "../common/utils/promise-exec";
export class ResourceApplier {
constructor(protected cluster: Cluster) {}
/**
* Patch a kube resource's manifest, throwing any error that occurs.
* @param name The name of the kube resource
* @param kind The kind of the kube resource
* @param patch The list of JSON operations
* @param ns The optional namespace of the kube resource
*/
async patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string> {
appEventBus.emit({ name: "resource", action: "patch" });
const kubectl = await this.cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const args = [
"--kubeconfig", proxyKubeconfigPath,
"patch",
kind,
name,
];
if (ns) {
args.push("--namespace", ns);
}
args.push(
"--type", "json",
"--patch", JSON.stringify(patch),
"-o", "json",
);
try {
const { stdout } = await promiseExecFile(kubectlPath, args);
return stdout;
} catch (error) {
if (isChildProcessError(error)) {
throw error.stderr ?? error;
}
throw error;
}
}
async apply(resource: KubernetesObject | any): Promise<string> {
resource = this.sanitizeObject(resource);
appEventBus.emit({ name: "resource", action: "apply" });
return this.kubectlApply(yaml.dump(resource));
}
protected async kubectlApply(content: string): Promise<string> {
const kubectl = await this.cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
const fileName = tempy.file({ name: "resource.yaml" });
const args = [
"apply",
"--kubeconfig", proxyKubeconfigPath,
"-o", "json",
"-f", fileName,
];
logger.debug(`shooting manifests with ${kubectlPath}`, { args });
const execEnv = { ...process.env };
const httpsProxy = this.cluster.preferences?.httpsProxy;
if (httpsProxy) {
execEnv.HTTPS_PROXY = httpsProxy;
}
try {
await fs.writeFile(fileName, content);
const { stdout } = await promiseExecFile(kubectlPath, args);
return stdout;
} catch (error) {
if (isChildProcessError(error)) {
throw error.stderr ?? error;
}
throw error;
} finally {
await fs.unlink(fileName);
}
}
public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise<string> {
return this.kubectlCmdAll("apply", resources, extraArgs);
}
public async kubectlDeleteAll(resources: string[], extraArgs?: string[]): Promise<string> {
return this.kubectlCmdAll("delete", resources, extraArgs);
}
protected async kubectlCmdAll(subCmd: string, resources: string[], args: string[] = []): Promise<string> {
const kubectl = await this.cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
return new Promise((resolve, reject) => {
const tmpDir = tempy.directory();
// Dump each resource into tmpDir
resources.forEach((resource, index) => {
fs.writeFileSync(path.join(tmpDir, `${index}.yaml`), resource);
});
args.push("-f", `"${tmpDir}"`);
const cmd = `"${kubectlPath}" ${subCmd} --kubeconfig "${proxyKubeconfigPath}" ${args.join(" ")}`;
logger.info(`[RESOURCE-APPLIER] running cmd ${cmd}`);
exec(cmd, (error, stdout) => {
if (error) {
logger.error(`[RESOURCE-APPLIER] cmd errored: ${error}`);
const splitError = error.toString().split(`.yaml": `);
if (splitError[1]) {
reject(splitError[1]);
} else {
reject(error);
}
return;
}
resolve(stdout);
});
});
}
protected sanitizeObject(resource: KubernetesObject | any) {
const res = JSON.parse(JSON.stringify(resource));
delete res.status;
delete res.metadata?.resourceVersion;
const annotations = res.metadata?.annotations;
if (annotations) {
delete annotations["kubectl.kubernetes.io/last-applied-configuration"];
}
return res;
}
}

View File

@ -2,8 +2,8 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import type { Injectable } from "@ogre-tools/injectable"; import type { Injectable, InjectionToken } from "@ogre-tools/injectable";
import { getInjectable, getInjectionToken, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { Router } from "./router"; import { Router } from "./router";
import parseRequestInjectable from "./parse-request.injectable"; import parseRequestInjectable from "./parse-request.injectable";
import type { Route } from "./route"; import type { Route } from "./route";
@ -18,11 +18,10 @@ export function getRouteInjectable<
>( >(
opts: Omit<Injectable<Route<T, Path>, Route<T, Path>, void>, "lifecycle" | "injectionToken">, opts: Omit<Injectable<Route<T, Path>, Route<T, Path>, void>, "lifecycle" | "injectionToken">,
): Injectable<Route<T, Path>, Route<T, Path>, void> { ): Injectable<Route<T, Path>, Route<T, Path>, void> {
return { return getInjectable({
...opts, ...opts,
injectionToken: routeInjectionToken as never, injectionToken: routeInjectionToken as unknown as InjectionToken<Route<T, Path>, void>,
lifecycle: lifecycleEnum.singleton as never, });
};
} }
const routerInjectable = getInjectable({ const routerInjectable = getInjectable({

View File

@ -13,6 +13,7 @@ import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest"; import asyncFn from "@async-fn/jest";
import parseRequestInjectable from "./parse-request.injectable"; import parseRequestInjectable from "./parse-request.injectable";
import { contentTypes } from "./router-content-types"; import { contentTypes } from "./router-content-types";
import createK8sResourceApplierInjectable from "../k8s/resource-applier/create.injectable";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
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 type { Route } from "./route"; import type { Route } from "./route";
@ -40,6 +41,12 @@ describe("router", () => {
di.override(kubectlBinaryNameInjectable, () => "kubectl"); di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64"); di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin"); di.override(normalizedPlatformInjectable, () => "darwin");
di.override(createK8sResourceApplierInjectable, () => () => ({
apply: jest.fn(),
kubectlApplyAll: jest.fn(),
kubectlDeleteAll: jest.fn(),
patch: jest.fn(),
}));
const injectable = getInjectable({ const injectable = getInjectable({
id: "some-route", id: "some-route",

View File

@ -4,18 +4,22 @@
*/ */
import { getRouteInjectable } from "../../router/router.injectable"; import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { ResourceApplier } from "../../resource-applier";
import { clusterRoute } from "../../router/route"; import { clusterRoute } from "../../router/route";
import createK8sResourceApplierInjectable from "../../k8s/resource-applier/create.injectable";
const applyResourceRouteInjectable = getRouteInjectable({ const applyResourceRouteInjectable = getRouteInjectable({
id: "apply-resource-route", id: "apply-resource-route",
instantiate: () => clusterRoute({ instantiate: (di) => {
method: "post", const createK8sResourceApplier = di.inject(createK8sResourceApplierInjectable);
path: `${apiPrefix}/stack`,
})(async ({ cluster, payload }) => ({ return clusterRoute({
response: await new ResourceApplier(cluster).apply(payload), method: "post",
})), path: `${apiPrefix}/stack`,
})(async ({ cluster, payload }) => ({
response: await createK8sResourceApplier(cluster).apply(payload),
}));
},
}); });
export default applyResourceRouteInjectable; export default applyResourceRouteInjectable;

View File

@ -4,10 +4,10 @@
*/ */
import { getRouteInjectable } from "../../router/router.injectable"; import { getRouteInjectable } from "../../router/router.injectable";
import { apiPrefix } from "../../../common/vars"; import { apiPrefix } from "../../../common/vars";
import { ResourceApplier } from "../../resource-applier";
import { payloadValidatedClusterRoute } from "../../router/route"; import { payloadValidatedClusterRoute } from "../../router/route";
import Joi from "joi"; import Joi from "joi";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
import createK8sResourceApplierInjectable from "../../k8s/resource-applier/create.injectable";
interface PatchResourcePayload { interface PatchResourcePayload {
name: string; name: string;
@ -40,18 +40,22 @@ const patchResourcePayloadValidator = Joi.object<PatchResourcePayload, true, Pat
const patchResourceRouteInjectable = getRouteInjectable({ const patchResourceRouteInjectable = getRouteInjectable({
id: "patch-resource-route", id: "patch-resource-route",
instantiate: () => payloadValidatedClusterRoute({ instantiate: (di) => {
method: "patch", const createK8sResourceApplier = di.inject(createK8sResourceApplierInjectable);
path: `${apiPrefix}/stack`,
payloadValidator: patchResourcePayloadValidator, return payloadValidatedClusterRoute({
})(async ({ cluster, payload }) => ({ method: "patch",
response: await new ResourceApplier(cluster).patch( path: `${apiPrefix}/stack`,
payload.name, payloadValidator: patchResourcePayloadValidator,
payload.kind, })(async ({ cluster, payload }) => ({
payload.patch, response: await createK8sResourceApplier(cluster).patch(
payload.ns, payload.name,
), payload.kind,
})), payload.patch,
payload.ns,
),
}));
},
}); });
export default patchResourceRouteInjectable; export default patchResourceRouteInjectable;

View File

@ -28,6 +28,8 @@ import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.injectable"; import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../../../main/kubectl/normalized-arch.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../../../../main/kubectl/normalized-arch.injectable";
import readFileSyncInjectable from "../../../../common/fs/read-file-sync.injectable";
import { readFileSync } from "fs";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
@ -95,12 +97,11 @@ users:
token: kubeconfig-user-q4lm4:xxxyyyy token: kubeconfig-user-q4lm4:xxxyyyy
`; `;
let config: KubeConfig;
describe("<DeleteClusterDialog />", () => { describe("<DeleteClusterDialog />", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
let createCluster: CreateCluster; let createCluster: CreateCluster;
let openDeleteClusterDialog: OpenDeleteClusterDialog; let openDeleteClusterDialog: OpenDeleteClusterDialog;
let config: KubeConfig;
beforeEach(async () => { beforeEach(async () => {
applicationBuilder = getApplicationBuilder(); applicationBuilder = getApplicationBuilder();
@ -114,6 +115,7 @@ describe("<DeleteClusterDialog />", () => {
rendererDi.override(hotbarStoreInjectable, () => ({})); rendererDi.override(hotbarStoreInjectable, () => ({}));
rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true); rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true);
rendererDi.override(readFileSyncInjectable, () => readFileSync);
}); });
const { rendererDi } = applicationBuilder.dis; const { rendererDi } = applicationBuilder.dis;

36
src/test-utils/expects.ts Normal file
View File

@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { inspect } from "util";
export interface OnlyOnce {
(value: any): void;
allSatisfied(): void;
}
export function expectInSetOnce(set: Set<any>): OnlyOnce {
const alreadySeen = new Set();
const haventSeen = new Set(set);
return Object.assign(
(value: any) => {
if (haventSeen.has(value)) {
haventSeen.delete(value);
alreadySeen.add(value);
} else if (alreadySeen.has(value)) {
throw new Error(`Expected ${inspect(value)} only once`);
} else {
throw new Error(`Unexpected value ${inspect(value)}`);
}
},
{
allSatisfied: () => {
if (haventSeen.size) {
throw new Error(`${haventSeen.size} items not seen`);
}
},
},
);
}