@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
|
@@ -1,2 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { IExecutionDetailsSectionProps, IStage, IStageConfigProps } from '@spinnaker/core';
|
|
3
|
+
interface IBaseOsOption {
|
|
4
|
+
id: string;
|
|
5
|
+
shortDescription?: string;
|
|
6
|
+
detailedDescription?: string;
|
|
7
|
+
isImageFamily?: boolean;
|
|
8
|
+
displayName?: string;
|
|
9
|
+
}
|
|
10
|
+
interface IDockerBakeStageDefaults {
|
|
11
|
+
user: string;
|
|
12
|
+
baseOsOptions: IBaseOsOption[];
|
|
13
|
+
baseLabelOptions: string[];
|
|
14
|
+
}
|
|
15
|
+
interface IDockerBakeStageConfigState {
|
|
16
|
+
baseLabelOptions: string[];
|
|
17
|
+
baseOsOptions: IBaseOsOption[];
|
|
18
|
+
loadError: boolean;
|
|
19
|
+
loading: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function applyDockerBakeStageDefaults(stage: IStage, defaults: IDockerBakeStageDefaults): IStage;
|
|
22
|
+
export declare class DockerBakeStageConfig extends React.Component<IStageConfigProps, IDockerBakeStageConfigState> {
|
|
23
|
+
private mounted;
|
|
24
|
+
state: IDockerBakeStageConfigState;
|
|
25
|
+
componentDidMount(): void;
|
|
26
|
+
componentWillUnmount(): void;
|
|
27
|
+
render(): JSX.Element;
|
|
28
|
+
}
|
|
29
|
+
export declare function DockerBakeExecutionDetails(props: IExecutionDetailsSectionProps): JSX.Element;
|
|
30
|
+
export declare const DOCKER_BAKE_STAGE_CONFIG: any;
|
|
31
|
+
export {};
|
|
@@ -11,7 +11,8 @@ export interface IDockerTriggerTemplateState {
|
|
|
11
11
|
export declare class DockerTriggerTemplate extends React.Component<ITriggerTemplateComponentProps, IDockerTriggerTemplateState> {
|
|
12
12
|
private queryStream;
|
|
13
13
|
private subscription;
|
|
14
|
-
|
|
14
|
+
private tagRequest;
|
|
15
|
+
static formatLabel(trigger: IDockerTrigger): Promise<string>;
|
|
15
16
|
constructor(props: ITriggerTemplateComponentProps);
|
|
16
17
|
private handleQuery;
|
|
17
18
|
private lookupTypeChanged;
|
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.3.0",
|
|
9
9
|
"module": "dist/index.js",
|
|
10
10
|
"typings": "dist/index.d.ts",
|
|
11
11
|
"publishConfig": {
|
|
@@ -22,12 +22,9 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@spinnaker/core": "^0.29.1",
|
|
25
|
-
"@uirouter/angularjs": "1.0.26",
|
|
26
|
-
"angular": "1.8.3",
|
|
27
25
|
"date-fns": "2.21.1",
|
|
28
26
|
"formik": "1.5.1",
|
|
29
27
|
"lodash": "4.18.1",
|
|
30
|
-
"ngimport": "0.6.1",
|
|
31
28
|
"react": "16.14.0",
|
|
32
29
|
"react-select": "1.2.1",
|
|
33
30
|
"rxjs": "6.6.7"
|
|
@@ -35,12 +32,11 @@
|
|
|
35
32
|
"devDependencies": {
|
|
36
33
|
"@spinnaker/eslint-plugin": "^3.0.2",
|
|
37
34
|
"@spinnaker/scripts": "^0.4.0",
|
|
38
|
-
"@types/angular": "1.6.26",
|
|
39
35
|
"@types/lodash": "4.14.64",
|
|
40
36
|
"@types/react": "16.14.10",
|
|
41
37
|
"@types/react-select": "1.3.4",
|
|
42
38
|
"shx": "0.3.3",
|
|
43
39
|
"typescript": "5.0.4"
|
|
44
40
|
},
|
|
45
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "0f7d75f5d4b42c1675e2ecc69a53f8eb73090eb9"
|
|
46
42
|
}
|
package/src/docker.module.ts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { DOCKER_PIPELINE_STAGES_BAKE_DOCKERBAKESTAGE } from './pipeline/stages/bake/dockerBakeStage';
|
|
1
|
+
import './pipeline/stages/bake/dockerBakeStage';
|
|
4
2
|
import './pipeline/trigger/DockerTrigger';
|
|
5
|
-
|
|
6
|
-
export const DOCKER_MODULE = 'spinnaker.docker';
|
|
7
|
-
module(DOCKER_MODULE, [DOCKER_PIPELINE_STAGES_BAKE_DOCKERBAKESTAGE]);
|
|
@@ -71,6 +71,7 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
private unmounted = false;
|
|
74
|
+
private imageRequest: AbortController;
|
|
74
75
|
private images: IDockerImage[];
|
|
75
76
|
private accounts: string[];
|
|
76
77
|
|
|
@@ -109,7 +110,7 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
109
110
|
|
|
110
111
|
private getAccountMap(images: IDockerImage[]): { [key: string]: string[] } {
|
|
111
112
|
const groupedImages = groupBy(
|
|
112
|
-
images.filter((image) => image.account),
|
|
113
|
+
images.filter((image) => image.account && image.repository),
|
|
113
114
|
'account',
|
|
114
115
|
);
|
|
115
116
|
return reduce<IDockerImage[], { [key: string]: string[] }>(
|
|
@@ -192,11 +193,12 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
public componentWillReceiveProps(nextProps: IDockerChartAndTagSelectorProps) {
|
|
196
|
+
const requestAccount = this.props.showRegistry ? this.props.account : this.props.registry;
|
|
197
|
+
const nextRequestAccount = nextProps.showRegistry ? nextProps.account : nextProps.registry;
|
|
195
198
|
if (
|
|
196
|
-
!this.images ||
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
199
|
+
(!this.images && !this.imageRequest) ||
|
|
200
|
+
this.props.showRegistry !== nextProps.showRegistry ||
|
|
201
|
+
requestAccount !== nextRequestAccount
|
|
200
202
|
) {
|
|
201
203
|
this.refreshImages(nextProps);
|
|
202
204
|
} else if (
|
|
@@ -214,6 +216,7 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
214
216
|
|
|
215
217
|
componentWillUnmount() {
|
|
216
218
|
this.unmounted = true;
|
|
219
|
+
this.imageRequest?.abort();
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
private synchronizeChanges(values: IDockerImageParts, registry: string) {
|
|
@@ -315,10 +318,14 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
315
318
|
}
|
|
316
319
|
|
|
317
320
|
private initializeImages(props: IDockerChartAndTagSelectorProps) {
|
|
318
|
-
if (this.
|
|
321
|
+
if (this.unmounted) {
|
|
319
322
|
return;
|
|
320
323
|
}
|
|
321
324
|
|
|
325
|
+
this.imageRequest?.abort();
|
|
326
|
+
const imageRequest = new AbortController();
|
|
327
|
+
this.imageRequest = imageRequest;
|
|
328
|
+
|
|
322
329
|
const { showRegistry, account, registry } = props;
|
|
323
330
|
|
|
324
331
|
const imageConfig: IFindImageParams = {
|
|
@@ -327,8 +334,11 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
327
334
|
};
|
|
328
335
|
|
|
329
336
|
this.setState({ imagesLoading: true });
|
|
330
|
-
DockerChartImageReader.findImages(imageConfig)
|
|
337
|
+
DockerChartImageReader.findImages(imageConfig, imageRequest.signal)
|
|
331
338
|
.then((images: IDockerImage[]) => {
|
|
339
|
+
if (imageRequest.signal.aborted || this.unmounted) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
332
342
|
this.images = images;
|
|
333
343
|
this.registryMap = this.getRegistryMap(this.images);
|
|
334
344
|
this.accountMap = this.getAccountMap(this.images);
|
|
@@ -337,10 +347,11 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
337
347
|
this.organizationMap = this.getOrganizationMap(this.images);
|
|
338
348
|
this.repositoryMap = this.getRepositoryMap(this.images);
|
|
339
349
|
this.organizations = this.getOrganizationsList(this.accountMap);
|
|
340
|
-
this.updateThings(props, true);
|
|
350
|
+
this.updateThings(this.props, true);
|
|
341
351
|
})
|
|
342
352
|
.finally(() => {
|
|
343
|
-
if (!this.unmounted) {
|
|
353
|
+
if (!this.unmounted && this.imageRequest === imageRequest) {
|
|
354
|
+
this.imageRequest = null;
|
|
344
355
|
this.setState({ imagesLoading: false });
|
|
345
356
|
}
|
|
346
357
|
});
|
|
@@ -354,9 +365,10 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
354
365
|
this.initializeImages(props);
|
|
355
366
|
}
|
|
356
367
|
|
|
357
|
-
private initializeAccounts(
|
|
358
|
-
let { account } = props;
|
|
368
|
+
private initializeAccounts() {
|
|
359
369
|
AccountService.listAccounts('dockerRegistry').then((allAccounts: IAccount[]) => {
|
|
370
|
+
const props = this.props;
|
|
371
|
+
let { account } = props;
|
|
360
372
|
const accounts = allAccounts.map((a: IAccount) => a.name);
|
|
361
373
|
if (this.props.showRegistry && !account) {
|
|
362
374
|
account = accounts[0];
|
|
@@ -373,7 +385,7 @@ export class DockerChartAndTagSelector extends React.Component<
|
|
|
373
385
|
|
|
374
386
|
public componentDidMount() {
|
|
375
387
|
if (!this.props.deferInitialization && (this.props.registry || this.isNew())) {
|
|
376
|
-
this.initializeAccounts(
|
|
388
|
+
this.initializeAccounts();
|
|
377
389
|
}
|
|
378
390
|
}
|
|
379
391
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { DockerChartAndTagSelector } from './DockerChartAndTagSelector';
|
|
2
|
+
import { DockerImageAndTagSelector } from './DockerImageAndTagSelector';
|
|
3
|
+
import { DockerChartImageReader, DockerImageReader } from './DockerImageReader';
|
|
4
|
+
import { AccountService } from '@spinnaker/core';
|
|
5
|
+
|
|
6
|
+
interface IDeferred<T> {
|
|
7
|
+
promise: Promise<T>;
|
|
8
|
+
resolve: (value: T) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function deferred<T>(): IDeferred<T> {
|
|
12
|
+
let resolve: (value: T) => void;
|
|
13
|
+
const promise = new Promise<T>((promiseResolve) => (resolve = promiseResolve));
|
|
14
|
+
return { promise, resolve };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Docker image selectors', () => {
|
|
18
|
+
const props = {
|
|
19
|
+
specifyTagByRegex: false,
|
|
20
|
+
imageId: '',
|
|
21
|
+
organization: '',
|
|
22
|
+
registry: '',
|
|
23
|
+
repository: '',
|
|
24
|
+
tag: '',
|
|
25
|
+
digest: '',
|
|
26
|
+
account: '',
|
|
27
|
+
onChange: () => undefined,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
it('ignores image records without repositories when grouping account organizations', () => {
|
|
31
|
+
const images = [
|
|
32
|
+
{ account: 'test', registry: 'registry.example', repository: undefined },
|
|
33
|
+
{ account: 'test', registry: 'registry.example', repository: 'spinnaker/deck', tag: 'latest' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
expect((new DockerImageAndTagSelector(props) as any).getAccountMap(images)).toEqual({ test: ['spinnaker'] });
|
|
37
|
+
expect((new DockerChartAndTagSelector(props) as any).getAccountMap(images)).toEqual({ test: ['spinnaker'] });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('ignores non-Docker image records when no repositories are present', () => {
|
|
41
|
+
const images = [
|
|
42
|
+
{ account: 'gce', imageName: 'ubuntu-1804-bionic-v20181003' },
|
|
43
|
+
{ account: 'compute-engine', imageName: 'ubuntu-1804-bionic-v20181003' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
expect((new DockerImageAndTagSelector(props) as any).getAccountMap(images)).toEqual({});
|
|
47
|
+
expect((new DockerChartAndTagSelector(props) as any).getAccountMap(images)).toEqual({});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
[
|
|
51
|
+
{ name: 'image', Selector: DockerImageAndTagSelector, Reader: DockerImageReader },
|
|
52
|
+
{ name: 'chart', Selector: DockerChartAndTagSelector, Reader: DockerChartImageReader },
|
|
53
|
+
].forEach(({ name, Selector, Reader }) => {
|
|
54
|
+
it(`aborts replaced and unmounted ${name} loads without applying late state`, async () => {
|
|
55
|
+
const firstRequest = deferred<any[]>();
|
|
56
|
+
const secondRequest = deferred<any[]>();
|
|
57
|
+
const findImages = spyOn(Reader, 'findImages').and.returnValues(firstRequest.promise, secondRequest.promise);
|
|
58
|
+
const component = new Selector(props);
|
|
59
|
+
const setState = spyOn(component, 'setState');
|
|
60
|
+
|
|
61
|
+
component.refreshImages(props);
|
|
62
|
+
component.refreshImages({ ...props, account: 'replacement' });
|
|
63
|
+
const firstSignal = findImages.calls.argsFor(0)[1] as AbortSignal;
|
|
64
|
+
const secondSignal = findImages.calls.argsFor(1)[1] as AbortSignal;
|
|
65
|
+
|
|
66
|
+
expect(firstSignal.aborted).toBe(true);
|
|
67
|
+
expect(secondSignal.aborted).toBe(false);
|
|
68
|
+
component.componentWillUnmount();
|
|
69
|
+
expect(secondSignal.aborted).toBe(true);
|
|
70
|
+
|
|
71
|
+
firstRequest.resolve([]);
|
|
72
|
+
secondRequest.resolve([]);
|
|
73
|
+
await Promise.all([firstRequest.promise, secondRequest.promise]);
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
|
|
76
|
+
expect(setState.calls.allArgs()).toEqual([[{ imagesLoading: true }], [{ imagesLoading: true }]]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it(`preserves a pending ${name} load across unrelated prop updates`, async () => {
|
|
80
|
+
const request = deferred<any[]>();
|
|
81
|
+
const findImages = spyOn(Reader, 'findImages').and.returnValue(request.promise);
|
|
82
|
+
const component = new Selector(props);
|
|
83
|
+
spyOn(component, 'setState');
|
|
84
|
+
const updateThings = spyOn(component as any, 'updateThings').and.callThrough();
|
|
85
|
+
const replacementProps = { ...props, repository: 'replacement' };
|
|
86
|
+
|
|
87
|
+
component.refreshImages(props);
|
|
88
|
+
component.componentWillReceiveProps(replacementProps);
|
|
89
|
+
(component as any).props = replacementProps;
|
|
90
|
+
const signal = findImages.calls.argsFor(0)[1] as AbortSignal;
|
|
91
|
+
|
|
92
|
+
expect(findImages).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(signal.aborted).toBe(false);
|
|
94
|
+
|
|
95
|
+
updateThings.calls.reset();
|
|
96
|
+
request.resolve([]);
|
|
97
|
+
await request.promise;
|
|
98
|
+
await Promise.resolve();
|
|
99
|
+
|
|
100
|
+
expect(updateThings).toHaveBeenCalledOnceWith(replacementProps, true);
|
|
101
|
+
component.componentWillUnmount();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it(`replaces a pending ${name} load when its effective registry account changes`, async () => {
|
|
105
|
+
const firstRequest = deferred<any[]>();
|
|
106
|
+
const secondRequest = deferred<any[]>();
|
|
107
|
+
const findImages = spyOn(Reader, 'findImages').and.returnValues(firstRequest.promise, secondRequest.promise);
|
|
108
|
+
const initialProps = { ...props, registry: 'initial', showRegistry: false };
|
|
109
|
+
const replacementProps = { ...initialProps, registry: 'replacement' };
|
|
110
|
+
const component = new Selector(initialProps);
|
|
111
|
+
spyOn(component, 'setState');
|
|
112
|
+
|
|
113
|
+
component.refreshImages(initialProps);
|
|
114
|
+
component.componentWillReceiveProps(replacementProps);
|
|
115
|
+
(component as any).props = replacementProps;
|
|
116
|
+
const firstSignal = findImages.calls.argsFor(0)[1] as AbortSignal;
|
|
117
|
+
const secondSignal = findImages.calls.argsFor(1)[1] as AbortSignal;
|
|
118
|
+
|
|
119
|
+
expect(findImages.calls.argsFor(1)[0].account).toBe('replacement');
|
|
120
|
+
expect(firstSignal.aborted).toBe(true);
|
|
121
|
+
expect(secondSignal.aborted).toBe(false);
|
|
122
|
+
|
|
123
|
+
component.componentWillUnmount();
|
|
124
|
+
firstRequest.resolve([]);
|
|
125
|
+
secondRequest.resolve([]);
|
|
126
|
+
await Promise.all([firstRequest.promise, secondRequest.promise]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it(`uses current props when delayed account initialization refreshes ${name} loads`, async () => {
|
|
130
|
+
const accountsRequest = deferred<any[]>();
|
|
131
|
+
const firstImageRequest = deferred<any[]>();
|
|
132
|
+
const secondImageRequest = deferred<any[]>();
|
|
133
|
+
spyOn(AccountService, 'listAccounts').and.returnValue(accountsRequest.promise);
|
|
134
|
+
const findImages = spyOn(Reader, 'findImages').and.returnValues(
|
|
135
|
+
firstImageRequest.promise,
|
|
136
|
+
secondImageRequest.promise,
|
|
137
|
+
);
|
|
138
|
+
const initialProps = { ...props, account: 'initial', showRegistry: true };
|
|
139
|
+
const replacementProps = { ...initialProps, account: 'replacement' };
|
|
140
|
+
const component = new Selector(initialProps);
|
|
141
|
+
spyOn(component, 'setState');
|
|
142
|
+
|
|
143
|
+
(component as any).initializeAccounts(initialProps);
|
|
144
|
+
(component as any).props = replacementProps;
|
|
145
|
+
component.refreshImages(replacementProps);
|
|
146
|
+
accountsRequest.resolve([{ name: 'default' }]);
|
|
147
|
+
await accountsRequest.promise;
|
|
148
|
+
await Promise.resolve();
|
|
149
|
+
|
|
150
|
+
expect(findImages.calls.argsFor(0)[0].account).toBe('replacement');
|
|
151
|
+
expect(findImages.calls.argsFor(1)[0].account).toBe('replacement');
|
|
152
|
+
|
|
153
|
+
component.componentWillUnmount();
|
|
154
|
+
firstImageRequest.resolve([]);
|
|
155
|
+
secondImageRequest.resolve([]);
|
|
156
|
+
await Promise.all([firstImageRequest.promise, secondImageRequest.promise]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -71,6 +71,7 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
private unmounted = false;
|
|
74
|
+
private imageRequest: AbortController;
|
|
74
75
|
private images: IDockerImage[];
|
|
75
76
|
private accounts: string[];
|
|
76
77
|
|
|
@@ -109,7 +110,7 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
109
110
|
|
|
110
111
|
private getAccountMap(images: IDockerImage[]): { [key: string]: string[] } {
|
|
111
112
|
const groupedImages = groupBy(
|
|
112
|
-
images.filter((image) => image.account),
|
|
113
|
+
images.filter((image) => image.account && image.repository),
|
|
113
114
|
'account',
|
|
114
115
|
);
|
|
115
116
|
return reduce<IDockerImage[], { [key: string]: string[] }>(
|
|
@@ -192,11 +193,12 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
public componentWillReceiveProps(nextProps: IDockerImageAndTagSelectorProps) {
|
|
196
|
+
const requestAccount = this.props.showRegistry ? this.props.account : this.props.registry;
|
|
197
|
+
const nextRequestAccount = nextProps.showRegistry ? nextProps.account : nextProps.registry;
|
|
195
198
|
if (
|
|
196
|
-
!this.images ||
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
199
|
+
(!this.images && !this.imageRequest) ||
|
|
200
|
+
this.props.showRegistry !== nextProps.showRegistry ||
|
|
201
|
+
requestAccount !== nextRequestAccount
|
|
200
202
|
) {
|
|
201
203
|
this.refreshImages(nextProps);
|
|
202
204
|
} else if (
|
|
@@ -214,6 +216,7 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
214
216
|
|
|
215
217
|
componentWillUnmount() {
|
|
216
218
|
this.unmounted = true;
|
|
219
|
+
this.imageRequest?.abort();
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
private synchronizeChanges(values: IDockerImageParts, registry: string) {
|
|
@@ -315,10 +318,14 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
315
318
|
}
|
|
316
319
|
|
|
317
320
|
private initializeImages(props: IDockerImageAndTagSelectorProps) {
|
|
318
|
-
if (this.
|
|
321
|
+
if (this.unmounted) {
|
|
319
322
|
return;
|
|
320
323
|
}
|
|
321
324
|
|
|
325
|
+
this.imageRequest?.abort();
|
|
326
|
+
const imageRequest = new AbortController();
|
|
327
|
+
this.imageRequest = imageRequest;
|
|
328
|
+
|
|
322
329
|
const { showRegistry, account, registry } = props;
|
|
323
330
|
|
|
324
331
|
const imageConfig: IFindImageParams = {
|
|
@@ -327,8 +334,11 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
327
334
|
};
|
|
328
335
|
|
|
329
336
|
this.setState({ imagesLoading: true });
|
|
330
|
-
DockerImageReader.findImages(imageConfig)
|
|
337
|
+
DockerImageReader.findImages(imageConfig, imageRequest.signal)
|
|
331
338
|
.then((images: IDockerImage[]) => {
|
|
339
|
+
if (imageRequest.signal.aborted || this.unmounted) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
332
342
|
this.images = images;
|
|
333
343
|
this.registryMap = this.getRegistryMap(this.images);
|
|
334
344
|
this.accountMap = this.getAccountMap(this.images);
|
|
@@ -337,10 +347,11 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
337
347
|
this.organizationMap = this.getOrganizationMap(this.images);
|
|
338
348
|
this.repositoryMap = this.getRepositoryMap(this.images);
|
|
339
349
|
this.organizations = this.getOrganizationsList(this.accountMap);
|
|
340
|
-
this.updateThings(props, true);
|
|
350
|
+
this.updateThings(this.props, true);
|
|
341
351
|
})
|
|
342
352
|
.finally(() => {
|
|
343
|
-
if (!this.unmounted) {
|
|
353
|
+
if (!this.unmounted && this.imageRequest === imageRequest) {
|
|
354
|
+
this.imageRequest = null;
|
|
344
355
|
this.setState({ imagesLoading: false });
|
|
345
356
|
}
|
|
346
357
|
});
|
|
@@ -354,9 +365,10 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
354
365
|
this.initializeImages(props);
|
|
355
366
|
}
|
|
356
367
|
|
|
357
|
-
private initializeAccounts(
|
|
358
|
-
let { account } = props;
|
|
368
|
+
private initializeAccounts() {
|
|
359
369
|
AccountService.listAccounts('dockerRegistry').then((allAccounts: IAccount[]) => {
|
|
370
|
+
const props = this.props;
|
|
371
|
+
let { account } = props;
|
|
360
372
|
const accounts = allAccounts.map((a: IAccount) => a.name);
|
|
361
373
|
if (this.props.showRegistry && !account) {
|
|
362
374
|
account = accounts[0];
|
|
@@ -373,7 +385,7 @@ export class DockerImageAndTagSelector extends React.Component<
|
|
|
373
385
|
|
|
374
386
|
public componentDidMount() {
|
|
375
387
|
if (!this.props.deferInitialization && (this.props.registry || this.isNew())) {
|
|
376
|
-
this.initializeAccounts(
|
|
388
|
+
this.initializeAccounts();
|
|
377
389
|
}
|
|
378
390
|
}
|
|
379
391
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RetryService } from '@spinnaker/core';
|
|
2
|
+
|
|
3
|
+
import { DockerChartImageReader, DockerImageReader } from './DockerImageReader';
|
|
4
|
+
|
|
5
|
+
describe('Docker image readers', () => {
|
|
6
|
+
it('passes owner signals through all image and tag retry sequences', async () => {
|
|
7
|
+
const retry = spyOn(RetryService, 'buildRetrySequence').and.returnValue(Promise.resolve([]));
|
|
8
|
+
const signal = new AbortController().signal;
|
|
9
|
+
const imageParams = { provider: 'dockerRegistry', account: 'registry.example' };
|
|
10
|
+
const tagParams = { ...imageParams, repository: 'example/service' };
|
|
11
|
+
|
|
12
|
+
await Promise.all([
|
|
13
|
+
DockerImageReader.findImages(imageParams, signal),
|
|
14
|
+
DockerImageReader.findTags(tagParams, signal),
|
|
15
|
+
DockerChartImageReader.findImages(imageParams, signal),
|
|
16
|
+
DockerChartImageReader.findTags(tagParams, signal),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
expect(retry.calls.allArgs().map((args) => args[4])).toEqual([signal, signal, signal, signal]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -9,7 +9,7 @@ export interface IDockerImage extends IImage {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export class DockerImageReader {
|
|
12
|
-
public static getImage(imageName: string, region: string, credentials: string):
|
|
12
|
+
public static getImage(imageName: string, region: string, credentials: string): Promise<IDockerImage> {
|
|
13
13
|
return REST('/images')
|
|
14
14
|
.path(credentials, region, imageName)
|
|
15
15
|
.query({ provider: 'docker' })
|
|
@@ -18,23 +18,25 @@ export class DockerImageReader {
|
|
|
18
18
|
.catch((): IDockerImage => null);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public static findImages(params: IFindImageParams):
|
|
21
|
+
public static findImages(params: IFindImageParams, signal?: AbortSignal): Promise<IDockerImage[]> {
|
|
22
22
|
return RetryService.buildRetrySequence<IDockerImage[]>(
|
|
23
23
|
() => REST('/images/find').query(params).get(),
|
|
24
24
|
(results: IDockerImage[]) => results.length > 0,
|
|
25
25
|
10,
|
|
26
26
|
1000,
|
|
27
|
+
signal,
|
|
27
28
|
)
|
|
28
29
|
.then((results: IDockerImage[]) => results)
|
|
29
30
|
.catch((): IDockerImage[] => []);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
public static findTags(params: IFindTagsParams):
|
|
33
|
+
public static findTags(params: IFindTagsParams, signal?: AbortSignal): Promise<string[]> {
|
|
33
34
|
return RetryService.buildRetrySequence<string[]>(
|
|
34
35
|
() => REST('/images/tags').query(params).get(),
|
|
35
36
|
(results: string[]) => results.length > 0,
|
|
36
37
|
10,
|
|
37
38
|
1000,
|
|
39
|
+
signal,
|
|
38
40
|
)
|
|
39
41
|
.then((results: string[]) => results)
|
|
40
42
|
.catch((): string[] => []);
|
|
@@ -42,7 +44,7 @@ export class DockerImageReader {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
export class DockerChartImageReader {
|
|
45
|
-
public static getImage(imageName: string, region: string, credentials: string):
|
|
47
|
+
public static getImage(imageName: string, region: string, credentials: string): Promise<IDockerImage> {
|
|
46
48
|
return REST('/charts')
|
|
47
49
|
.path(credentials, region, imageName)
|
|
48
50
|
.query({ provider: 'docker' })
|
|
@@ -51,23 +53,25 @@ export class DockerChartImageReader {
|
|
|
51
53
|
.catch((): IDockerImage => null);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
public static findImages(params: IFindImageParams):
|
|
56
|
+
public static findImages(params: IFindImageParams, signal?: AbortSignal): Promise<IDockerImage[]> {
|
|
55
57
|
return RetryService.buildRetrySequence<IDockerImage[]>(
|
|
56
58
|
() => REST('/charts/find').query(params).get(),
|
|
57
59
|
(results: IDockerImage[]) => results.length > 0,
|
|
58
60
|
10,
|
|
59
61
|
1000,
|
|
62
|
+
signal,
|
|
60
63
|
)
|
|
61
64
|
.then((results: IDockerImage[]) => results)
|
|
62
65
|
.catch((): IDockerImage[] => []);
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
public static findTags(params: IFindTagsParams):
|
|
68
|
+
public static findTags(params: IFindTagsParams, signal?: AbortSignal): Promise<string[]> {
|
|
66
69
|
return RetryService.buildRetrySequence<string[]>(
|
|
67
70
|
() => REST('/charts/tags').query(params).get(),
|
|
68
71
|
(results: string[]) => results.length > 0,
|
|
69
72
|
10,
|
|
70
73
|
1000,
|
|
74
|
+
signal,
|
|
71
75
|
)
|
|
72
76
|
.then((results: string[]) => results)
|
|
73
77
|
.catch((): string[] => []);
|
package/src/index.ts
CHANGED