@spinnaker/ecs 2026.1.1 → 2026.2.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/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/serverGroup/configure/wizard/container/Container.d.ts +4 -0
- package/dist/serverGroup/configure/wizard/taskDefinition/TaskDefinition.d.ts +4 -0
- package/package.json +3 -3
- package/src/serverGroup/configure/serverGroupConfiguration.service.ts +6 -1
- package/src/serverGroup/configure/wizard/container/Container.spec.tsx +121 -0
- package/src/serverGroup/configure/wizard/container/Container.tsx +40 -1
- package/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.spec.tsx +124 -0
- package/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.tsx +41 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import type { IAccountDetails } from '@spinnaker/core';
|
|
2
3
|
import type { IEcsDockerImage, IEcsServerGroupCommand, IEcsTargetGroupMapping } from '../../serverGroupConfiguration.service';
|
|
3
4
|
export interface IContainerProps {
|
|
4
5
|
command: IEcsServerGroupCommand;
|
|
@@ -10,6 +11,8 @@ interface IContainerState {
|
|
|
10
11
|
computeUnits: number;
|
|
11
12
|
reservedMemory: number;
|
|
12
13
|
dockerImages: IEcsDockerImage[];
|
|
14
|
+
dockerRegistryAccounts: IAccountDetails[];
|
|
15
|
+
selectedDockerAccount: string;
|
|
13
16
|
targetGroupsAvailable: string[];
|
|
14
17
|
targetGroupMappings: IEcsTargetGroupMapping[];
|
|
15
18
|
}
|
|
@@ -18,6 +21,7 @@ export declare class Container extends React.Component<IContainerProps, IContain
|
|
|
18
21
|
componentDidMount(): void;
|
|
19
22
|
private getIdToImageMap;
|
|
20
23
|
private getEmptyImageDescription;
|
|
24
|
+
private updateDockerRegistryAccount;
|
|
21
25
|
private pushTargetGroupMapping;
|
|
22
26
|
private updateContainerMappingImage;
|
|
23
27
|
private updateComputeUnits;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { IAccountDetails } from '@spinnaker/core';
|
|
2
3
|
import type { IEcsContainerMapping, IEcsDockerImage, IEcsServerGroupCommand, IEcsTargetGroupMapping, IEcsTaskDefinitionArtifact } from '../../serverGroupConfiguration.service';
|
|
3
4
|
export interface ITaskDefinitionProps {
|
|
4
5
|
command: IEcsServerGroupCommand;
|
|
@@ -11,6 +12,8 @@ interface ITaskDefinitionState {
|
|
|
11
12
|
containerMappings: IEcsContainerMapping[];
|
|
12
13
|
targetGroupMappings: IEcsTargetGroupMapping[];
|
|
13
14
|
dockerImages: IEcsDockerImage[];
|
|
15
|
+
dockerRegistryAccounts: IAccountDetails[];
|
|
16
|
+
selectedDockerAccount: string;
|
|
14
17
|
targetGroupsAvailable: string[];
|
|
15
18
|
loadBalancedContainer: string;
|
|
16
19
|
evaluateTaskDefinitionArtifactExpressions: boolean;
|
|
@@ -18,6 +21,7 @@ interface ITaskDefinitionState {
|
|
|
18
21
|
export declare class TaskDefinition extends React.Component<ITaskDefinitionProps, ITaskDefinitionState> {
|
|
19
22
|
constructor(props: ITaskDefinitionProps);
|
|
20
23
|
componentDidMount(): void;
|
|
24
|
+
private updateDockerRegistryAccount;
|
|
21
25
|
private getIdToImageMap;
|
|
22
26
|
private getEmptyImageDescription;
|
|
23
27
|
private excludedArtifactTypePatterns;
|
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.
|
|
8
|
+
"version": "2026.2.0",
|
|
9
9
|
"module": "dist/index.js",
|
|
10
10
|
"typings": "dist/index.d.ts",
|
|
11
11
|
"publishConfig": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@spinnaker/docker": "^0.0.147",
|
|
27
27
|
"@uirouter/angularjs": "1.0.26",
|
|
28
28
|
"@uirouter/react": "1.0.7",
|
|
29
|
-
"angular": "1.
|
|
29
|
+
"angular": "1.8.3",
|
|
30
30
|
"angular-ui-bootstrap": "2.5.0",
|
|
31
31
|
"lodash": "4.18.1",
|
|
32
32
|
"ngimport": "0.6.1",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"shx": "0.3.3",
|
|
48
48
|
"typescript": "5.0.4"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "a9155eb5c4e40b9db5aa9741af28fc2412f7c7d8"
|
|
51
51
|
}
|
|
@@ -239,13 +239,18 @@ export class EcsServerGroupConfigurationService {
|
|
|
239
239
|
imageQueries.push(imageQuery);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
// Scope image search to a known account. If no account is available yet the Docker registry
|
|
243
|
+
// account picker in the wizard will trigger an account-scoped reload once the user selects one.
|
|
244
|
+
const searchAccount = cmd.imageDescription?.account;
|
|
245
|
+
|
|
242
246
|
let imagesPromise;
|
|
243
|
-
if (imageQueries.length) {
|
|
247
|
+
if (imageQueries.length && searchAccount) {
|
|
244
248
|
imagesPromise = this.$q
|
|
245
249
|
.all(
|
|
246
250
|
imageQueries.map((q) =>
|
|
247
251
|
DockerImageReader.findImages({
|
|
248
252
|
provider: 'dockerRegistry',
|
|
253
|
+
account: searchAccount,
|
|
249
254
|
count: 50,
|
|
250
255
|
q: q,
|
|
251
256
|
}),
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mount } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { IAccountDetails } from '@spinnaker/core';
|
|
5
|
+
import { AccountService } from '@spinnaker/core';
|
|
6
|
+
import type { IDockerImage } from '@spinnaker/docker';
|
|
7
|
+
import { DockerImageReader } from '@spinnaker/docker';
|
|
8
|
+
|
|
9
|
+
import type { IEcsDockerImage, IEcsServerGroupCommand } from '../../serverGroupConfiguration.service';
|
|
10
|
+
import { Container } from './Container';
|
|
11
|
+
|
|
12
|
+
const flushPromises = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
|
13
|
+
|
|
14
|
+
describe('Container', () => {
|
|
15
|
+
let command: IEcsServerGroupCommand;
|
|
16
|
+
|
|
17
|
+
const configureCommand = (_query: string) => Promise.resolve() as PromiseLike<void>;
|
|
18
|
+
const notifyAngular = (_key: string, _value: any) => {};
|
|
19
|
+
|
|
20
|
+
const dockerAccounts: IAccountDetails[] = [
|
|
21
|
+
{
|
|
22
|
+
name: 'my-docker-account',
|
|
23
|
+
accountId: '1',
|
|
24
|
+
requiredGroupMembership: [],
|
|
25
|
+
type: 'dockerRegistry',
|
|
26
|
+
} as IAccountDetails,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const dockerImages: IDockerImage[] = [
|
|
30
|
+
{ account: 'my-docker-account', registry: 'my-registry', repository: 'my-repo', tag: 'latest' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
command = ({
|
|
35
|
+
computeUnits: 256,
|
|
36
|
+
reservedMemory: 512,
|
|
37
|
+
imageDescription: null as any,
|
|
38
|
+
targetGroupMappings: [],
|
|
39
|
+
containerMappings: null as any,
|
|
40
|
+
targetGroup: '',
|
|
41
|
+
loadBalancedContainer: '',
|
|
42
|
+
viewState: { dirty: { targetGroups: [] } } as any,
|
|
43
|
+
backingData: { filtered: { images: [], targetGroups: [] } } as any,
|
|
44
|
+
} as any) as IEcsServerGroupCommand;
|
|
45
|
+
|
|
46
|
+
spyOn(AccountService, 'listAccounts').and.returnValue(Promise.resolve(dockerAccounts));
|
|
47
|
+
spyOn(DockerImageReader, 'findImages').and.returnValue(Promise.resolve(dockerImages));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('updateDockerRegistryAccount', () => {
|
|
51
|
+
it('calls DockerImageReader.findImages with the selected account', async () => {
|
|
52
|
+
const wrapper = mount(
|
|
53
|
+
<Container command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await flushPromises();
|
|
57
|
+
|
|
58
|
+
(wrapper.instance() as any).updateDockerRegistryAccount({ value: 'my-docker-account' });
|
|
59
|
+
|
|
60
|
+
expect(DockerImageReader.findImages).toHaveBeenCalledWith({
|
|
61
|
+
provider: 'dockerRegistry',
|
|
62
|
+
account: 'my-docker-account',
|
|
63
|
+
count: 50,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('updates component state and backingData with images returned for the account', async () => {
|
|
68
|
+
const wrapper = mount(
|
|
69
|
+
<Container command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
(wrapper.instance() as any).updateDockerRegistryAccount({ value: 'my-docker-account' });
|
|
73
|
+
await flushPromises();
|
|
74
|
+
wrapper.update();
|
|
75
|
+
|
|
76
|
+
expect((wrapper.instance() as Container).state.dockerImages).toEqual(dockerImages as IEcsDockerImage[]);
|
|
77
|
+
expect(command.backingData.filtered.images).toEqual(dockerImages as IEcsDockerImage[]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('clears existing images immediately when account changes', () => {
|
|
81
|
+
command.backingData.filtered.images = dockerImages as IEcsDockerImage[];
|
|
82
|
+
const wrapper = mount(
|
|
83
|
+
<Container command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
(wrapper.instance() as any).updateDockerRegistryAccount({ value: 'my-docker-account' });
|
|
87
|
+
|
|
88
|
+
// State clears synchronously before the findImages promise resolves
|
|
89
|
+
expect((wrapper.instance() as Container).state.dockerImages).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('pre-selects the account from imageDescription when command already has one', () => {
|
|
93
|
+
command.imageDescription = {
|
|
94
|
+
account: 'my-docker-account',
|
|
95
|
+
registry: 'my-registry',
|
|
96
|
+
repository: 'my-repo',
|
|
97
|
+
tag: 'latest',
|
|
98
|
+
imageId: 'my-registry/my-repo:latest',
|
|
99
|
+
message: '',
|
|
100
|
+
fromTrigger: false,
|
|
101
|
+
fromContext: false,
|
|
102
|
+
stageId: '',
|
|
103
|
+
imageLabelOrSha: '',
|
|
104
|
+
} as IEcsDockerImage;
|
|
105
|
+
|
|
106
|
+
const wrapper = mount(
|
|
107
|
+
<Container command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect((wrapper.instance() as Container).state.selectedDockerAccount).toBe('my-docker-account');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('starts with no account selected when imageDescription has no account', () => {
|
|
114
|
+
const wrapper = mount(
|
|
115
|
+
<Container command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect((wrapper.instance() as Container).state.selectedDockerAccount).toBe('');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -5,7 +5,9 @@ import { Alert } from 'react-bootstrap';
|
|
|
5
5
|
import type { Option } from 'react-select';
|
|
6
6
|
import { react2angular } from 'react2angular';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import type { IAccountDetails } from '@spinnaker/core';
|
|
9
|
+
import { AccountService, HelpField, TetheredSelect, withErrorBoundary } from '@spinnaker/core';
|
|
10
|
+
import { DockerImageReader } from '@spinnaker/docker';
|
|
9
11
|
|
|
10
12
|
import type {
|
|
11
13
|
IEcsDockerImage,
|
|
@@ -24,6 +26,8 @@ interface IContainerState {
|
|
|
24
26
|
computeUnits: number;
|
|
25
27
|
reservedMemory: number;
|
|
26
28
|
dockerImages: IEcsDockerImage[];
|
|
29
|
+
dockerRegistryAccounts: IAccountDetails[];
|
|
30
|
+
selectedDockerAccount: string;
|
|
27
31
|
targetGroupsAvailable: string[];
|
|
28
32
|
targetGroupMappings: IEcsTargetGroupMapping[];
|
|
29
33
|
}
|
|
@@ -60,6 +64,8 @@ export class Container extends React.Component<IContainerProps, IContainerState>
|
|
|
60
64
|
computeUnits: cmd.computeUnits,
|
|
61
65
|
reservedMemory: cmd.reservedMemory,
|
|
62
66
|
dockerImages: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.images : [],
|
|
67
|
+
dockerRegistryAccounts: [],
|
|
68
|
+
selectedDockerAccount: cmd.imageDescription?.account ?? '',
|
|
63
69
|
targetGroupMappings: cmd.targetGroupMappings,
|
|
64
70
|
targetGroupsAvailable: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.targetGroups : [],
|
|
65
71
|
};
|
|
@@ -70,6 +76,10 @@ export class Container extends React.Component<IContainerProps, IContainerState>
|
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
public componentDidMount() {
|
|
79
|
+
AccountService.listAccounts('dockerRegistry').then((accounts: IAccountDetails[]) => {
|
|
80
|
+
this.setState({ dockerRegistryAccounts: accounts });
|
|
81
|
+
});
|
|
82
|
+
|
|
73
83
|
this.props.configureCommand('1').then(() => {
|
|
74
84
|
this.setState({
|
|
75
85
|
dockerImages: this.props.command.backingData.filtered.images,
|
|
@@ -104,6 +114,16 @@ export class Container extends React.Component<IContainerProps, IContainerState>
|
|
|
104
114
|
};
|
|
105
115
|
};
|
|
106
116
|
|
|
117
|
+
private updateDockerRegistryAccount = (newAccount: Option<string>) => {
|
|
118
|
+
const account = newAccount.value;
|
|
119
|
+
this.setState({ selectedDockerAccount: account, dockerImages: [] });
|
|
120
|
+
DockerImageReader.findImages({ provider: 'dockerRegistry', account, count: 50 }).then((images) => {
|
|
121
|
+
const ecsImages = images as IEcsDockerImage[];
|
|
122
|
+
this.props.command.backingData.filtered.images = ecsImages;
|
|
123
|
+
this.setState({ dockerImages: ecsImages });
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
107
127
|
private pushTargetGroupMapping = () => {
|
|
108
128
|
const targetMaps = this.state.targetGroupMappings;
|
|
109
129
|
targetMaps.push({ containerName: '', targetGroup: '', containerPort: 80 });
|
|
@@ -171,6 +191,11 @@ export class Container extends React.Component<IContainerProps, IContainerState>
|
|
|
171
191
|
? this.props.command.viewState.dirty.targetGroups
|
|
172
192
|
: [];
|
|
173
193
|
|
|
194
|
+
const dockerRegistryAccountOptions = this.state.dockerRegistryAccounts.map((account) => ({
|
|
195
|
+
label: account.name,
|
|
196
|
+
value: account.name,
|
|
197
|
+
}));
|
|
198
|
+
|
|
174
199
|
const dockerImageOptions = this.state.dockerImages.map(function (image) {
|
|
175
200
|
let msg = '';
|
|
176
201
|
if (image.fromTrigger || image.fromContext) {
|
|
@@ -257,6 +282,20 @@ export class Container extends React.Component<IContainerProps, IContainerState>
|
|
|
257
282
|
return (
|
|
258
283
|
<div className="container-fluid form-horizontal">
|
|
259
284
|
{dirtyTagetGroups.length > 0 ? <div>{dirtyTargetGroupSection}</div> : ''}
|
|
285
|
+
<div className="form-group">
|
|
286
|
+
<div className="col-md-3 sm-label-right">
|
|
287
|
+
<b>Docker Registry Account</b>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="col-md-9" data-test-id="ContainerInputs.dockerRegistryAccount">
|
|
290
|
+
<TetheredSelect
|
|
291
|
+
placeholder="Select a Docker registry account..."
|
|
292
|
+
options={dockerRegistryAccountOptions}
|
|
293
|
+
value={this.state.selectedDockerAccount}
|
|
294
|
+
onChange={(e: Option) => this.updateDockerRegistryAccount(e as Option<string>)}
|
|
295
|
+
clearable={false}
|
|
296
|
+
/>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
260
299
|
<div className="form-group">
|
|
261
300
|
<div className="col-md-3 sm-label-right">
|
|
262
301
|
<b>Container Image</b>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { mount } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { IAccountDetails } from '@spinnaker/core';
|
|
5
|
+
import { AccountService } from '@spinnaker/core';
|
|
6
|
+
import type { IDockerImage } from '@spinnaker/docker';
|
|
7
|
+
import { DockerImageReader } from '@spinnaker/docker';
|
|
8
|
+
|
|
9
|
+
import type { IEcsDockerImage, IEcsServerGroupCommand } from '../../serverGroupConfiguration.service';
|
|
10
|
+
import { TaskDefinition } from './TaskDefinition';
|
|
11
|
+
|
|
12
|
+
const flushPromises = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
|
13
|
+
|
|
14
|
+
describe('TaskDefinition', () => {
|
|
15
|
+
let command: IEcsServerGroupCommand;
|
|
16
|
+
|
|
17
|
+
const configureCommand = (_query: string) => Promise.resolve() as PromiseLike<void>;
|
|
18
|
+
const notifyAngular = (_key: string, _value: any) => {};
|
|
19
|
+
|
|
20
|
+
const dockerAccounts: IAccountDetails[] = [
|
|
21
|
+
{
|
|
22
|
+
name: 'my-docker-account',
|
|
23
|
+
accountId: '1',
|
|
24
|
+
requiredGroupMembership: [],
|
|
25
|
+
type: 'dockerRegistry',
|
|
26
|
+
} as IAccountDetails,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const dockerImages: IDockerImage[] = [
|
|
30
|
+
{ account: 'my-docker-account', registry: 'my-registry', repository: 'my-repo', tag: 'latest' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
command = ({
|
|
35
|
+
computeUnits: 256,
|
|
36
|
+
reservedMemory: 512,
|
|
37
|
+
imageDescription: null as any,
|
|
38
|
+
targetGroupMappings: [],
|
|
39
|
+
containerMappings: [] as any,
|
|
40
|
+
targetGroup: '',
|
|
41
|
+
loadBalancedContainer: '',
|
|
42
|
+
taskDefinitionArtifact: {} as any,
|
|
43
|
+
taskDefinitionArtifactAccount: '',
|
|
44
|
+
evaluateTaskDefinitionArtifactExpressions: false,
|
|
45
|
+
useTaskDefinitionArtifact: false,
|
|
46
|
+
viewState: { dirty: { targetGroups: [] }, pipeline: null as any, currentStage: null as any } as any,
|
|
47
|
+
backingData: { filtered: { images: [], targetGroups: [] } } as any,
|
|
48
|
+
} as any) as IEcsServerGroupCommand;
|
|
49
|
+
|
|
50
|
+
spyOn(AccountService, 'listAccounts').and.returnValue(Promise.resolve(dockerAccounts));
|
|
51
|
+
spyOn(DockerImageReader, 'findImages').and.returnValue(Promise.resolve(dockerImages));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('updateDockerRegistryAccount', () => {
|
|
55
|
+
it('calls DockerImageReader.findImages with the selected account', async () => {
|
|
56
|
+
const wrapper = mount(
|
|
57
|
+
<TaskDefinition command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
await flushPromises();
|
|
61
|
+
|
|
62
|
+
(wrapper.instance() as any).updateDockerRegistryAccount({ value: 'my-docker-account' });
|
|
63
|
+
|
|
64
|
+
expect(DockerImageReader.findImages).toHaveBeenCalledWith({
|
|
65
|
+
provider: 'dockerRegistry',
|
|
66
|
+
account: 'my-docker-account',
|
|
67
|
+
count: 50,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('updates component state and backingData with images returned for the account', async () => {
|
|
72
|
+
const wrapper = mount(
|
|
73
|
+
<TaskDefinition command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
(wrapper.instance() as any).updateDockerRegistryAccount({ value: 'my-docker-account' });
|
|
77
|
+
await flushPromises();
|
|
78
|
+
wrapper.update();
|
|
79
|
+
|
|
80
|
+
expect((wrapper.instance() as TaskDefinition).state.dockerImages).toEqual(dockerImages as IEcsDockerImage[]);
|
|
81
|
+
expect(command.backingData.filtered.images).toEqual(dockerImages as IEcsDockerImage[]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('clears existing images immediately when account changes', () => {
|
|
85
|
+
command.backingData.filtered.images = dockerImages as IEcsDockerImage[];
|
|
86
|
+
const wrapper = mount(
|
|
87
|
+
<TaskDefinition command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
(wrapper.instance() as any).updateDockerRegistryAccount({ value: 'my-docker-account' });
|
|
91
|
+
|
|
92
|
+
expect((wrapper.instance() as TaskDefinition).state.dockerImages).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('pre-selects the account from imageDescription when command already has one', () => {
|
|
96
|
+
command.imageDescription = {
|
|
97
|
+
account: 'my-docker-account',
|
|
98
|
+
registry: 'my-registry',
|
|
99
|
+
repository: 'my-repo',
|
|
100
|
+
tag: 'latest',
|
|
101
|
+
imageId: 'my-registry/my-repo:latest',
|
|
102
|
+
message: '',
|
|
103
|
+
fromTrigger: false,
|
|
104
|
+
fromContext: false,
|
|
105
|
+
stageId: '',
|
|
106
|
+
imageLabelOrSha: '',
|
|
107
|
+
} as IEcsDockerImage;
|
|
108
|
+
|
|
109
|
+
const wrapper = mount(
|
|
110
|
+
<TaskDefinition command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect((wrapper.instance() as TaskDefinition).state.selectedDockerAccount).toBe('my-docker-account');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('starts with no account selected when imageDescription has no account', () => {
|
|
117
|
+
const wrapper = mount(
|
|
118
|
+
<TaskDefinition command={command} notifyAngular={notifyAngular} configureCommand={configureCommand} />,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect((wrapper.instance() as TaskDefinition).state.selectedDockerAccount).toBe('');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -5,8 +5,9 @@ import { Alert } from 'react-bootstrap';
|
|
|
5
5
|
import type { Option } from 'react-select';
|
|
6
6
|
import { react2angular } from 'react2angular';
|
|
7
7
|
|
|
8
|
-
import type { IArtifact, IExpectedArtifact } from '@spinnaker/core';
|
|
8
|
+
import type { IAccountDetails, IArtifact, IExpectedArtifact } from '@spinnaker/core';
|
|
9
9
|
import {
|
|
10
|
+
AccountService,
|
|
10
11
|
ArtifactTypePatterns,
|
|
11
12
|
CheckboxInput,
|
|
12
13
|
HelpField,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
TetheredSelect,
|
|
16
17
|
withErrorBoundary,
|
|
17
18
|
} from '@spinnaker/core';
|
|
19
|
+
import { DockerImageReader } from '@spinnaker/docker';
|
|
18
20
|
|
|
19
21
|
import type {
|
|
20
22
|
IEcsContainerMapping,
|
|
@@ -36,6 +38,8 @@ interface ITaskDefinitionState {
|
|
|
36
38
|
containerMappings: IEcsContainerMapping[];
|
|
37
39
|
targetGroupMappings: IEcsTargetGroupMapping[];
|
|
38
40
|
dockerImages: IEcsDockerImage[];
|
|
41
|
+
dockerRegistryAccounts: IAccountDetails[];
|
|
42
|
+
selectedDockerAccount: string;
|
|
39
43
|
targetGroupsAvailable: string[];
|
|
40
44
|
loadBalancedContainer: string;
|
|
41
45
|
evaluateTaskDefinitionArtifactExpressions: boolean;
|
|
@@ -73,6 +77,8 @@ export class TaskDefinition extends React.Component<ITaskDefinitionProps, ITaskD
|
|
|
73
77
|
targetGroupMappings: cmd.targetGroupMappings,
|
|
74
78
|
targetGroupsAvailable: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.targetGroups : [],
|
|
75
79
|
dockerImages: cmd.backingData && cmd.backingData.filtered ? cmd.backingData.filtered.images : [],
|
|
80
|
+
dockerRegistryAccounts: [],
|
|
81
|
+
selectedDockerAccount: cmd.imageDescription?.account ?? '',
|
|
76
82
|
loadBalancedContainer: cmd.loadBalancedContainer || defaultContainer,
|
|
77
83
|
taskDefArtifactAccount: cmd.taskDefinitionArtifactAccount,
|
|
78
84
|
evaluateTaskDefinitionArtifactExpressions: cmd.evaluateTaskDefinitionArtifactExpressions,
|
|
@@ -81,6 +87,10 @@ export class TaskDefinition extends React.Component<ITaskDefinitionProps, ITaskD
|
|
|
81
87
|
|
|
82
88
|
// TODO: Separate docker image component used by both TaskDefinition and Container
|
|
83
89
|
public componentDidMount() {
|
|
90
|
+
AccountService.listAccounts('dockerRegistry').then((accounts: IAccountDetails[]) => {
|
|
91
|
+
this.setState({ dockerRegistryAccounts: accounts });
|
|
92
|
+
});
|
|
93
|
+
|
|
84
94
|
this.props.configureCommand('1').then(() => {
|
|
85
95
|
this.setState({
|
|
86
96
|
dockerImages: this.props.command.backingData.filtered.images,
|
|
@@ -89,6 +99,16 @@ export class TaskDefinition extends React.Component<ITaskDefinitionProps, ITaskD
|
|
|
89
99
|
});
|
|
90
100
|
}
|
|
91
101
|
|
|
102
|
+
private updateDockerRegistryAccount = (newAccount: Option<string>) => {
|
|
103
|
+
const account = newAccount.value;
|
|
104
|
+
this.setState({ selectedDockerAccount: account, dockerImages: [] });
|
|
105
|
+
DockerImageReader.findImages({ provider: 'dockerRegistry', account, count: 50 }).then((images) => {
|
|
106
|
+
const ecsImages = images as IEcsDockerImage[];
|
|
107
|
+
this.props.command.backingData.filtered.images = ecsImages;
|
|
108
|
+
this.setState({ dockerImages: ecsImages });
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
92
112
|
private getIdToImageMap = (): Map<string, IEcsDockerImage> => {
|
|
93
113
|
const imageIdToDescription = new Map<string, IEcsDockerImage>();
|
|
94
114
|
this.props.command.backingData.filtered.images.forEach((e) => {
|
|
@@ -224,6 +244,11 @@ export class TaskDefinition extends React.Component<ITaskDefinitionProps, ITaskD
|
|
|
224
244
|
const updateTargetGroupMappingPort = this.updateTargetGroupMappingPort;
|
|
225
245
|
const updateEvaluateTaskDefArtifactFlag = this.updateEvaluateTaskDefArtifactFlag;
|
|
226
246
|
|
|
247
|
+
const dockerRegistryAccountOptions = this.state.dockerRegistryAccounts.map((account) => ({
|
|
248
|
+
label: account.name,
|
|
249
|
+
value: account.name,
|
|
250
|
+
}));
|
|
251
|
+
|
|
227
252
|
const dockerImageOptions = this.state.dockerImages.map(function (image) {
|
|
228
253
|
let msg = '';
|
|
229
254
|
if (image.fromTrigger || image.fromContext) {
|
|
@@ -373,6 +398,21 @@ export class TaskDefinition extends React.Component<ITaskDefinitionProps, ITaskD
|
|
|
373
398
|
/>
|
|
374
399
|
</StageConfigField>
|
|
375
400
|
|
|
401
|
+
<div className="form-group">
|
|
402
|
+
<div className="col-md-3 sm-label-right">
|
|
403
|
+
<b>Docker Registry Account</b>
|
|
404
|
+
</div>
|
|
405
|
+
<div className="col-md-9" data-test-id="Artifacts.dockerRegistryAccount">
|
|
406
|
+
<TetheredSelect
|
|
407
|
+
placeholder="Select a Docker registry account..."
|
|
408
|
+
options={dockerRegistryAccountOptions}
|
|
409
|
+
value={this.state.selectedDockerAccount}
|
|
410
|
+
onChange={(e: Option) => this.updateDockerRegistryAccount(e as Option<string>)}
|
|
411
|
+
clearable={false}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
376
416
|
<div className="form-group">
|
|
377
417
|
<div className="sm-label-left">
|
|
378
418
|
<b>Container Mappings</b>
|