From 7355073d63d848666fa4b44df8b7e23ff89733bc Mon Sep 17 00:00:00 2001 From: Ole Lensmar Date: Sat, 26 Feb 2022 16:38:03 +0100 Subject: [PATCH 01/12] fix: improved error handling for apply functionality --- src/redux/thunks/applyFile.ts | 12 +++++++----- src/redux/thunks/applyResource.ts | 24 ++++++++++++------------ src/redux/thunks/applyYaml.ts | 4 ++++ src/utils/alert.ts | 25 +++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 src/utils/alert.ts diff --git a/src/redux/thunks/applyFile.ts b/src/redux/thunks/applyFile.ts index 3bcaa31ba2..d63c623b24 100644 --- a/src/redux/thunks/applyFile.ts +++ b/src/redux/thunks/applyFile.ts @@ -10,6 +10,8 @@ import {setApplyingResource} from '@redux/reducers/main'; import {getAbsoluteFileEntryPath} from '@redux/services/fileEntry'; import {applyYamlToCluster} from '@redux/thunks/applyYaml'; +import {errorAlert} from '@utils/alert'; + /** * Invokes kubectl for the content of the specified resource */ @@ -64,14 +66,14 @@ export async function applyFile( dispatch(setApplyingResource(false)); }); } catch (e: any) { - log.error(e.message); - dispatch(setApplyingResource(true)); + log.error(e); + dispatch(setAlert(errorAlert('Deploy failed', e.message))); + dispatch(setApplyingResource(false)); } } - } catch (e) { - log.error('Failed to apply file'); + } catch (e: any) { log.error(e); - + dispatch(setAlert(errorAlert('Deploy failed', e.message))); dispatch(setApplyingResource(false)); } } diff --git a/src/redux/thunks/applyResource.ts b/src/redux/thunks/applyResource.ts index a34683a002..e540a18c79 100644 --- a/src/redux/thunks/applyResource.ts +++ b/src/redux/thunks/applyResource.ts @@ -25,6 +25,7 @@ import {extractK8sResources} from '@redux/services/resource'; import {applyYamlToCluster} from '@redux/thunks/applyYaml'; import {getResourceFromCluster, removeNamespaceFromCluster} from '@redux/thunks/utils'; +import {errorAlert} from '@utils/alert'; import {PROCESS_ENV} from '@utils/env'; import {getShellPath} from '@utils/shell'; @@ -131,7 +132,9 @@ export async function applyResource( : applyK8sResource(resource, kubeconfig, context, namespace); child.on('exit', (code, signal) => { - log.info(`kubectl exited with code ${code} and signal ${signal}`); + if (code !== null && code !== 0) { + log.warn(`apply exited with code ${code} and signal ${signal}`); + } dispatch(setApplyingResource(false)); }); @@ -185,27 +188,24 @@ export async function applyResource( }); child.stderr.on('data', async data => { - const alert: AlertType = { - type: AlertEnum.Error, - title: `Applying ${resource.name} to cluster ${context} failed`, - message: data.toString(), - }; - if (namespace && namespace.new) { await removeNamespaceFromCluster(namespace.name, kubeconfig, context); } + + const alert = errorAlert(`Applying ${resource.name} to cluster ${context} failed`, data.toString()); + dispatch(setAlert(alert)); dispatch(setApplyingResource(false)); }); } catch (e: any) { - log.error(e.message); - dispatch(setApplyingResource(true)); + log.error(e); + dispatch(setAlert(errorAlert('Deploy failed', e.message))); + dispatch(setApplyingResource(false)); } } - } catch (e) { - log.error('Failed to apply resource'); + } catch (e: any) { log.error(e); - + dispatch(setAlert(errorAlert('Deploy failed', e.message))); dispatch(setApplyingResource(false)); } } diff --git a/src/redux/thunks/applyYaml.ts b/src/redux/thunks/applyYaml.ts index 8ea84b8a72..e0269465fd 100644 --- a/src/redux/thunks/applyYaml.ts +++ b/src/redux/thunks/applyYaml.ts @@ -26,8 +26,12 @@ export function applyYamlToCluster( PATH: getShellPath(), KUBECONFIG: kubeconfig, }, + shell: true, + windowsHide: true, }); + child.stdin.write(yaml); child.stdin.end(); + return child; } diff --git a/src/utils/alert.ts b/src/utils/alert.ts new file mode 100644 index 0000000000..b313f41797 --- /dev/null +++ b/src/utils/alert.ts @@ -0,0 +1,25 @@ +import {AlertEnum, AlertType} from '@models/alert'; + +export function errorAlert(title: string, message: string): AlertType { + return createAlert(AlertEnum.Error, title, message); +} + +export function successAlert(title: string, message: string): AlertType { + return createAlert(AlertEnum.Success, title, message); +} + +export function infoAlert(title: string, message: string): AlertType { + return createAlert(AlertEnum.Info, title, message); +} + +export function warningAlert(title: string, message: string): AlertType { + return createAlert(AlertEnum.Warning, title, message); +} + +export function createAlert(type: AlertEnum, title: string, message: string): AlertType { + return { + type, + title, + message, + }; +} From 069641eaeffed8c9b78ca9f637811833a4e02a90 Mon Sep 17 00:00:00 2001 From: olensmar Date: Thu, 3 Mar 2022 09:32:47 +0100 Subject: [PATCH 02/12] fix: moved kubectl commands to main thread and improved main env handling --- electron/commands.ts | 157 ++++++++++++++---- electron/main.ts | 21 ++- electron/src/initKubeconfig.ts | 9 +- electron/utils.ts | 21 ++- package.json | 5 +- .../molecules/ResourceDiff/ResourceDiff.tsx | 33 ++-- .../organisms/ActionsPane/ActionsPane.tsx | 26 ++- .../ClusterResourceDiffModal.tsx | 6 +- .../HotKeysHandler/HotKeysHandler.tsx | 7 +- .../LocalResourceDiffModal.tsx | 8 +- .../ResourceMatchNameDisplay.tsx | 15 +- src/redux/services/applyMultipleResources.ts | 25 +-- src/redux/services/index.ts | 9 +- .../services/previewReferencedHelmChart.ts | 6 +- src/redux/thunks/applyFile.ts | 31 ++-- src/redux/thunks/applyHelmChart.ts | 38 ++--- src/redux/thunks/applyResource.ts | 109 ++++-------- src/redux/thunks/applyYaml.ts | 28 +--- src/redux/thunks/previewHelmValuesFile.ts | 18 +- src/redux/thunks/previewKustomization.ts | 16 +- src/utils/alert.ts | 12 +- src/utils/env.ts | 6 +- src/utils/helm.ts | 14 +- src/utils/index.ts | 25 --- src/utils/kubectl.ts | 27 +++ src/utils/shell.ts | 12 -- src/utils/thread.ts | 28 ++++ 27 files changed, 390 insertions(+), 322 deletions(-) create mode 100644 src/utils/kubectl.ts create mode 100644 src/utils/thread.ts diff --git a/electron/commands.ts b/electron/commands.ts index 67184b207b..398c47c2f2 100644 --- a/electron/commands.ts +++ b/electron/commands.ts @@ -1,6 +1,6 @@ import {BrowserWindow, dialog} from 'electron'; -import {execSync} from 'child_process'; +import {exec, execSync, spawn} from 'child_process'; import {AnyAction} from 'redux'; import {VM} from 'vm2'; @@ -12,7 +12,9 @@ import {KustomizeCommandOptions} from '@redux/thunks/previewKustomization'; import {FileExplorerOptions, FileOptions} from '@atoms/FileExplorer/FileExplorerOptions'; -import {PROCESS_ENV} from '@utils/env'; +import {HelmCommand} from '@utils/helm'; +import {KubectlOptions, SpawnResult} from '@utils/kubectl'; +import {ensureMainThread} from '@utils/thread'; import autoUpdater from './auto-update'; @@ -21,25 +23,65 @@ import autoUpdater from './auto-update'; */ export const runKustomize = (options: KustomizeCommandOptions, event: Electron.IpcMainEvent) => { - try { - let cmd = options.kustomizeCommand === 'kubectl' ? 'kubectl kustomize ' : 'kustomize build '; - if (options.enableHelm) { - cmd += '--enable-helm '; - } + ensureMainThread(); - let stdout = execSync(`${cmd} "${options.folder}"`, { - env: { - NODE_ENV: PROCESS_ENV.NODE_ENV, - PUBLIC_URL: PROCESS_ENV.PUBLIC_URL, - PATH: PROCESS_ENV.PATH, - }, - maxBuffer: 1024 * 1024 * 10, - windowsHide: true, - }); + const result: SpawnResult = {exitCode: null, signal: null}; - event.sender.send('kustomize-result', {stdout: stdout.toString()}); + try { + if (options.applyArgs) { + const args = options.applyArgs; + + if (options.kustomizeCommand === 'kubectl') { + args.push(...['apply', '-k', `"${options.folder}"`]); + } else { + if (options.enableHelm) { + args.splice(0, 0, '--enable-helm '); + } + + args.splice(0, 0, ...['build', `"${options.folder}"`, '|', 'kubectl']); + args.push(...['apply', '-k']); + } + + const child = spawn(options.kustomizeCommand, args, { + env: { + KUBECONFIG: options.kubeconfig, + ...process.env, + }, + shell: true, + windowsHide: true, + }); + + child.on('exit', (code, signal) => { + result.exitCode = code; + result.signal = signal && signal.toString(); + event.sender.send('kustomize-result', result); + }); + + child.stdout.on('data', data => { + result.stdout = result.stdout ? result + data.toString() : data.toString(); + }); + + child.stderr.on('data', data => { + result.stderr = result.stderr ? result + data.toString() : data.toString(); + }); + } else { + let cmd = options.kustomizeCommand === 'kubectl' ? 'kubectl kustomize ' : 'kustomize build '; + if (options.enableHelm) { + cmd += '--enable-helm '; + } + + let stdout = execSync(`${cmd} "${options.folder}"`, { + env: process.env, + maxBuffer: 1024 * 1024 * 10, + windowsHide: true, + }); + + result.stdout = stdout.toString(); + event.sender.send('kustomize-result', result); + } } catch (e: any) { - event.sender.send('kustomize-result', {error: e.toString()}); + result.error = e.message; + event.sender.send('kustomize-result', result); } }; @@ -104,22 +146,32 @@ export const saveFileDialog = (event: Electron.IpcMainInvokeEvent, options: File * called by thunk to preview a helm chart with values file */ -export const runHelm = (args: any, event: Electron.IpcMainEvent) => { +export const runHelm = (args: HelmCommand, event: Electron.IpcMainEvent) => { + ensureMainThread(); + const result: SpawnResult = {exitCode: null, signal: null}; + try { - let stdout = execSync(args.helmCommand, { - env: { - NODE_ENV: PROCESS_ENV.NODE_ENV, - PUBLIC_URL: PROCESS_ENV.PUBLIC_URL, - KUBECONFIG: args.kubeconfig, - PATH: PROCESS_ENV.PATH, + const child = exec( + args.helmCommand, + { + env: { + KUBECONFIG: args.kubeconfig, + ...process.env, + }, + maxBuffer: 1024 * 1024 * 10, + windowsHide: true, }, - maxBuffer: 1024 * 1024 * 10, - windowsHide: true, - }); - - event.sender.send('helm-result', {stdout: stdout.toString()}); + (error, stdout, stderr) => { + result.stdout = stdout; + result.stderr = stderr; + result.error = error?.message; + + event.sender.send('helm-result', result); + } + ); } catch (e: any) { - event.sender.send('helm-result', {error: e.toString()}); + result.error = e.message; + event.sender.send('helm-result', result); } }; @@ -180,3 +232,46 @@ export const interpolateTemplate = (args: InterpolateTemplateOptions, event: Ele event.sender.send('interpolate-vanilla-template-result', result); }; + +/** + * called by thunk to preview a helm chart with values file + */ + +export const runKubectl = (args: KubectlOptions, event: Electron.IpcMainEvent) => { + ensureMainThread(); + + const result: SpawnResult = {exitCode: null, signal: null}; + + try { + const child = spawn('kubectl', args.kubectlArgs, { + env: { + KUBECONFIG: args.kubeconfig, + ...process.env, + }, + shell: true, + windowsHide: true, + }); + + if (args.yaml) { + child.stdin.write(args.yaml); + child.stdin.end(); + } + + child.on('exit', (code, signal) => { + result.exitCode = code; + result.signal = signal && signal.toString(); + event.sender.send('kubectl-result', result); + }); + + child.stdout.on('data', data => { + result.stdout = result.stdout ? result + data.toString() : data.toString(); + }); + + child.stderr.on('data', data => { + result.stderr = result.stderr ? result + data.toString() : data.toString(); + }); + } catch (e: any) { + result.error = e.message; + event.sender.send('kubectl-result', result); + } +}; diff --git a/electron/main.ts b/electron/main.ts index 3ba0a229b4..aafa0000a1 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -27,7 +27,6 @@ import { ROOT_FILE_ENTRY, } from '@constants/constants'; import {DOWNLOAD_PLUGIN, DOWNLOAD_PLUGIN_RESULT, DOWNLOAD_TEMPLATE, DOWNLOAD_TEMPLATE_RESULT, DOWNLOAD_TEMPLATE_PACK, DOWNLOAD_TEMPLATE_PACK_RESULT, UPDATE_EXTENSIONS, UPDATE_EXTENSIONS_RESULT} from '@constants/ipcEvents'; -import {checkMissingDependencies} from '@utils/index'; import ElectronStore from 'electron-store'; import {setUserDirs, updateNewVersion} from '@redux/reducers/appConfig'; import {NewVersionCode} from '@models/appconfig'; @@ -35,7 +34,6 @@ import {K8sResource} from '@models/k8sresource'; import {isInPreviewModeSelector, kubeConfigContextSelector, unsavedResourcesSelector} from '@redux/selectors'; import {HelmChart, HelmValuesFile} from '@models/helm'; import log from 'loglevel'; -import {PROCESS_ENV} from '@utils/env'; import asyncLib from "async"; import {createMenu, getDockMenu} from './menu'; @@ -47,7 +45,7 @@ import {setAlert} from '@redux/reducers/alert'; import { checkNewVersion, interpolateTemplate, - runHelm, + runHelm, runKubectl, runKustomize, saveFileDialog, selectFileDialog, @@ -64,15 +62,22 @@ import {AnyTemplate, TemplatePack} from '@models/template'; import {AnyPlugin} from '@models/plugin'; import {AnyExtension, DownloadPluginResult, DownloadTemplatePackResult, DownloadTemplateResult, UpdateExtensionsResult} from '@models/extension'; import {KustomizeCommandOptions} from '@redux/thunks/previewKustomization'; -import { askActionConfirmation, convertRecentFilesToRecentProjects, getSerializedProcessEnv, saveInitialK8sSchema, setProjectsRootFolder } from './utils'; +import { + askActionConfirmation, + checkMissingDependencies, + convertRecentFilesToRecentProjects, + getSerializedProcessEnv, + saveInitialK8sSchema, + setProjectsRootFolder, +} from './utils'; import {InterpolateTemplateOptions} from '@redux/services/templates'; import {StartupFlags} from '@utils/startupFlag'; +import {KubectlOptions} from '@utils/kubectl'; Object.assign(console, ElectronLog.functions); const {MONOKLE_RUN_AS_NODE} = process.env; - -const isDev = PROCESS_ENV.NODE_ENV === 'development'; +const isDev = process.env.NODE_ENV === 'development'; const userHomeDir = app.getPath('home'); const userDataDir = app.getPath('userData'); @@ -232,6 +237,10 @@ ipcMain.on('run-helm', (event, args: any) => { runHelm(args, event); }); +ipcMain.on('run-kubectl', (event, args: KubectlOptions) => { + runKubectl(args, event); +}); + ipcMain.on('app-version', event => { event.sender.send('app-version', {version: app.getVersion()}); }); diff --git a/electron/src/initKubeconfig.ts b/electron/src/initKubeconfig.ts index 9755bb7a8b..7050f699be 100644 --- a/electron/src/initKubeconfig.ts +++ b/electron/src/initKubeconfig.ts @@ -11,11 +11,10 @@ import {setKubeConfig} from '@redux/reducers/appConfig'; import {monitorKubeConfig} from '@redux/services/kubeConfigMonitor'; import electronStore from '@utils/electronStore'; -import {PROCESS_ENV} from '@utils/env'; function initKubeconfig(dispatch: (action: AnyAction) => void, userHomeDir: string) { - if (PROCESS_ENV.KUBECONFIG) { - const envKubeconfigParts = PROCESS_ENV.KUBECONFIG.split(path.delimiter); + if (process.env.KUBECONFIG) { + const envKubeconfigParts = process.env.KUBECONFIG.split(path.delimiter); if (envKubeconfigParts.length > 1) { dispatch(setKubeConfig(getKubeConfigContext(envKubeconfigParts[0]))); monitorKubeConfig(envKubeconfigParts[0], dispatch); @@ -28,8 +27,8 @@ function initKubeconfig(dispatch: (action: AnyAction) => void, userHomeDir: stri }) ); } else { - dispatch(setKubeConfig(getKubeConfigContext(PROCESS_ENV.KUBECONFIG))); - monitorKubeConfig(PROCESS_ENV.KUBECONFIG, dispatch); + dispatch(setKubeConfig(getKubeConfigContext(process.env.KUBECONFIG))); + monitorKubeConfig(process.env.KUBECONFIG, dispatch); } return; } diff --git a/electron/utils.ts b/electron/utils.ts index c748d99539..ac3c947ee2 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -2,8 +2,10 @@ import {dialog} from 'electron'; import {AnyAction} from '@reduxjs/toolkit'; +import {execSync} from 'child_process'; import {existsSync, mkdirSync, writeFileSync} from 'fs'; import _ from 'lodash'; +import log from 'loglevel'; import path, {join} from 'path'; import {PREDEFINED_K8S_VERSION} from '@constants/constants'; @@ -14,7 +16,6 @@ import {createProject} from '@redux/reducers/appConfig'; import {loadResource} from '@redux/services'; import electronStore from '@utils/electronStore'; -import {PROCESS_ENV} from '@utils/env'; const GITHUB_URL = 'https://github.com'; const GITHUB_REPOSITORY_REGEX = /^https:\/\/github.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/i; @@ -81,7 +82,7 @@ export const setProjectsRootFolder = (userHomeDir: string) => { export const getSerializedProcessEnv = () => { const serializedProcessEnv: Record = {}; - const processEnv = _.isObject(PROCESS_ENV) ? PROCESS_ENV : _.isObject(process.env) ? process.env : {}; + const processEnv = _.isObject(process.env) ? process.env : {}; Object.entries(processEnv).forEach(([key, value]) => { if (typeof value === 'string') { serializedProcessEnv[key] = value; @@ -142,3 +143,19 @@ export function askActionConfirmation({ return choice === 0; } + +export const checkMissingDependencies = (dependencies: Array): Array => { + log.info(`checking dependencies with process path: ${process.env.PATH}`); + + return dependencies.filter(d => { + try { + execSync(d, { + env: process.env, + windowsHide: true, + }); + return false; + } catch (e: any) { + return true; + } + }); +}; diff --git a/package.json b/package.json index 70f777b7fd..a2e582d2aa 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "craco-alias": "3.0.1", "craco-less": "1.20.0", "cross-env": "7.0.3", - "electron": "17.0.1", + "electron": "17.1.0", "electron-builder": "23.0.0-alpha.3", "electron-notarize": "1.1.1", "electron-reload": "2.0.0-alpha.1", @@ -92,8 +92,8 @@ "@types/flat": "5.0.2", "@types/lodash": "4.14.178", "ajv": "6.12.6", - "asar": "3.1.0", "antd": "4.18.5", + "asar": "3.1.0", "async": "3.2.3", "chokidar": "3.5.3", "dagre": "0.8.5", @@ -128,7 +128,6 @@ "reselect": "4.1.5", "runtypes": "6.5.0", "semver": "7.3.5", - "shell-path": "2.1.0", "shelljs": "0.8.5", "strip-ansi": "6.0.1", "styled-components": "5.3.3", diff --git a/src/components/molecules/ResourceDiff/ResourceDiff.tsx b/src/components/molecules/ResourceDiff/ResourceDiff.tsx index 2f219944a9..3a108ca316 100644 --- a/src/components/molecules/ResourceDiff/ResourceDiff.tsx +++ b/src/components/molecules/ResourceDiff/ResourceDiff.tsx @@ -1,4 +1,4 @@ -import {useMemo, useState} from 'react'; +import {useCallback, useMemo, useState} from 'react'; import {MonacoDiffEditor} from 'react-monaco-editor'; import {useMeasure} from 'react-use'; @@ -17,7 +17,7 @@ import {K8sResource} from '@models/k8sresource'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {updateResource} from '@redux/reducers/main'; -import {kubeConfigContextSelector, kubeConfigPathSelector} from '@redux/selectors'; +import {currentConfigSelector, kubeConfigContextSelector} from '@redux/selectors'; import {isKustomizationResource} from '@redux/services/kustomize'; import {applyResource} from '@redux/thunks/applyResource'; @@ -93,7 +93,7 @@ const ResourceDiff = (props: { const resourceMap = useAppSelector(state => state.main.resourceMap); const previewType = useAppSelector(state => state.main.previewType); const fileMap = useAppSelector(state => state.main.fileMap); - const kubeConfigPath = useAppSelector(kubeConfigPathSelector); + const projectConfig = useAppSelector(currentConfigSelector); const kubeConfigContext = useAppSelector(kubeConfigContextSelector); const [shouldDiffIgnorePaths, setShouldDiffIgnorePaths] = useState(true); @@ -161,18 +161,21 @@ const ResourceDiff = (props: { ); }; - const onClickApplyResource = (namespace?: {name: string; new: boolean}) => { - if (onApply) { - onApply(); - } - - applyResource(localResource.id, resourceMap, fileMap, dispatch, kubeConfigPath, kubeConfigContext, namespace, { - isClusterPreview: previewType === 'cluster', - shouldPerformDiff: true, - isInClusterDiff, - }); - setIsApplyModalVisible(false); - }; + const onClickApplyResource = useCallback( + (namespace?: {name: string; new: boolean}) => { + if (onApply) { + onApply(); + } + + applyResource(localResource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { + isClusterPreview: previewType === 'cluster', + shouldPerformDiff: true, + isInClusterDiff, + }); + setIsApplyModalVisible(false); + }, + [resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, isInClusterDiff, localResource] + ); return ( <> diff --git a/src/components/organisms/ActionsPane/ActionsPane.tsx b/src/components/organisms/ActionsPane/ActionsPane.tsx index 6b15578a6a..eb4dc4aeef 100644 --- a/src/components/organisms/ActionsPane/ActionsPane.tsx +++ b/src/components/organisms/ActionsPane/ActionsPane.tsx @@ -35,6 +35,7 @@ import {setAlert} from '@redux/reducers/alert'; import {openResourceDiffModal} from '@redux/reducers/main'; import {openSaveResourcesToFileFolderModal, setMonacoEditor, setPaneConfiguration} from '@redux/reducers/ui'; import { + currentConfigSelector, knownResourceKindsSelector, kubeConfigContextSelector, kubeConfigPathSelector, @@ -90,6 +91,7 @@ const ActionsPane: React.FC = props => { const isFolderLoading = useAppSelector(state => state.ui.isFolderLoading); const kubeConfigContext = useAppSelector(kubeConfigContextSelector); const kubeConfigPath = useAppSelector(kubeConfigPathSelector); + const projectConfig = useAppSelector(currentConfigSelector); const {kustomizeCommand} = useAppSelector(settingsSelector); const knownResourceKinds = useAppSelector(knownResourceKindsSelector); const monacoEditor = useAppSelector(state => state.ui.monacoEditor); @@ -259,21 +261,17 @@ const ActionsPane: React.FC = props => { return false; }, [selectedResource, knownResourceKinds]); - const onClickApplyResource = useCallback( - (namespace?: {name: string; new: boolean}) => { - if (!selectedResource) { - setIsApplyModalVisible(false); - return; - } - const isClusterPreview = previewType === 'cluster'; - applyResource(selectedResource.id, resourceMap, fileMap, dispatch, kubeConfigPath, kubeConfigContext, namespace, { - isClusterPreview, - kustomizeCommand, - }); + const onClickApplyResource = (namespace?: {name: string; new: boolean}) => { + if (!selectedResource) { setIsApplyModalVisible(false); - }, - [dispatch, fileMap, kubeConfigContext, kubeConfigPath, kustomizeCommand, previewType, resourceMap, selectedResource] - ); + return; + } + const isClusterPreview = previewType === 'cluster'; + applyResource(selectedResource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { + isClusterPreview, + }); + setIsApplyModalVisible(false); + }; const onClickApplyHelmChart = useCallback( (namespace?: string, shouldCreateNamespace?: boolean) => { diff --git a/src/components/organisms/ClusterResourceDiffModal/ClusterResourceDiffModal.tsx b/src/components/organisms/ClusterResourceDiffModal/ClusterResourceDiffModal.tsx index 71dcca3267..95fa8e8692 100644 --- a/src/components/organisms/ClusterResourceDiffModal/ClusterResourceDiffModal.tsx +++ b/src/components/organisms/ClusterResourceDiffModal/ClusterResourceDiffModal.tsx @@ -17,7 +17,7 @@ import {AlertEnum, AlertType} from '@models/alert'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {setAlert} from '@redux/reducers/alert'; import {closeResourceDiffModal, updateResource} from '@redux/reducers/main'; -import {isInClusterModeSelector, kubeConfigContextSelector, kubeConfigPathSelector} from '@redux/selectors'; +import {currentConfigSelector, isInClusterModeSelector, kubeConfigContextSelector} from '@redux/selectors'; import {isKustomizationResource} from '@redux/services/kustomize'; import {applyResource} from '@redux/thunks/applyResource'; @@ -40,7 +40,7 @@ const ClusterResourceDiffModal = () => { const dispatch = useAppDispatch(); const fileMap = useAppSelector(state => state.main.fileMap); const kubeConfigContext = useAppSelector(kubeConfigContextSelector); - const kubeConfigPath = useAppSelector(kubeConfigPathSelector); + const projectConfig = useAppSelector(currentConfigSelector); const previewType = useAppSelector(state => state.main.previewType); const resourceMap = useAppSelector(state => state.main.resourceMap); const targetResourceId = useAppSelector(state => state.main.resourceDiff.targetResourceId); @@ -136,7 +136,7 @@ const ClusterResourceDiffModal = () => { if (selectedMatchingResourceId) { const resource = resourceMap[selectedMatchingResourceId]; if (resource) { - applyResource(resource.id, resourceMap, fileMap, dispatch, kubeConfigPath, kubeConfigContext, namespace, { + applyResource(resource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { isClusterPreview: previewType === 'cluster', }); onCloseHandler(); diff --git a/src/components/organisms/HotKeysHandler/HotKeysHandler.tsx b/src/components/organisms/HotKeysHandler/HotKeysHandler.tsx index c7ba3d9e38..2f5a5c1924 100644 --- a/src/components/organisms/HotKeysHandler/HotKeysHandler.tsx +++ b/src/components/organisms/HotKeysHandler/HotKeysHandler.tsx @@ -18,10 +18,10 @@ import { toggleStartProjectPane, } from '@redux/reducers/ui'; import { + currentConfigSelector, isInPreviewModeSelector, kubeConfigContextSelector, kubeConfigPathSelector, - settingsSelector, } from '@redux/selectors'; import {applyFileWithConfirm} from '@redux/services/applyFileWithConfirm'; import {isKustomizationResource} from '@redux/services/kustomize'; @@ -45,7 +45,7 @@ const HotKeysHandler = () => { const isInPreviewMode = useSelector(isInPreviewModeSelector); const kubeConfigContext = useAppSelector(kubeConfigContextSelector); const kubeConfigPath = useAppSelector(kubeConfigPathSelector); - const {kustomizeCommand} = useAppSelector(settingsSelector); + const projectConfig = useAppSelector(currentConfigSelector); const [isApplyModalVisible, setIsApplyModalVisible] = useState(false); @@ -126,12 +126,11 @@ const HotKeysHandler = () => { mainState.resourceMap, mainState.fileMap, dispatch, - kubeConfigPath, + projectConfig, kubeConfigContext, namespace, { isClusterPreview, - kustomizeCommand, } ); setIsApplyModalVisible(false); diff --git a/src/components/organisms/LocalResourceDiffModal/LocalResourceDiffModal.tsx b/src/components/organisms/LocalResourceDiffModal/LocalResourceDiffModal.tsx index 089a6a2c9b..36c05eb77b 100644 --- a/src/components/organisms/LocalResourceDiffModal/LocalResourceDiffModal.tsx +++ b/src/components/organisms/LocalResourceDiffModal/LocalResourceDiffModal.tsx @@ -16,7 +16,7 @@ import {AlertEnum, AlertType} from '@models/alert'; import {useAppDispatch, useAppSelector} from '@redux/hooks'; import {setAlert} from '@redux/reducers/alert'; import {closeResourceDiffModal, openResourceDiffModal, updateResource} from '@redux/reducers/main'; -import {isInClusterModeSelector, kubeConfigContextSelector, kubeConfigPathSelector} from '@redux/selectors'; +import {currentConfigSelector, isInClusterModeSelector, kubeConfigContextSelector} from '@redux/selectors'; import {isKustomizationResource} from '@redux/services/kustomize'; import {applyResource} from '@redux/thunks/applyResource'; @@ -38,7 +38,7 @@ const DiffModal = () => { const fileMap = useAppSelector(state => state.main.fileMap); const isInClusterMode = useAppSelector(isInClusterModeSelector); const kubeConfigContext = useAppSelector(kubeConfigContextSelector); - const kubeConfigPath = useAppSelector(kubeConfigPathSelector); + const projectConfig = useAppSelector(currentConfigSelector); const previewType = useAppSelector(state => state.main.previewType); const resourceFilter = useAppSelector(state => state.main.resourceFilter); const resourceMap = useAppSelector(state => state.main.resourceMap); @@ -97,7 +97,7 @@ const DiffModal = () => { if (targetResource?.id) { const resource = resourceMap[targetResource.id]; if (resource) { - applyResource(resource.id, resourceMap, fileMap, dispatch, kubeConfigPath, kubeConfigContext, namespace, { + applyResource(resource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { isClusterPreview: previewType === 'cluster', shouldPerformDiff: true, }); @@ -261,7 +261,7 @@ const DiffModal = () => { }, [ kubeConfigContext, dispatch, - kubeConfigPath, + projectConfig.kubeConfig?.path, resourceMap, resourceFilter.namespace, targetResource, diff --git a/src/navsections/ClusterDiffSectionBlueprint/ResourceMatchNameDisplay.tsx b/src/navsections/ClusterDiffSectionBlueprint/ResourceMatchNameDisplay.tsx index 01278e316b..002cc6a51e 100644 --- a/src/navsections/ClusterDiffSectionBlueprint/ResourceMatchNameDisplay.tsx +++ b/src/navsections/ClusterDiffSectionBlueprint/ResourceMatchNameDisplay.tsx @@ -21,7 +21,7 @@ import { unselectClusterDiffMatch, updateResource, } from '@redux/reducers/main'; -import {kubeConfigContextSelector, kubeConfigPathSelector} from '@redux/selectors'; +import {currentConfigSelector, kubeConfigContextSelector} from '@redux/selectors'; import {isKustomizationResource} from '@redux/services/kustomize'; import {applyResource} from '@redux/thunks/applyResource'; @@ -89,7 +89,7 @@ function ResourceMatchNameDisplay(props: ItemCustomComponentProps) { const resourceMap = useAppSelector(state => state.main.resourceMap); const fileMap = useAppSelector(state => state.main.fileMap); const kubeConfigContext = useAppSelector(kubeConfigContextSelector); - const kubeConfigPath = useAppSelector(kubeConfigPathSelector); + const projectConfig = useAppSelector(currentConfigSelector); const resourceFilterNamespace = useAppSelector(state => state.main.resourceFilter.namespace); const [isHovered, setIsHovered] = useState(false); @@ -143,16 +143,7 @@ function ResourceMatchNameDisplay(props: ItemCustomComponentProps) { return; } - applyResource( - firstLocalResource.id, - resourceMap, - fileMap, - dispatch, - - kubeConfigPath, - kubeConfigContext, - namespace - ); + applyResource(firstLocalResource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace); setIsApplyModalVisible(false); }; diff --git a/src/redux/services/applyMultipleResources.ts b/src/redux/services/applyMultipleResources.ts index 2d5051d2d5..43bf4950d9 100644 --- a/src/redux/services/applyMultipleResources.ts +++ b/src/redux/services/applyMultipleResources.ts @@ -15,7 +15,7 @@ import {removeNamespaceFromCluster} from '@redux/thunks/utils'; import {doesTextStartWithYamlDocumentDelimiter} from './resource'; -const applyMultipleResources = ( +const applyMultipleResources = async ( config: AppConfig, resourcesToApply: K8sResource[], dispatch: AppDispatch, @@ -46,16 +46,10 @@ const applyMultipleResources = ( }, ''); try { - const child = applyYamlToCluster(yamlToApply, kubeConfigPath, currentContext, namespace); - child.on('exit', (code, signal) => { - log.info(`kubectl exited with code ${code} and signal ${signal}`); - }); - - let alertMessage: string = ''; - - child.stdout.on('data', data => { - alertMessage += `\n${data.toString()}`; + const result = await applyYamlToCluster(yamlToApply, kubeConfigPath, currentContext, namespace); + log.info(`kubectl exited with code ${result.exitCode} and signal ${result.signal}`); + if (result.stdout) { if (namespace && namespace.new) { const namespaceAlert: AlertType = { type: AlertEnum.Success, @@ -69,7 +63,7 @@ const applyMultipleResources = ( const alert: AlertType = { type: AlertEnum.Success, title: `Applied selected resources to cluster ${currentContext} successfully`, - message: alertMessage, + message: result.stdout, }; if (onSuccessCallback) { @@ -77,13 +71,12 @@ const applyMultipleResources = ( } setTimeout(() => dispatch(setAlert(alert)), 400); - }); - - child.stderr.on('data', async data => { + } + if (result.stderr) { const alert: AlertType = { type: AlertEnum.Error, title: `Applying selected resources to cluster ${currentContext} failed`, - message: data.toString(), + message: result.stderr, }; if (namespace && namespace.new) { @@ -91,7 +84,7 @@ const applyMultipleResources = ( } dispatch(setAlert(alert)); - }); + } } catch (e) { log.error('Failed to apply selected resources'); log.error(e); diff --git a/src/redux/services/index.ts b/src/redux/services/index.ts index 9ef36d29c8..701925d594 100644 --- a/src/redux/services/index.ts +++ b/src/redux/services/index.ts @@ -1,19 +1,20 @@ import fs from 'fs'; import path from 'path'; -import {PROCESS_ENV, RESOURCES_PATH} from '@utils/env'; +import {getMainProcessEnv} from '@utils/env'; /** * Gets the absolute path to a statically bundled resource in the /resources folder */ export function getStaticResourcePath(resourcePath: string) { - if (PROCESS_ENV.NODE_ENV === 'test') { + const mainProcessEnv = getMainProcessEnv(); + if (mainProcessEnv?.NODE_ENV === 'test') { return path.join('resources', resourcePath); } - return PROCESS_ENV.NODE_ENV !== 'development' - ? path.join(RESOURCES_PATH, 'resources', resourcePath) + return mainProcessEnv?.NODE_ENV !== 'development' + ? path.join(process.resourcesPath, 'resources', resourcePath) : path.join('resources', resourcePath); } diff --git a/src/redux/services/previewReferencedHelmChart.ts b/src/redux/services/previewReferencedHelmChart.ts index 2a6700d1f4..a868869f8d 100644 --- a/src/redux/services/previewReferencedHelmChart.ts +++ b/src/redux/services/previewReferencedHelmChart.ts @@ -6,7 +6,7 @@ import {v4 as uuidv4} from 'uuid'; import {AppDispatch} from '@models/appdispatch'; import {K8sResource} from '@models/k8sresource'; -import {runHelm} from '@utils/helm'; +import {HelmCommand, runHelmInMainThread} from '@utils/helm'; import {extractObjectsFromYaml} from './manifest-utils'; import {interpolateTemplate} from './templates'; @@ -34,12 +34,12 @@ export const previewReferencedHelmChart = async ( const parsedValuesFileContent: string = await interpolateTemplate(valuesFileContent, formsData); await fsWriteFilePromise(newTempValuesFilePath, parsedValuesFileContent); - const helmArgs = { + const helmArgs: HelmCommand = { helmCommand: `helm install --kube-context ${kubeconfigContext} -f "${newTempValuesFilePath}" --repo ${chartRepo} ${chartName} --version ${chartVersion} --generate-name --dry-run`, kubeconfig: kubeconfigPath, }; - const result = await runHelm(helmArgs); + const result = await runHelmInMainThread(helmArgs); if (result.error) { throw new Error(result.error); diff --git a/src/redux/thunks/applyFile.ts b/src/redux/thunks/applyFile.ts index d63c623b24..d0688fd626 100644 --- a/src/redux/thunks/applyFile.ts +++ b/src/redux/thunks/applyFile.ts @@ -33,47 +33,44 @@ export async function applyFile( kubeconfig: string, context: string ) { + dispatch(setApplyingResource(true)); + try { const fileEntry = fileMap[filePath]; if (fileEntry && !fileEntry.children) { - dispatch(setApplyingResource(true)); - try { - const child = applyFileToCluster(getAbsoluteFileEntryPath(fileEntry, fileMap), kubeconfig, context); + const result = await applyFileToCluster(getAbsoluteFileEntryPath(fileEntry, fileMap), kubeconfig, context); - child.on('exit', (code, signal) => { - log.info(`kubectl exited with code ${code} and signal ${signal}`); - dispatch(setApplyingResource(false)); - }); + if (result.exitCode && result.exitCode !== 0) { + log.info(`Apply exited with code ${result.exitCode} and signal ${result.signal}`); + } - child.stdout.on('data', data => { + if (result.stdout) { const alert: AlertType = { type: AlertEnum.Success, title: 'Apply completed', - message: data.toString(), + message: result.stdout, }; dispatch(setAlert(alert)); - dispatch(setApplyingResource(false)); - }); + } - child.stderr.on('data', data => { + if (result.stderr) { const alert: AlertType = { type: AlertEnum.Error, title: 'Apply failed', - message: data.toString(), + message: result.stderr, }; dispatch(setAlert(alert)); - dispatch(setApplyingResource(false)); - }); + } } catch (e: any) { log.error(e); dispatch(setAlert(errorAlert('Deploy failed', e.message))); - dispatch(setApplyingResource(false)); } } } catch (e: any) { log.error(e); dispatch(setAlert(errorAlert('Deploy failed', e.message))); - dispatch(setApplyingResource(false)); } + + dispatch(setApplyingResource(false)); } diff --git a/src/redux/thunks/applyHelmChart.ts b/src/redux/thunks/applyHelmChart.ts index e3c4ec17ef..f60fd5a918 100644 --- a/src/redux/thunks/applyHelmChart.ts +++ b/src/redux/thunks/applyHelmChart.ts @@ -1,4 +1,3 @@ -import {spawn} from 'child_process'; import log from 'loglevel'; import path from 'path'; @@ -11,8 +10,7 @@ import {setAlert} from '@redux/reducers/alert'; import {setApplyingResource} from '@redux/reducers/main'; import {getAbsoluteHelmChartPath, getAbsoluteValuesFilePath} from '@redux/services/fileEntry'; -import {PROCESS_ENV} from '@utils/env'; -import {getShellPath} from '@utils/shell'; +import {runHelmInMainThread} from '@utils/helm'; /** * Invokes helm install for the specified helm chart and values file @@ -30,6 +28,7 @@ function applyHelmChartToCluster( const chartPath = path.dirname(getAbsoluteHelmChartPath(helmChart, fileMap)); let helmArgs = [ + 'helm', 'install', '-f', getAbsoluteValuesFilePath(valuesFile, fileMap), @@ -47,16 +46,10 @@ function applyHelmChartToCluster( } } - const child = spawn('helm', helmArgs, { - env: { - NODE_ENV: PROCESS_ENV.NODE_ENV, - PUBLIC_URL: PROCESS_ENV.PUBLIC_URL, - PATH: getShellPath(), - KUBECONFIG: kubeconfig, - }, + return runHelmInMainThread({ + helmCommand: helmArgs.join(' '), + kubeconfig, }); - - return child; } /** @@ -79,7 +72,7 @@ export async function applyHelmChart( dispatch(setApplyingResource(true)); try { - const child = applyHelmChartToCluster( + const result = await applyHelmChartToCluster( valuesFile, helmChart, fileMap, @@ -89,30 +82,27 @@ export async function applyHelmChart( shouldCreateNamespace ); - child.on('exit', (code, signal) => { - log.info(`Helm exited with code ${code} and signal ${signal}`); - dispatch(setApplyingResource(false)); - }); + if (result.exitCode && result.exitCode !== 0) { + log.info(`Helm exited with code ${result.exitCode} and signal ${result.signal}`); + } - child.stdout.on('data', data => { + if (result.stdout) { const alert: AlertType = { type: AlertEnum.Success, title: `Installing Helm Chart ${helmChart.name} in cluster ${context} completed`, - message: data.toString(), + message: result.stdout, }; dispatch(setAlert(alert)); dispatch(setApplyingResource(false)); - }); - - child.stderr.on('data', data => { + } else if (result.stderr) { const alert: AlertType = { type: AlertEnum.Error, title: `Installing Helm Chart ${helmChart.name} in cluster ${context} failed`, - message: data.toString(), + message: result.stderr, }; dispatch(setAlert(alert)); dispatch(setApplyingResource(false)); - }); + } } catch (e) { if (e instanceof Error) { log.error(e.message); diff --git a/src/redux/thunks/applyResource.ts b/src/redux/thunks/applyResource.ts index e540a18c79..fcb667a208 100644 --- a/src/redux/thunks/applyResource.ts +++ b/src/redux/thunks/applyResource.ts @@ -1,4 +1,3 @@ -import {spawn} from 'child_process'; import _ from 'lodash'; import log from 'loglevel'; import {stringify} from 'yaml'; @@ -6,10 +5,10 @@ import {stringify} from 'yaml'; import {PREVIEW_PREFIX} from '@constants/constants'; import {AlertEnum, AlertType} from '@models/alert'; +import {ProjectConfig} from '@models/appconfig'; import {AppDispatch} from '@models/appdispatch'; import {FileMapType, ResourceMapType} from '@models/appstate'; import {K8sResource} from '@models/k8sresource'; -import {KustomizeCommandType} from '@models/kustomize'; import {setAlert} from '@redux/reducers/alert'; import { @@ -23,11 +22,10 @@ import {getAbsoluteResourceFolder} from '@redux/services/fileEntry'; import {isKustomizationResource} from '@redux/services/kustomize'; import {extractK8sResources} from '@redux/services/resource'; import {applyYamlToCluster} from '@redux/thunks/applyYaml'; +import {runKustomizeInMainThread} from '@redux/thunks/previewKustomization'; import {getResourceFromCluster, removeNamespaceFromCluster} from '@redux/thunks/utils'; -import {errorAlert} from '@utils/alert'; -import {PROCESS_ENV} from '@utils/env'; -import {getShellPath} from '@utils/shell'; +import {errorAlert, successAlert} from '@utils/alert'; /** * Invokes kubectl for the content of the specified resource @@ -35,8 +33,8 @@ import {getShellPath} from '@utils/shell'; function applyK8sResource( resource: K8sResource, - kubeconfig: string, context: string, + kubeconfig?: string, namespace?: {name: string; new: boolean} ) { const resourceContent = _.cloneDeep(resource.content); @@ -44,7 +42,7 @@ function applyK8sResource( delete resourceContent.metadata.namespace; } - return applyYamlToCluster(stringify(resourceContent), kubeconfig, context, namespace); + return applyYamlToCluster(stringify(resourceContent), context, kubeconfig, namespace); } /** @@ -54,43 +52,18 @@ function applyK8sResource( function applyKustomization( resource: K8sResource, fileMap: FileMapType, - kubeconfig: string, context: string, - kustomizeCommand: KustomizeCommandType, + projectConfig: ProjectConfig, namespace?: {name: string; new: boolean} ) { const folder = getAbsoluteResourceFolder(resource, fileMap); - const args = - kustomizeCommand === 'kubectl' - ? namespace - ? `kubectl --context ${context} --namespace ${namespace.name} apply -k "${folder}"` - : `kubectl --context ${context} apply -k "${folder}"` - : namespace - ? `kustomize build "${folder}" | kubectl --context ${context} --namespace ${namespace.name} apply -k -` - : `kustomize build "${folder}" | kubectl --context ${context} apply -k -`; - - const child = - kustomizeCommand === 'kubectl' - ? spawn(args, { - shell: true, - env: { - NODE_ENV: PROCESS_ENV.NODE_ENV, - PUBLIC_URL: PROCESS_ENV.PUBLIC_URL, - PATH: getShellPath(), - KUBECONFIG: kubeconfig, - }, - }) - : spawn(args, { - shell: true, - env: { - NODE_ENV: PROCESS_ENV.NODE_ENV, - PUBLIC_URL: PROCESS_ENV.PUBLIC_URL, - PATH: getShellPath(), - KUBECONFIG: kubeconfig, - }, - }); - return child; + const args: string[] = ['--context', context]; + if (namespace) { + args.push(...['--namespace', namespace.name]); + } + + return runKustomizeInMainThread(folder, projectConfig, args); } /** @@ -104,14 +77,13 @@ export async function applyResource( resourceMap: ResourceMapType, fileMap: FileMapType, dispatch: AppDispatch, - kubeconfig: string, + projectConfig: ProjectConfig, context: string, namespace?: {name: string; new: boolean}, options?: { isClusterPreview?: boolean; isInClusterDiff?: boolean; shouldPerformDiff?: boolean; - kustomizeCommand?: 'kubectl' | 'kustomize'; } ) { try { @@ -120,27 +92,20 @@ export async function applyResource( dispatch(setApplyingResource(true)); try { - const child = isKustomizationResource(resource) - ? applyKustomization( - resource, - fileMap, - kubeconfig, - context, - options && options.kustomizeCommand ? options.kustomizeCommand : 'kubectl', - namespace - ) - : applyK8sResource(resource, kubeconfig, context, namespace); - - child.on('exit', (code, signal) => { - if (code !== null && code !== 0) { - log.warn(`apply exited with code ${code} and signal ${signal}`); - } - dispatch(setApplyingResource(false)); - }); + const kubeconfigPath = projectConfig.kubeConfig?.path; + const result = isKustomizationResource(resource) + ? await applyKustomization(resource, fileMap, context, projectConfig, namespace) + : await applyK8sResource(resource, context, kubeconfigPath, namespace); + + if (result.exitCode !== null && result.exitCode !== 0) { + log.warn(`apply exited with code ${result.exitCode} and signal ${result.signal}`); + } - child.stdout.on('data', data => { - if (options?.isClusterPreview) { - getResourceFromCluster(resource, kubeconfig, context).then(resourceFromCluster => { + dispatch(setApplyingResource(false)); + + if (result.stdout) { + if (options?.isClusterPreview && kubeconfigPath) { + getResourceFromCluster(resource, kubeconfigPath, context).then(resourceFromCluster => { delete resourceFromCluster.body.metadata?.managedFields; const updatedResourceText = stringify(resourceFromCluster.body, {sortMapEntries: true}); if (resourceMap[resourceFromCluster.body.metadata?.uid]) { @@ -151,7 +116,7 @@ export async function applyResource( }) ); } else { - const newK8sResource = extractK8sResources(updatedResourceText, PREVIEW_PREFIX + kubeconfig)[0]; + const newK8sResource = extractK8sResources(updatedResourceText, PREVIEW_PREFIX + kubeconfigPath)[0]; dispatch(addResource(newK8sResource)); } @@ -166,7 +131,6 @@ export async function applyResource( dispatch(openResourceDiffModal(resource.id)); } } - dispatch(setApplyingResource(false)); if (namespace && namespace.new) { const namespaceAlert: AlertType = { @@ -178,25 +142,20 @@ export async function applyResource( dispatch(setAlert(namespaceAlert)); } - const alert: AlertType = { - type: AlertEnum.Success, - title: `Applied ${resource.name} to cluster ${context} successfully`, - message: data.toString(), - }; - + const alert = successAlert(`Applied ${resource.name} to cluster ${context} successfully`, result.stdout); setTimeout(() => dispatch(setAlert(alert)), 400); - }); + } - child.stderr.on('data', async data => { - if (namespace && namespace.new) { - await removeNamespaceFromCluster(namespace.name, kubeconfig, context); + if (result.stderr) { + if (namespace && namespace.new && kubeconfigPath) { + await removeNamespaceFromCluster(namespace.name, kubeconfigPath, context); } - const alert = errorAlert(`Applying ${resource.name} to cluster ${context} failed`, data.toString()); + const alert = errorAlert(`Applying ${resource.name} to cluster ${context} failed`, result.stderr); dispatch(setAlert(alert)); dispatch(setApplyingResource(false)); - }); + } } catch (e: any) { log.error(e); dispatch(setAlert(errorAlert('Deploy failed', e.message))); diff --git a/src/redux/thunks/applyYaml.ts b/src/redux/thunks/applyYaml.ts index e0269465fd..be89579ebb 100644 --- a/src/redux/thunks/applyYaml.ts +++ b/src/redux/thunks/applyYaml.ts @@ -1,7 +1,4 @@ -import {spawn} from 'child_process'; - -import {PROCESS_ENV} from '@utils/env'; -import {getShellPath} from '@utils/shell'; +import {runKubectlInMainThread} from '@utils/kubectl'; /** * Invokes kubectl apply for the specified yaml @@ -9,29 +6,14 @@ import {getShellPath} from '@utils/shell'; export function applyYamlToCluster( yaml: string, - kubeconfig: string, context: string, + kubeconfig?: string, namespace?: {name: string; new: boolean} ) { - const spawnArgs = ['--context', context, 'apply', '-f', '-']; + const kubectlArgs = ['--context', context, 'apply', '-f', '-']; if (namespace) { - spawnArgs.unshift(...['--namespace', namespace.name]); + kubectlArgs.unshift(...['--namespace', namespace.name]); } - - const child = spawn('kubectl', spawnArgs, { - env: { - NODE_ENV: PROCESS_ENV.NODE_ENV, - PUBLIC_URL: PROCESS_ENV.PUBLIC_URL, - PATH: getShellPath(), - KUBECONFIG: kubeconfig, - }, - shell: true, - windowsHide: true, - }); - - child.stdin.write(yaml); - child.stdin.end(); - - return child; + return runKubectlInMainThread(kubectlArgs, kubeconfig, yaml); } diff --git a/src/redux/thunks/previewHelmValuesFile.ts b/src/redux/thunks/previewHelmValuesFile.ts index 0713a1294c..b5377bbf98 100644 --- a/src/redux/thunks/previewHelmValuesFile.ts +++ b/src/redux/thunks/previewHelmValuesFile.ts @@ -13,7 +13,7 @@ import {SetPreviewDataPayload} from '@redux/reducers/main'; import {currentConfigSelector} from '@redux/selectors'; import {createPreviewResult, createRejectionWithAlert} from '@redux/thunks/utils'; -import {runHelm} from '@utils/helm'; +import {HelmCommand, runHelmInMainThread} from '@utils/helm'; /** * Thunk to preview a Helm Chart @@ -29,15 +29,14 @@ export const previewHelmValuesFile = createAsyncThunk< >('main/previewHelmValuesFile', async (valuesFileId, thunkAPI) => { const configState = thunkAPI.getState().config; const state = thunkAPI.getState().main; - const kubeconfig = configState.projectConfig?.kubeConfig?.path || configState.kubeConfig.path; - const k8sVersion = configState.projectConfig?.k8sVersion; + const projectConfig = currentConfigSelector(thunkAPI.getState()); + const kubeconfig = projectConfig.kubeConfig?.path; + const k8sVersion = projectConfig.k8sVersion; const userDataDir = configState.userDataDir; - const currentContext = - thunkAPI.getState().config.projectConfig?.kubeConfig?.currentContext || - thunkAPI.getState().config.kubeConfig.currentContext; + const currentContext = projectConfig.kubeConfig?.currentContext; const valuesFile = state.helmValuesMap[valuesFileId]; - if (valuesFile && valuesFile.filePath) { + if (kubeconfig && valuesFile && valuesFile.filePath) { const rootFolder = state.fileMap[ROOT_FILE_ENTRY].filePath; const chart = state.helmChartMap[valuesFile.helmChartId]; const folder = path.join(rootFolder, path.dirname(chart.filePath)); @@ -46,10 +45,9 @@ export const previewHelmValuesFile = createAsyncThunk< if (fs.existsSync(folder) && fs.existsSync(path.join(folder, valuesFile.name))) { log.info(`previewing ${valuesFile.name} in folder ${folder} using ${configState.settings.helmPreviewMode} mode`); - const projectConfig = currentConfigSelector(thunkAPI.getState()); const helmPreviewMode = projectConfig.settings ? projectConfig.settings.helmPreviewMode : 'template'; - const args = { + const args: HelmCommand = { helmCommand: helmPreviewMode === 'template' ? `helm template -f "${folder}${path.sep}${valuesFile.name}" ${chart.name} "${folder}"` @@ -57,7 +55,7 @@ export const previewHelmValuesFile = createAsyncThunk< kubeconfig, }; - const result = await runHelm(args); + const result = await runHelmInMainThread(args); if (result.error) { return createRejectionWithAlert(thunkAPI, 'Helm Error', result.error); diff --git a/src/redux/thunks/previewKustomization.ts b/src/redux/thunks/previewKustomization.ts index 4238e17e84..7f7bc7bf1f 100644 --- a/src/redux/thunks/previewKustomization.ts +++ b/src/redux/thunks/previewKustomization.ts @@ -16,10 +16,15 @@ import {SetPreviewDataPayload} from '@redux/reducers/main'; import {currentConfigSelector} from '@redux/selectors'; import {createPreviewResult, createRejectionWithAlert} from '@redux/thunks/utils'; +import {SpawnResult} from '@utils/kubectl'; +import {ensureRendererThread} from '@utils/thread'; + export type KustomizeCommandOptions = { folder: string; kustomizeCommand: KustomizeCommandType; + applyArgs?: string[]; enableHelm: boolean; + kubeconfig?: string; }; /** @@ -44,7 +49,7 @@ export const previewKustomization = createAsyncThunk< const folder = path.join(rootFolder, resource.filePath.substr(0, resource.filePath.lastIndexOf(path.sep))); log.info(`previewing ${resource.id} in folder ${folder}`); - const result = await runKustomize(folder, projectConfig); + const result = await runKustomizeInMainThread(folder, projectConfig); if (result.error) { return createRejectionWithAlert(thunkAPI, 'Kustomize Error', result.error); @@ -69,18 +74,23 @@ export const previewKustomization = createAsyncThunk< * Invokes kustomize in main thread */ -function runKustomize(folder: string, projectConfig: ProjectConfig): any { - return new Promise(resolve => { +export function runKustomizeInMainThread(folder: string, projectConfig: ProjectConfig, applyArgs?: string[]) { + ensureRendererThread(); + + return new Promise(resolve => { ipcRenderer.once('kustomize-result', (event, arg) => { resolve(arg); }); const kustomizeCommand = projectConfig?.settings?.kustomizeCommand || 'kubectl'; const enableHelmWithKustomize = projectConfig?.settings?.enableHelmWithKustomize || false; + const kubeconfig = projectConfig.kubeConfig?.path; ipcRenderer.send('run-kustomize', { folder, kustomizeCommand, enableHelm: enableHelmWithKustomize, + applyArgs, + kubeconfig, } as KustomizeCommandOptions); }); } diff --git a/src/utils/alert.ts b/src/utils/alert.ts index b313f41797..1a3f1b09a5 100644 --- a/src/utils/alert.ts +++ b/src/utils/alert.ts @@ -1,25 +1,25 @@ import {AlertEnum, AlertType} from '@models/alert'; -export function errorAlert(title: string, message: string): AlertType { +export function errorAlert(title: string, message?: string): AlertType { return createAlert(AlertEnum.Error, title, message); } -export function successAlert(title: string, message: string): AlertType { +export function successAlert(title: string, message?: string): AlertType { return createAlert(AlertEnum.Success, title, message); } -export function infoAlert(title: string, message: string): AlertType { +export function infoAlert(title: string, message?: string): AlertType { return createAlert(AlertEnum.Info, title, message); } -export function warningAlert(title: string, message: string): AlertType { +export function warningAlert(title: string, message?: string): AlertType { return createAlert(AlertEnum.Warning, title, message); } -export function createAlert(type: AlertEnum, title: string, message: string): AlertType { +export function createAlert(type: AlertEnum, title: string, message?: string): AlertType { return { type, title, - message, + message: message || '', }; } diff --git a/src/utils/env.ts b/src/utils/env.ts index 44bf2de0df..bfb72fd969 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -62,12 +62,12 @@ function shellEnvSync() { let mainProcessEnv: any | undefined; export function getMainProcessEnv() { + if (!mainProcessEnv) { + mainProcessEnv = shellEnvSync(); + } return mainProcessEnv; } export function setMainProcessEnv(env: any) { mainProcessEnv = env; } - -export const PROCESS_ENV: any = shellEnvSync(); -export const RESOURCES_PATH = process.resourcesPath; diff --git a/src/utils/helm.ts b/src/utils/helm.ts index 6dc9f3c805..9f7df503c4 100644 --- a/src/utils/helm.ts +++ b/src/utils/helm.ts @@ -1,10 +1,20 @@ import {ipcRenderer} from 'electron'; +import {SpawnResult} from '@utils/kubectl'; +import {ensureRendererThread} from '@utils/thread'; + +export type HelmCommand = { + helmCommand: string; + kubeconfig: string; +}; + /** * Invokes Helm in main thread */ -export function runHelm(cmd: any): any { - return new Promise(resolve => { +export function runHelmInMainThread(cmd: HelmCommand) { + ensureRendererThread(); + + return new Promise(resolve => { ipcRenderer.once('helm-result', (event, arg) => { resolve(arg); }); diff --git a/src/utils/index.ts b/src/utils/index.ts index 61655b735b..c880386fb5 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,3 @@ -import {execSync} from 'child_process'; -import log from 'loglevel'; - -import {PROCESS_ENV} from '@utils/env'; - let lastId: number = 0; export const generateId = (prefix: string = 'id'): string => { @@ -13,23 +8,3 @@ export const generateId = (prefix: string = 'id'): string => { export const uniqueArr = (arr: Array): Array => { return Array.from(new Set(arr)); }; - -export const checkMissingDependencies = (dependencies: Array): Array => { - log.info(`checking dependencies with process path: ${PROCESS_ENV.PATH}`); - - return dependencies.filter(d => { - try { - execSync(d, { - env: { - NODE_ENV: PROCESS_ENV.NODE_ENV, - PUBLIC_URL: PROCESS_ENV.PUBLIC_URL, - PATH: PROCESS_ENV.PATH, - }, - windowsHide: true, - }); - return false; - } catch (e: any) { - return true; - } - }); -}; diff --git a/src/utils/kubectl.ts b/src/utils/kubectl.ts new file mode 100644 index 0000000000..babf12756d --- /dev/null +++ b/src/utils/kubectl.ts @@ -0,0 +1,27 @@ +import {ipcRenderer} from 'electron'; + +import {ensureRendererThread} from '@utils/thread'; + +export type KubectlOptions = { + kubectlArgs: string[]; + kubeconfig?: string; + yaml?: string; +}; + +export type SpawnResult = { + exitCode: null | number; + signal: null | string; + stderr?: string; + stdout?: string; + error?: string; +}; + +export function runKubectlInMainThread(spawnArgs: string[], kubeconfig?: string, yaml?: string) { + ensureRendererThread(); + return new Promise(resolve => { + ipcRenderer.once('kubectl-result', (event, arg: SpawnResult) => { + resolve(arg); + }); + ipcRenderer.send('run-kubectl', {kubectlArgs: spawnArgs, kubeconfig, yaml} as KubectlOptions); + }); +} diff --git a/src/utils/shell.ts b/src/utils/shell.ts index b55b4cc7ae..77b74c079f 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -1,18 +1,6 @@ import {shell} from 'electron'; import * as os from 'os'; -// @ts-ignore -import shellPath from 'shell-path'; - -let cachedShellPath: string | undefined; - -export function getShellPath() { - if (cachedShellPath === undefined) { - cachedShellPath = shellPath.sync(); - } - - return cachedShellPath; -} export function showItemInFolder(fullPath: string) { shell.showItemInFolder(fullPath); diff --git a/src/utils/thread.ts b/src/utils/thread.ts new file mode 100644 index 0000000000..1260f7d43f --- /dev/null +++ b/src/utils/thread.ts @@ -0,0 +1,28 @@ +/** + * Inspired by https://github.com/jprichardson/is-electron-renderer + */ + +export function isRendererThread() { + // running in a web browser + if (typeof process === 'undefined') return true; + + // node-integration is disabled + if (!process) return true; + + // We're in node.js somehow + if (!process.type) return false; + + return process.type === 'renderer'; +} + +export function ensureMainThread(message?: string) { + if (isRendererThread()) { + throw new Error(message || 'Not running in main thread'); + } +} + +export function ensureRendererThread(message?: string) { + if (!isRendererThread()) { + throw new Error(message || 'Not running in renderer thread'); + } +} From 7fe9b6241f47fb8baa425abdebfef468dfb94644 Mon Sep 17 00:00:00 2001 From: olensmar Date: Sat, 5 Mar 2022 13:48:10 +0100 Subject: [PATCH 03/12] fix: upgraded client-node dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a2e582d2aa..f8d75157d6 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ }, "dependencies": { "@ant-design/icons": "4.7.0", - "@kubernetes/client-node": "0.16.2", + "@kubernetes/client-node": "0.16.3", "@reduxjs/toolkit": "1.7.1", "@rjsf/antd": "3.2.1", "@rjsf/core": "3.2.1", From c73a38d3fbd67d1ba80133a93b950f2b4b802094 Mon Sep 17 00:00:00 2001 From: olensmar Date: Sun, 6 Mar 2022 11:41:47 +0100 Subject: [PATCH 04/12] fix: improve error logging of invalid documents --- src/redux/services/resource.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/redux/services/resource.ts b/src/redux/services/resource.ts index 8169ddeaf3..51bea63c46 100644 --- a/src/redux/services/resource.ts +++ b/src/redux/services/resource.ts @@ -652,18 +652,24 @@ export function extractK8sResources(fileContent: string, relativePath: string) { const lineCounter: LineCounter = new LineCounter(); const documents = parseAllYamlDocuments(fileContent, lineCounter); const result: K8sResource[] = []; + let splitDocs: any; if (documents) { let docIndex = 0; documents.forEach(doc => { if (doc.errors.length > 0) { + if (!splitDocs) { + splitDocs = fileContent.split('---'); + } + log.warn( `Ignoring document ${docIndex} in ${path.parse(relativePath).name} due to ${doc.errors.length} error(s)`, - documents[docIndex] + documents[docIndex], + splitDocs && docIndex < splitDocs.length ? splitDocs[docIndex] : '' ); } else { if (doc.warnings.length > 0) { - log.warn('[extractK8sResources]: Doc has warnings', doc.warnings, doc); + log.warn('[extractK8sResources]: Doc has warnings', doc); } const content = doc.toJS(); From a9799580d9e8d311e3e94fcd130befae35d8c36f Mon Sep 17 00:00:00 2001 From: olensmar Date: Sun, 6 Mar 2022 11:43:13 +0100 Subject: [PATCH 05/12] fix: refactored preview/deploy actions to use shared command mechanism --- electron/commands.ts | 126 ++---------------- electron/main.ts | 19 +-- .../organisms/ActionsPane/ActionsPane.tsx | 2 - .../services/previewReferencedHelmChart.ts | 25 +++- src/redux/thunks/applyHelmChart.ts | 18 +-- src/redux/thunks/applyResource.ts | 4 +- src/redux/thunks/applyYaml.ts | 4 +- src/redux/thunks/previewHelmValuesFile.ts | 37 +++-- src/redux/thunks/previewKustomization.ts | 73 +++++----- src/utils/command.ts | 32 +++++ src/utils/helm.ts | 23 ---- src/utils/kubectl.ts | 27 ---- 12 files changed, 149 insertions(+), 241 deletions(-) create mode 100644 src/utils/command.ts delete mode 100644 src/utils/helm.ts delete mode 100644 src/utils/kubectl.ts diff --git a/electron/commands.ts b/electron/commands.ts index 398c47c2f2..f8159899b5 100644 --- a/electron/commands.ts +++ b/electron/commands.ts @@ -1,6 +1,6 @@ import {BrowserWindow, dialog} from 'electron'; -import {exec, execSync, spawn} from 'child_process'; +import {spawn} from 'child_process'; import {AnyAction} from 'redux'; import {VM} from 'vm2'; @@ -8,83 +8,14 @@ import {NewVersionCode} from '@models/appconfig'; import {updateNewVersion} from '@redux/reducers/appConfig'; import {InterpolateTemplateOptions} from '@redux/services/templates'; -import {KustomizeCommandOptions} from '@redux/thunks/previewKustomization'; import {FileExplorerOptions, FileOptions} from '@atoms/FileExplorer/FileExplorerOptions'; -import {HelmCommand} from '@utils/helm'; -import {KubectlOptions, SpawnResult} from '@utils/kubectl'; +import {CommandOptions, CommandResult} from '@utils/command'; import {ensureMainThread} from '@utils/thread'; import autoUpdater from './auto-update'; -/** - * called by thunk to preview a kustomization - */ - -export const runKustomize = (options: KustomizeCommandOptions, event: Electron.IpcMainEvent) => { - ensureMainThread(); - - const result: SpawnResult = {exitCode: null, signal: null}; - - try { - if (options.applyArgs) { - const args = options.applyArgs; - - if (options.kustomizeCommand === 'kubectl') { - args.push(...['apply', '-k', `"${options.folder}"`]); - } else { - if (options.enableHelm) { - args.splice(0, 0, '--enable-helm '); - } - - args.splice(0, 0, ...['build', `"${options.folder}"`, '|', 'kubectl']); - args.push(...['apply', '-k']); - } - - const child = spawn(options.kustomizeCommand, args, { - env: { - KUBECONFIG: options.kubeconfig, - ...process.env, - }, - shell: true, - windowsHide: true, - }); - - child.on('exit', (code, signal) => { - result.exitCode = code; - result.signal = signal && signal.toString(); - event.sender.send('kustomize-result', result); - }); - - child.stdout.on('data', data => { - result.stdout = result.stdout ? result + data.toString() : data.toString(); - }); - - child.stderr.on('data', data => { - result.stderr = result.stderr ? result + data.toString() : data.toString(); - }); - } else { - let cmd = options.kustomizeCommand === 'kubectl' ? 'kubectl kustomize ' : 'kustomize build '; - if (options.enableHelm) { - cmd += '--enable-helm '; - } - - let stdout = execSync(`${cmd} "${options.folder}"`, { - env: process.env, - maxBuffer: 1024 * 1024 * 10, - windowsHide: true, - }); - - result.stdout = stdout.toString(); - event.sender.send('kustomize-result', result); - } - } catch (e: any) { - result.error = e.message; - event.sender.send('kustomize-result', result); - } -}; - /** * prompts to select a file using the native dialogs */ @@ -142,39 +73,6 @@ export const saveFileDialog = (event: Electron.IpcMainInvokeEvent, options: File return dialog.showSaveDialogSync(dialogOptions); }; -/** - * called by thunk to preview a helm chart with values file - */ - -export const runHelm = (args: HelmCommand, event: Electron.IpcMainEvent) => { - ensureMainThread(); - const result: SpawnResult = {exitCode: null, signal: null}; - - try { - const child = exec( - args.helmCommand, - { - env: { - KUBECONFIG: args.kubeconfig, - ...process.env, - }, - maxBuffer: 1024 * 1024 * 10, - windowsHide: true, - }, - (error, stdout, stderr) => { - result.stdout = stdout; - result.stderr = stderr; - result.error = error?.message; - - event.sender.send('helm-result', result); - } - ); - } catch (e: any) { - result.error = e.message; - event.sender.send('helm-result', result); - } -}; - /** * Checks for a new version of monokle */ @@ -237,41 +135,41 @@ export const interpolateTemplate = (args: InterpolateTemplateOptions, event: Ele * called by thunk to preview a helm chart with values file */ -export const runKubectl = (args: KubectlOptions, event: Electron.IpcMainEvent) => { +export const runCommand = (options: CommandOptions, event: Electron.IpcMainEvent) => { ensureMainThread(); - const result: SpawnResult = {exitCode: null, signal: null}; + const result: CommandResult = {exitCode: null, signal: null}; try { - const child = spawn('kubectl', args.kubectlArgs, { + const child = spawn(options.cmd, options.args, { env: { - KUBECONFIG: args.kubeconfig, + ...options.env, ...process.env, }, shell: true, windowsHide: true, }); - if (args.yaml) { - child.stdin.write(args.yaml); + if (options.input) { + child.stdin.write(options.input); child.stdin.end(); } child.on('exit', (code, signal) => { result.exitCode = code; result.signal = signal && signal.toString(); - event.sender.send('kubectl-result', result); + event.sender.send('command-result', result); }); child.stdout.on('data', data => { - result.stdout = result.stdout ? result + data.toString() : data.toString(); + result.stdout = result.stdout ? result.stdout + data.toString() : data.toString(); }); child.stderr.on('data', data => { - result.stderr = result.stderr ? result + data.toString() : data.toString(); + result.stderr = result.stderr ? result.stderr + data.toString() : data.toString(); }); } catch (e: any) { result.error = e.message; - event.sender.send('kubectl-result', result); + event.sender.send('command-result', result); } }; diff --git a/electron/main.ts b/electron/main.ts index aafa0000a1..c073390500 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -44,9 +44,7 @@ import {AlertEnum, AlertType} from '@models/alert'; import {setAlert} from '@redux/reducers/alert'; import { checkNewVersion, - interpolateTemplate, - runHelm, runKubectl, - runKustomize, + interpolateTemplate, runCommand, saveFileDialog, selectFileDialog, } from '@root/electron/commands'; @@ -61,7 +59,6 @@ import {downloadTemplate, downloadTemplatePack, loadTemplatePackMap, loadTemplat import {AnyTemplate, TemplatePack} from '@models/template'; import {AnyPlugin} from '@models/plugin'; import {AnyExtension, DownloadPluginResult, DownloadTemplatePackResult, DownloadTemplateResult, UpdateExtensionsResult} from '@models/extension'; -import {KustomizeCommandOptions} from '@redux/thunks/previewKustomization'; import { askActionConfirmation, checkMissingDependencies, @@ -72,7 +69,7 @@ import { } from './utils'; import {InterpolateTemplateOptions} from '@redux/services/templates'; import {StartupFlags} from '@utils/startupFlag'; -import {KubectlOptions} from '@utils/kubectl'; +import {CommandOptions} from '@utils/command'; Object.assign(console, ElectronLog.functions); @@ -221,10 +218,6 @@ ipcMain.on('interpolate-vanilla-template', (event, args: InterpolateTemplateOpti interpolateTemplate(args, event); }); -ipcMain.on('run-kustomize', (event, cmdOptions: KustomizeCommandOptions) => { - runKustomize(cmdOptions, event); -}); - ipcMain.handle('select-file', async (event, options: FileExplorerOptions) => { return selectFileDialog(event, options); }); @@ -233,12 +226,8 @@ ipcMain.handle('save-file', async (event, options: FileOptions) => { return saveFileDialog(event, options); }); -ipcMain.on('run-helm', (event, args: any) => { - runHelm(args, event); -}); - -ipcMain.on('run-kubectl', (event, args: KubectlOptions) => { - runKubectl(args, event); +ipcMain.on('run-command', (event, args: CommandOptions) => { + runCommand(args, event); }); ipcMain.on('app-version', event => { diff --git a/src/components/organisms/ActionsPane/ActionsPane.tsx b/src/components/organisms/ActionsPane/ActionsPane.tsx index eb4dc4aeef..dd7cee5197 100644 --- a/src/components/organisms/ActionsPane/ActionsPane.tsx +++ b/src/components/organisms/ActionsPane/ActionsPane.tsx @@ -39,7 +39,6 @@ import { knownResourceKindsSelector, kubeConfigContextSelector, kubeConfigPathSelector, - settingsSelector, } from '@redux/selectors'; import {applyFileWithConfirm} from '@redux/services/applyFileWithConfirm'; import {getResourcesForPath} from '@redux/services/fileEntry'; @@ -92,7 +91,6 @@ const ActionsPane: React.FC = props => { const kubeConfigContext = useAppSelector(kubeConfigContextSelector); const kubeConfigPath = useAppSelector(kubeConfigPathSelector); const projectConfig = useAppSelector(currentConfigSelector); - const {kustomizeCommand} = useAppSelector(settingsSelector); const knownResourceKinds = useAppSelector(knownResourceKindsSelector); const monacoEditor = useAppSelector(state => state.ui.monacoEditor); const paneConfiguration = useAppSelector(state => state.ui.paneConfiguration); diff --git a/src/redux/services/previewReferencedHelmChart.ts b/src/redux/services/previewReferencedHelmChart.ts index a868869f8d..e4329e220d 100644 --- a/src/redux/services/previewReferencedHelmChart.ts +++ b/src/redux/services/previewReferencedHelmChart.ts @@ -6,7 +6,7 @@ import {v4 as uuidv4} from 'uuid'; import {AppDispatch} from '@models/appdispatch'; import {K8sResource} from '@models/k8sresource'; -import {HelmCommand, runHelmInMainThread} from '@utils/helm'; +import {CommandOptions, runCommandInMainThread} from '@utils/command'; import {extractObjectsFromYaml} from './manifest-utils'; import {interpolateTemplate} from './templates'; @@ -18,6 +18,7 @@ const fsReadFilePromise = promisify(fs.readFile); /** * Thunk to preview a Helm Chart */ + export const previewReferencedHelmChart = async ( chartName: string, chartVersion: string, @@ -34,12 +35,26 @@ export const previewReferencedHelmChart = async ( const parsedValuesFileContent: string = await interpolateTemplate(valuesFileContent, formsData); await fsWriteFilePromise(newTempValuesFilePath, parsedValuesFileContent); - const helmArgs: HelmCommand = { - helmCommand: `helm install --kube-context ${kubeconfigContext} -f "${newTempValuesFilePath}" --repo ${chartRepo} ${chartName} --version ${chartVersion} --generate-name --dry-run`, - kubeconfig: kubeconfigPath, + const options: CommandOptions = { + cmd: 'helm', + args: [ + 'install', + '--kube-context', + kubeconfigContext, + '-f', + `"${newTempValuesFilePath}"`, + '--repo', + chartRepo, + chartName, + '--version', + chartVersion, + '--generate-name', + '--dry-run', + ], + env: {KUBECONFIG: kubeconfigPath}, }; - const result = await runHelmInMainThread(helmArgs); + const result = await runCommandInMainThread(options); if (result.error) { throw new Error(result.error); diff --git a/src/redux/thunks/applyHelmChart.ts b/src/redux/thunks/applyHelmChart.ts index f60fd5a918..1dca48c568 100644 --- a/src/redux/thunks/applyHelmChart.ts +++ b/src/redux/thunks/applyHelmChart.ts @@ -10,7 +10,7 @@ import {setAlert} from '@redux/reducers/alert'; import {setApplyingResource} from '@redux/reducers/main'; import {getAbsoluteHelmChartPath, getAbsoluteValuesFilePath} from '@redux/services/fileEntry'; -import {runHelmInMainThread} from '@utils/helm'; +import {runCommandInMainThread} from '@utils/command'; /** * Invokes helm install for the specified helm chart and values file @@ -27,8 +27,7 @@ function applyHelmChartToCluster( ) { const chartPath = path.dirname(getAbsoluteHelmChartPath(helmChart, fileMap)); - let helmArgs = [ - 'helm', + let args = [ 'install', '-f', getAbsoluteValuesFilePath(valuesFile, fileMap), @@ -39,16 +38,19 @@ function applyHelmChartToCluster( ]; if (namespace) { - helmArgs.push(...['--namespace', namespace]); + args.push(...['--namespace', namespace]); if (shouldCreateNamespace) { - helmArgs.push('--create-namespace'); + args.push('--create-namespace'); } } - return runHelmInMainThread({ - helmCommand: helmArgs.join(' '), - kubeconfig, + return runCommandInMainThread({ + cmd: 'helm', + args, + env: { + KUBECONFIG: kubeconfig, + }, }); } diff --git a/src/redux/thunks/applyResource.ts b/src/redux/thunks/applyResource.ts index fcb667a208..afcc9322cd 100644 --- a/src/redux/thunks/applyResource.ts +++ b/src/redux/thunks/applyResource.ts @@ -22,7 +22,7 @@ import {getAbsoluteResourceFolder} from '@redux/services/fileEntry'; import {isKustomizationResource} from '@redux/services/kustomize'; import {extractK8sResources} from '@redux/services/resource'; import {applyYamlToCluster} from '@redux/thunks/applyYaml'; -import {runKustomizeInMainThread} from '@redux/thunks/previewKustomization'; +import {runKustomize} from '@redux/thunks/previewKustomization'; import {getResourceFromCluster, removeNamespaceFromCluster} from '@redux/thunks/utils'; import {errorAlert, successAlert} from '@utils/alert'; @@ -63,7 +63,7 @@ function applyKustomization( args.push(...['--namespace', namespace.name]); } - return runKustomizeInMainThread(folder, projectConfig, args); + return runKustomize(folder, projectConfig, args); } /** diff --git a/src/redux/thunks/applyYaml.ts b/src/redux/thunks/applyYaml.ts index be89579ebb..4df06b40f7 100644 --- a/src/redux/thunks/applyYaml.ts +++ b/src/redux/thunks/applyYaml.ts @@ -1,4 +1,4 @@ -import {runKubectlInMainThread} from '@utils/kubectl'; +import {runCommandInMainThread} from '@utils/command'; /** * Invokes kubectl apply for the specified yaml @@ -15,5 +15,5 @@ export function applyYamlToCluster( if (namespace) { kubectlArgs.unshift(...['--namespace', namespace.name]); } - return runKubectlInMainThread(kubectlArgs, kubeconfig, yaml); + return runCommandInMainThread({args: kubectlArgs, cmd: 'kubectl', input: yaml, env: {KUBECONFIG: kubeconfig}}); } diff --git a/src/redux/thunks/previewHelmValuesFile.ts b/src/redux/thunks/previewHelmValuesFile.ts index b5377bbf98..49c207c61f 100644 --- a/src/redux/thunks/previewHelmValuesFile.ts +++ b/src/redux/thunks/previewHelmValuesFile.ts @@ -13,7 +13,7 @@ import {SetPreviewDataPayload} from '@redux/reducers/main'; import {currentConfigSelector} from '@redux/selectors'; import {createPreviewResult, createRejectionWithAlert} from '@redux/thunks/utils'; -import {HelmCommand, runHelmInMainThread} from '@utils/helm'; +import {CommandOptions, runCommandInMainThread} from '@utils/command'; /** * Thunk to preview a Helm Chart @@ -36,7 +36,7 @@ export const previewHelmValuesFile = createAsyncThunk< const currentContext = projectConfig.kubeConfig?.currentContext; const valuesFile = state.helmValuesMap[valuesFileId]; - if (kubeconfig && valuesFile && valuesFile.filePath) { + if (kubeconfig && valuesFile && valuesFile.filePath && currentContext) { const rootFolder = state.fileMap[ROOT_FILE_ENTRY].filePath; const chart = state.helmChartMap[valuesFile.helmChartId]; const folder = path.join(rootFolder, path.dirname(chart.filePath)); @@ -47,18 +47,33 @@ export const previewHelmValuesFile = createAsyncThunk< const helmPreviewMode = projectConfig.settings ? projectConfig.settings.helmPreviewMode : 'template'; - const args: HelmCommand = { - helmCommand: + const options: CommandOptions = { + cmd: 'helm', + args: helmPreviewMode === 'template' - ? `helm template -f "${folder}${path.sep}${valuesFile.name}" ${chart.name} "${folder}"` - : `helm install --kube-context ${currentContext} -f "${folder}${path.sep}${valuesFile.name}" ${chart.name} "${folder}" --dry-run`, - kubeconfig, + ? ['template', '-f', `"${path.join(folder, valuesFile.name)}"`, chart.name, `"${folder}"`] + : [ + 'install', + '--kube-context', + currentContext, + '-f', + `"${path.join(folder, valuesFile.name)}"`, + chart.name, + `"${folder}"`, + '--dry-run', + ], + env: {KUBECONFIG: kubeconfig}, }; - const result = await runHelmInMainThread(args); + const result = await runCommandInMainThread(options); + console.log('got helm result', result); - if (result.error) { - return createRejectionWithAlert(thunkAPI, 'Helm Error', result.error); + if (result.error || result.stderr) { + return createRejectionWithAlert( + thunkAPI, + 'Helm Error', + result.error || result.stderr || `Unknown error ${result.exitCode}` + ); } if (result.stdout) { @@ -71,6 +86,8 @@ export const previewHelmValuesFile = createAsyncThunk< state.resourceRefsProcessingOptions ); } + + return createRejectionWithAlert(thunkAPI, 'Helm Error', 'Helm returned no resources'); } return createRejectionWithAlert( diff --git a/src/redux/thunks/previewKustomization.ts b/src/redux/thunks/previewKustomization.ts index 7f7bc7bf1f..76e51ef3db 100644 --- a/src/redux/thunks/previewKustomization.ts +++ b/src/redux/thunks/previewKustomization.ts @@ -1,5 +1,3 @@ -import {ipcRenderer} from 'electron'; - import {createAsyncThunk} from '@reduxjs/toolkit'; import log from 'loglevel'; @@ -9,23 +7,13 @@ import {ROOT_FILE_ENTRY} from '@constants/constants'; import {ProjectConfig} from '@models/appconfig'; import {AppDispatch} from '@models/appdispatch'; -import {KustomizeCommandType} from '@models/kustomize'; import {RootState} from '@models/rootstate'; import {SetPreviewDataPayload} from '@redux/reducers/main'; import {currentConfigSelector} from '@redux/selectors'; import {createPreviewResult, createRejectionWithAlert} from '@redux/thunks/utils'; -import {SpawnResult} from '@utils/kubectl'; -import {ensureRendererThread} from '@utils/thread'; - -export type KustomizeCommandOptions = { - folder: string; - kustomizeCommand: KustomizeCommandType; - applyArgs?: string[]; - enableHelm: boolean; - kubeconfig?: string; -}; +import {CommandResult, runCommandInMainThread} from '@utils/command'; /** * Thunk to preview kustomizations @@ -46,10 +34,10 @@ export const previewKustomization = createAsyncThunk< const resource = state.resourceMap[resourceId]; if (resource && resource.filePath) { const rootFolder = state.fileMap[ROOT_FILE_ENTRY].filePath; - const folder = path.join(rootFolder, resource.filePath.substr(0, resource.filePath.lastIndexOf(path.sep))); + const folder = path.join(rootFolder, path.dirname(resource.filePath)); log.info(`previewing ${resource.id} in folder ${folder}`); - const result = await runKustomizeInMainThread(folder, projectConfig); + const result = await runKustomize(folder, projectConfig); if (result.error) { return createRejectionWithAlert(thunkAPI, 'Kustomize Error', result.error); @@ -74,23 +62,42 @@ export const previewKustomization = createAsyncThunk< * Invokes kustomize in main thread */ -export function runKustomizeInMainThread(folder: string, projectConfig: ProjectConfig, applyArgs?: string[]) { - ensureRendererThread(); - - return new Promise(resolve => { - ipcRenderer.once('kustomize-result', (event, arg) => { - resolve(arg); - }); - const kustomizeCommand = projectConfig?.settings?.kustomizeCommand || 'kubectl'; - const enableHelmWithKustomize = projectConfig?.settings?.enableHelmWithKustomize || false; - const kubeconfig = projectConfig.kubeConfig?.path; - - ipcRenderer.send('run-kustomize', { - folder, - kustomizeCommand, - enableHelm: enableHelmWithKustomize, - applyArgs, - kubeconfig, - } as KustomizeCommandOptions); +export function runKustomize( + folder: string, + projectConfig: ProjectConfig, + applyArgs?: string[] +): Promise { + const args: string[] = []; + + // use kustomize? + if (projectConfig?.settings?.kustomizeCommand === 'kustomize') { + args.push('build'); + if (projectConfig.settings?.enableHelmWithKustomize) { + args.push('--enable-helm '); + } + args.push(`"${folder}"`); + } else { + // preview using kubectl + args.push('kustomize'); + args.push(`"${folder}"`); + + if (projectConfig.settings?.enableHelmWithKustomize) { + args.push('--enable-helm '); + } + } + + // apply using kubectl + if (applyArgs) { + args.push(...['|', 'kubectl']); + args.push(...applyArgs); + args.push(...['apply', '-f', '-']); + } + + return runCommandInMainThread({ + cmd: projectConfig?.settings?.kustomizeCommand ? String(projectConfig.settings.kustomizeCommand) : 'kubectl', + args, + env: { + KUBECONFIG: projectConfig.kubeConfig?.path, + }, }); } diff --git a/src/utils/command.ts b/src/utils/command.ts new file mode 100644 index 0000000000..2717e20a35 --- /dev/null +++ b/src/utils/command.ts @@ -0,0 +1,32 @@ +import {ipcRenderer} from 'electron'; + +import log from 'loglevel'; + +import {ensureRendererThread} from '@utils/thread'; + +export type CommandOptions = { + cmd: string; + args: string[]; + env?: any; + input?: string; +}; + +export type CommandResult = { + exitCode: null | number; + signal: null | string; + stderr?: string; + stdout?: string; + error?: string; +}; + +export function runCommandInMainThread(options: CommandOptions): Promise { + ensureRendererThread(); + log.info('sending command to main thread', options); + + return new Promise(resolve => { + ipcRenderer.once('command-result', (event, arg: CommandResult) => { + resolve(arg); + }); + ipcRenderer.send('run-command', options); + }); +} diff --git a/src/utils/helm.ts b/src/utils/helm.ts deleted file mode 100644 index 9f7df503c4..0000000000 --- a/src/utils/helm.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {ipcRenderer} from 'electron'; - -import {SpawnResult} from '@utils/kubectl'; -import {ensureRendererThread} from '@utils/thread'; - -export type HelmCommand = { - helmCommand: string; - kubeconfig: string; -}; - -/** - * Invokes Helm in main thread - */ -export function runHelmInMainThread(cmd: HelmCommand) { - ensureRendererThread(); - - return new Promise(resolve => { - ipcRenderer.once('helm-result', (event, arg) => { - resolve(arg); - }); - ipcRenderer.send('run-helm', cmd); - }); -} diff --git a/src/utils/kubectl.ts b/src/utils/kubectl.ts deleted file mode 100644 index babf12756d..0000000000 --- a/src/utils/kubectl.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {ipcRenderer} from 'electron'; - -import {ensureRendererThread} from '@utils/thread'; - -export type KubectlOptions = { - kubectlArgs: string[]; - kubeconfig?: string; - yaml?: string; -}; - -export type SpawnResult = { - exitCode: null | number; - signal: null | string; - stderr?: string; - stdout?: string; - error?: string; -}; - -export function runKubectlInMainThread(spawnArgs: string[], kubeconfig?: string, yaml?: string) { - ensureRendererThread(); - return new Promise(resolve => { - ipcRenderer.once('kubectl-result', (event, arg: SpawnResult) => { - resolve(arg); - }); - ipcRenderer.send('run-kubectl', {kubectlArgs: spawnArgs, kubeconfig, yaml} as KubectlOptions); - }); -} From d47d1c37e69a513c2db8a374bbfb5ee688381c17 Mon Sep 17 00:00:00 2001 From: olensmar Date: Sun, 6 Mar 2022 11:54:56 +0100 Subject: [PATCH 06/12] fix: improved deploy/install button and tooltips --- .../organisms/ActionsPane/ActionsPane.tsx | 16 +++++++++++++--- src/constants/tooltips.ts | 8 +++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/organisms/ActionsPane/ActionsPane.tsx b/src/components/organisms/ActionsPane/ActionsPane.tsx index dd7cee5197..3a41df18ab 100644 --- a/src/components/organisms/ActionsPane/ActionsPane.tsx +++ b/src/components/organisms/ActionsPane/ActionsPane.tsx @@ -19,7 +19,9 @@ import {makeApplyKustomizationText, makeApplyResourceText} from '@constants/make import { ApplyFileTooltip, ApplyTooltip, + DeployKustomizationTooltip, DiffTooltip, + InstallValuesFileTooltip, OpenExternalDocumentationTooltip, OpenHelmChartDocumentationTooltip, OpenKustomizeDocumentationTooltip, @@ -42,7 +44,7 @@ import { } from '@redux/selectors'; import {applyFileWithConfirm} from '@redux/services/applyFileWithConfirm'; import {getResourcesForPath} from '@redux/services/fileEntry'; -import {isHelmChartFile} from '@redux/services/helm'; +import {isHelmChartFile, isHelmValuesFile} from '@redux/services/helm'; import {isKustomizationPatch, isKustomizationResource} from '@redux/services/kustomize'; import {isUnsavedResource} from '@redux/services/resource'; import {getResourceSchema, getSchemaForPath, getUiSchemaForPath} from '@redux/services/schema'; @@ -401,7 +403,15 @@ const ActionsPane: React.FC = props => { diff --git a/src/constants/tooltips.ts b/src/constants/tooltips.ts index b1ec1eaf6d..98139bcd45 100644 --- a/src/constants/tooltips.ts +++ b/src/constants/tooltips.ts @@ -22,9 +22,11 @@ export const ReloadKustomizationPreviewTooltip = 'Reload the preview of this Kus export const HelmPreviewTooltip = 'Preview the Helm Chart with this values file'; export const ReloadHelmPreviewTooltip = 'Reload the Helm Chart preview with this values file'; export const ExitHelmPreviewTooltip = 'Exit Helm Chart preview (Escape)'; -export const ApplyFileTooltip = `Deploy this file to your configured cluster (${KEY_CTRL_CMD}+ALT+S)`; -export const ApplyTooltip = `Deploy this resource to your configured cluster (${KEY_CTRL_CMD}+ALT+S)`; -export const DiffTooltip = `Diff this resource against your configured cluster (${KEY_CTRL_CMD}+ALT+D)`; +export const ApplyFileTooltip = `Deploy this file to your selected cluster (${KEY_CTRL_CMD}+ALT+S)`; +export const DeployKustomizationTooltip = `Deploy this kustomization to your selected cluster (${KEY_CTRL_CMD}+ALT+S)`; +export const InstallValuesFileTooltip = `Install Helm Chart using this values file in your selected cluster (${KEY_CTRL_CMD}+ALT+S)`; +export const ApplyTooltip = `Deploy this resource to your selected cluster (${KEY_CTRL_CMD}+ALT+S)`; +export const DiffTooltip = `Diff this resource against your selected cluster (${KEY_CTRL_CMD}+ALT+D)`; export const NamespacesFilterTooltip = 'Filter visible resources on selected namespace'; export const KubeconfigPathTooltip = 'The path to the kubeconfig to use for cluster/kubectl commands'; export const AddInclusionPatternTooltip = 'Add pattern for files that contain resource manifests'; From c11c91eb093a848740ac746b8db5d6494480d449 Mon Sep 17 00:00:00 2001 From: olensmar Date: Sun, 6 Mar 2022 12:22:09 +0100 Subject: [PATCH 07/12] fix: merged main into branch --- electron/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 04f4bcd538..6047867785 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -71,10 +71,11 @@ import { checkMissingDependencies, convertRecentFilesToRecentProjects, getSerializedProcessEnv, + initNucleus, saveInitialK8sSchema, + setDeviceID, setProjectsRootFolder, } from './utils'; -import { askActionConfirmation, convertRecentFilesToRecentProjects, getSerializedProcessEnv, saveInitialK8sSchema, setProjectsRootFolder, setDeviceID, initNucleus } from './utils'; import {InterpolateTemplateOptions} from '@redux/services/templates'; import {StartupFlags} from '@utils/startupFlag'; import {ProjectNameChange, StorePropagation} from '@utils/global-electron-store'; @@ -446,7 +447,6 @@ export const createWindow = (givenPath?: string) => { return win; }; - export const openApplication = async (givenPath?: string) => { await app.whenReady(); From 0f1da6913008cfb648ea99dbd7713831db681a8e Mon Sep 17 00:00:00 2001 From: olensmar Date: Sun, 6 Mar 2022 12:48:16 +0100 Subject: [PATCH 08/12] fix: cleanups --- electron/commands.ts | 2 +- .../ModalConfirmWithNamespaceSelect.tsx | 2 +- .../molecules/ResourceDiff/ResourceDiff.tsx | 29 +++++++------- .../organisms/ActionsPane/ActionsPane.tsx | 39 +++++++++---------- src/redux/thunks/previewKustomization.ts | 1 - 5 files changed, 33 insertions(+), 40 deletions(-) diff --git a/electron/commands.ts b/electron/commands.ts index f8159899b5..bc5e6f685e 100644 --- a/electron/commands.ts +++ b/electron/commands.ts @@ -132,7 +132,7 @@ export const interpolateTemplate = (args: InterpolateTemplateOptions, event: Ele }; /** - * called by thunk to preview a helm chart with values file + * called by the renderer thread to run a command and capture its output */ export const runCommand = (options: CommandOptions, event: Electron.IpcMainEvent) => { diff --git a/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx b/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx index 76a263a784..8193ac2fdc 100644 --- a/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx +++ b/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx @@ -97,7 +97,7 @@ const ModalConfirmWithNamespaceSelect: React.FC = props => { } else if (selectedOption === 'none') { onOk(); } - }, [kubeConfigContext, createNamespaceName, kubeConfigPath, selectedNamespace, selectedOption, onOk, configState]); + }, [createNamespaceName, kubeConfigPath, selectedNamespace, selectedOption, onOk, configState]); useEffect(() => { if (defaultOption && defaultOption === 'none') { diff --git a/src/components/molecules/ResourceDiff/ResourceDiff.tsx b/src/components/molecules/ResourceDiff/ResourceDiff.tsx index 3a108ca316..f0c9b6baa5 100644 --- a/src/components/molecules/ResourceDiff/ResourceDiff.tsx +++ b/src/components/molecules/ResourceDiff/ResourceDiff.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useState} from 'react'; +import {useMemo, useState} from 'react'; import {MonacoDiffEditor} from 'react-monaco-editor'; import {useMeasure} from 'react-use'; @@ -161,21 +161,18 @@ const ResourceDiff = (props: { ); }; - const onClickApplyResource = useCallback( - (namespace?: {name: string; new: boolean}) => { - if (onApply) { - onApply(); - } - - applyResource(localResource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { - isClusterPreview: previewType === 'cluster', - shouldPerformDiff: true, - isInClusterDiff, - }); - setIsApplyModalVisible(false); - }, - [resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, isInClusterDiff, localResource] - ); + const onClickApplyResource = (namespace?: {name: string; new: boolean}) => { + if (onApply) { + onApply(); + } + + applyResource(localResource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { + isClusterPreview: previewType === 'cluster', + shouldPerformDiff: true, + isInClusterDiff, + }); + setIsApplyModalVisible(false); + }; return ( <> diff --git a/src/components/organisms/ActionsPane/ActionsPane.tsx b/src/components/organisms/ActionsPane/ActionsPane.tsx index 3a41df18ab..99db23677a 100644 --- a/src/components/organisms/ActionsPane/ActionsPane.tsx +++ b/src/components/organisms/ActionsPane/ActionsPane.tsx @@ -273,28 +273,25 @@ const ActionsPane: React.FC = props => { setIsApplyModalVisible(false); }; - const onClickApplyHelmChart = useCallback( - (namespace?: string, shouldCreateNamespace?: boolean) => { - if (!selectedValuesFileId) { - setIsHelmChartApplyModalVisible(false); - return; - } - - const helmValuesFile = helmValuesMap[selectedValuesFileId]; - applyHelmChart( - helmValuesFile, - helmChartMap[helmValuesFile.helmChartId], - fileMap, - dispatch, - kubeConfigPath, - kubeConfigContext, - namespace, - shouldCreateNamespace - ); + const onClickApplyHelmChart = (namespace?: string, shouldCreateNamespace?: boolean) => { + if (!selectedValuesFileId) { setIsHelmChartApplyModalVisible(false); - }, - [dispatch, fileMap, helmChartMap, helmValuesMap, kubeConfigPath, kubeConfigContext, selectedValuesFileId] - ); + return; + } + + const helmValuesFile = helmValuesMap[selectedValuesFileId]; + applyHelmChart( + helmValuesFile, + helmChartMap[helmValuesFile.helmChartId], + fileMap, + dispatch, + kubeConfigPath, + kubeConfigContext, + namespace, + shouldCreateNamespace + ); + setIsHelmChartApplyModalVisible(false); + }; const confirmModalTitle = useMemo(() => { if (!selectedResource) { diff --git a/src/redux/thunks/previewKustomization.ts b/src/redux/thunks/previewKustomization.ts index 78311f29fb..40371c4af5 100644 --- a/src/redux/thunks/previewKustomization.ts +++ b/src/redux/thunks/previewKustomization.ts @@ -16,7 +16,6 @@ import {createPreviewResult, createRejectionWithAlert} from '@redux/thunks/utils import {CommandResult, runCommandInMainThread} from '@utils/command'; import {DO_KUSTOMIZE_PREVIEW, trackEvent} from '@utils/telemetry'; - /** * Thunk to preview kustomizations */ From 0ff7014077e20e7a3f76ad07e9823cee847b8cc6 Mon Sep 17 00:00:00 2001 From: olensmar Date: Sun, 6 Mar 2022 16:35:37 +0100 Subject: [PATCH 09/12] fix: more cleanups --- .../ModalConfirmWithNamespaceSelect.tsx | 6 +- .../organisms/ActionsPane/ActionsPane.tsx | 78 +++++++++---------- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx b/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx index 8193ac2fdc..70b7a0995d 100644 --- a/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx +++ b/src/components/molecules/ModalConfirmWithNamespaceSelect/ModalConfirmWithNamespaceSelect.tsx @@ -11,7 +11,6 @@ import styled from 'styled-components'; import {K8sResource} from '@models/k8sresource'; import {useAppSelector} from '@redux/hooks'; -import {kubeConfigContextSelector, kubeConfigPathSelector} from '@redux/selectors'; import {useTargetClusterNamespaces} from '@hooks/useTargetClusterNamespaces'; @@ -58,9 +57,6 @@ interface IProps { const ModalConfirmWithNamespaceSelect: React.FC = props => { const {isVisible, resources = [], title, onCancel, onOk} = props; - const kubeConfigPath = useAppSelector(kubeConfigPathSelector); - const kubeConfigContext = useAppSelector(kubeConfigContextSelector); - const configState = useAppSelector(state => state.config); const {defaultNamespace, defaultOption} = getDefaultNamespaceForApply(resources); const [namespaces] = useTargetClusterNamespaces(); @@ -97,7 +93,7 @@ const ModalConfirmWithNamespaceSelect: React.FC = props => { } else if (selectedOption === 'none') { onOk(); } - }, [createNamespaceName, kubeConfigPath, selectedNamespace, selectedOption, onOk, configState]); + }, [createNamespaceName, selectedNamespace, selectedOption, onOk, configState]); useEffect(() => { if (defaultOption && defaultOption === 'none') { diff --git a/src/components/organisms/ActionsPane/ActionsPane.tsx b/src/components/organisms/ActionsPane/ActionsPane.tsx index 99db23677a..8e54e97e0a 100644 --- a/src/components/organisms/ActionsPane/ActionsPane.tsx +++ b/src/components/organisms/ActionsPane/ActionsPane.tsx @@ -19,9 +19,7 @@ import {makeApplyKustomizationText, makeApplyResourceText} from '@constants/make import { ApplyFileTooltip, ApplyTooltip, - DeployKustomizationTooltip, DiffTooltip, - InstallValuesFileTooltip, OpenExternalDocumentationTooltip, OpenHelmChartDocumentationTooltip, OpenKustomizeDocumentationTooltip, @@ -44,7 +42,7 @@ import { } from '@redux/selectors'; import {applyFileWithConfirm} from '@redux/services/applyFileWithConfirm'; import {getResourcesForPath} from '@redux/services/fileEntry'; -import {isHelmChartFile, isHelmValuesFile} from '@redux/services/helm'; +import {isHelmChartFile} from '@redux/services/helm'; import {isKustomizationPatch, isKustomizationResource} from '@redux/services/kustomize'; import {isUnsavedResource} from '@redux/services/resource'; import {getResourceSchema, getSchemaForPath, getUiSchemaForPath} from '@redux/services/schema'; @@ -261,37 +259,43 @@ const ActionsPane: React.FC = props => { return false; }, [selectedResource, knownResourceKinds]); - const onClickApplyResource = (namespace?: {name: string; new: boolean}) => { - if (!selectedResource) { + const onClickApplyResource = useCallback( + (namespace?: {name: string; new: boolean}) => { + if (!selectedResource) { + setIsApplyModalVisible(false); + return; + } + const isClusterPreview = previewType === 'cluster'; + applyResource(selectedResource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { + isClusterPreview, + }); setIsApplyModalVisible(false); - return; - } - const isClusterPreview = previewType === 'cluster'; - applyResource(selectedResource.id, resourceMap, fileMap, dispatch, projectConfig, kubeConfigContext, namespace, { - isClusterPreview, - }); - setIsApplyModalVisible(false); - }; + }, + [dispatch, fileMap, kubeConfigContext, kubeConfigPath, projectConfig, previewType, resourceMap, selectedResource] + ); - const onClickApplyHelmChart = (namespace?: string, shouldCreateNamespace?: boolean) => { - if (!selectedValuesFileId) { - setIsHelmChartApplyModalVisible(false); - return; - } + const onClickApplyHelmChart = useCallback( + (namespace?: string, shouldCreateNamespace?: boolean) => { + if (!selectedValuesFileId) { + setIsHelmChartApplyModalVisible(false); + return; + } - const helmValuesFile = helmValuesMap[selectedValuesFileId]; - applyHelmChart( - helmValuesFile, - helmChartMap[helmValuesFile.helmChartId], - fileMap, - dispatch, - kubeConfigPath, - kubeConfigContext, - namespace, - shouldCreateNamespace - ); - setIsHelmChartApplyModalVisible(false); - }; + const helmValuesFile = helmValuesMap[selectedValuesFileId]; + applyHelmChart( + helmValuesFile, + helmChartMap[helmValuesFile.helmChartId], + fileMap, + dispatch, + kubeConfigPath, + kubeConfigContext, + namespace, + shouldCreateNamespace + ); + setIsHelmChartApplyModalVisible(false); + }, + [dispatch, fileMap, helmChartMap, helmValuesMap, kubeConfigPath, kubeConfigContext, selectedValuesFileId] + ); const confirmModalTitle = useMemo(() => { if (!selectedResource) { @@ -400,15 +404,7 @@ const ActionsPane: React.FC = props => { From 9dd24594368a2f23ddbf22ceef1b47c54bf95cc2 Mon Sep 17 00:00:00 2001 From: olensmar Date: Sun, 6 Mar 2022 16:41:07 +0100 Subject: [PATCH 10/12] fix: improved Deploy/Install button --- .../organisms/ActionsPane/ActionsPane.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/organisms/ActionsPane/ActionsPane.tsx b/src/components/organisms/ActionsPane/ActionsPane.tsx index 8e54e97e0a..a867f6962e 100644 --- a/src/components/organisms/ActionsPane/ActionsPane.tsx +++ b/src/components/organisms/ActionsPane/ActionsPane.tsx @@ -20,6 +20,7 @@ import { ApplyFileTooltip, ApplyTooltip, DiffTooltip, + InstallValuesFileTooltip, OpenExternalDocumentationTooltip, OpenHelmChartDocumentationTooltip, OpenKustomizeDocumentationTooltip, @@ -42,7 +43,7 @@ import { } from '@redux/selectors'; import {applyFileWithConfirm} from '@redux/services/applyFileWithConfirm'; import {getResourcesForPath} from '@redux/services/fileEntry'; -import {isHelmChartFile} from '@redux/services/helm'; +import {isHelmChartFile, isHelmValuesFile} from '@redux/services/helm'; import {isKustomizationPatch, isKustomizationResource} from '@redux/services/kustomize'; import {isUnsavedResource} from '@redux/services/resource'; import {getResourceSchema, getSchemaForPath, getUiSchemaForPath} from '@redux/services/schema'; @@ -404,7 +405,13 @@ const ActionsPane: React.FC = props => { From 372a401949cde973bbca24adb2f75bee015aed9e Mon Sep 17 00:00:00 2001 From: olensmar Date: Tue, 8 Mar 2022 20:41:29 +0100 Subject: [PATCH 11/12] fix: merged main into branch --- .../organisms/ActionsPane/ActionsPane.tsx | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/src/components/organisms/ActionsPane/ActionsPane.tsx b/src/components/organisms/ActionsPane/ActionsPane.tsx index 0fe8f9f713..abc675dd3b 100644 --- a/src/components/organisms/ActionsPane/ActionsPane.tsx +++ b/src/components/organisms/ActionsPane/ActionsPane.tsx @@ -451,47 +451,48 @@ const ActionsPane: React.FC = props => { )} - - - - - - Diff - - - - + + + + + Diff + + + + + )} {!selectedPreviewConfigurationId ? ( From d3ed55e4fb883ab7efc9406c9206033513233101 Mon Sep 17 00:00:00 2001 From: olensmar Date: Tue, 8 Mar 2022 21:28:43 +0100 Subject: [PATCH 12/12] fix: use runCommandInMainThread to run preview configurations + electron update --- package.json | 2 +- .../PreviewConfigurationDetails.tsx | 4 ++-- src/redux/thunks/runPreviewConfiguration.ts | 17 ++++++++--------- src/utils/helm.ts | 18 ++---------------- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index b06a5829e4..fec24a0e0f 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "craco-alias": "3.0.1", "craco-less": "1.20.0", "cross-env": "7.0.3", - "electron": "17.1.0", + "electron": "17.1.1", "electron-builder": "23.0.0-alpha.3", "electron-notarize": "1.1.1", "electron-reload": "2.0.0-alpha.1", diff --git a/src/components/molecules/PreviewConfigurationDetails/PreviewConfigurationDetails.tsx b/src/components/molecules/PreviewConfigurationDetails/PreviewConfigurationDetails.tsx index 10923697ee..7304f07e81 100644 --- a/src/components/molecules/PreviewConfigurationDetails/PreviewConfigurationDetails.tsx +++ b/src/components/molecules/PreviewConfigurationDetails/PreviewConfigurationDetails.tsx @@ -43,7 +43,7 @@ const PreviwConfigurationDetails: React.FC = () => { const builtCommand = useMemo(() => { if (!previewConfiguration || !helmChart) { - return ''; + return ['']; } return buildHelmCommand( helmChart, @@ -72,7 +72,7 @@ const PreviwConfigurationDetails: React.FC = () => { )} - {builtCommand} + {builtCommand.join(' ')} ); diff --git a/src/redux/thunks/runPreviewConfiguration.ts b/src/redux/thunks/runPreviewConfiguration.ts index fb7b25fe8f..c5e3c92634 100644 --- a/src/redux/thunks/runPreviewConfiguration.ts +++ b/src/redux/thunks/runPreviewConfiguration.ts @@ -1,7 +1,6 @@ import {createAsyncThunk} from '@reduxjs/toolkit'; import fs from 'fs'; -import log from 'loglevel'; import path from 'path'; import {ROOT_FILE_ENTRY} from '@constants/constants'; @@ -13,7 +12,8 @@ import {RootState} from '@models/rootstate'; import {SetPreviewDataPayload} from '@redux/reducers/main'; import {createPreviewResult, createRejectionWithAlert} from '@redux/thunks/utils'; -import {buildHelmCommand, runHelm} from '@utils/helm'; +import {CommandOptions, runCommandInMainThread} from '@utils/command'; +import {buildHelmCommand} from '@utils/helm'; /** * Thunk to preview a Helm Chart @@ -89,9 +89,7 @@ export const runPreviewConfiguration = createAsyncThunk< ); } - log.info(`Running the following Preview Configuration: ${previewConfiguration.id}`); - - const helmCommand = buildHelmCommand( + const args = buildHelmCommand( chart, previewConfiguration.orderedValuesFilePaths, previewConfiguration.command, @@ -100,12 +98,13 @@ export const runPreviewConfiguration = createAsyncThunk< currentContext ); - const args = { - helmCommand, - kubeconfig, + const commandOptions: CommandOptions = { + cmd: 'helm', + args: args.splice(1), + env: {KUBECONFIG: kubeconfig}, }; - const result = await runHelm(args); + const result = await runCommandInMainThread(commandOptions); if (result.error) { return createRejectionWithAlert(thunkAPI, 'Helm Error', result.error); diff --git a/src/utils/helm.ts b/src/utils/helm.ts index 84b3dc35cb..0cca4e8122 100644 --- a/src/utils/helm.ts +++ b/src/utils/helm.ts @@ -1,21 +1,7 @@ -import {ipcRenderer} from 'electron'; - import path from 'path'; import {HelmChart} from '@models/helm'; -/** - * Invokes Helm in main thread - */ -export function runHelm(cmd: any): any { - return new Promise(resolve => { - ipcRenderer.once('helm-result', (event, arg) => { - resolve(arg); - }); - ipcRenderer.send('run-helm', cmd); - }); -} - export function buildHelmCommand( helmChart: HelmChart, valuesFilePaths: string[], @@ -23,7 +9,7 @@ export function buildHelmCommand( options: Record, rootFolderPath: string, clusterContext?: string -): string { +): string[] { const chartFolderPath = path.join(rootFolderPath, path.dirname(helmChart.filePath)); const args = [ @@ -44,5 +30,5 @@ export function buildHelmCommand( args.push('--dry-run'); } - return args.join(' '); + return args; }