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:
parent
fc770b4b44
commit
695f660387
@ -94,10 +94,12 @@ describe("cluster-store", () => {
|
||||
|
||||
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
mainDi.permitSideEffects(appVersionInjectable);
|
||||
mainDi.permitSideEffects(clusterStoreInjectable);
|
||||
mainDi.permitSideEffects(fsInjectable);
|
||||
|
||||
mainDi.unoverride(clusterStoreInjectable);
|
||||
mainDi.permitSideEffects(clusterStoreInjectable);
|
||||
|
||||
mainDi.unoverride(fsInjectable);
|
||||
mainDi.permitSideEffects(fsInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@ -95,7 +95,6 @@ describe("HotbarStore", () => {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
silly: jest.fn(),
|
||||
};
|
||||
|
||||
di.override(loggerInjectable, () => loggerMock);
|
||||
|
||||
@ -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
|
||||
*/
|
||||
export function authorizationReview(proxyConfig: KubeConfig): CanI {
|
||||
function createAuthorizationReview(proxyConfig: KubeConfig): CanI {
|
||||
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
|
||||
|
||||
/**
|
||||
@ -38,9 +38,9 @@ export function authorizationReview(proxyConfig: KubeConfig): CanI {
|
||||
};
|
||||
}
|
||||
|
||||
const authorizationReviewInjectable = getInjectable({
|
||||
id: "authorization-review",
|
||||
instantiate: () => authorizationReview,
|
||||
const createAuthorizationReviewInjectable = getInjectable({
|
||||
id: "create-authorization-review",
|
||||
instantiate: () => createAuthorizationReview,
|
||||
});
|
||||
|
||||
export default authorizationReviewInjectable;
|
||||
export default createAuthorizationReviewInjectable;
|
||||
|
||||
@ -581,7 +581,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @param state cluster state
|
||||
*/
|
||||
pushState(state = this.getState()) {
|
||||
this.dependencies.logger.silly(`[CLUSTER]: push-state`, state);
|
||||
broadcastMessage("cluster:state", this.id, state);
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { isDefined } from "../utils";
|
||||
|
||||
export type ListNamespaces = () => Promise<string[]>;
|
||||
|
||||
export function listNamespaces(config: KubeConfig): ListNamespaces {
|
||||
function createListNamespaces(config: KubeConfig): ListNamespaces {
|
||||
const coreApi = config.makeApiClient(CoreV1Api);
|
||||
|
||||
return async () => {
|
||||
@ -21,9 +21,9 @@ export function listNamespaces(config: KubeConfig): ListNamespaces {
|
||||
};
|
||||
}
|
||||
|
||||
const listNamespacesInjectable = getInjectable({
|
||||
const createListNamespacesInjectable = getInjectable({
|
||||
id: "list-namespaces",
|
||||
instantiate: () => listNamespaces,
|
||||
instantiate: () => createListNamespaces,
|
||||
});
|
||||
|
||||
export default listNamespacesInjectable;
|
||||
export default createListNamespacesInjectable;
|
||||
|
||||
15
src/common/fs/remove.injectable.ts
Normal file
15
src/common/fs/remove.injectable.ts
Normal 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;
|
||||
19
src/common/fs/temp-dir.injectable.ts
Normal file
19
src/common/fs/temp-dir.injectable.ts
Normal 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;
|
||||
23
src/common/fs/temp-file.injectable.ts
Normal file
23
src/common/fs/temp-file.injectable.ts
Normal 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;
|
||||
|
||||
16
src/common/fs/unlink.injectable.ts
Normal file
16
src/common/fs/unlink.injectable.ts
Normal 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;
|
||||
@ -6,13 +6,15 @@ import { getInjectable } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
export type WriteFile = (filePath: string, data: string | Buffer) => Promise<void>;
|
||||
|
||||
const writeFileInjectable = getInjectable({
|
||||
id: "write-file",
|
||||
|
||||
instantiate: (di) => {
|
||||
instantiate: (di): WriteFile => {
|
||||
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 writeFile(filePath, content, {
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
import fse from "fs-extra";
|
||||
import path from "path";
|
||||
import hb from "handlebars";
|
||||
import { ResourceApplier } from "../../main/resource-applier";
|
||||
import type { KubernetesCluster } from "../catalog-entities";
|
||||
import logger from "../../main/logger";
|
||||
import { app } from "electron";
|
||||
@ -13,6 +12,10 @@ import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import yaml from "js-yaml";
|
||||
import { productName } from "../vars";
|
||||
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 {
|
||||
constructor(protected cluster: KubernetesCluster, protected name: string) {}
|
||||
@ -51,7 +54,7 @@ export class ResourceStack {
|
||||
kubectlArgs = this.appendKubectlArgs(kubectlArgs);
|
||||
|
||||
if (app) {
|
||||
return await new ResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
|
||||
return await createK8sResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
|
||||
} else {
|
||||
const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs);
|
||||
|
||||
@ -75,7 +78,7 @@ export class ResourceStack {
|
||||
kubectlArgs = this.appendKubectlArgs(kubectlArgs);
|
||||
|
||||
if (app) {
|
||||
return await new ResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
|
||||
return await createK8sResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
|
||||
} else {
|
||||
const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs);
|
||||
|
||||
|
||||
@ -15,7 +15,6 @@ export interface Logger {
|
||||
error: (message: string, ...args: any) => void;
|
||||
debug: (message: string, ...args: any) => void;
|
||||
warn: (message: string, ...args: any) => void;
|
||||
silly: (message: string, ...args: any) => void;
|
||||
}
|
||||
|
||||
const logLevel = process.env.LOG_LEVEL
|
||||
|
||||
@ -13,8 +13,6 @@ import { Kubectl } from "../kubectl/kubectl";
|
||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||
import type { CreateCluster } 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 type { ClusterContextHandler } from "../context-handler/context-handler";
|
||||
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 kubectlBinaryNameInjectable from "../kubectl/binary-name.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
|
||||
|
||||
@ -41,8 +43,8 @@ describe("create clusters", () => {
|
||||
di.override(kubectlBinaryNameInjectable, () => "kubectl");
|
||||
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
||||
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||
di.override(createAuthorizationReviewInjectable, () => () => () => Promise.resolve(true));
|
||||
di.override(createListNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
|
||||
di.override(createContextHandlerInjectable, () => (cluster) => ({
|
||||
restartServer: jest.fn(),
|
||||
stopServer: jest.fn(),
|
||||
@ -54,6 +56,7 @@ describe("create clusters", () => {
|
||||
setupPrometheus: jest.fn(),
|
||||
ensureServer: jest.fn(),
|
||||
} as ClusterContextHandler));
|
||||
di.override(readFileSyncInjectable, () => readFileSync); // TODO: don't bypass injectables
|
||||
|
||||
createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ jest.mock("winston", () => ({
|
||||
splat: jest.fn(),
|
||||
},
|
||||
createLogger: jest.fn().mockReturnValue({
|
||||
silly: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
log: 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 kubectlBinaryNameInjectable from "../kubectl/binary-name.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);
|
||||
|
||||
@ -116,8 +117,8 @@ describe("kube auth proxy tests", () => {
|
||||
|
||||
mockFs(mockMinikubeConfig);
|
||||
|
||||
di.override(readFileSyncInjectable, () => readFileSync); // TODO: don't bypass injectables
|
||||
createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
|
||||
});
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ import directoryForUserDataInjectable from "../../common/app-paths/directory-for
|
||||
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
|
||||
import kubectlBinaryNameInjectable from "../kubectl/binary-name.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
|
||||
|
||||
@ -48,7 +49,6 @@ describe("kubeconfig manager tests", () => {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
silly: jest.fn(),
|
||||
};
|
||||
|
||||
di.override(loggerInjectable, () => loggerMock);
|
||||
@ -89,6 +89,7 @@ describe("kubeconfig manager tests", () => {
|
||||
ensureServer: jest.fn(),
|
||||
}));
|
||||
|
||||
di.override(readFileSyncInjectable, () => fse.readFileSync); // TODO: don't bypass injectables
|
||||
const createCluster = di.inject(createClusterInjectionToken);
|
||||
|
||||
createKubeconfigManager = di.inject(createKubeconfigManagerInjectable);
|
||||
|
||||
@ -55,7 +55,9 @@ describe("kubeconfig-sync.source tests", () => {
|
||||
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
||||
|
||||
di.unoverride(fsInjectable);
|
||||
di.permitSideEffects(fsInjectable);
|
||||
|
||||
di.unoverride(clusterStoreInjectable);
|
||||
di.permitSideEffects(clusterStoreInjectable);
|
||||
di.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
|
||||
16
src/main/child-process/exec-file.injectable.ts
Normal file
16
src/main/child-process/exec-file.injectable.ts
Normal 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;
|
||||
@ -10,8 +10,8 @@ import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kube
|
||||
import createKubectlInjectable from "../kubectl/create-kubectl.injectable";
|
||||
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
|
||||
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 createAuthorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
|
||||
import createListNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
|
||||
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
|
||||
@ -25,8 +25,8 @@ const createClusterInjectable = getInjectable({
|
||||
createKubeconfigManager: di.inject(createKubeconfigManagerInjectable),
|
||||
createKubectl: di.inject(createKubectlInjectable),
|
||||
createContextHandler: di.inject(createContextHandlerInjectable),
|
||||
createAuthorizationReview: di.inject(authorizationReviewInjectable),
|
||||
createListNamespaces: di.inject(listNamespacesInjectable),
|
||||
createAuthorizationReview: di.inject(createAuthorizationReviewInjectable),
|
||||
createListNamespaces: di.inject(createListNamespacesInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
detectorRegistry: di.inject(detectorRegistryInjectable),
|
||||
createVersionDetector: di.inject(createVersionDetectorInjectable),
|
||||
|
||||
@ -14,17 +14,14 @@ import { onLoadOfApplicationInjectionToken } from "../../../start-main-applicati
|
||||
import operatingSystemThemeInjectable from "../../../theme/operating-system-theme.injectable";
|
||||
import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable";
|
||||
import askUserForFilePathsInjectable from "../../../ipc/ask-user-for-file-paths.injectable";
|
||||
import createK8sResourceApplierInjectable from "../../../k8s/resource-applier/create.injectable";
|
||||
|
||||
const setupIpcMainHandlersInjectable = getInjectable({
|
||||
id: "setup-ipc-main-handlers",
|
||||
|
||||
instantiate: (di) => {
|
||||
const logger = di.inject(loggerInjectable);
|
||||
|
||||
const directoryForLensLocalStorage = di.inject(
|
||||
directoryForLensLocalStorageInjectable,
|
||||
);
|
||||
|
||||
const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable);
|
||||
const clusterManager = di.inject(clusterManagerInjectable);
|
||||
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
|
||||
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
|
||||
@ -32,6 +29,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
|
||||
const clusterStore = di.inject(clusterStoreInjectable);
|
||||
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
|
||||
const askUserForFilePaths = di.inject(askUserForFilePathsInjectable);
|
||||
const createK8sResourceApplier = di.inject(createK8sResourceApplierInjectable);
|
||||
|
||||
return {
|
||||
run: () => {
|
||||
@ -46,6 +44,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
|
||||
clusterStore,
|
||||
operatingSystemTheme,
|
||||
askUserForFilePaths,
|
||||
createK8sResourceApplier,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -13,7 +13,6 @@ import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from
|
||||
import type { CatalogEntityRegistry } from "../../../catalog";
|
||||
import { pushCatalogToRenderer } from "../../../catalog-pusher";
|
||||
import type { ClusterManager } from "../../../cluster-manager";
|
||||
import { ResourceApplier } from "../../../resource-applier";
|
||||
import { remove } from "fs-extra";
|
||||
import type { IComputedValue } from "mobx";
|
||||
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 type { Theme } from "../../../theme/operating-system-theme-state.injectable";
|
||||
import type { AskUserForFilePaths } from "../../../ipc/ask-user-for-file-paths.injectable";
|
||||
import type { CreateK8sResourceApplier } from "../../../k8s/resource-applier/create.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
directoryForLensLocalStorage: string;
|
||||
@ -34,9 +34,20 @@ interface Dependencies {
|
||||
clusterStore: ClusterStore;
|
||||
operatingSystemTheme: IComputedValue<Theme>;
|
||||
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) => {
|
||||
return ClusterStore.getInstance()
|
||||
.getById(clusterId)
|
||||
@ -113,10 +124,8 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
const applier = new ResourceApplier(cluster);
|
||||
|
||||
try {
|
||||
const stdout = await applier.kubectlApplyAll(resources, extraArgs);
|
||||
const stdout = await createK8sResourceApplier(cluster).kubectlApplyAll(resources, extraArgs);
|
||||
|
||||
return { stdout };
|
||||
} catch (error: any) {
|
||||
@ -132,10 +141,8 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
const applier = new ResourceApplier(cluster);
|
||||
|
||||
try {
|
||||
const stdout = await applier.kubectlDeleteAll(resources, extraArgs);
|
||||
const stdout = await createK8sResourceApplier(cluster).kubectlDeleteAll(resources, extraArgs);
|
||||
|
||||
return { stdout };
|
||||
} catch (error: any) {
|
||||
@ -172,4 +179,4 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc
|
||||
});
|
||||
|
||||
clusterStore.provideInitialFromMain();
|
||||
};
|
||||
}
|
||||
|
||||
250
src/main/k8s/resource-applier/__tests__/applier.test.ts
Normal file
250
src/main/k8s/resource-applier/__tests__/applier.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
179
src/main/k8s/resource-applier/applier.ts
Normal file
179
src/main/k8s/resource-applier/applier.ts
Normal 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;
|
||||
}
|
||||
36
src/main/k8s/resource-applier/create.injectable.ts
Normal file
36
src/main/k8s/resource-applier/create.injectable.ts
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,8 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Injectable } from "@ogre-tools/injectable";
|
||||
import { getInjectable, getInjectionToken, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { Injectable, InjectionToken } from "@ogre-tools/injectable";
|
||||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
|
||||
import { Router } from "./router";
|
||||
import parseRequestInjectable from "./parse-request.injectable";
|
||||
import type { Route } from "./route";
|
||||
@ -18,11 +18,10 @@ export function getRouteInjectable<
|
||||
>(
|
||||
opts: Omit<Injectable<Route<T, Path>, Route<T, Path>, void>, "lifecycle" | "injectionToken">,
|
||||
): Injectable<Route<T, Path>, Route<T, Path>, void> {
|
||||
return {
|
||||
return getInjectable({
|
||||
...opts,
|
||||
injectionToken: routeInjectionToken as never,
|
||||
lifecycle: lifecycleEnum.singleton as never,
|
||||
};
|
||||
injectionToken: routeInjectionToken as unknown as InjectionToken<Route<T, Path>, void>,
|
||||
});
|
||||
}
|
||||
|
||||
const routerInjectable = getInjectable({
|
||||
|
||||
@ -13,6 +13,7 @@ import type { AsyncFnMock } from "@async-fn/jest";
|
||||
import asyncFn from "@async-fn/jest";
|
||||
import parseRequestInjectable from "./parse-request.injectable";
|
||||
import { contentTypes } from "./router-content-types";
|
||||
import createK8sResourceApplierInjectable from "../k8s/resource-applier/create.injectable";
|
||||
import mockFs from "mock-fs";
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import type { Route } from "./route";
|
||||
@ -40,6 +41,12 @@ describe("router", () => {
|
||||
di.override(kubectlBinaryNameInjectable, () => "kubectl");
|
||||
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
|
||||
di.override(normalizedPlatformInjectable, () => "darwin");
|
||||
di.override(createK8sResourceApplierInjectable, () => () => ({
|
||||
apply: jest.fn(),
|
||||
kubectlApplyAll: jest.fn(),
|
||||
kubectlDeleteAll: jest.fn(),
|
||||
patch: jest.fn(),
|
||||
}));
|
||||
|
||||
const injectable = getInjectable({
|
||||
id: "some-route",
|
||||
|
||||
@ -4,18 +4,22 @@
|
||||
*/
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import { ResourceApplier } from "../../resource-applier";
|
||||
import { clusterRoute } from "../../router/route";
|
||||
import createK8sResourceApplierInjectable from "../../k8s/resource-applier/create.injectable";
|
||||
|
||||
const applyResourceRouteInjectable = getRouteInjectable({
|
||||
id: "apply-resource-route",
|
||||
|
||||
instantiate: () => clusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/stack`,
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await new ResourceApplier(cluster).apply(payload),
|
||||
})),
|
||||
instantiate: (di) => {
|
||||
const createK8sResourceApplier = di.inject(createK8sResourceApplierInjectable);
|
||||
|
||||
return clusterRoute({
|
||||
method: "post",
|
||||
path: `${apiPrefix}/stack`,
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await createK8sResourceApplier(cluster).apply(payload),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default applyResourceRouteInjectable;
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
*/
|
||||
import { getRouteInjectable } from "../../router/router.injectable";
|
||||
import { apiPrefix } from "../../../common/vars";
|
||||
import { ResourceApplier } from "../../resource-applier";
|
||||
import { payloadValidatedClusterRoute } from "../../router/route";
|
||||
import Joi from "joi";
|
||||
import type { Patch } from "rfc6902";
|
||||
import createK8sResourceApplierInjectable from "../../k8s/resource-applier/create.injectable";
|
||||
|
||||
interface PatchResourcePayload {
|
||||
name: string;
|
||||
@ -40,18 +40,22 @@ const patchResourcePayloadValidator = Joi.object<PatchResourcePayload, true, Pat
|
||||
const patchResourceRouteInjectable = getRouteInjectable({
|
||||
id: "patch-resource-route",
|
||||
|
||||
instantiate: () => payloadValidatedClusterRoute({
|
||||
method: "patch",
|
||||
path: `${apiPrefix}/stack`,
|
||||
payloadValidator: patchResourcePayloadValidator,
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await new ResourceApplier(cluster).patch(
|
||||
payload.name,
|
||||
payload.kind,
|
||||
payload.patch,
|
||||
payload.ns,
|
||||
),
|
||||
})),
|
||||
instantiate: (di) => {
|
||||
const createK8sResourceApplier = di.inject(createK8sResourceApplierInjectable);
|
||||
|
||||
return payloadValidatedClusterRoute({
|
||||
method: "patch",
|
||||
path: `${apiPrefix}/stack`,
|
||||
payloadValidator: patchResourcePayloadValidator,
|
||||
})(async ({ cluster, payload }) => ({
|
||||
response: await createK8sResourceApplier(cluster).patch(
|
||||
payload.name,
|
||||
payload.kind,
|
||||
payload.patch,
|
||||
payload.ns,
|
||||
),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default patchResourceRouteInjectable;
|
||||
|
||||
@ -28,6 +28,8 @@ import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable";
|
||||
import normalizedPlatformInjectable from "../../../../common/vars/normalized-platform.injectable";
|
||||
import kubectlBinaryNameInjectable from "../../../../main/kubectl/binary-name.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", () => ({
|
||||
app: {
|
||||
@ -95,12 +97,11 @@ users:
|
||||
token: kubeconfig-user-q4lm4:xxxyyyy
|
||||
`;
|
||||
|
||||
let config: KubeConfig;
|
||||
|
||||
describe("<DeleteClusterDialog />", () => {
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
let createCluster: CreateCluster;
|
||||
let openDeleteClusterDialog: OpenDeleteClusterDialog;
|
||||
let config: KubeConfig;
|
||||
|
||||
beforeEach(async () => {
|
||||
applicationBuilder = getApplicationBuilder();
|
||||
@ -114,6 +115,7 @@ describe("<DeleteClusterDialog />", () => {
|
||||
|
||||
rendererDi.override(hotbarStoreInjectable, () => ({}));
|
||||
rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true);
|
||||
rendererDi.override(readFileSyncInjectable, () => readFileSync);
|
||||
});
|
||||
|
||||
const { rendererDi } = applicationBuilder.dis;
|
||||
|
||||
36
src/test-utils/expects.ts
Normal file
36
src/test-utils/expects.ts
Normal 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`);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user