@spinnaker/core 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/config/settings.d.ts +1 -0
  3. package/dist/domain/IArtifact.d.ts +2 -0
  4. package/dist/domain/ITrigger.d.ts +5 -0
  5. package/dist/index.js +36 -36
  6. package/dist/index.js.map +1 -1
  7. package/dist/manifest/ManifestYaml.d.ts +10 -4
  8. package/dist/pipeline/config/stages/bakeManifest/ManifestRenderers.d.ts +2 -0
  9. package/dist/pipeline/config/stages/bakeManifest/helmfile/BakeHelmfileConfigForm.d.ts +18 -0
  10. package/dist/pipeline/config/stages/bakeManifest/utils/getBakedArtifacts.d.ts +4 -0
  11. package/dist/pipeline/config/stages/bakeManifest/utils/getContentReference.d.ts +1 -0
  12. package/dist/pipeline/config/triggers/artifacts/ArtifactService.d.ts +3 -0
  13. package/dist/pipeline/config/triggers/cdevents/CDEventsTrigger.d.ts +7 -0
  14. package/dist/pipeline/config/triggers/cdevents/cdevents.trigger.d.ts +1 -0
  15. package/dist/pipeline/config/triggers/index.d.ts +1 -0
  16. package/dist/pipeline/index.d.ts +2 -0
  17. package/dist/presentation/forms/inputs/NumberConcurrencyInput.d.ts +7 -0
  18. package/package.json +2 -2
  19. package/src/artifact/ArtifactIconService.ts +1 -0
  20. package/src/artifact/ArtifactTypes.ts +1 -0
  21. package/src/config/settings.ts +1 -0
  22. package/src/domain/IArtifact.ts +3 -0
  23. package/src/domain/ITrigger.ts +4 -0
  24. package/src/help/help.contents.ts +10 -0
  25. package/src/manifest/ManifestYaml.tsx +29 -7
  26. package/src/pipeline/config/stages/bakeManifest/BakeManifestConfig.tsx +4 -1
  27. package/src/pipeline/config/stages/bakeManifest/BakeManifestDetailsTab.tsx +24 -12
  28. package/src/pipeline/config/stages/bakeManifest/BakeManifestStageForm.tsx +9 -2
  29. package/src/pipeline/config/stages/bakeManifest/ManifestRenderers.ts +2 -0
  30. package/src/pipeline/config/stages/bakeManifest/helmfile/BakeHelmfileConfigForm.spec.tsx +132 -0
  31. package/src/pipeline/config/stages/bakeManifest/helmfile/BakeHelmfileConfigForm.tsx +271 -0
  32. package/src/pipeline/config/stages/bakeManifest/utils/getBakedArtifacts.ts +13 -0
  33. package/src/pipeline/config/stages/bakeManifest/utils/getContentReference.ts +3 -0
  34. package/src/pipeline/config/triggers/artifacts/ArtifactService.ts +4 -0
  35. package/src/pipeline/config/triggers/cdevents/CDEventsTrigger.tsx +48 -0
  36. package/src/pipeline/config/triggers/cdevents/cdevents.trigger.ts +19 -0
  37. package/src/pipeline/config/triggers/index.ts +1 -0
  38. package/src/pipeline/create/CreatePipelineModal.tsx +1 -1
  39. package/src/pipeline/index.ts +2 -0
  40. package/src/presentation/forms/inputs/NumberConcurrencyInput.tsx +29 -0
@@ -0,0 +1,271 @@
1
+ import React from 'react';
2
+
3
+ import type { IFormikStageConfigInjectedProps } from '../../FormikStageConfig';
4
+ import { AccountService } from '../../../../../account';
5
+ import {
6
+ ArtifactTypePatterns,
7
+ excludeAllTypesExcept,
8
+ ExpectedArtifactService,
9
+ StageArtifactSelectorDelegate,
10
+ } from '../../../../../artifact';
11
+ import { StageConfigField } from '../../common/stageConfigField/StageConfigField';
12
+ import type { IArtifact, IExpectedArtifact } from '../../../../../domain';
13
+ import { MapEditor } from '../../../../../forms';
14
+ import { CheckboxInput, TextInput } from '../../../../../presentation';
15
+
16
+ export interface IBakeHelmfileConfigFormState {
17
+ gitRepoArtifactAccountNames: string[];
18
+ }
19
+
20
+ export class BakeHelmfileConfigForm extends React.Component<
21
+ IFormikStageConfigInjectedProps,
22
+ IBakeHelmfileConfigFormState
23
+ > {
24
+ constructor(props: IFormikStageConfigInjectedProps) {
25
+ super(props);
26
+ this.state = { gitRepoArtifactAccountNames: [] };
27
+ }
28
+
29
+ private static readonly excludedArtifactTypes = excludeAllTypesExcept(
30
+ ArtifactTypePatterns.BITBUCKET_FILE,
31
+ ArtifactTypePatterns.CUSTOM_OBJECT,
32
+ ArtifactTypePatterns.EMBEDDED_BASE64,
33
+ ArtifactTypePatterns.GCS_OBJECT,
34
+ ArtifactTypePatterns.GIT_REPO,
35
+ ArtifactTypePatterns.GITHUB_FILE,
36
+ ArtifactTypePatterns.GITLAB_FILE,
37
+ ArtifactTypePatterns.S3_OBJECT,
38
+ ArtifactTypePatterns.HELM_CHART,
39
+ ArtifactTypePatterns.HTTP_FILE,
40
+ ArtifactTypePatterns.ORACLE_OBJECT,
41
+ );
42
+
43
+ public componentDidMount() {
44
+ const stage = this.props.formik.values;
45
+ if (stage.inputArtifacts && stage.inputArtifacts.length === 0) {
46
+ this.props.formik.setFieldValue('inputArtifacts', [
47
+ {
48
+ account: '',
49
+ id: '',
50
+ },
51
+ ]);
52
+ }
53
+
54
+ // If the Expected Artifact id is provided but the account is not, then attempt to find the artifact from
55
+ // upstream stages and set the account value.
56
+ // This is needed because helmfile file path field will need to be rendered if the artifact has a git repo account type
57
+ const expectedArtifact = this.getInputArtifact(stage, 0);
58
+ if (expectedArtifact.id && !expectedArtifact.account) {
59
+ const availableArtifacts = ExpectedArtifactService.getExpectedArtifactsAvailableToStage(
60
+ stage,
61
+ this.props.pipeline,
62
+ );
63
+ const expectedMatchedArtifact = availableArtifacts.find((a) => a.id === expectedArtifact.id);
64
+ if (expectedMatchedArtifact && expectedMatchedArtifact.matchArtifact) {
65
+ this.props.formik.setFieldValue(
66
+ `inputArtifacts[0].account`,
67
+ expectedMatchedArtifact.matchArtifact.artifactAccount,
68
+ );
69
+ }
70
+ }
71
+
72
+ AccountService.getArtifactAccounts().then((artifactAccounts) => {
73
+ this.setState({
74
+ gitRepoArtifactAccountNames: artifactAccounts
75
+ .filter((account) => account.types.some((type) => ArtifactTypePatterns.GIT_REPO.test(type)))
76
+ .map((account) => account.name),
77
+ });
78
+ });
79
+ }
80
+
81
+ private onTemplateArtifactEdited = (artifact: IArtifact, index: number) => {
82
+ this.props.formik.setFieldValue(`inputArtifacts[${index}].id`, null);
83
+ this.props.formik.setFieldValue(`inputArtifacts[${index}].artifact`, artifact);
84
+ this.props.formik.setFieldValue(`inputArtifacts[${index}].account`, artifact.artifactAccount);
85
+ };
86
+
87
+ private onTemplateArtifactSelected = (artifact: IExpectedArtifact, index: number) => {
88
+ this.props.formik.setFieldValue(`inputArtifacts[${index}].id`, artifact.id);
89
+ this.props.formik.setFieldValue(`inputArtifacts[${index}].artifact`, null);
90
+ // Set the account to matchArtifact.artifactAccount if it exists.
91
+ // This account value will be used to determine if the Helm Chart File Path should be displayed.
92
+ if (artifact.matchArtifact) {
93
+ this.props.formik.setFieldValue(`inputArtifacts[${index}].account`, artifact.matchArtifact.artifactAccount);
94
+ } else {
95
+ this.props.formik.setFieldValue(`inputArtifacts[${index}].account`, null);
96
+ }
97
+ };
98
+
99
+ private addInputArtifact = () => {
100
+ const stage = this.props.formik.values;
101
+ const newInputArtifacts = [
102
+ ...stage.inputArtifacts,
103
+ {
104
+ account: '',
105
+ id: '',
106
+ },
107
+ ];
108
+
109
+ this.props.formik.setFieldValue('inputArtifacts', newInputArtifacts);
110
+ };
111
+
112
+ private removeInputArtifact = (index: number) => {
113
+ const stage = this.props.formik.values;
114
+ const newInputArtifacts = [...stage.inputArtifacts];
115
+ newInputArtifacts.splice(index, 1);
116
+ this.props.formik.setFieldValue('inputArtifacts', newInputArtifacts);
117
+ };
118
+
119
+ private getInputArtifact = (stage: any, index: number) => {
120
+ if (!stage.inputArtifacts || stage.inputArtifacts.length === 0) {
121
+ return {
122
+ account: '',
123
+ id: '',
124
+ };
125
+ } else {
126
+ return stage.inputArtifacts[index];
127
+ }
128
+ };
129
+
130
+ private outputNameChange = (outputName: string) => {
131
+ const stage = this.props.formik.values;
132
+ const expectedArtifacts = stage.expectedArtifacts;
133
+ if (
134
+ expectedArtifacts &&
135
+ expectedArtifacts.length === 1 &&
136
+ expectedArtifacts[0].matchArtifact &&
137
+ expectedArtifacts[0].matchArtifact.type === 'embedded/base64'
138
+ ) {
139
+ this.props.formik.setFieldValue('expectedArtifacts', [
140
+ {
141
+ ...expectedArtifacts[0],
142
+ matchArtifact: {
143
+ ...expectedArtifacts[0].matchArtifact,
144
+ name: outputName,
145
+ },
146
+ },
147
+ ]);
148
+ }
149
+ };
150
+
151
+ private overrideChanged = (overrides: any) => {
152
+ this.props.formik.setFieldValue('overrides', overrides);
153
+ };
154
+
155
+ public render() {
156
+ const stage = this.props.formik.values;
157
+ return (
158
+ <>
159
+ <h4>Helmfile Options</h4>
160
+ <StageConfigField fieldColumns={3} label={'Name'} helpKey="pipeline.config.bake.manifest.helmfile.name">
161
+ <TextInput
162
+ onChange={(e: React.ChangeEvent<any>) => {
163
+ this.props.formik.setFieldValue('outputName', e.target.value);
164
+ this.outputNameChange(e.target.value);
165
+ }}
166
+ value={stage.outputName}
167
+ />
168
+ </StageConfigField>
169
+ <h4>Template Artifact</h4>
170
+ <StageArtifactSelectorDelegate
171
+ artifact={this.getInputArtifact(stage, 0).artifact}
172
+ excludedArtifactTypePatterns={BakeHelmfileConfigForm.excludedArtifactTypes}
173
+ expectedArtifactId={this.getInputArtifact(stage, 0).id}
174
+ helpKey="pipeline.config.bake.manifest.expectedArtifact"
175
+ label="Expected Artifact"
176
+ onArtifactEdited={(artifact) => {
177
+ this.onTemplateArtifactEdited(artifact, 0);
178
+ }}
179
+ onExpectedArtifactSelected={(artifact: IExpectedArtifact) => this.onTemplateArtifactSelected(artifact, 0)}
180
+ pipeline={this.props.pipeline}
181
+ stage={stage}
182
+ />
183
+ {this.state.gitRepoArtifactAccountNames.includes(this.getInputArtifact(stage, 0).account) && (
184
+ <StageConfigField label="Helmfile File Path" helpKey="pipeline.config.bake.manifest.helmfile.filePath">
185
+ <TextInput
186
+ onChange={(e: React.ChangeEvent<any>) => {
187
+ this.props.formik.setFieldValue('helmfileFilePath', e.target.value);
188
+ }}
189
+ value={stage.helmfileFilePath}
190
+ />
191
+ </StageConfigField>
192
+ )}
193
+ <h4>Overrides</h4>
194
+ {stage.inputArtifacts && stage.inputArtifacts.length > 1 && (
195
+ <div className="row form-group">
196
+ {stage.inputArtifacts.slice(1).map((a: any, index: number) => {
197
+ return (
198
+ <div key={index}>
199
+ <div className="col-md-offset-1 col-md-9">
200
+ <StageArtifactSelectorDelegate
201
+ artifact={a.artifact}
202
+ excludedArtifactTypePatterns={[]}
203
+ expectedArtifactId={a.id}
204
+ label="Expected Artifact"
205
+ onArtifactEdited={(artifact) => {
206
+ this.onTemplateArtifactEdited(artifact, index + 1);
207
+ }}
208
+ onExpectedArtifactSelected={(artifact: IExpectedArtifact) =>
209
+ this.onTemplateArtifactSelected(artifact, index + 1)
210
+ }
211
+ pipeline={this.props.pipeline}
212
+ stage={stage}
213
+ />
214
+ </div>
215
+ <div className="col-md-1">
216
+ <div className="form-control-static">
217
+ <button onClick={() => this.removeInputArtifact(index + 1)}>
218
+ <span className="glyphicon glyphicon-trash" />
219
+ <span className="sr-only">Remove field</span>
220
+ </button>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ );
225
+ })}
226
+ </div>
227
+ )}
228
+ <StageConfigField fieldColumns={8} label={''}>
229
+ <button className="btn btn-block btn-sm add-new" onClick={() => this.addInputArtifact()}>
230
+ <span className="glyphicon glyphicon-plus-sign" />
231
+ Add value artifact
232
+ </button>
233
+ </StageConfigField>
234
+ <StageConfigField fieldColumns={6} label="Overrides">
235
+ {stage.overrides && (
236
+ <MapEditor
237
+ addButtonLabel={'Add override'}
238
+ model={stage.overrides}
239
+ allowEmpty={true}
240
+ onChange={(o: any) => this.overrideChanged(o)}
241
+ />
242
+ )}
243
+ </StageConfigField>
244
+ <StageConfigField
245
+ fieldColumns={6}
246
+ helpKey={'pipeline.config.bake.manifest.helm.includeCRDs'}
247
+ label="Include CRDs"
248
+ >
249
+ <CheckboxInput
250
+ value={stage.includeCRDs}
251
+ text={''}
252
+ onChange={() => this.props.formik.setFieldValue('includeCRDs', !stage.includeCRDs)}
253
+ />
254
+ </StageConfigField>
255
+ <StageConfigField
256
+ fieldColumns={6}
257
+ helpKey={'pipeline.config.bake.manifest.overrideExpressionEvaluation'}
258
+ label="Expression Evaluation"
259
+ >
260
+ <CheckboxInput
261
+ value={stage.evaluateOverrideExpressions}
262
+ text={'Evaluate SpEL expressions in overrides at bake time'}
263
+ onChange={() =>
264
+ this.props.formik.setFieldValue('evaluateOverrideExpressions', !stage.evaluateOverrideExpressions)
265
+ }
266
+ />
267
+ </StageConfigField>
268
+ </>
269
+ );
270
+ }
271
+ }
@@ -0,0 +1,13 @@
1
+ import type { IArtifact, IExecutionContext } from '../../../../../domain';
2
+ import { ARTIFACT_TYPE_EMBEDDED, ARTIFACT_TYPE_REMOTE } from '../../../../../domain';
3
+
4
+ // IArtifact type is wrong and does not represent the real value
5
+ export const getBakedArtifacts = (context: IExecutionContext): Array<IArtifact & { reference: string }> => {
6
+ if ('artifacts' in context) {
7
+ return context.artifacts.filter(
8
+ (a: IArtifact) => (a.type === ARTIFACT_TYPE_EMBEDDED || a.type === ARTIFACT_TYPE_REMOTE) && a.reference,
9
+ );
10
+ } else {
11
+ return [];
12
+ }
13
+ };
@@ -0,0 +1,3 @@
1
+ export const getContentReference = (uri: string): string => {
2
+ return uri.replace(/^ref?:\/\//, '');
3
+ };
@@ -11,4 +11,8 @@ export class ArtifactService {
11
11
  .query({ type: type, artifactName: artifactName })
12
12
  .get();
13
13
  }
14
+
15
+ public static getArtifactByContentReference(contentRef: string): PromiseLike<{ reference: string }> {
16
+ return REST(`/artifacts/content-address/${contentRef}`).get();
17
+ }
14
18
  }
@@ -0,0 +1,48 @@
1
+ import type { FormikProps } from 'formik';
2
+ import React from 'react';
3
+
4
+ import { SETTINGS } from '../../../../config/settings';
5
+ import type { ICDEventsTrigger } from '../../../../domain';
6
+ import { MapEditorInput } from '../../../../forms';
7
+ import { HelpField } from '../../../../help';
8
+ import { FormikFormField, TextInput } from '../../../../presentation';
9
+
10
+ export interface ICDEventsTriggerProps {
11
+ formik: FormikProps<ICDEventsTrigger>;
12
+ }
13
+
14
+ export function CDEventsTrigger(cdeventsTriggerProps: ICDEventsTriggerProps) {
15
+ const { formik } = cdeventsTriggerProps;
16
+ const trigger = formik.values;
17
+ const { source, type } = trigger;
18
+
19
+ return (
20
+ <>
21
+ <FormikFormField
22
+ name="source"
23
+ label="Source"
24
+ help={<HelpField id="pipeline.config.trigger.webhook.source" />}
25
+ input={(props) => (
26
+ <div className="flex-container-v">
27
+ <TextInput {...props} />
28
+ <i>{`${SETTINGS.gateUrl}/webhooks/${type}/${source || '<source>'}`}</i>
29
+ </div>
30
+ )}
31
+ />
32
+
33
+ <FormikFormField
34
+ name="payloadConstraints"
35
+ label="Payload Constraints"
36
+ help={<HelpField id="pipeline.config.trigger.webhook.payloadConstraints" />}
37
+ input={(props) => <MapEditorInput {...props} addButtonLabel="Add payload constraint" />}
38
+ />
39
+
40
+ <FormikFormField
41
+ name="attributeConstraints"
42
+ label="Attribute Constraints "
43
+ help={<HelpField id="pipeline.config.trigger.cdevents.attributeConstraints" />}
44
+ input={(props) => <MapEditorInput {...props} addButtonLabel="Add attribute constraint" />}
45
+ />
46
+ </>
47
+ );
48
+ }
@@ -0,0 +1,19 @@
1
+ import { CDEventsTrigger } from './CDEventsTrigger';
2
+ import { ArtifactTypePatterns } from '../../../../artifact';
3
+ import { Registry } from '../../../../registry';
4
+
5
+ Registry.pipeline.registerTrigger({
6
+ component: CDEventsTrigger,
7
+ description: 'Executes the pipeline when a CDEvents webhook is received.',
8
+ excludedArtifactTypePatterns: [ArtifactTypePatterns.JENKINS_FILE],
9
+ key: 'cdevents',
10
+ label: 'CDEvents',
11
+ validators: [
12
+ {
13
+ type: 'serviceAccountAccess',
14
+ message: `You do not have access to the service account configured in this pipeline's CDEvents trigger.
15
+ You will not be able to save your edits to this pipeline.`,
16
+ preventSave: true,
17
+ },
18
+ ],
19
+ });
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  import './artifactory/artifactory.trigger';
4
+ import './cdevents/cdevents.trigger';
4
5
  import './concourse/concourse.trigger';
5
6
  import './cron/cron.trigger';
6
7
  import './git/git.trigger';
@@ -129,7 +129,7 @@ export class CreatePipelineModal extends React.Component<ICreatePipelineModalPro
129
129
  ? this.getDefaultConfig()
130
130
  : command.config;
131
131
 
132
- pipelineConfig.name = command.name;
132
+ pipelineConfig.name = command.name.trim();
133
133
  pipelineConfig.index = this.props.application.getDataSource('pipelineConfigs').data.length;
134
134
  delete pipelineConfig.id;
135
135
 
@@ -23,3 +23,5 @@ export * from './manualExecution/TriggerTemplate';
23
23
  export * from './service/ExecutionsTransformer';
24
24
  export * from './service/execution.service';
25
25
  export * from './status/ArtifactList';
26
+ export * from './config/stages/bakeManifest/utils/getBakedArtifacts';
27
+ export * from './config/stages/bakeManifest/utils/getContentReference';
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+
3
+ import { useInternalValidator } from './hooks';
4
+ import type { IFormInputProps, OmitControlledInputPropsFrom } from './interface';
5
+ import { orEmptyString, validationClassName } from './utils';
6
+ import type { IValidator } from '../validation';
7
+ import { composeValidators, Validators } from '../validation';
8
+
9
+ interface INumberInputProps extends IFormInputProps, OmitControlledInputPropsFrom<React.InputHTMLAttributes<any>> {
10
+ inputClassName?: string;
11
+ }
12
+
13
+ const isNumber = (val: any): val is number => typeof val === 'number';
14
+
15
+ export function NumberConcurrencyInput(props: INumberInputProps) {
16
+ const { value, validation, inputClassName, ...otherProps } = props;
17
+
18
+ const minMaxValidator: IValidator = (val: any, label?: string) => {
19
+ const minValidator = isNumber(props.min) ? Validators.minValue(props.min) : undefined;
20
+ const maxValidator = isNumber(props.max) ? Validators.maxValue(props.max) : undefined;
21
+ const validator = composeValidators([minValidator, maxValidator]);
22
+ return validator ? validator(val, label) : null;
23
+ };
24
+
25
+ useInternalValidator(validation, minMaxValidator);
26
+
27
+ const className = `NumberInput form-control ${orEmptyString(inputClassName)} ${validationClassName(validation)}`;
28
+ return <input className={className} type="number" value={orEmptyString(value)} {...otherProps} />;
29
+ }