Skip to content

Commit

Permalink
Merge pull request #4156 from kubeshop/devcatalin/feat/install-helm-c…
Browse files Browse the repository at this point in the history
…onfig

feat: install action for helm configurations
  • Loading branch information
devcatalin authored Jul 26, 2023
2 parents 9e3c629 + 70e6959 commit 2413e07
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import {Breadcrumb, Typography} from 'antd';

import {sortBy} from 'lodash';

import {kubeConfigContextSelector} from '@redux/appConfig';
import {useAppSelector} from '@redux/hooks';

import {buildHelmCommand} from '@utils/helm';
import {buildHelmConfigCommand} from '@utils/helm';

import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry';
import {PreviewConfigValuesFileItem} from '@shared/models/config';
Expand All @@ -17,7 +16,6 @@ import * as S from './PreviewConfigurationDetails.styled';
const {Text} = Typography;

const PreviwConfigurationDetails: React.FC = () => {
const currentContext = useAppSelector(kubeConfigContextSelector);
const previewConfigurationMap = useAppSelector(state => state.config.projectConfig?.helm?.previewConfigurationMap);
const rootFolderPath = useAppSelector(state => state.main.fileMap[ROOT_FILE_ENTRY].filePath);
const selectedPreviewConfigurationId = useAppSelector(state =>
Expand Down Expand Up @@ -59,15 +57,14 @@ const PreviwConfigurationDetails: React.FC = () => {
return [''];
}

return buildHelmCommand(
return buildHelmConfigCommand(
helmChart,
orderedValuesFilePaths,
previewConfiguration.command,
previewConfiguration.options,
rootFolderPath,
currentContext
rootFolderPath
);
}, [previewConfiguration, helmChart, currentContext, rootFolderPath, orderedValuesFilePaths]);
}, [previewConfiguration, helmChart, rootFolderPath, orderedValuesFilePaths]);

if (!previewConfiguration || !helmChart) {
return (
Expand Down
28 changes: 26 additions & 2 deletions src/components/organisms/ActionsPane/ActionsPaneHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {useCallback, useMemo} from 'react';

import {Button, Dropdown, Tooltip} from 'antd';
import {Button, Dropdown, Modal, Tooltip} from 'antd';

import {LeftOutlined, RightOutlined} from '@ant-design/icons';

import {PANE_CONSTRAINT_VALUES, TOOLTIP_DELAY} from '@constants/constants';
import {
EditPreviewConfigurationTooltip,
InstallPreviewConfigurationTooltip,
RunPreviewConfigurationTooltip,
SaveTransientResourceTooltip,
} from '@constants/tooltips';
Expand All @@ -16,6 +17,7 @@ import {openPreviewConfigurationEditor} from '@redux/reducers/main';
import {openSaveResourcesToFileFolderModal} from '@redux/reducers/ui';
import {selectedHelmConfigSelector, selectedImageSelector} from '@redux/selectors';
import {startPreview} from '@redux/thunks/preview';
import {runPreviewConfiguration} from '@redux/thunks/runPreviewConfiguration';
import {selectFromHistory} from '@redux/thunks/selectFromHistory';

import {TitleBarWrapper} from '@components/atoms';
Expand Down Expand Up @@ -72,6 +74,19 @@ const ActionsPaneHeader: React.FC<IProps> = props => {
dispatch(startPreview({type: 'helm-config', configId: selectedHelmConfig.id}));
}, [dispatch, selectedHelmConfig]);

const onClickInstallPreviewConfiguration = useCallback(() => {
Modal.confirm({
title: 'Install Helm Chart',
content: `Are you sure you want to install the ${selectedHelmConfig?.name} configuration to the cluster?`,
onOk: () => {
if (!selectedHelmConfig) {
return;
}
dispatch(runPreviewConfiguration({helmConfigId: selectedHelmConfig.id, performDeploy: true}));
},
});
}, [dispatch, selectedHelmConfig]);

const onClickLeftArrow = useCallback(() => {
dispatch(selectFromHistory('left'));
}, [dispatch]);
Expand Down Expand Up @@ -125,9 +140,18 @@ const ActionsPaneHeader: React.FC<IProps> = props => {
type="secondary"
actions={
<div style={{display: 'flex', justifyContent: 'flex-end', gap: '10px'}}>
<Tooltip
mouseEnterDelay={TOOLTIP_DELAY}
title={InstallPreviewConfigurationTooltip}
placement="bottomLeft"
>
<Button type="primary" size="small" ghost onClick={onClickInstallPreviewConfiguration}>
Install
</Button>
</Tooltip>
<Tooltip mouseEnterDelay={TOOLTIP_DELAY} title={RunPreviewConfigurationTooltip} placement="bottomLeft">
<Button type="primary" size="small" ghost onClick={onClickRunPreviewConfiguration}>
Preview
Dry-run
</Button>
</Tooltip>
<Tooltip mouseEnterDelay={TOOLTIP_DELAY} title={EditPreviewConfigurationTooltip} placement="bottomLeft">
Expand Down
1 change: 1 addition & 0 deletions src/constants/tooltips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const ProjectManagementTooltip = 'Select and manage your projects';
export const ReloadHelmPreviewTooltip = 'Reload the Helm Chart preview with this values file';
export const ReloadKustomizationPreviewTooltip = 'Reload the preview of this Kustomization';
export const RunPreviewConfigurationTooltip = 'Run this Dry-run Configuration';
export const InstallPreviewConfigurationTooltip = 'Install this Dry-run Configuration to the cluster.';
export const SaveTransientResourceTooltip = 'Save resource to file/folder';
export const SearchProjectTooltip = 'Search for project by name or path';
export const TelemetryDocumentationUrl = 'https://kubeshop.github.io/monokle/telemetry';
Expand Down
10 changes: 9 additions & 1 deletion src/redux/reducers/main/previewReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,16 @@ export const previewExtraReducers = createSliceExtraReducers('main', builder =>
.addCase(previewHelmValuesFile.rejected, onPreviewRejected);

builder
.addCase(runPreviewConfiguration.pending, onPreviewPending)
.addCase(runPreviewConfiguration.pending, (state, action) => {
if (action.meta.arg.performDeploy) {
return;
}
onPreviewPending(state);
})
.addCase(runPreviewConfiguration.fulfilled, (state, action) => {
if (!action.payload) {
return;
}
const initialSelection: PreviewConfigurationSelection = {
type: 'preview.configuration',
previewConfigurationId: action.payload.preview.configId,
Expand Down
5 changes: 4 additions & 1 deletion src/redux/reducers/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,10 @@ export const uiSlice = createSlice({
.addCase(previewSavedCommand.fulfilled, state => {
state.navigator.collapsedResourceKinds = [];
})
.addCase(runPreviewConfiguration.fulfilled, state => {
.addCase(runPreviewConfiguration.fulfilled, (state, action) => {
if (action.meta.arg.performDeploy) {
return;
}
state.navigator.collapsedResourceKinds = [];
});
},
Expand Down
8 changes: 4 additions & 4 deletions src/redux/services/compare/fetchResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {createKubeClientWithSetup} from '@redux/cluster/service/kube-client';
import {getCommitResources} from '@redux/git/git.ipc';
import {runKustomize} from '@redux/thunks/preview';

import {buildHelmCommand, createHelmInstallCommand, createHelmTemplateCommand} from '@utils/helm';
import {buildHelmConfigCommand, createHelmInstallCommand, createHelmTemplateCommand} from '@utils/helm';

import {ERROR_MSG_FALLBACK} from '@shared/constants/constants';
import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry';
Expand Down Expand Up @@ -189,6 +189,7 @@ async function previewHelmResources(state: RootState, options: HelmResourceSet):
values: path.join(folder, valuesFile.name),
name: folder,
chart: chart.name,
dryRun: true,
},
{
KUBECONFIG: kubeconfig.path,
Expand Down Expand Up @@ -255,13 +256,12 @@ async function previewCustomHelmResources(

checkAllFilesExist(orderedValuesFilePaths, rootFolder);

const args = buildHelmCommand(
const args = buildHelmConfigCommand(
chart,
orderedValuesFilePaths,
helmConfig.command,
helmConfig.options,
rootFolder,
currentContext
rootFolder
);

const command: CommandOptions = {
Expand Down
2 changes: 1 addition & 1 deletion src/redux/thunks/preview/startPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const startPreview = createAsyncThunk<void, AnyPreview, {dispatch: AppDis
thunkAPI.dispatch(previewHelmValuesFile(preview.valuesFileId));
}
if (preview.type === 'helm-config') {
thunkAPI.dispatch(runPreviewConfiguration(preview.configId));
thunkAPI.dispatch(runPreviewConfiguration({helmConfigId: preview.configId}));
}
if (preview.type === 'command') {
thunkAPI.dispatch(previewSavedCommand(preview.commandId));
Expand Down
46 changes: 31 additions & 15 deletions src/redux/thunks/runPreviewConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {sortBy} from 'lodash';
import path from 'path';
import {v4 as uuid} from 'uuid';

import {setAlert} from '@redux/reducers/alert';
import {extractK8sResources} from '@redux/services/resource';
import {createRejectionWithAlert} from '@redux/thunks/utils';

import {buildHelmCommand} from '@utils/helm';
import {buildHelmConfigCommand} from '@utils/helm';

import {ROOT_FILE_ENTRY} from '@shared/constants/fileEntry';
import {AlertEnum} from '@shared/models/alert';
import {AppDispatch} from '@shared/models/appDispatch';
import {CommandOptions} from '@shared/models/commands';
import {HelmPreviewConfiguration, PreviewConfigValuesFileItem} from '@shared/models/config';
Expand All @@ -26,16 +28,20 @@ import {trackEvent} from '@shared/utils/telemetry';
*/

export const runPreviewConfiguration = createAsyncThunk<
| {
resources: K8sResource<'preview'>[];
preview: HelmConfigPreview;
}
| undefined,
{
resources: K8sResource<'preview'>[];
preview: HelmConfigPreview;
helmConfigId: string;
performDeploy?: boolean;
},
string,
{
dispatch: AppDispatch;
state: RootState;
}
>('main/runPreviewConfiguration', async (previewConfigurationId, thunkAPI) => {
>('main/runPreviewConfiguration', async ({helmConfigId, performDeploy}, thunkAPI) => {
const startTime = new Date().getTime();
const configState = thunkAPI.getState().config;
const mainState = thunkAPI.getState().main;
Expand All @@ -45,23 +51,22 @@ export const runPreviewConfiguration = createAsyncThunk<
if (!kubeconfig?.isValid) {
return createRejectionWithAlert(
thunkAPI,
'Dry-run Configuration Error',
'Helm Configuration Error',
`Could not preview due to invalid kubeconfig`
);
}

const currentContext = kubeconfig.currentContext;
const rootFolderPath = mainState.fileMap[ROOT_FILE_ENTRY].filePath;

let previewConfiguration: HelmPreviewConfiguration | null | undefined;
if (previewConfigurationMap) {
previewConfiguration = previewConfigurationMap[previewConfigurationId];
previewConfiguration = previewConfigurationMap[helmConfigId];
}
if (!previewConfiguration) {
return createRejectionWithAlert(
thunkAPI,
'Dry-run Configuration Error',
`Could not find the Dry-run Configuration with id ${previewConfigurationId}`
'Helm Configuration Error',
`Could not find the Helm Configuration with id ${helmConfigId}`
);
}

Expand Down Expand Up @@ -110,15 +115,15 @@ export const runPreviewConfiguration = createAsyncThunk<
);
}

trackEvent('preview/helm_config/start');
trackEvent('preview/helm_config/start', {isInstall: Boolean(performDeploy)});

const args = buildHelmCommand(
const args = buildHelmConfigCommand(
chart,
orderedValuesFilePaths,
previewConfiguration.command,
previewConfiguration.options,
rootFolderPath,
currentContext
performDeploy
);

const commandOptions: CommandOptions = {
Expand All @@ -136,6 +141,19 @@ export const runPreviewConfiguration = createAsyncThunk<
}

const endTime = new Date().getTime();
trackEvent('preview/helm_config/end', {executionTime: endTime - startTime});

if (performDeploy) {
thunkAPI.dispatch(
setAlert({
type: AlertEnum.Success,
title: 'Installed Helm Chart',
message: `Successfully installed the ${chart.name} Helm Chart using the ${previewConfiguration.name} configuration!`,
})
);
// If we are performing a deploy, we don't want to return any resources or preview
return;
}

if (result.stdout) {
const preview: HelmConfigPreview = {type: 'helm-config', configId: previewConfiguration.id};
Expand All @@ -152,8 +170,6 @@ export const runPreviewConfiguration = createAsyncThunk<
};
}

trackEvent('preview/helm_config/end', {executionTime: endTime - startTime});

return createRejectionWithAlert(
thunkAPI,
'Helm Error',
Expand Down
8 changes: 8 additions & 0 deletions src/redux/validation/validation.listeners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ const validateListener: AppListenerFn = listen => {
await delay(1);
if (signal.aborted) return;

if (isAnyOf(runPreviewConfiguration.fulfilled, runPreviewConfiguration.rejected)(_action)) {
// if runPreviewConfiguration was used to perform a deploy and not a preview
// then, we don't have to revalidate
if (_action.meta.arg.performDeploy) {
return;
}
}

let resourceStorage: ResourceStorage | undefined;

if (isAnyOf(loadClusterResources.fulfilled, reloadClusterResources.fulfilled)(_action)) {
Expand Down
1 change: 1 addition & 0 deletions src/shared/models/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type HelmInstallArgs = {
values: string;
name: string;
chart: string;
dryRun?: boolean;
};

export type HelmTemplateArgs = {
Expand Down
5 changes: 4 additions & 1 deletion src/shared/models/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export type EventMap = {
'preview/helm/start': undefined;
'preview/helm/fail': {reason: string};
'preview/helm/end': {resourcesCount?: number; executionTime: number};
'preview/helm_config/start': undefined;
'preview/helm_config/start': {isInstall: boolean};
'preview/helm_config/fail': {reason: string};
'preview/helm_config/end': {resourcesCount?: number; executionTime: number};
'preview/kustomize/start': undefined;
Expand All @@ -115,6 +115,9 @@ export type EventMap = {
'helm/command/start': {command: string[]};
'helm/command/fail': {reason: string};
'helm/command/end': undefined;
'helm/config/install/start': undefined;
'helm/config/install/end': undefined;
'helm/config/install/fail': {reason: string};
'cluster/diff_resource': undefined;
'cluster/deploy_resource': {kind: string};
'cluster/deploy_file': undefined;
Expand Down
22 changes: 13 additions & 9 deletions src/utils/helm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import {getHelmClusterArgs} from '@utils/cluster';
import {CommandOptions, HelmEnv, HelmInstallArgs, HelmTemplateArgs} from '@shared/models/commands';
import {HelmChart} from '@shared/models/helm';

export function buildHelmCommand(
export function buildHelmConfigCommand(
helmChart: HelmChart,
valuesFilePaths: string[],
command: 'template' | 'install',
helmCommand: 'template' | 'install',
options: Record<string, string | null>,
rootFolderPath: string,
clusterContext?: string
performDeploy?: boolean
): string[] {
let chartFolderPath = join(rootFolderPath, dirname(helmChart.filePath));
let command = performDeploy ? 'install' : helmCommand;

if (chartFolderPath.endsWith(sep)) {
chartFolderPath = chartFolderPath.slice(0, -1);
Expand All @@ -38,10 +39,7 @@ export function buildHelmCommand(
);
}

if (command === 'install') {
if (clusterContext) {
args.splice(1, 0, ...['--kube-context', clusterContext]);
}
if (!performDeploy && command === 'install') {
args.push('--dry-run');
}

Expand All @@ -51,15 +49,21 @@ export function buildHelmCommand(
return args;
}

export function createHelmInstallCommand({values, name, chart}: HelmInstallArgs, env?: HelmEnv): CommandOptions {
export function createHelmInstallCommand(
{values, name, chart, dryRun}: HelmInstallArgs,
env?: HelmEnv
): CommandOptions {
const clusterArgs = getHelmClusterArgs();

const command = {
commandId: uuid(),
cmd: 'helm',
args: ['install', ...clusterArgs, '-f', `"${values}"`, chart, `"${name}"`, '--dry-run'],
args: ['install', ...clusterArgs, '-f', `"${values}"`, chart, `"${name}"`],
env,
};
if (dryRun) {
command.args.push('--dry-run');
}
log.debug('createHelmInstallCommand', command);
return command;
}
Expand Down

0 comments on commit 2413e07

Please sign in to comment.