@spinnaker/docker 2026.2.2 → 2026.3.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.
- package/dist/docker.module.d.ts +1 -1
- package/dist/image/DockerChartAndTagSelector.d.ts +1 -0
- package/dist/image/DockerImageAndTagSelector.d.ts +1 -0
- package/dist/image/DockerImageReader.d.ts +6 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/stages/bake/dockerBakeStage.d.ts +31 -2
- package/dist/pipeline/trigger/DockerTriggerTemplate.d.ts +2 -1
- package/package.json +2 -6
- package/src/docker.module.ts +1 -6
- package/src/image/DockerChartAndTagSelector.tsx +24 -12
- package/src/image/DockerImageAndTagSelector.spec.tsx +159 -0
- package/src/image/DockerImageAndTagSelector.tsx +24 -12
- package/src/image/DockerImageReader.spec.ts +21 -0
- package/src/image/DockerImageReader.ts +10 -6
- package/src/index.ts +2 -1
- package/src/pipeline/stages/bake/dockerBakeStage.spec.tsx +187 -0
- package/src/pipeline/stages/bake/dockerBakeStage.tsx +313 -0
- package/src/pipeline/trigger/DockerTrigger.tsx +2 -2
- package/src/pipeline/trigger/DockerTriggerTemplate.spec.tsx +124 -0
- package/src/pipeline/trigger/DockerTriggerTemplate.tsx +25 -13
- package/dist/pipeline/stages/bake/bakeExecutionDetails.controller.d.ts +0 -2
- package/src/pipeline/stages/bake/bakeExecutionDetails.controller.js +0 -36
- package/src/pipeline/stages/bake/bakeExecutionDetails.html +0 -47
- package/src/pipeline/stages/bake/bakeStage.html +0 -32
- package/src/pipeline/stages/bake/dockerBakeStage.js +0 -81
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { shallow } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
AuthenticationService,
|
|
6
|
+
BakeExecutionLabel,
|
|
7
|
+
BakeryReader,
|
|
8
|
+
ExecutionDetailsTasks,
|
|
9
|
+
Registry,
|
|
10
|
+
SETTINGS,
|
|
11
|
+
Spinner,
|
|
12
|
+
} from '@spinnaker/core';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
applyDockerBakeStageDefaults,
|
|
16
|
+
DockerBakeExecutionDetails,
|
|
17
|
+
DockerBakeStageConfig,
|
|
18
|
+
DOCKER_BAKE_STAGE_CONFIG,
|
|
19
|
+
} from './dockerBakeStage';
|
|
20
|
+
|
|
21
|
+
describe('Docker bake stage', () => {
|
|
22
|
+
let originalBakeryDetailUrl: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
originalBakeryDetailUrl = SETTINGS.bakeryDetailUrl;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
beforeEach(() => Registry.reinitialize());
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
SETTINGS.bakeryDetailUrl = originalBakeryDetailUrl;
|
|
31
|
+
Registry.reinitialize();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('registers the package-local React stage config', () => {
|
|
35
|
+
Registry.pipeline.registerStage(DOCKER_BAKE_STAGE_CONFIG);
|
|
36
|
+
|
|
37
|
+
const stageConfig = Registry.pipeline.getStageConfig({ type: 'bake', cloudProvider: 'docker' } as any);
|
|
38
|
+
|
|
39
|
+
expect(stageConfig.provides).toBe('bake');
|
|
40
|
+
expect(stageConfig.cloudProvider).toBe('docker');
|
|
41
|
+
expect(stageConfig.component).toBe(DockerBakeStageConfig);
|
|
42
|
+
expect(stageConfig.executionDetailsSections).toEqual([DockerBakeExecutionDetails, ExecutionDetailsTasks]);
|
|
43
|
+
expect(stageConfig.executionLabelComponent).toBe(BakeExecutionLabel);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('applies Docker bake defaults and removes empty string properties', () => {
|
|
47
|
+
const stage: any = {
|
|
48
|
+
package: 'my-package',
|
|
49
|
+
organization: '',
|
|
50
|
+
extendedAttributes: {},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const result = applyDockerBakeStageDefaults(stage, {
|
|
54
|
+
user: 'user@example.com',
|
|
55
|
+
baseOsOptions: [{ id: 'ubuntu' }, { id: 'debian' }],
|
|
56
|
+
baseLabelOptions: ['release', 'snapshot'],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual({
|
|
60
|
+
package: 'my-package',
|
|
61
|
+
extendedAttributes: {},
|
|
62
|
+
region: 'global',
|
|
63
|
+
user: 'user@example.com',
|
|
64
|
+
baseOs: 'ubuntu',
|
|
65
|
+
baseLabel: 'release',
|
|
66
|
+
});
|
|
67
|
+
expect(result).not.toBe(stage);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('keeps existing Docker bake values when defaults are available', () => {
|
|
71
|
+
const stage: any = {
|
|
72
|
+
region: 'custom-region',
|
|
73
|
+
user: 'existing-user',
|
|
74
|
+
baseOs: 'debian',
|
|
75
|
+
baseLabel: 'snapshot',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = applyDockerBakeStageDefaults(stage, {
|
|
79
|
+
user: 'user@example.com',
|
|
80
|
+
baseOsOptions: [{ id: 'ubuntu' }],
|
|
81
|
+
baseLabelOptions: ['release'],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result.region).toBe('custom-region');
|
|
85
|
+
expect(result.user).toBe('existing-user');
|
|
86
|
+
expect(result.baseOs).toBe('debian');
|
|
87
|
+
expect(result.baseLabel).toBe('snapshot');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('persists Docker bake defaults after loading options', async () => {
|
|
91
|
+
spyOn(AuthenticationService, 'getAuthenticatedUser').and.returnValue({ name: 'user@example.com' } as any);
|
|
92
|
+
spyOn(BakeryReader, 'getBaseOsOptions').and.returnValue(Promise.resolve({ baseImages: [{ id: 'ubuntu' }] } as any));
|
|
93
|
+
spyOn(BakeryReader, 'getBaseLabelOptions').and.returnValue(Promise.resolve(['release']));
|
|
94
|
+
|
|
95
|
+
const updateStage = jasmine.createSpy('updateStage');
|
|
96
|
+
|
|
97
|
+
shallow(
|
|
98
|
+
<DockerBakeStageConfig
|
|
99
|
+
application={{} as any}
|
|
100
|
+
pipeline={{} as any}
|
|
101
|
+
stage={{ package: 'my-package', organization: '' } as any}
|
|
102
|
+
stageFieldUpdated={jasmine.createSpy('stageFieldUpdated')}
|
|
103
|
+
updateStage={updateStage}
|
|
104
|
+
updateStageField={jasmine.createSpy('updateStageField')}
|
|
105
|
+
/>,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
109
|
+
|
|
110
|
+
expect(updateStage).toHaveBeenCalledWith({
|
|
111
|
+
package: 'my-package',
|
|
112
|
+
region: 'global',
|
|
113
|
+
user: 'user@example.com',
|
|
114
|
+
baseOs: 'ubuntu',
|
|
115
|
+
baseLabel: 'release',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('shows an error instead of a permanent spinner when bake options fail to load', async () => {
|
|
120
|
+
spyOn(BakeryReader, 'getBaseOsOptions').and.returnValue(Promise.reject(new Error('boom')));
|
|
121
|
+
spyOn(BakeryReader, 'getBaseLabelOptions').and.returnValue(Promise.resolve(['release']));
|
|
122
|
+
|
|
123
|
+
const wrapper = shallow(
|
|
124
|
+
<DockerBakeStageConfig
|
|
125
|
+
application={{} as any}
|
|
126
|
+
pipeline={{} as any}
|
|
127
|
+
stage={{ package: 'my-package' } as any}
|
|
128
|
+
stageFieldUpdated={jasmine.createSpy('stageFieldUpdated')}
|
|
129
|
+
updateStage={jasmine.createSpy('updateStage')}
|
|
130
|
+
updateStageField={jasmine.createSpy('updateStageField')}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
135
|
+
wrapper.update();
|
|
136
|
+
|
|
137
|
+
expect(wrapper.find(Spinner).exists()).toBe(false);
|
|
138
|
+
expect(wrapper.text()).toContain('Unable to load Docker bake options');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('does not update state after unmounting before bake options load', async () => {
|
|
142
|
+
let resolveBaseOsOptions: (value: any) => void;
|
|
143
|
+
let resolveBaseLabelOptions: (value: string[]) => void;
|
|
144
|
+
spyOn(BakeryReader, 'getBaseOsOptions').and.returnValue(new Promise((resolve) => (resolveBaseOsOptions = resolve)));
|
|
145
|
+
spyOn(BakeryReader, 'getBaseLabelOptions').and.returnValue(
|
|
146
|
+
new Promise((resolve) => (resolveBaseLabelOptions = resolve)),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const wrapper = shallow(
|
|
150
|
+
<DockerBakeStageConfig
|
|
151
|
+
application={{} as any}
|
|
152
|
+
pipeline={{} as any}
|
|
153
|
+
stage={{ package: 'my-package' } as any}
|
|
154
|
+
stageFieldUpdated={jasmine.createSpy('stageFieldUpdated')}
|
|
155
|
+
updateStage={jasmine.createSpy('updateStage')}
|
|
156
|
+
updateStageField={jasmine.createSpy('updateStageField')}
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
const setState = spyOn(wrapper.instance() as DockerBakeStageConfig, 'setState');
|
|
160
|
+
|
|
161
|
+
wrapper.unmount();
|
|
162
|
+
resolveBaseOsOptions!({ baseImages: [{ id: 'ubuntu' }] });
|
|
163
|
+
resolveBaseLabelOptions!(['release']);
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
165
|
+
|
|
166
|
+
expect(setState).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('replaces every bakery detail URL placeholder occurrence', () => {
|
|
170
|
+
SETTINGS.bakeryDetailUrl =
|
|
171
|
+
'/bakery/{{context.region}}/{{context.region}}/{{context.status.resourceId}}/{{context.status.resourceId}}';
|
|
172
|
+
|
|
173
|
+
const wrapper = shallow(
|
|
174
|
+
<DockerBakeExecutionDetails
|
|
175
|
+
current={true}
|
|
176
|
+
name="bakeConfig"
|
|
177
|
+
stage={
|
|
178
|
+
{
|
|
179
|
+
context: { region: 'us-west-2', status: { resourceId: 'image-123' } },
|
|
180
|
+
} as any
|
|
181
|
+
}
|
|
182
|
+
/>,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(wrapper.find('a').prop('href')).toBe('/bakery/us-west-2/us-west-2/image-123/image-123');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { isEqual } from 'lodash';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
IExecutionDetailsSectionProps,
|
|
6
|
+
IFormikStageConfigInjectedProps,
|
|
7
|
+
IStage,
|
|
8
|
+
IStageConfigProps,
|
|
9
|
+
} from '@spinnaker/core';
|
|
10
|
+
import {
|
|
11
|
+
AuthenticationService,
|
|
12
|
+
BakeExecutionLabel,
|
|
13
|
+
BakeryReader,
|
|
14
|
+
ExecutionDetailsSection,
|
|
15
|
+
ExecutionDetailsTasks,
|
|
16
|
+
FormikStageConfig,
|
|
17
|
+
Registry,
|
|
18
|
+
SETTINGS,
|
|
19
|
+
Spinner,
|
|
20
|
+
StageConfigField,
|
|
21
|
+
StageFailureMessage,
|
|
22
|
+
} from '@spinnaker/core';
|
|
23
|
+
|
|
24
|
+
interface IBaseOsOption {
|
|
25
|
+
id: string;
|
|
26
|
+
shortDescription?: string;
|
|
27
|
+
detailedDescription?: string;
|
|
28
|
+
isImageFamily?: boolean;
|
|
29
|
+
displayName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface IDockerBakeStageDefaults {
|
|
33
|
+
user: string;
|
|
34
|
+
baseOsOptions: IBaseOsOption[];
|
|
35
|
+
baseLabelOptions: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface IDockerBakeStageConfigState {
|
|
39
|
+
baseLabelOptions: string[];
|
|
40
|
+
baseOsOptions: IBaseOsOption[];
|
|
41
|
+
loadError: boolean;
|
|
42
|
+
loading: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function deleteEmptyProperties(stage: IStage): IStage {
|
|
46
|
+
return Object.keys(stage).reduce((acc, key) => {
|
|
47
|
+
if ((stage as any)[key] !== '') {
|
|
48
|
+
(acc as any)[key] = (stage as any)[key];
|
|
49
|
+
}
|
|
50
|
+
return acc;
|
|
51
|
+
}, {} as IStage);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function applyDockerBakeStageDefaults(stage: IStage, defaults: IDockerBakeStageDefaults): IStage {
|
|
55
|
+
const nextStage = deleteEmptyProperties({ ...stage });
|
|
56
|
+
|
|
57
|
+
nextStage.region = nextStage.region || 'global';
|
|
58
|
+
nextStage.user = nextStage.user || defaults.user;
|
|
59
|
+
|
|
60
|
+
if (!nextStage.baseOs && defaults.baseOsOptions?.length) {
|
|
61
|
+
nextStage.baseOs = defaults.baseOsOptions[0].id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!nextStage.baseLabel && defaults.baseLabelOptions?.length) {
|
|
65
|
+
nextStage.baseLabel = defaults.baseLabelOptions[0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return nextStage;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function baseOsDescription(baseOsOption: IBaseOsOption): string {
|
|
72
|
+
const baseOsName = baseOsOption?.displayName || baseOsOption?.id || '';
|
|
73
|
+
return baseOsOption?.shortDescription ? `${baseOsName} (${baseOsOption.shortDescription})` : baseOsName;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function bakeryDetailUrl(stage: IStage): string {
|
|
77
|
+
const context = stage.context || {};
|
|
78
|
+
const urlTemplate = SETTINGS.bakeryDetailUrl || '';
|
|
79
|
+
return urlTemplate
|
|
80
|
+
.replace(/\{\{context\.region\}\}/g, context.region || '')
|
|
81
|
+
.replace(/\{\{context\.status\.resourceId\}\}/g, context.status?.resourceId || '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function DockerBakeStageForm({
|
|
85
|
+
baseLabelOptions,
|
|
86
|
+
baseOsOptions,
|
|
87
|
+
formik,
|
|
88
|
+
}: IFormikStageConfigInjectedProps & IDockerBakeStageConfigState) {
|
|
89
|
+
const stage = formik.values;
|
|
90
|
+
const extendedAttributes = stage.extendedAttributes || {};
|
|
91
|
+
const setFieldValue = (field: string) => (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
92
|
+
formik.setFieldValue(field, event.target.value);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<StageConfigField label="Package" helpKey="pipeline.config.bake.package">
|
|
98
|
+
<input
|
|
99
|
+
type="text"
|
|
100
|
+
className="form-control input-sm"
|
|
101
|
+
value={stage.package || ''}
|
|
102
|
+
onChange={setFieldValue('package')}
|
|
103
|
+
/>
|
|
104
|
+
</StageConfigField>
|
|
105
|
+
<StageConfigField label="Organization" helpKey="pipeline.config.docker.bake.organization">
|
|
106
|
+
<input
|
|
107
|
+
type="text"
|
|
108
|
+
className="form-control input-sm"
|
|
109
|
+
value={stage.organization || ''}
|
|
110
|
+
onChange={setFieldValue('organization')}
|
|
111
|
+
/>
|
|
112
|
+
</StageConfigField>
|
|
113
|
+
<StageConfigField label="Image Name" helpKey="pipeline.config.docker.bake.targetImage">
|
|
114
|
+
<input
|
|
115
|
+
type="text"
|
|
116
|
+
className="form-control input-sm"
|
|
117
|
+
value={stage.ami_name || ''}
|
|
118
|
+
onChange={setFieldValue('ami_name')}
|
|
119
|
+
/>
|
|
120
|
+
</StageConfigField>
|
|
121
|
+
<StageConfigField label="Image tag" helpKey="pipeline.config.docker.bake.targetImageTag">
|
|
122
|
+
<input
|
|
123
|
+
type="text"
|
|
124
|
+
className="form-control input-sm"
|
|
125
|
+
value={extendedAttributes.docker_target_image_tag || ''}
|
|
126
|
+
onChange={setFieldValue('extendedAttributes.docker_target_image_tag')}
|
|
127
|
+
/>
|
|
128
|
+
</StageConfigField>
|
|
129
|
+
<StageConfigField label="Base OS">
|
|
130
|
+
<select className="form-control input-sm" value={stage.baseOs || ''} onChange={setFieldValue('baseOs')}>
|
|
131
|
+
{baseOsOptions.map((baseOsOption: IBaseOsOption) => (
|
|
132
|
+
<option key={baseOsOption.id} value={baseOsOption.id}>
|
|
133
|
+
{baseOsDescription(baseOsOption)}
|
|
134
|
+
</option>
|
|
135
|
+
))}
|
|
136
|
+
</select>
|
|
137
|
+
</StageConfigField>
|
|
138
|
+
<StageConfigField label="Base Label">
|
|
139
|
+
{baseLabelOptions.map((baseLabel: string) => (
|
|
140
|
+
<label key={baseLabel} className="radio-inline">
|
|
141
|
+
<input
|
|
142
|
+
type="radio"
|
|
143
|
+
checked={stage.baseLabel === baseLabel}
|
|
144
|
+
onChange={() => formik.setFieldValue('baseLabel', baseLabel)}
|
|
145
|
+
/>
|
|
146
|
+
{baseLabel}
|
|
147
|
+
</label>
|
|
148
|
+
))}
|
|
149
|
+
</StageConfigField>
|
|
150
|
+
<StageConfigField label="Rebake">
|
|
151
|
+
<div className="checkbox" style={{ marginBottom: 0 }}>
|
|
152
|
+
<label>
|
|
153
|
+
<input
|
|
154
|
+
type="checkbox"
|
|
155
|
+
checked={!!stage.rebake}
|
|
156
|
+
onChange={(event) => formik.setFieldValue('rebake', event.target.checked)}
|
|
157
|
+
/>
|
|
158
|
+
Rebake image without regard to the status of any existing bake
|
|
159
|
+
</label>
|
|
160
|
+
</div>
|
|
161
|
+
</StageConfigField>
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class DockerBakeStageConfig extends React.Component<IStageConfigProps, IDockerBakeStageConfigState> {
|
|
167
|
+
private mounted = false;
|
|
168
|
+
|
|
169
|
+
public state: IDockerBakeStageConfigState = {
|
|
170
|
+
baseLabelOptions: [],
|
|
171
|
+
baseOsOptions: [],
|
|
172
|
+
loadError: false,
|
|
173
|
+
loading: true,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
public componentDidMount(): void {
|
|
177
|
+
this.mounted = true;
|
|
178
|
+
Promise.all([BakeryReader.getBaseOsOptions('docker'), BakeryReader.getBaseLabelOptions()])
|
|
179
|
+
.then(([baseOsOptions, baseLabelOptions]) => {
|
|
180
|
+
if (!this.mounted) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const baseOsOptionsList = baseOsOptions.baseImages || [];
|
|
185
|
+
const stageWithDefaults = applyDockerBakeStageDefaults(this.props.stage, {
|
|
186
|
+
user: AuthenticationService.getAuthenticatedUser()?.name,
|
|
187
|
+
baseOsOptions: baseOsOptionsList,
|
|
188
|
+
baseLabelOptions,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (!isEqual(stageWithDefaults, this.props.stage)) {
|
|
192
|
+
this.props.updateStage(stageWithDefaults);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.setState({
|
|
196
|
+
baseLabelOptions,
|
|
197
|
+
baseOsOptions: baseOsOptionsList,
|
|
198
|
+
loadError: false,
|
|
199
|
+
loading: false,
|
|
200
|
+
});
|
|
201
|
+
})
|
|
202
|
+
.catch(() => {
|
|
203
|
+
if (!this.mounted) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.setState({ loadError: true, loading: false });
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public componentWillUnmount(): void {
|
|
212
|
+
this.mounted = false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public render() {
|
|
216
|
+
if (this.state.loading) {
|
|
217
|
+
return <Spinner />;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (this.state.loadError) {
|
|
221
|
+
return <div className="alert alert-danger">Unable to load Docker bake options.</div>;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const authenticatedUser = AuthenticationService.getAuthenticatedUser();
|
|
225
|
+
const stageWithDefaults = applyDockerBakeStageDefaults(this.props.stage, {
|
|
226
|
+
user: authenticatedUser?.name,
|
|
227
|
+
baseOsOptions: this.state.baseOsOptions,
|
|
228
|
+
baseLabelOptions: this.state.baseLabelOptions,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<FormikStageConfig
|
|
233
|
+
application={this.props.application}
|
|
234
|
+
onChange={this.props.updateStage}
|
|
235
|
+
pipeline={this.props.pipeline}
|
|
236
|
+
stage={stageWithDefaults}
|
|
237
|
+
render={(props: IFormikStageConfigInjectedProps) => <DockerBakeStageForm {...props} {...this.state} />}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function DockerBakeExecutionDetails(props: IExecutionDetailsSectionProps) {
|
|
244
|
+
const { current, name, stage } = props;
|
|
245
|
+
const context = stage.context || {};
|
|
246
|
+
const resourceId = context.status?.resourceId;
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<ExecutionDetailsSection name={name} current={current}>
|
|
250
|
+
<div className="row">
|
|
251
|
+
<div className="col-md-6">
|
|
252
|
+
<dl className="dl-narrow dl-horizontal">
|
|
253
|
+
<dt>Organization</dt>
|
|
254
|
+
<dd>{context.organization}</dd>
|
|
255
|
+
<dt>Image Name</dt>
|
|
256
|
+
<dd>{context.ami_name}</dd>
|
|
257
|
+
<dt>Image Tag</dt>
|
|
258
|
+
<dd>{context.extendedAttributes?.docker_target_image_tag}</dd>
|
|
259
|
+
<dt>Image</dt>
|
|
260
|
+
<dd>{context.ami}</dd>
|
|
261
|
+
</dl>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="col-md-6">
|
|
264
|
+
<dl className="dl-narrow dl-horizontal">
|
|
265
|
+
<dt>Base OS</dt>
|
|
266
|
+
<dd>{context.baseOs}</dd>
|
|
267
|
+
<dt>Region</dt>
|
|
268
|
+
<dd>{context.region}</dd>
|
|
269
|
+
<dt>Package</dt>
|
|
270
|
+
<dd>{context.package}</dd>
|
|
271
|
+
<dt>Label</dt>
|
|
272
|
+
<dd>{context.baseLabel}</dd>
|
|
273
|
+
</dl>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
<StageFailureMessage stage={stage} message={stage.failureMessage} />
|
|
277
|
+
{context.region && resourceId && (
|
|
278
|
+
<div className="row">
|
|
279
|
+
<div className="col-md-12">
|
|
280
|
+
<div className={`alert alert-${stage.isFailed ? 'danger' : 'info'}`}>
|
|
281
|
+
<a target="_blank" rel="noopener noreferrer" href={bakeryDetailUrl(stage)}>
|
|
282
|
+
View Bakery Details
|
|
283
|
+
</a>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</ExecutionDetailsSection>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
(DockerBakeExecutionDetails as any).title = 'bakeConfig';
|
|
293
|
+
|
|
294
|
+
export const DOCKER_BAKE_STAGE_CONFIG: any = {
|
|
295
|
+
provides: 'bake',
|
|
296
|
+
cloudProvider: 'docker',
|
|
297
|
+
label: 'Bake',
|
|
298
|
+
description: 'Bakes an image',
|
|
299
|
+
component: DockerBakeStageConfig,
|
|
300
|
+
executionDetailsSections: [DockerBakeExecutionDetails as any, ExecutionDetailsTasks],
|
|
301
|
+
executionLabelComponent: BakeExecutionLabel,
|
|
302
|
+
extraLabelLines: (stage: IStage) => {
|
|
303
|
+
return (stage as any).masterStage.context.allPreviouslyBaked ||
|
|
304
|
+
(stage as any).masterStage.context.somePreviouslyBaked
|
|
305
|
+
? 1
|
|
306
|
+
: 0;
|
|
307
|
+
},
|
|
308
|
+
supportsCustomTimeout: true,
|
|
309
|
+
validators: [{ type: 'requiredField', fieldName: 'package' }],
|
|
310
|
+
restartable: true,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
Registry.pipeline.registerStage(DOCKER_BAKE_STAGE_CONFIG);
|
|
@@ -45,8 +45,8 @@ Registry.pipeline.registerTrigger({
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
Registry.pipeline.registerTrigger({
|
|
48
|
-
label: '
|
|
49
|
-
description: 'Executes the pipeline on
|
|
48
|
+
label: 'Docker Registry (OCI)',
|
|
49
|
+
description: 'Executes the pipeline on a Docker OCI image update',
|
|
50
50
|
key: 'helm/oci',
|
|
51
51
|
component: DockerHelmOciTriggerConfig,
|
|
52
52
|
manualExecutionComponent: DockerTriggerTemplate,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { shallow } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { DockerTriggerTemplate } from './DockerTriggerTemplate';
|
|
5
|
+
import { DockerImageReader } from '../../image';
|
|
6
|
+
|
|
7
|
+
interface IDeferred<T> {
|
|
8
|
+
promise: Promise<T>;
|
|
9
|
+
resolve: (value: T) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function deferred<T>(): IDeferred<T> {
|
|
13
|
+
let resolve: (value: T) => void;
|
|
14
|
+
const promise = new Promise<T>((promiseResolve) => (resolve = promiseResolve));
|
|
15
|
+
return { promise, resolve };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('<DockerTriggerTemplate/>', () => {
|
|
19
|
+
it('formats Docker trigger labels', async () => {
|
|
20
|
+
await expectAsync(
|
|
21
|
+
Promise.resolve(
|
|
22
|
+
DockerTriggerTemplate.formatLabel({ account: 'prod-registry', repository: 'example/service' } as any),
|
|
23
|
+
),
|
|
24
|
+
).toBeResolvedTo('(Docker Registry) prod-registry: example/service');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('writes docker image artifacts using tag references', () => {
|
|
28
|
+
const updateCommand = jasmine.createSpy('updateCommand');
|
|
29
|
+
const component = new DockerTriggerTemplate({
|
|
30
|
+
command: {
|
|
31
|
+
trigger: {
|
|
32
|
+
type: 'docker',
|
|
33
|
+
registry: 'registry.example.com',
|
|
34
|
+
repository: 'example/service',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
updateCommand,
|
|
38
|
+
} as any);
|
|
39
|
+
|
|
40
|
+
(component as any).updateArtifact((component.props as any).command, '1.260101.000000-0000000');
|
|
41
|
+
|
|
42
|
+
expect(updateCommand).toHaveBeenCalledWith('extraFields.tag', '1.260101.000000-0000000');
|
|
43
|
+
expect(updateCommand).toHaveBeenCalledWith('extraFields.artifacts', [
|
|
44
|
+
{
|
|
45
|
+
type: 'docker/image',
|
|
46
|
+
name: 'registry.example.com/example/service',
|
|
47
|
+
version: '1.260101.000000-0000000',
|
|
48
|
+
reference: 'registry.example.com/example/service:1.260101.000000-0000000',
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('writes Helm OCI image artifacts using digest references', () => {
|
|
54
|
+
const updateCommand = jasmine.createSpy('updateCommand');
|
|
55
|
+
const component = new DockerTriggerTemplate({
|
|
56
|
+
command: {
|
|
57
|
+
trigger: {
|
|
58
|
+
type: 'helm/oci',
|
|
59
|
+
registry: 'registry.example.com',
|
|
60
|
+
repository: 'charts/service',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
updateCommand,
|
|
64
|
+
} as any);
|
|
65
|
+
(component as any).state.lookupType = 'digest';
|
|
66
|
+
|
|
67
|
+
(component as any).updateArtifact((component.props as any).command, 'sha256:abc123');
|
|
68
|
+
|
|
69
|
+
expect(updateCommand).toHaveBeenCalledWith('extraFields.tag', 'sha256:abc123');
|
|
70
|
+
expect(updateCommand).toHaveBeenCalledWith('extraFields.artifacts', [
|
|
71
|
+
{
|
|
72
|
+
type: 'helm/image',
|
|
73
|
+
name: 'registry.example.com/charts/service',
|
|
74
|
+
version: 'sha256:abc123',
|
|
75
|
+
reference: 'registry.example.com/charts/service@sha256:abc123',
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
it('aborts superseded and unmounted tag queries without publishing cancellation errors', async () => {
|
|
80
|
+
jasmine.clock().install();
|
|
81
|
+
try {
|
|
82
|
+
const firstRequest = deferred<string[]>();
|
|
83
|
+
const secondRequest = deferred<string[]>();
|
|
84
|
+
const findTags = spyOn(DockerImageReader, 'findTags').and.returnValues(
|
|
85
|
+
firstRequest.promise,
|
|
86
|
+
secondRequest.promise,
|
|
87
|
+
);
|
|
88
|
+
const wrapper = shallow(
|
|
89
|
+
<DockerTriggerTemplate
|
|
90
|
+
command={{
|
|
91
|
+
trigger: { type: 'docker', repository: 'example/service' },
|
|
92
|
+
}}
|
|
93
|
+
updateCommand={jasmine.createSpy('updateCommand')}
|
|
94
|
+
/>,
|
|
95
|
+
{ disableLifecycleMethods: true },
|
|
96
|
+
);
|
|
97
|
+
const component = wrapper.instance() as DockerTriggerTemplate;
|
|
98
|
+
const tagLoadSuccess = spyOn(component as any, 'tagLoadSuccess').and.callThrough();
|
|
99
|
+
const tagLoadFailure = spyOn(component as any, 'tagLoadFailure').and.callThrough();
|
|
100
|
+
|
|
101
|
+
(component as any).initialize();
|
|
102
|
+
jasmine.clock().tick(250);
|
|
103
|
+
(component as any).searchTags();
|
|
104
|
+
jasmine.clock().tick(250);
|
|
105
|
+
const firstSignal = findTags.calls.argsFor(0)[1] as AbortSignal;
|
|
106
|
+
const secondSignal = findTags.calls.argsFor(1)[1] as AbortSignal;
|
|
107
|
+
|
|
108
|
+
expect(firstSignal.aborted).toBe(true);
|
|
109
|
+
expect(secondSignal.aborted).toBe(false);
|
|
110
|
+
wrapper.unmount();
|
|
111
|
+
expect(secondSignal.aborted).toBe(true);
|
|
112
|
+
|
|
113
|
+
firstRequest.resolve(['stale']);
|
|
114
|
+
secondRequest.resolve(['late']);
|
|
115
|
+
await Promise.all([firstRequest.promise, secondRequest.promise]);
|
|
116
|
+
await Promise.resolve();
|
|
117
|
+
|
|
118
|
+
expect(tagLoadSuccess).not.toHaveBeenCalled();
|
|
119
|
+
expect(tagLoadFailure).not.toHaveBeenCalled();
|
|
120
|
+
} finally {
|
|
121
|
+
jasmine.clock().uninstall();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|