Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(stages/bakeManifests): add helmfile support #9998

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/core/src/help/help.contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ const helpContents: { [key: string]: string } = {
'pipeline.config.bake.manifest.kustomize.filePath': `
<p>This is the relative path to the kustomization.yaml file within your Git repo.</p>
<p>e.g.: <b>examples/wordpress/mysql/kustomization.yaml</b></p>`,
'pipeline.config.bake.manifest.helmfile.filePath': `
<p>This is the relative path to the directory containing the helmfile.yaml file within your Git repo.</p>
<p>e.g.: <b>chart/helmfile.yml</b></p>`,
'pipeline.config.bake.manifest.helmfile.name':
'<p> Name is used to set the expected artifact in the Produces Artifact section. </p>',
'pipeline.config.bake.cf.manifest.name':
'<p> Name should be the same as the expected artifact in the Produces Artifact section. </p>',
'pipeline.config.bake.cf.manifest.templateArtifact': `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react';

import { BakeManifestStageForm, validateProducedArtifacts } from './BakeManifestStageForm';
import { FormikStageConfig } from '../FormikStageConfig';
import { HELM_RENDERERS } from './ManifestRenderers';
import { HELM_RENDERERS, HELMFILE_RENDERER } from './ManifestRenderers';
import type { IStageConfigProps } from '../common';
import type { IExpectedArtifact, IStage } from '../../../../domain';
import { FormValidator } from '../../../../presentation';
Expand Down Expand Up @@ -46,6 +46,9 @@ export function validateBakeManifestStage(stage: IStage): FormikErrors<IStage> {
if (HELM_RENDERERS.includes(stage.templateRenderer)) {
formValidator.field('outputName', 'Name').required();
}
if (HELMFILE_RENDERER === stage.templateRenderer) {
formValidator.field('outputName', 'Name').required();
}

return formValidator.validateForm();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { isNil } from 'lodash';
import React from 'react';

import type { IFormikStageConfigInjectedProps } from '../FormikStageConfig';
import { HELM_RENDERERS, KUSTOMIZE_RENDERERS } from './ManifestRenderers';
import { HELM_RENDERERS, HELMFILE_RENDERER, KUSTOMIZE_RENDERERS } from './ManifestRenderers';
import { ExpectedArtifactService } from '../../../../artifact';
import { StageConfigField } from '../common';
import type { IExpectedArtifact } from '../../../../domain';
import { BakeHelmConfigForm } from './helm/BakeHelmConfigForm';
import { BakeHelmfileConfigForm } from './helmfile/BakeHelmfileConfigForm';
import { BakeKustomizeConfigForm } from './kustomize/BakeKustomizeConfigForm';
import { ReactSelectInput } from '../../../../presentation';
import { BASE_64_ARTIFACT_ACCOUNT, BASE_64_ARTIFACT_TYPE } from '../../triggers/artifacts/base64/Base64ArtifactEditor';
Expand All @@ -32,10 +33,13 @@ export function BakeManifestStageForm({ application, formik, pipeline }: IFormik
if (HELM_RENDERERS.includes(stage.templateRenderer) && !isNil(stage.inputArtifact)) {
formik.setFieldValue('inputArtifact', null);
}
if (HELMFILE_RENDERER === stage.templateRenderer && !isNil(stage.inputArtifact)) {
formik.setFieldValue('inputArtifact', null);
}
}, [stage.templateRenderer]);

const templateRenderers = React.useMemo(() => {
return [...KUSTOMIZE_RENDERERS, ...HELM_RENDERERS];
return [...KUSTOMIZE_RENDERERS, ...HELM_RENDERERS, HELMFILE_RENDERER];
}, []);

return (
Expand All @@ -62,6 +66,9 @@ export function BakeManifestStageForm({ application, formik, pipeline }: IFormik
{HELM_RENDERERS.includes(stage.templateRenderer) && (
<BakeHelmConfigForm pipeline={pipeline} application={application} formik={formik} />
)}
{HELMFILE_RENDERER === stage.templateRenderer && (
<BakeHelmfileConfigForm pipeline={pipeline} application={application} formik={formik} />
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export enum ManifestRenderers {
HELM2 = 'HELM2',
HELM3 = 'HELM3',
HELMFILE = 'HELMFILE',
KUSTOMIZE = 'KUSTOMIZE',
KUSTOMIZE4 = 'KUSTOMIZE4',
}

export const HELM_RENDERERS: Readonly<ManifestRenderers[]> = [ManifestRenderers.HELM2, ManifestRenderers.HELM3];
export const HELMFILE_RENDERER: Readonly<ManifestRenderers> = ManifestRenderers.HELMFILE;
export const KUSTOMIZE_RENDERERS: Readonly<ManifestRenderers[]> = [
ManifestRenderers.KUSTOMIZE,
ManifestRenderers.KUSTOMIZE4,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { mock } from 'angular';
import { mount } from 'enzyme';
import React from 'react';

import { StageConfigField } from '../../../..';
import { BakeHelmfileConfigForm } from './BakeHelmfileConfigForm';
import { AccountService } from '../../../../../account';
import { ApplicationModelBuilder } from '../../../../../application';
import { ExpectedArtifactService } from '../../../../../artifact';
import type { IExpectedArtifact, IStage } from '../../../../../domain';
import { SpinFormik } from '../../../../../presentation';
import { REACT_MODULE } from '../../../../../reactShims';

describe('<BakeHelmfileConfigForm />', () => {
beforeEach(mock.module(REACT_MODULE));
beforeEach(mock.inject());

const helmfileFilePathFieldName = 'Helmfile File Path';

const getProps = () => {
return {
application: ApplicationModelBuilder.createApplicationForTests('my-application'),
pipeline: {
application: 'my-application',
id: 'pipeline-id',
limitConcurrent: true,
keepWaitingPipelines: true,
name: 'My Pipeline',
parameterConfig: [],
stages: [],
triggers: [],
},
} as any;
};

beforeEach(() =>
spyOn(AccountService, 'getArtifactAccounts').and.returnValue(
Promise.resolve([
{ name: 'gitrepo', types: ['something-else', 'git/repo'] },
{ name: 'notgitrepo', types: ['something-else'] },
]),
),
);

it('renders the helmfile file path element when the template artifact is from an account that handles git/repo artifacts', async () => {
const stage = ({
inputArtifacts: [{ account: 'gitrepo' }],
} as unknown) as IStage;

const props = getProps();

const component = mount(
<SpinFormik
initialValues={stage}
onSubmit={() => null}
validate={() => null}
render={(formik) => <BakeHelmfileConfigForm {...props} formik={formik} />}
/>,
);

await new Promise((resolve) => setTimeout(resolve)); // wait one js tick for promise to resolve
component.setProps({}); // force a re-render

expect(component.find(StageConfigField).findWhere((x) => x.text() === helmfileFilePathFieldName).length).toBe(1);
});

it('does not render the helmfile file path element when the template artifact is from an account that does not handle git/repo artifacts', async () => {
const stage = ({
inputArtifacts: [{ account: 'notgitrepo' }],
} as unknown) as IStage;

const props = getProps();

const component = mount(
<SpinFormik
initialValues={stage}
onSubmit={() => null}
validate={() => null}
render={(formik) => <BakeHelmfileConfigForm {...props} formik={formik} />}
/>,
);

await new Promise((resolve) => setTimeout(resolve)); // wait one js tick for promise to resolve
component.setProps({}); // force a re-render

expect(component.find(StageConfigField).findWhere((x) => x.text() === helmfileFilePathFieldName).length).toBe(0);
});

it('render the helmfile file path if the id of the git artifact is given but the account value does not exist', async () => {
const expectedArtifactDisplayName = 'test-artifact';
const expectedArtifactId = 'test-artifact-id';
const expectedGitArtifact: IExpectedArtifact = {
defaultArtifact: {
customKind: true,
id: 'defaultArtifact-id',
},
displayName: expectedArtifactDisplayName,
id: expectedArtifactId,
matchArtifact: {
artifactAccount: 'gitrepo',
id: expectedArtifactId,
reference: 'git repo',
type: 'git/repo',
version: 'master',
},
useDefaultArtifact: false,
usePriorArtifact: false,
};
const stage = ({
inputArtifacts: [{ id: expectedArtifactId }],
} as unknown) as IStage;

spyOn(ExpectedArtifactService, 'getExpectedArtifactsAvailableToStage').and.returnValue([expectedGitArtifact]);

const props = getProps();

const component = mount(
<SpinFormik
initialValues={stage}
onSubmit={() => null}
validate={() => null}
render={(formik) => <BakeHelmfileConfigForm {...props} formik={formik} />}
/>,
);

await new Promise((resolve) => setTimeout(resolve)); // wait one js tick for promise to resolve
component.setProps({}); // force a re-render

expect(component.find('.Select-value-label > span').text().includes(expectedArtifactDisplayName)).toBe(true);
expect(component.find(StageConfigField).findWhere((x) => x.text() === helmfileFilePathFieldName).length).toBe(1);
});
});
Loading