Skip to content

Commit

Permalink
Make ResourceStack fully injectable (#6591)
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
  • Loading branch information
Nokel81 authored Nov 24, 2022
1 parent 055a13d commit 6142aad
Show file tree
Hide file tree
Showing 15 changed files with 341 additions and 138 deletions.
2 changes: 0 additions & 2 deletions src/common/ipc/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ export const clusterActivateHandler = "cluster:activate";
export const clusterSetFrameIdHandler = "cluster:set-frame-id";
export const clusterVisibilityHandler = "cluster:visibility";
export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all";
export const clusterStates = "cluster:states";

/**
Expand Down
33 changes: 33 additions & 0 deletions src/common/k8s/create-resource-stack.injectable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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 { KubernetesCluster } from "../catalog-entities";
import readDirectoryInjectable from "../fs/read-directory.injectable";
import readFileInjectable from "../fs/read-file.injectable";
import { kubectlApplyAllInjectionToken, kubectlDeleteAllInjectionToken } from "../kube-helpers/channels";
import loggerInjectable from "../logger.injectable";
import joinPathsInjectable from "../path/join-paths.injectable";
import type { ResourceApplyingStack, ResourceStackDependencies } from "./resource-stack";
import { ResourceStack } from "./resource-stack";

export type CreateResourceStack = (cluster: KubernetesCluster, name: string) => ResourceApplyingStack;

const createResourceStackInjectable = getInjectable({
id: "create-resource-stack",
instantiate: (di): CreateResourceStack => {
const deps: ResourceStackDependencies = {
joinPaths: di.inject(joinPathsInjectable),
kubectlApplyAll: di.inject(kubectlApplyAllInjectionToken),
kubectlDeleteAll: di.inject(kubectlDeleteAllInjectionToken),
logger: di.inject(loggerInjectable),
readDirectory: di.inject(readDirectoryInjectable),
readFile: di.inject(readFileInjectable),
};

return (cluster, name) => new ResourceStack(deps, cluster, name);
},
});

export default createResourceStackInjectable;
142 changes: 70 additions & 72 deletions src/common/k8s/resource-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,39 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import fse from "fs-extra";
import path from "path";
import hb from "handlebars";
import type { KubernetesCluster } from "../catalog-entities";
import logger from "../../main/logger";
import { app } from "electron";
import { ClusterStore } from "../cluster-store/cluster-store";
import yaml from "js-yaml";
import { requestKubectlApplyAll, requestKubectlDeleteAll } from "../../renderer/ipc";
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import productNameInjectable from "../vars/product-name.injectable";
import { asLegacyGlobalFunctionForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import createResourceApplierInjectable from "../../main/resource-applier/create-resource-applier.injectable";
import type { AsyncResult } from "../utils/async-result";
import type { Logger } from "../logger";
import type { KubectlApplyAll, KubectlDeleteAll } from "../kube-helpers/channels";
import type { ReadDirectory } from "../fs/read-directory.injectable";
import type { JoinPaths } from "../path/join-paths.injectable";
import type { ReadFile } from "../fs/read-file.injectable";
import { hasTypedProperty, isObject } from "../utils";

export interface ResourceApplyingStack {
kubectlApplyFolder(folderPath: string, templateContext?: any, extraArgs?: string[]): Promise<string>;
kubectlDeleteFolder(folderPath: string, templateContext?: any, extraArgs?: string[]): Promise<string>;
}

export interface ResourceStackDependencies {
readonly logger: Logger;
kubectlApplyAll: KubectlApplyAll;
kubectlDeleteAll: KubectlDeleteAll;
readDirectory: ReadDirectory;
joinPaths: JoinPaths;
readFile: ReadFile;
}

export class ResourceStack {
constructor(protected cluster: KubernetesCluster, protected name: string) {}
constructor(
protected readonly dependencies: ResourceStackDependencies,
protected readonly cluster: KubernetesCluster,
protected readonly name: string,
) {}

/**
*
Expand All @@ -26,8 +43,15 @@ export class ResourceStack {
*/
async kubectlApplyFolder(folderPath: string, templateContext?: any, extraArgs?: string[]): Promise<string> {
const resources = await this.renderTemplates(folderPath, templateContext);
const result = await this.applyResources(resources, extraArgs);

if (result.callWasSuccessful) {
return result.response;
}

this.dependencies.logger.warn(`[RESOURCE-STACK]: failed to apply resources: ${result.error}`);

return this.applyResources(resources, extraArgs);
return "";
}

/**
Expand All @@ -37,85 +61,60 @@ export class ResourceStack {
*/
async kubectlDeleteFolder(folderPath: string, templateContext?: any, extraArgs?: string[]): Promise<string> {
const resources = await this.renderTemplates(folderPath, templateContext);
const result = await this.deleteResources(resources, extraArgs);

return this.deleteResources(resources, extraArgs);
}

protected async applyResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());

if (!clusterModel) {
throw new Error(`cluster not found`);
if (result.callWasSuccessful) {
return result.response;
}

let kubectlArgs = extraArgs || [];

kubectlArgs = this.appendKubectlArgs(kubectlArgs);

if (app) {
const createResourceApplier = asLegacyGlobalFunctionForExtensionApi(createResourceApplierInjectable);

return await createResourceApplier(clusterModel).kubectlApplyAll(resources, kubectlArgs);
} else {
const response = await requestKubectlApplyAll(this.cluster.getId(), resources, kubectlArgs);
this.dependencies.logger.warn(`[RESOURCE-STACK]: failed to delete resources: ${result.error}`);

if (response.stderr) {
throw new Error(response.stderr);
}

return response.stdout ?? "";
}
return "";
}

protected async deleteResources(resources: string[], extraArgs?: string[]): Promise<string> {
const clusterModel = ClusterStore.getInstance().getById(this.cluster.getId());

if (!clusterModel) {
throw new Error(`cluster not found`);
}

let kubectlArgs = extraArgs || [];
protected async applyResources(resources: string[], extraArgs: string[] = []): Promise<AsyncResult<string, string>> {
const kubectlArgs = [...extraArgs, ...this.getAdditionalArgs(extraArgs)];

kubectlArgs = this.appendKubectlArgs(kubectlArgs);

if (app) {
const createResourceApplier = asLegacyGlobalFunctionForExtensionApi(createResourceApplierInjectable);
return this.dependencies.kubectlApplyAll({
clusterId: this.cluster.getId(),
resources,
extraArgs: kubectlArgs,
});
}

return await createResourceApplier(clusterModel).kubectlDeleteAll(resources, kubectlArgs);
} else {
const response = await requestKubectlDeleteAll(this.cluster.getId(), resources, kubectlArgs);
protected async deleteResources(resources: string[], extraArgs: string[] = []): Promise<AsyncResult<string, string>> {
const kubectlArgs = [...extraArgs, ...this.getAdditionalArgs(extraArgs)];

if (response.stderr) {
throw new Error(response.stderr);
}

return response.stdout ?? "";
}
return this.dependencies.kubectlDeleteAll({
clusterId: this.cluster.getId(),
resources,
extraArgs: kubectlArgs,
});
}

protected appendKubectlArgs(kubectlArgs: string[]) {
protected getAdditionalArgs(kubectlArgs: string[]): string[] {
if (!kubectlArgs.includes("-l") && !kubectlArgs.includes("--label")) {
return kubectlArgs.concat(["-l", `app.kubernetes.io/name=${this.name}`]);
return ["-l", `app.kubernetes.io/name=${this.name}`];
}

return kubectlArgs;
return [];
}

protected async renderTemplates(folderPath: string, templateContext: any): Promise<string[]> {
const resources: string[] = [];
const di = getLegacyGlobalDiForExtensionApi();
const productName = di.inject(productNameInjectable);

logger.info(`[RESOURCE-STACK]: render templates from ${folderPath}`);
const files = await fse.readdir(folderPath);
this.dependencies.logger.info(`[RESOURCE-STACK]: render templates from ${folderPath}`);
const files = await this.dependencies.readDirectory(folderPath);

for(const filename of files) {
const file = path.join(folderPath, filename);
const raw = await fse.readFile(file);
for (const filename of files) {
const file = this.dependencies.joinPaths(folderPath, filename);
const raw = await this.dependencies.readFile(file);
const data = (
filename.endsWith(".hb")
? hb.compile(raw.toString())(templateContext)
: raw.toString()
? hb.compile(raw)(templateContext)
: raw
).trim();

if (!data) {
Expand All @@ -127,16 +126,15 @@ export class ResourceStack {
continue;
}

const resource = entry as Record<string, any>;
if (hasTypedProperty(entry, "metadata", isObject)) {
const labels = (entry.metadata.labels ??= {}) as Partial<Record<string, string>>;

if (typeof resource.metadata === "object") {
resource.metadata.labels ??= {};
resource.metadata.labels["app.kubernetes.io/name"] = this.name;
resource.metadata.labels["app.kubernetes.io/managed-by"] = productName;
resource.metadata.labels["app.kubernetes.io/created-by"] = "resource-stack";
labels["app.kubernetes.io/name"] = this.name;
labels["app.kubernetes.io/managed-by"] = productName;
labels["app.kubernetes.io/created-by"] = "resource-stack";
}

resources.push(yaml.dump(resource));
resources.push(yaml.dump(entry));
}
}

Expand Down
43 changes: 43 additions & 0 deletions src/common/kube-helpers/channels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/

import { getInjectionToken } from "@ogre-tools/injectable";
import type { Asyncify } from "type-fest";
import type { RequestChannelHandler } from "../../main/utils/channel/channel-listeners/listener-tokens";
import type { ClusterId } from "../cluster-types";
import type { AsyncResult } from "../utils/async-result";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";

export interface KubectlApplyAllArgs {
clusterId: ClusterId;
resources: string[];
extraArgs: string[];
}

export const kubectlApplyAllChannel: RequestChannel<KubectlApplyAllArgs, AsyncResult<string, string>> = {
id: "kubectl-apply-all",
};

export type KubectlApplyAll = Asyncify<RequestChannelHandler<typeof kubectlApplyAllChannel>>;

export const kubectlApplyAllInjectionToken = getInjectionToken<KubectlApplyAll>({
id: "kubectl-apply-all",
});

export interface KubectlDeleteAllArgs {
clusterId: ClusterId;
resources: string[];
extraArgs: string[];
}

export const kubectlDeleteAllChannel: RequestChannel<KubectlDeleteAllArgs, AsyncResult<string, string>> = {
id: "kubectl-delete-all",
};

export type KubectlDeleteAll = Asyncify<RequestChannelHandler<typeof kubectlDeleteAllChannel>>;

export const kubectlDeleteAllInjectionToken = getInjectionToken<KubectlDeleteAll>({
id: "kubectl-delete-all",
});
25 changes: 24 additions & 1 deletion src/extensions/common-api/k8s-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,42 @@
// It is here to consolidate the common parts which are exported to `Main`
// and to `Renderer`

export { ResourceStack } from "../../common/k8s/resource-stack";
import apiManagerInjectable from "../../common/k8s-api/api-manager/manager.injectable";
import createKubeApiForClusterInjectable from "../../common/k8s-api/create-kube-api-for-cluster.injectable";
import createKubeApiForRemoteClusterInjectable from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable";
import createResourceStackInjectable from "../../common/k8s/create-resource-stack.injectable";
import type { ResourceApplyingStack } from "../../common/k8s/resource-stack";
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import type { KubernetesCluster } from "./catalog";

export const apiManager = asLegacyGlobalForExtensionApi(apiManagerInjectable);
export const forCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForClusterInjectable);
export const forRemoteCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForRemoteClusterInjectable);

export { KubeApi } from "../../common/k8s-api/kube-api";

export const createResourceStack = asLegacyGlobalFunctionForExtensionApi(createResourceStackInjectable);

/**
* @deprecated Switch to using `Common.createResourceStack` instead
*/
export class ResourceStack implements ResourceApplyingStack {
private readonly impl: ResourceApplyingStack;

constructor(cluster: KubernetesCluster, name: string) {
this.impl = createResourceStack(cluster, name);
}

kubectlApplyFolder(folderPath: string, templateContext?: any, extraArgs?: string[] | undefined): Promise<string> {
return this.impl.kubectlApplyFolder(folderPath, templateContext, extraArgs);
}

kubectlDeleteFolder(folderPath: string, templateContext?: any, extraArgs?: string[] | undefined): Promise<string> {
return this.impl.kubectlDeleteFolder(folderPath, templateContext, extraArgs);
}
}

/**
* @deprecated This type is unused
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.in
import askUserForFilePathsInjectable from "../../../ipc/ask-user-for-file-paths.injectable";
import applicationMenuItemCompositeInjectable from "../../../../features/application-menu/main/application-menu-item-composite.injectable";
import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.injectable";
import createResourceApplierInjectable from "../../../resource-applier/create-resource-applier.injectable";

const setupIpcMainHandlersInjectable = getInjectable({
id: "setup-ipc-main-handlers",
Expand All @@ -25,7 +24,6 @@ const setupIpcMainHandlersInjectable = getInjectable({
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
const askUserForFilePaths = di.inject(askUserForFilePathsInjectable);
const emitAppEvent = di.inject(emitAppEventInjectable);
const createResourceApplier = di.inject(createResourceApplierInjectable);

return {
id: "setup-ipc-main-handlers",
Expand All @@ -39,7 +37,6 @@ const setupIpcMainHandlersInjectable = getInjectable({
operatingSystemTheme,
askUserForFilePaths,
emitAppEvent,
createResourceApplier,
});
},
};
Expand Down
Loading

0 comments on commit 6142aad

Please sign in to comment.