@spinnaker/core 2026.2.3 → 2026.2.4
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.
- package/dist/index.js +34 -34
- package/dist/index.js.map +1 -1
- package/dist/pipeline/config/stages/evaluateVariables/EvaluateVariablesStageConfig.d.ts +1 -1
- package/dist/pipeline/config/stages/evaluateVariables/ExpressionCapabilitiesReader.d.ts +8 -0
- package/package.json +2 -2
- package/src/pipeline/config/stages/evaluateVariables/EvaluateVariablesStageConfig.spec.ts +70 -0
- package/src/pipeline/config/stages/evaluateVariables/EvaluateVariablesStageConfig.tsx +22 -5
- package/src/pipeline/config/stages/evaluateVariables/ExpressionCapabilitiesReader.ts +13 -0
- package/src/pipeline/config/triggers/artifacts/helm-image/HelmImageArtifactEditor.tsx +58 -142
|
@@ -6,5 +6,5 @@ export interface IEvaluatedVariable {
|
|
|
6
6
|
key: string;
|
|
7
7
|
value: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function validateEvaluateVariablesStage(stage: IStage): any;
|
|
9
|
+
export declare function validateEvaluateVariablesStage(stage: IStage, dashedIdentifiersEnabled?: boolean): any;
|
|
10
10
|
export declare function EvaluateVariablesStageConfig(props: IStageConfigProps): JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface IExpressionCapabilities {
|
|
2
|
+
functions: unknown[];
|
|
3
|
+
spelEvaluators: unknown[];
|
|
4
|
+
dashedIdentifiersEnabled: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class ExpressionCapabilitiesReader {
|
|
7
|
+
static getExpressionCapabilities(): PromiseLike<IExpressionCapabilities>;
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "https://github.com/spinnaker/spinnaker.git"
|
|
6
6
|
},
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
|
-
"version": "2026.2.
|
|
8
|
+
"version": "2026.2.4",
|
|
9
9
|
"module": "dist/index.js",
|
|
10
10
|
"typings": "dist/index.d.ts",
|
|
11
11
|
"publishConfig": {
|
|
@@ -133,5 +133,5 @@
|
|
|
133
133
|
"shx": "0.3.3",
|
|
134
134
|
"typescript": "5.0.4"
|
|
135
135
|
},
|
|
136
|
-
"gitHead": "
|
|
136
|
+
"gitHead": "f71c76e3c4b7791db81f01212e2487f1b637484c"
|
|
137
137
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { validateEvaluateVariablesStage } from './EvaluateVariablesStageConfig';
|
|
2
|
+
import type { IStage } from '../../../../domain';
|
|
3
|
+
|
|
4
|
+
function buildStage(variables: Array<{ key: string; value: string }>): IStage {
|
|
5
|
+
return ({ variables } as unknown) as IStage;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('validateEvaluateVariablesStage', () => {
|
|
9
|
+
describe('when dashed identifier support is disabled (default)', () => {
|
|
10
|
+
it('rejects a variable name containing a hyphen', () => {
|
|
11
|
+
const stage = buildStage([{ key: 'my-container-name', value: '${1}' }]);
|
|
12
|
+
|
|
13
|
+
const result = validateEvaluateVariablesStage(stage);
|
|
14
|
+
|
|
15
|
+
expect(result.variables[0].key).toEqual(
|
|
16
|
+
'Error: Variable Name should consist only of letters, numbers, or underscore',
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('accepts a variable name using only letters, numbers, and underscore', () => {
|
|
21
|
+
const stage = buildStage([{ key: 'my_container_name1', value: '${1}' }]);
|
|
22
|
+
|
|
23
|
+
const result = validateEvaluateVariablesStage(stage);
|
|
24
|
+
|
|
25
|
+
expect(result.variables).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when dashed identifier support is enabled', () => {
|
|
30
|
+
it('accepts a variable name containing a hyphen', () => {
|
|
31
|
+
const stage = buildStage([{ key: 'my-container-name', value: '${1}' }]);
|
|
32
|
+
|
|
33
|
+
const result = validateEvaluateVariablesStage(stage, true);
|
|
34
|
+
|
|
35
|
+
expect(result.variables).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('still rejects a variable name starting with a digit', () => {
|
|
39
|
+
const stage = buildStage([{ key: '1-my-container-name', value: '${1}' }]);
|
|
40
|
+
|
|
41
|
+
const result = validateEvaluateVariablesStage(stage, true);
|
|
42
|
+
|
|
43
|
+
expect(result.variables[0].key).toEqual(
|
|
44
|
+
'Error: Variable Name should consist only of letters, numbers, underscore, or hyphen',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('still rejects a variable name containing a space or other invalid character', () => {
|
|
49
|
+
const stage = buildStage([{ key: 'my container name', value: '${1}' }]);
|
|
50
|
+
|
|
51
|
+
const result = validateEvaluateVariablesStage(stage, true);
|
|
52
|
+
|
|
53
|
+
expect(result.variables[0].key).toEqual(
|
|
54
|
+
'Error: Variable Name should consist only of letters, numbers, underscore, or hyphen',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('still enforces the duplicate key validator regardless of dashed identifier support', () => {
|
|
60
|
+
const stage = buildStage([
|
|
61
|
+
{ key: 'my-name', value: '${1}' },
|
|
62
|
+
{ key: 'my-name', value: '${2}' },
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const result = validateEvaluateVariablesStage(stage, true);
|
|
66
|
+
|
|
67
|
+
expect(result.variables[0].key).toEqual("Duplicate key 'my-name'");
|
|
68
|
+
expect(result.variables[1].key).toEqual("Duplicate key 'my-name'");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -4,6 +4,7 @@ import React from 'react';
|
|
|
4
4
|
|
|
5
5
|
import type { IExecutionAndStagePickerProps } from './ExecutionAndStagePicker';
|
|
6
6
|
import { ExecutionAndStagePicker } from './ExecutionAndStagePicker';
|
|
7
|
+
import { ExpressionCapabilitiesReader } from './ExpressionCapabilitiesReader';
|
|
7
8
|
import type { IFormikStageConfigInjectedProps } from '../FormikStageConfig';
|
|
8
9
|
import { FormikStageConfig } from '../FormikStageConfig';
|
|
9
10
|
import type { IStageConfigProps } from '../common';
|
|
@@ -31,9 +32,14 @@ export interface IEvaluatedVariable {
|
|
|
31
32
|
value: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const createVariableNameValidator = (dashedIdentifiersEnabled: boolean): IValidator => {
|
|
36
|
+
const pattern = dashedIdentifiersEnabled ? /^[a-zA-Z_][a-zA-Z0-9_-]*$/ : /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
37
|
+
const allowedCharacters = dashedIdentifiersEnabled
|
|
38
|
+
? 'letters, numbers, underscore, or hyphen'
|
|
39
|
+
: 'letters, numbers, or underscore';
|
|
40
|
+
return (val: string, label: string) =>
|
|
41
|
+
!val.match(pattern) && errorMessage(`${label} should consist only of ${allowedCharacters}`);
|
|
42
|
+
};
|
|
37
43
|
|
|
38
44
|
const duplicateKeyValidatorFactory = (variables: IEvaluatedVariable[] = []) => {
|
|
39
45
|
const keyCounts = countBy(
|
|
@@ -43,9 +49,10 @@ const duplicateKeyValidatorFactory = (variables: IEvaluatedVariable[] = []) => {
|
|
|
43
49
|
return (key: string) => keyCounts[key] > 1 && `Duplicate key '${key}'`;
|
|
44
50
|
};
|
|
45
51
|
|
|
46
|
-
export function validateEvaluateVariablesStage(stage: IStage) {
|
|
52
|
+
export function validateEvaluateVariablesStage(stage: IStage, dashedIdentifiersEnabled = false) {
|
|
47
53
|
const formValidator = new FormValidator(stage);
|
|
48
54
|
const duplicateKeyValidator = duplicateKeyValidatorFactory(stage.variables);
|
|
55
|
+
const variableNameValidator = createVariableNameValidator(dashedIdentifiersEnabled);
|
|
49
56
|
formValidator.field('variables').withValidators(
|
|
50
57
|
formValidator.arrayForEach((item) => {
|
|
51
58
|
item.field('key', 'Variable Name').required().withValidators(variableNameValidator, duplicateKeyValidator);
|
|
@@ -75,6 +82,14 @@ function PreviewConfiguration(props: IExecutionAndStagePickerProps) {
|
|
|
75
82
|
export function EvaluateVariablesStageConfig(props: IStageConfigProps) {
|
|
76
83
|
const { application, stage, pipeline, updateStage } = props;
|
|
77
84
|
const [chosenStage, setChosenStage] = React.useState({} as IStageForSpelPreview);
|
|
85
|
+
const [dashedIdentifiersEnabled, setDashedIdentifiersEnabled] = React.useState(false);
|
|
86
|
+
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
ExpressionCapabilitiesReader.getExpressionCapabilities().then(
|
|
89
|
+
(capabilities) => setDashedIdentifiersEnabled(capabilities.dashedIdentifiersEnabled),
|
|
90
|
+
() => setDashedIdentifiersEnabled(false),
|
|
91
|
+
);
|
|
92
|
+
}, []);
|
|
78
93
|
|
|
79
94
|
const helpMessage =
|
|
80
95
|
'Define one or more variables by assigning a **name** _(string)_ and a **value** ' +
|
|
@@ -86,7 +101,9 @@ export function EvaluateVariablesStageConfig(props: IStageConfigProps) {
|
|
|
86
101
|
application={application}
|
|
87
102
|
stage={stage}
|
|
88
103
|
pipeline={pipeline}
|
|
89
|
-
validate={
|
|
104
|
+
validate={(stageToValidate) =>
|
|
105
|
+
validateEvaluateVariablesStage(stageToValidate as IStage, dashedIdentifiersEnabled)
|
|
106
|
+
}
|
|
90
107
|
onChange={updateStage}
|
|
91
108
|
render={(renderProps) => {
|
|
92
109
|
return (
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { REST } from '../../../../api';
|
|
2
|
+
|
|
3
|
+
export interface IExpressionCapabilities {
|
|
4
|
+
functions: unknown[];
|
|
5
|
+
spelEvaluators: unknown[];
|
|
6
|
+
dashedIdentifiersEnabled: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ExpressionCapabilitiesReader {
|
|
10
|
+
public static getExpressionCapabilities(): PromiseLike<IExpressionCapabilities> {
|
|
11
|
+
return REST('/capabilities/expressions').useCache(true).get();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,169 +1,85 @@
|
|
|
1
1
|
import { cloneDeep } from 'lodash';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type { Option } from 'react-select';
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { ArtifactEditor } from '../ArtifactEditor';
|
|
6
5
|
import { ArtifactTypePatterns } from '../../../../../artifact/ArtifactTypes';
|
|
7
6
|
import type { IArtifact, IArtifactEditorProps, IArtifactKindConfig } from '../../../../../domain';
|
|
8
|
-
import {
|
|
7
|
+
import { singleFieldArtifactEditor } from '../singleFieldArtifactEditor';
|
|
9
8
|
import { StageConfigField } from '../../../stages/common';
|
|
10
|
-
import {
|
|
9
|
+
import { SpelText } from '../../../../../widgets/spelText/SpelText';
|
|
11
10
|
|
|
12
11
|
const TYPE = 'helm/image';
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
namesLoading: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
class HelmImageEditor extends React.Component<IArtifactEditorProps, IHelmImageArtifactEditorState> {
|
|
22
|
-
public state: IHelmImageArtifactEditorState = {
|
|
23
|
-
names: [],
|
|
24
|
-
versions: [],
|
|
25
|
-
versionsLoading: true,
|
|
26
|
-
namesLoading: true,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// taken from https://github.com/semver/semver/issues/232
|
|
30
|
-
private SEMVER = new RegExp(
|
|
31
|
-
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/,
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
constructor(props: IArtifactEditorProps) {
|
|
35
|
-
super(props);
|
|
36
|
-
const { artifact } = this.props;
|
|
37
|
-
if (artifact.type !== TYPE) {
|
|
38
|
-
const clonedArtifact = cloneDeep(artifact);
|
|
39
|
-
clonedArtifact.type = TYPE;
|
|
40
|
-
props.onChange(clonedArtifact);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
ArtifactService.getArtifactNames(TYPE, this.props.account.name).then(
|
|
44
|
-
(names) => {
|
|
45
|
-
this.setState({
|
|
46
|
-
names,
|
|
47
|
-
namesLoading: false,
|
|
48
|
-
});
|
|
49
|
-
},
|
|
50
|
-
() => {
|
|
51
|
-
this.setState({
|
|
52
|
-
names: [],
|
|
53
|
-
namesLoading: false,
|
|
54
|
-
versionsLoading: false,
|
|
55
|
-
versions: [],
|
|
56
|
-
});
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public componentDidMount() {
|
|
62
|
-
const { artifact } = this.props;
|
|
63
|
-
if (artifact.name) {
|
|
64
|
-
this.getChartVersionOptions(artifact.name);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
public componentDidUpdate(prevProps: IArtifactEditorProps) {
|
|
69
|
-
if (this.props.account.name !== prevProps.account.name) {
|
|
70
|
-
ArtifactService.getArtifactNames(TYPE, this.props.account.name).then(
|
|
71
|
-
(names) => {
|
|
72
|
-
this.setState({
|
|
73
|
-
names,
|
|
74
|
-
namesLoading: false,
|
|
75
|
-
versions: [],
|
|
76
|
-
});
|
|
77
|
-
},
|
|
78
|
-
() => {
|
|
79
|
-
this.setState({
|
|
80
|
-
names: [],
|
|
81
|
-
namesLoading: false,
|
|
82
|
-
versions: [],
|
|
83
|
-
});
|
|
84
|
-
},
|
|
85
|
-
);
|
|
86
|
-
}
|
|
13
|
+
const setNameAndVersionFromReference = (artifact: IArtifact) => {
|
|
14
|
+
const ref = artifact.reference;
|
|
15
|
+
if (ref == null) {
|
|
16
|
+
return artifact;
|
|
87
17
|
}
|
|
88
18
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const nameOptions = this.state.names.map((name) => ({ value: name, label: name }));
|
|
19
|
+
const atIndex = ref.indexOf('@');
|
|
20
|
+
const lastColonIndex = ref.lastIndexOf(':');
|
|
92
21
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.getChartVersionOptions(e.value.toString());
|
|
103
|
-
}}
|
|
104
|
-
clearable={false}
|
|
105
|
-
/>
|
|
106
|
-
)}
|
|
107
|
-
{this.state.namesLoading && <Spinner />}
|
|
108
|
-
</StageConfigField>
|
|
109
|
-
<StageConfigField label="Version">
|
|
110
|
-
{!this.state.versionsLoading && (
|
|
111
|
-
<TetheredCreatable
|
|
112
|
-
options={this.state.versions}
|
|
113
|
-
value={artifact.version || ''}
|
|
114
|
-
onChange={(e: Option) => {
|
|
115
|
-
this.onChange(e, 'version');
|
|
116
|
-
}}
|
|
117
|
-
clearable={false}
|
|
118
|
-
/>
|
|
119
|
-
)}
|
|
120
|
-
{this.state.versionsLoading && <Spinner />}
|
|
121
|
-
</StageConfigField>
|
|
122
|
-
</>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private onChange = (e: Option, field: keyof IArtifact) => {
|
|
127
|
-
const clone = cloneDeep(this.props.artifact);
|
|
128
|
-
(clone[field] as any) = e.value.toString();
|
|
129
|
-
this.props.onChange(clone);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
private getChartVersionOptions(chartName: string) {
|
|
133
|
-
const { artifact, account } = this.props;
|
|
134
|
-
this.setState({ versionsLoading: true });
|
|
135
|
-
ArtifactService.getArtifactVersions(TYPE, account.name, chartName).then((versions: string[]) => {
|
|
136
|
-
// if the version doesn't match SEMVER we assume that it's a regular expression or SpEL expression
|
|
137
|
-
// and add it to the list of valid versions
|
|
138
|
-
if (artifact.version && !this.SEMVER.test(artifact.version)) {
|
|
139
|
-
versions = versions.concat(artifact.version);
|
|
140
|
-
}
|
|
141
|
-
this.setState({
|
|
142
|
-
versions: versions.map((v) => ({ label: v, value: v })),
|
|
143
|
-
versionsLoading: false,
|
|
144
|
-
});
|
|
145
|
-
});
|
|
22
|
+
if (atIndex >= 0) {
|
|
23
|
+
const split = ref.split('@');
|
|
24
|
+
artifact.name = split[0];
|
|
25
|
+
artifact.version = split[1];
|
|
26
|
+
} else if (lastColonIndex > 0) {
|
|
27
|
+
artifact.name = ref.substring(0, lastColonIndex);
|
|
28
|
+
artifact.version = ref.substring(lastColonIndex + 1);
|
|
29
|
+
} else {
|
|
30
|
+
artifact.name = ref;
|
|
146
31
|
}
|
|
147
|
-
|
|
32
|
+
return artifact;
|
|
33
|
+
};
|
|
148
34
|
|
|
149
35
|
export const HelmImageMatch: IArtifactKindConfig = {
|
|
150
|
-
label: '
|
|
36
|
+
label: 'Docker Image (OCI)',
|
|
151
37
|
typePattern: ArtifactTypePatterns.HELM_IMAGE,
|
|
152
38
|
type: TYPE,
|
|
153
39
|
isDefault: false,
|
|
154
40
|
isMatch: true,
|
|
155
|
-
description: 'A
|
|
156
|
-
key: 'helm',
|
|
157
|
-
editCmp:
|
|
41
|
+
description: 'A Docker image stored in an OCI registry.',
|
|
42
|
+
key: 'helm-image',
|
|
43
|
+
editCmp: singleFieldArtifactEditor(
|
|
44
|
+
'name',
|
|
45
|
+
TYPE,
|
|
46
|
+
'Docker image',
|
|
47
|
+
'gcr.io/project/image',
|
|
48
|
+
'pipeline.config.expectedArtifact.docker.name',
|
|
49
|
+
),
|
|
158
50
|
};
|
|
159
51
|
|
|
160
52
|
export const HelmImageDefault: IArtifactKindConfig = {
|
|
161
|
-
label: '
|
|
53
|
+
label: 'Docker Image (OCI)',
|
|
162
54
|
typePattern: ArtifactTypePatterns.HELM_IMAGE,
|
|
163
55
|
type: TYPE,
|
|
164
56
|
isDefault: true,
|
|
165
57
|
isMatch: false,
|
|
166
|
-
description: 'A
|
|
167
|
-
key: 'default.helm',
|
|
168
|
-
editCmp:
|
|
58
|
+
description: 'A Docker image stored in an OCI registry.',
|
|
59
|
+
key: 'default.helm-image',
|
|
60
|
+
editCmp: class extends ArtifactEditor {
|
|
61
|
+
constructor(props: IArtifactEditorProps) {
|
|
62
|
+
super(props, TYPE);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private onReferenceChanged = (reference: string) => {
|
|
66
|
+
const clonedArtifact = cloneDeep(this.props.artifact);
|
|
67
|
+
clonedArtifact.reference = reference;
|
|
68
|
+
this.props.onChange(setNameAndVersionFromReference(clonedArtifact));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
public render() {
|
|
72
|
+
return (
|
|
73
|
+
<StageConfigField label="Object path" helpKey="pipeline.config.expectedArtifact.defaultDocker.reference">
|
|
74
|
+
<SpelText
|
|
75
|
+
placeholder="gcr.io/project/image@sha256:9efcc2818c9..."
|
|
76
|
+
value={this.props.artifact.reference}
|
|
77
|
+
onChange={this.onReferenceChanged}
|
|
78
|
+
pipeline={this.props.pipeline}
|
|
79
|
+
docLink={true}
|
|
80
|
+
/>
|
|
81
|
+
</StageConfigField>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
169
85
|
};
|