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(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(() => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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 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, {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
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 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),
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
};
|
}
|
||||||
|
|||||||
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.
|
* 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({
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
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