Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make ResourceStack fully injectable #6591

Merged
merged 1 commit into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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