@spinnaker/ecs 2026.2.4 → 2026.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/dist/ecs.module.d.ts +3 -1
  2. package/dist/ecsCluster/ecsCluster.read.service.d.ts +2 -3
  3. package/dist/iamRoles/iamRole.read.service.d.ts +1 -2
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/instance/details/EcsInstanceDetails.d.ts +50 -0
  8. package/dist/loadBalancer/loadBalancer.transformer.d.ts +1 -5
  9. package/dist/loadBalancer/targetGroup.states.d.ts +2 -1
  10. package/dist/metricAlarm/metricAlarm.read.service.d.ts +1 -2
  11. package/dist/pipeline/stages/cloneServerGroup/ecsCloneServerGroupStage.d.ts +1 -0
  12. package/dist/pipeline/stages/common/EcsServerGroupStepLabel.d.ts +7 -0
  13. package/dist/pipeline/stages/common/EcsStageConfigs.d.ts +11 -0
  14. package/dist/pipeline/stages/destroyAsg/ecsDestroyAsgStage.d.ts +1 -2
  15. package/dist/pipeline/stages/disableAsg/ecsDisableAsgStage.d.ts +1 -2
  16. package/dist/pipeline/stages/disableCluster/ecsDisableClusterStage.d.ts +1 -2
  17. package/dist/pipeline/stages/enableAsg/ecsEnableAsgStage.d.ts +1 -2
  18. package/dist/pipeline/stages/findImageFromTags/EcsFindImageFromTagsExecutionDetails.d.ts +5 -0
  19. package/dist/pipeline/stages/findImageFromTags/ecsFindImageFromTagStage.d.ts +1 -2
  20. package/dist/pipeline/stages/resizeAsg/ecsResizeAsgStage.d.ts +1 -2
  21. package/dist/pipeline/stages/scaleDownCluster/ecsScaleDownClusterStage.d.ts +1 -2
  22. package/dist/pipeline/stages/shrinkCluster/ecsShrinkClusterStage.d.ts +1 -2
  23. package/dist/placementStrategy/placementStrategy.service.d.ts +0 -1
  24. package/dist/secrets/secret.read.service.d.ts +1 -2
  25. package/dist/securityGroup/details/EcsSecurityGroupDetails.d.ts +45 -0
  26. package/dist/securityGroup/securityGroup.reader.d.ts +0 -1
  27. package/dist/securityGroup/securityGroup.transformer.d.ts +3 -2
  28. package/dist/serverGroup/configure/dockerImage.util.d.ts +3 -0
  29. package/dist/serverGroup/configure/serverGroupCommandBuilder.service.d.ts +111 -2
  30. package/dist/serverGroup/configure/serverGroupConfiguration.service.d.ts +13 -63
  31. package/dist/serverGroup/configure/wizard/EcsCloneServerGroupModal.d.ts +64 -0
  32. package/dist/serverGroup/configure/wizard/capacityProvider/CapacityProvider.d.ts +29 -4
  33. package/dist/serverGroup/configure/wizard/container/Container.d.ts +3 -3
  34. package/dist/serverGroup/configure/wizard/networking/Networking.d.ts +3 -3
  35. package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
  36. package/dist/serverGroup/configure/wizard/pages/BasicSettings.d.ts +3 -0
  37. package/dist/serverGroup/configure/wizard/pages/ContainerSettings.d.ts +3 -0
  38. package/dist/serverGroup/configure/wizard/pages/HorizontalScalingSettings.d.ts +3 -0
  39. package/dist/serverGroup/configure/wizard/pages/LoggingSettings.d.ts +3 -0
  40. package/dist/serverGroup/configure/wizard/pages/NetworkingSettings.d.ts +3 -0
  41. package/dist/serverGroup/configure/wizard/pages/ServiceDiscoverySettings.d.ts +3 -0
  42. package/dist/serverGroup/configure/wizard/pages/TaskDefinitionSettings.d.ts +3 -0
  43. package/dist/serverGroup/configure/wizard/pages/common.d.ts +8 -0
  44. package/dist/serverGroup/configure/wizard/pages/validation.d.ts +22 -0
  45. package/dist/serverGroup/configure/wizard/serviceDiscovery/ServiceDiscovery.d.ts +5 -3
  46. package/dist/serverGroup/configure/wizard/taskDefinition/TaskDefinition.d.ts +3 -3
  47. package/dist/serverGroup/details/EcsServerGroupActions.d.ts +6 -0
  48. package/dist/serverGroup/details/ecsServerGroupDetailsGetter.d.ts +3 -0
  49. package/dist/serverGroup/details/resize/EcsResizeServerGroupModal.d.ts +43 -0
  50. package/dist/serverGroup/details/resize/index.d.ts +1 -0
  51. package/dist/serverGroup/details/rollback/EcsRollbackServerGroupModal.d.ts +46 -0
  52. package/dist/serverGroup/details/rollback/index.d.ts +1 -0
  53. package/dist/serverGroup/details/sections/EcsServerGroupDetailsSections.d.ts +9 -0
  54. package/dist/serverGroup/details/sections/EcsServerGroupEventsSection.d.ts +3 -0
  55. package/dist/serverGroup/details/sections/EcsServerGroupInformationSection.d.ts +3 -0
  56. package/dist/serverGroup/events/EcsServerGroupEventsModal.d.ts +22 -0
  57. package/dist/serverGroup/events/EventsLink.d.ts +2 -0
  58. package/dist/serverGroup/events/serverGroupEventsReader.service.d.ts +1 -1
  59. package/dist/serverGroup/serverGroup.transformer.d.ts +1 -2
  60. package/dist/serviceDiscovery/serviceDiscovery.read.service.d.ts +1 -1
  61. package/package.json +3 -9
  62. package/src/ecs.module.spec.ts +86 -0
  63. package/src/ecs.module.ts +68 -84
  64. package/src/ecsCluster/ecsCluster.read.service.ts +3 -9
  65. package/src/iamRoles/iamRole.read.service.ts +1 -7
  66. package/src/index.ts +6 -0
  67. package/src/instance/details/EcsInstanceDetails.spec.tsx +426 -0
  68. package/src/instance/details/EcsInstanceDetails.tsx +474 -0
  69. package/src/loadBalancer/details/loadBalancerDetails.tsx +1 -1
  70. package/src/loadBalancer/details/targetGroupDetails.tsx +1 -1
  71. package/src/loadBalancer/loadBalancer.transformer.ts +2 -7
  72. package/src/loadBalancer/targetGroup.states.ts +55 -53
  73. package/src/metricAlarm/metricAlarm.read.service.ts +1 -7
  74. package/src/pipeline/stages/cloneServerGroup/ecsCloneServerGroupStage.js +22 -94
  75. package/src/pipeline/stages/common/EcsServerGroupStepLabel.tsx +16 -0
  76. package/src/pipeline/stages/common/EcsStageConfigs.tsx +587 -0
  77. package/src/pipeline/stages/destroyAsg/ecsDestroyAsgStage.js +29 -65
  78. package/src/pipeline/stages/disableAsg/ecsDisableAsgStage.js +27 -70
  79. package/src/pipeline/stages/disableCluster/ecsDisableClusterStage.js +21 -84
  80. package/src/pipeline/stages/enableAsg/ecsEnableAsgStage.js +22 -73
  81. package/src/pipeline/stages/findImageFromTags/EcsFindImageFromTagsExecutionDetails.tsx +40 -0
  82. package/src/pipeline/stages/findImageFromTags/ecsFindImageFromTagStage.js +12 -24
  83. package/src/pipeline/stages/resizeAsg/ecsResizeAsgStage.js +30 -128
  84. package/src/pipeline/stages/scaleDownCluster/ecsScaleDownClusterStage.js +24 -78
  85. package/src/pipeline/stages/shrinkCluster/ecsShrinkClusterStage.js +19 -73
  86. package/src/placementStrategy/placementStrategy.service.ts +0 -6
  87. package/src/secrets/secret.read.service.ts +1 -7
  88. package/src/securityGroup/details/EcsSecurityGroupDetails.spec.tsx +245 -0
  89. package/src/securityGroup/details/EcsSecurityGroupDetails.tsx +285 -0
  90. package/src/securityGroup/securityGroup.reader.ts +0 -5
  91. package/src/securityGroup/securityGroup.transformer.js +5 -13
  92. package/src/serverGroup/configure/dockerImage.util.ts +29 -0
  93. package/src/serverGroup/configure/serverGroupCommandBuilder.service.js +215 -278
  94. package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.ts +83 -0
  95. package/src/serverGroup/configure/serverGroupConfiguration.service.ts +16 -633
  96. package/src/serverGroup/configure/wizard/EcsCloneServerGroupModal.spec.tsx +2078 -0
  97. package/src/serverGroup/configure/wizard/EcsCloneServerGroupModal.tsx +712 -0
  98. package/src/serverGroup/configure/wizard/capacityProvider/CapacityProvider.tsx +98 -49
  99. package/src/serverGroup/configure/wizard/container/Container.spec.tsx +20 -8
  100. package/src/serverGroup/configure/wizard/container/Container.tsx +53 -24
  101. package/src/serverGroup/configure/wizard/networking/Networking.tsx +60 -38
  102. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +290 -0
  103. package/src/serverGroup/configure/wizard/pages/BasicSettings.tsx +101 -0
  104. package/src/serverGroup/configure/wizard/pages/ContainerSettings.tsx +10 -0
  105. package/src/serverGroup/configure/wizard/pages/HorizontalScalingSettings.tsx +137 -0
  106. package/src/serverGroup/configure/wizard/pages/LoggingSettings.tsx +60 -0
  107. package/src/serverGroup/configure/wizard/pages/NetworkingSettings.tsx +10 -0
  108. package/src/serverGroup/configure/wizard/pages/ServiceDiscoverySettings.tsx +10 -0
  109. package/src/serverGroup/configure/wizard/pages/TaskDefinitionSettings.tsx +59 -0
  110. package/src/serverGroup/configure/wizard/pages/common.ts +10 -0
  111. package/src/serverGroup/configure/wizard/pages/validation.tsx +190 -0
  112. package/src/serverGroup/configure/wizard/serviceDiscovery/ServiceDiscovery.tsx +83 -34
  113. package/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.spec.tsx +21 -8
  114. package/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.tsx +63 -33
  115. package/src/serverGroup/details/EcsServerGroupActions.spec.tsx +244 -0
  116. package/src/serverGroup/details/EcsServerGroupActions.tsx +152 -0
  117. package/src/serverGroup/details/ecsServerGroupDetailsGetter.spec.ts +110 -0
  118. package/src/serverGroup/details/ecsServerGroupDetailsGetter.ts +73 -0
  119. package/src/serverGroup/details/resize/EcsResizeServerGroupModal.spec.tsx +130 -0
  120. package/src/serverGroup/details/resize/EcsResizeServerGroupModal.tsx +263 -0
  121. package/src/serverGroup/details/resize/index.ts +1 -0
  122. package/src/serverGroup/details/rollback/EcsRollbackServerGroupModal.spec.tsx +173 -0
  123. package/src/serverGroup/details/rollback/EcsRollbackServerGroupModal.tsx +285 -0
  124. package/src/serverGroup/details/rollback/index.ts +1 -0
  125. package/src/serverGroup/details/sections/EcsServerGroupDetailsSections.spec.tsx +130 -0
  126. package/src/serverGroup/details/sections/EcsServerGroupDetailsSections.tsx +172 -0
  127. package/src/serverGroup/details/sections/EcsServerGroupEventsSection.tsx +14 -0
  128. package/src/serverGroup/details/sections/EcsServerGroupInformationSection.tsx +25 -0
  129. package/src/serverGroup/events/EcsServerGroupEventsModal.spec.tsx +109 -0
  130. package/src/serverGroup/events/EcsServerGroupEventsModal.tsx +112 -0
  131. package/src/serverGroup/events/EventsLink.tsx +18 -0
  132. package/src/serverGroup/events/serverGroupEventsReader.service.spec.ts +41 -0
  133. package/src/serverGroup/events/serverGroupEventsReader.service.ts +2 -8
  134. package/src/serverGroup/serverGroup.transformer.spec.ts +10 -24
  135. package/src/serverGroup/serverGroup.transformer.ts +1 -5
  136. package/src/serviceDiscovery/serviceDiscovery.read.service.ts +1 -1
  137. package/dist/common/common.module.d.ts +0 -1
  138. package/dist/common/footer.component.d.ts +0 -1
  139. package/dist/instance/details/instance.details.controller.d.ts +0 -2
  140. package/dist/securityGroup/details/securityGroupDetail.controller.d.ts +0 -2
  141. package/dist/securityGroup/securityGroup.module.d.ts +0 -1
  142. package/dist/serverGroup/configure/wizard/CloneServerGroup.ecs.controller.d.ts +0 -2
  143. package/dist/serverGroup/configure/wizard/advancedSettings/advancedSettings.component.d.ts +0 -2
  144. package/dist/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component.d.ts +0 -2
  145. package/dist/serverGroup/configure/wizard/location/ServerGroupBasicSettings.controller.d.ts +0 -2
  146. package/dist/serverGroup/configure/wizard/logging/logging.component.d.ts +0 -1
  147. package/dist/serverGroup/details/resize/resizeCapacity.component.d.ts +0 -1
  148. package/dist/serverGroup/details/resize/resizeServerGroup.controller.d.ts +0 -2
  149. package/dist/serverGroup/details/rollback/rollbackServerGroup.controller.d.ts +0 -2
  150. package/dist/serverGroup/details/serverGroupDetails.ecs.controller.d.ts +0 -2
  151. package/dist/serverGroup/details/serverGroupDetails.module.d.ts +0 -1
  152. package/dist/serverGroup/events/events.component.d.ts +0 -3
  153. package/dist/serverGroup/events/events.controller.d.ts +0 -19
  154. package/dist/serverGroup/serverGroup.module.d.ts +0 -1
  155. package/src/common/common.module.ts +0 -6
  156. package/src/common/footer.component.ts +0 -29
  157. package/src/instance/details/instance.details.controller.js +0 -356
  158. package/src/instance/details/instanceDetails.html +0 -204
  159. package/src/pipeline/stages/cloneServerGroup/cloneServerGroupStage.html +0 -101
  160. package/src/pipeline/stages/cloneServerGroup/cloneServerGroupStepLabel.html +0 -1
  161. package/src/pipeline/stages/destroyAsg/destroyAsgStage.html +0 -9
  162. package/src/pipeline/stages/destroyAsg/destroyAsgStepLabel.html +0 -1
  163. package/src/pipeline/stages/disableAsg/disableAsgStage.html +0 -11
  164. package/src/pipeline/stages/disableAsg/disableAsgStepLabel.html +0 -1
  165. package/src/pipeline/stages/disableCluster/disableClusterStage.html +0 -26
  166. package/src/pipeline/stages/enableAsg/enableAsgStage.html +0 -11
  167. package/src/pipeline/stages/enableAsg/enableAsgStepLabel.html +0 -1
  168. package/src/pipeline/stages/findImageFromTags/findImageFromTagsExecutionDetails.html +0 -36
  169. package/src/pipeline/stages/findImageFromTags/findImageFromTagsStage.html +0 -9
  170. package/src/pipeline/stages/resizeAsg/resizeAsgStage.html +0 -87
  171. package/src/pipeline/stages/resizeAsg/resizeAsgStepLabel.html +0 -1
  172. package/src/pipeline/stages/scaleDownCluster/scaleDownClusterStage.html +0 -35
  173. package/src/pipeline/stages/shrinkCluster/shrinkClusterStage.html +0 -34
  174. package/src/securityGroup/details/securityGroupDetail.controller.js +0 -147
  175. package/src/securityGroup/details/securityGroupDetail.html +0 -104
  176. package/src/securityGroup/securityGroup.module.ts +0 -12
  177. package/src/serverGroup/configure/wizard/CloneServerGroup.ecs.controller.js +0 -259
  178. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.component.html +0 -194
  179. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.component.js +0 -17
  180. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.html +0 -10
  181. package/src/serverGroup/configure/wizard/capacityProvider/capacityProvider.html +0 -11
  182. package/src/serverGroup/configure/wizard/container/container.html +0 -9
  183. package/src/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component.html +0 -118
  184. package/src/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component.js +0 -20
  185. package/src/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.html +0 -13
  186. package/src/serverGroup/configure/wizard/location/ServerGroupBasicSettings.controller.js +0 -41
  187. package/src/serverGroup/configure/wizard/location/basicSettings.html +0 -137
  188. package/src/serverGroup/configure/wizard/logging/logging.component.html +0 -29
  189. package/src/serverGroup/configure/wizard/logging/logging.component.ts +0 -12
  190. package/src/serverGroup/configure/wizard/logging/logging.html +0 -7
  191. package/src/serverGroup/configure/wizard/networking/networking.html +0 -9
  192. package/src/serverGroup/configure/wizard/serverGroupWizard.html +0 -55
  193. package/src/serverGroup/configure/wizard/serviceDiscovery/serviceDiscovery.html +0 -11
  194. package/src/serverGroup/configure/wizard/taskDefinition/taskDefinition.html +0 -51
  195. package/src/serverGroup/configure/wizard/templateSelection/templateSelection.html +0 -9
  196. package/src/serverGroup/configure/wizard/verticalScaling/verticalScaling.html +0 -10
  197. package/src/serverGroup/details/resize/resizeCapacity.component.html +0 -94
  198. package/src/serverGroup/details/resize/resizeCapacity.component.ts +0 -14
  199. package/src/serverGroup/details/resize/resizeServerGroup.controller.js +0 -85
  200. package/src/serverGroup/details/resize/resizeServerGroup.html +0 -28
  201. package/src/serverGroup/details/rollback/rollbackServerGroup.controller.js +0 -97
  202. package/src/serverGroup/details/rollback/rollbackServerGroup.html +0 -67
  203. package/src/serverGroup/details/serverGroupDetails.ecs.controller.js +0 -366
  204. package/src/serverGroup/details/serverGroupDetails.html +0 -216
  205. package/src/serverGroup/details/serverGroupDetails.module.ts +0 -5
  206. package/src/serverGroup/events/events.component.ts +0 -35
  207. package/src/serverGroup/events/events.controller.ts +0 -45
  208. package/src/serverGroup/events/events.html +0 -36
  209. package/src/serverGroup/serverGroup.module.ts +0 -6
@@ -0,0 +1,130 @@
1
+ import { shallow } from 'enzyme';
2
+ import React from 'react';
3
+
4
+ import {
5
+ PlatformHealthOverride,
6
+ ReactModal,
7
+ SpinFormik,
8
+ TaskMonitorWrapper,
9
+ TaskReason,
10
+ UserVerification,
11
+ } from '@spinnaker/core';
12
+
13
+ import { EcsResizeServerGroupModal, validateEcsResizeValues } from './index';
14
+
15
+ describe('EcsResizeServerGroupModal', () => {
16
+ const serverGroup = {
17
+ account: 'test-account',
18
+ capacity: { desired: 4, max: 8, min: 2 },
19
+ name: 'fnord-main-v004',
20
+ region: 'eu-west-1',
21
+ };
22
+
23
+ function application(attributes: any = {}) {
24
+ return {
25
+ attributes,
26
+ serverGroups: { refresh: jasmine.createSpy('refresh') },
27
+ } as any;
28
+ }
29
+
30
+ function props(app = application()) {
31
+ return {
32
+ application: app,
33
+ closeModal: jasmine.createSpy('closeModal'),
34
+ dismissModal: jasmine.createSpy('dismissModal'),
35
+ serverGroup: serverGroup as any,
36
+ };
37
+ }
38
+
39
+ it('validates non-negative min, max, and desired capacities in range', () => {
40
+ expect(validateEcsResizeValues({ capacity: { desired: 4, max: 8, min: -1 } })).toEqual({
41
+ capacity: { min: 'Min cannot be negative' },
42
+ });
43
+ expect(validateEcsResizeValues({ capacity: { desired: 4, max: 1, min: 2 } })).toEqual({
44
+ capacity: { max: 'Max cannot be smaller than Min', min: 'Min cannot be larger than Max' },
45
+ });
46
+ expect(validateEcsResizeValues({ capacity: { desired: 1, max: 8, min: 2 } })).toEqual({
47
+ capacity: { desired: 'Desired cannot be smaller than Min' },
48
+ });
49
+ expect(validateEcsResizeValues({ capacity: { desired: 9, max: 8, min: 2 } })).toEqual({
50
+ capacity: { desired: 'Desired cannot be larger than Max' },
51
+ });
52
+ expect(validateEcsResizeValues({ capacity: { desired: 4, max: 8, min: 2 } })).toEqual({});
53
+ });
54
+
55
+ it('submits the exact shared resize writer contract through its task monitor', () => {
56
+ const app = application({ platformHealthOnly: true, platformHealthOnlyShowOverride: true });
57
+ const writer = { resizeServerGroup: jasmine.createSpy('resizeServerGroup').and.returnValue(Promise.resolve()) };
58
+ const component = new EcsResizeServerGroupModal(props(app));
59
+ (component as any).context = { services: { serverGroupWriter: writer } };
60
+ component.setState = ((update: any) => {
61
+ component.state = { ...component.state, ...(typeof update === 'function' ? update(component.state) : update) };
62
+ }) as any;
63
+ component.setState({ verified: true });
64
+ spyOn(component.state.taskMonitor, 'submit').and.callFake((submitMethod: any) => submitMethod());
65
+
66
+ (component as any).submit({
67
+ capacity: { desired: 6, max: 10, min: 3 },
68
+ interestingHealthProviderNames: ['Ecs'],
69
+ reason: ' preserve this resize reason exactly ',
70
+ });
71
+
72
+ expect(component.state.taskMonitor.submit).toHaveBeenCalled();
73
+ expect(writer.resizeServerGroup).toHaveBeenCalledOnceWith(serverGroup, app, {
74
+ capacity: { desired: 6, max: 10, min: 3 },
75
+ interestingHealthProviderNames: ['Ecs'],
76
+ reason: ' preserve this resize reason exactly ',
77
+ });
78
+ });
79
+
80
+ it('does not submit invalid or unverified resize commands', () => {
81
+ const writer = { resizeServerGroup: jasmine.createSpy('resizeServerGroup') };
82
+ const component = new EcsResizeServerGroupModal(props());
83
+ (component as any).context = { services: { serverGroupWriter: writer } };
84
+ spyOn(component.state.taskMonitor, 'submit');
85
+
86
+ (component as any).submit({ capacity: { desired: 9, max: 8, min: 2 } });
87
+ component.state.verified = true;
88
+ (component as any).submit({ capacity: { desired: 9, max: 8, min: 2 } });
89
+
90
+ expect(component.state.taskMonitor.submit).not.toHaveBeenCalled();
91
+ expect(writer.resizeServerGroup).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it('renders task, capacity, verification, reason, and ECS platform-health controls', () => {
95
+ const app = application({ platformHealthOnly: true, platformHealthOnlyShowOverride: true });
96
+ const wrapper = shallow(<EcsResizeServerGroupModal {...props(app)} />);
97
+ const formik = wrapper.find(SpinFormik);
98
+ expect(wrapper.find(TaskMonitorWrapper).prop('monitor')).toBe((wrapper.instance() as any).state.taskMonitor);
99
+
100
+ const content = shallow(
101
+ <div>
102
+ {(formik.prop('render') as any)({
103
+ errors: {},
104
+ isValid: true,
105
+ setFieldValue: jasmine.createSpy('setFieldValue'),
106
+ values: formik.prop('initialValues'),
107
+ })}
108
+ </div>,
109
+ );
110
+
111
+ expect(content.find(UserVerification).prop('account')).toBe('test-account');
112
+ expect(content.find(TaskReason).exists()).toBe(true);
113
+ expect(content.find('[name="capacity.min"]').exists()).toBe(true);
114
+ expect(content.find('[name="capacity.max"]').exists()).toBe(true);
115
+ expect(content.find('[name="capacity.desired"]').exists()).toBe(true);
116
+ expect(content.find(PlatformHealthOverride).props()).toEqual(
117
+ jasmine.objectContaining({ interestingHealthProviderNames: ['Ecs'], platformHealthType: 'Ecs' }),
118
+ );
119
+ });
120
+
121
+ it('exports a show primitive for later actions integration', () => {
122
+ const show = spyOn(ReactModal, 'show').and.returnValue(Promise.resolve() as any);
123
+ const modalProps = props();
124
+ const runtimeServices = {} as any;
125
+
126
+ EcsResizeServerGroupModal.show(modalProps, runtimeServices);
127
+
128
+ expect(show).toHaveBeenCalledOnceWith(EcsResizeServerGroupModal, modalProps, undefined, runtimeServices);
129
+ });
130
+ });
@@ -0,0 +1,263 @@
1
+ import React from 'react';
2
+ import { Modal } from 'react-bootstrap';
3
+
4
+ import type {
5
+ Application,
6
+ DeckRuntimeServices,
7
+ IModalComponentProps,
8
+ IServerGroup,
9
+ IServerGroupJob,
10
+ } from '@spinnaker/core';
11
+ import {
12
+ DeckRuntimeContext,
13
+ FormikFormField,
14
+ MinMaxDesiredChanges,
15
+ ModalClose,
16
+ noop,
17
+ NumberInput,
18
+ PlatformHealthOverride,
19
+ ReactModal,
20
+ SpinFormik,
21
+ TaskMonitor,
22
+ TaskMonitorWrapper,
23
+ TaskReason,
24
+ UserVerification,
25
+ ValidationMessage,
26
+ } from '@spinnaker/core';
27
+
28
+ export interface IEcsResizeServerGroupModalProps extends IModalComponentProps {
29
+ application: Application;
30
+ serverGroup: IServerGroup;
31
+ }
32
+
33
+ export interface IEcsResizeCapacity {
34
+ desired: number;
35
+ max: number;
36
+ min: number;
37
+ }
38
+
39
+ export interface IEcsResizeServerGroupValues {
40
+ capacity: IEcsResizeCapacity;
41
+ interestingHealthProviderNames?: string[];
42
+ reason?: string;
43
+ }
44
+
45
+ export interface IEcsResizeServerGroupJob extends IServerGroupJob {
46
+ capacity: IEcsResizeCapacity;
47
+ interestingHealthProviderNames?: string[];
48
+ reason?: string;
49
+ }
50
+
51
+ export interface IEcsResizeServerGroupErrors {
52
+ capacity?: Partial<Record<keyof IEcsResizeCapacity, string>>;
53
+ }
54
+
55
+ interface IEcsResizeServerGroupModalState {
56
+ initialValues: IEcsResizeServerGroupValues;
57
+ taskMonitor: TaskMonitor;
58
+ verified: boolean;
59
+ }
60
+
61
+ export function validateEcsResizeValues(values: IEcsResizeServerGroupValues): IEcsResizeServerGroupErrors {
62
+ const { desired, max, min } = values.capacity;
63
+ const capacity: IEcsResizeServerGroupErrors['capacity'] = {};
64
+
65
+ if (!Number.isFinite(min) || min < 0) {
66
+ capacity.min = 'Min cannot be negative';
67
+ }
68
+ if (!Number.isFinite(max) || max < 0) {
69
+ capacity.max = 'Max cannot be negative';
70
+ }
71
+ if (!Number.isFinite(desired) || desired < 0) {
72
+ capacity.desired = 'Desired cannot be negative';
73
+ }
74
+ if (Object.keys(capacity).length > 0) {
75
+ return { capacity };
76
+ }
77
+
78
+ if (min > max) {
79
+ capacity.min = 'Min cannot be larger than Max';
80
+ capacity.max = 'Max cannot be smaller than Min';
81
+ } else if (desired < min) {
82
+ capacity.desired = 'Desired cannot be smaller than Min';
83
+ } else if (desired > max) {
84
+ capacity.desired = 'Desired cannot be larger than Max';
85
+ }
86
+
87
+ return Object.keys(capacity).length > 0 ? { capacity } : {};
88
+ }
89
+
90
+ export class EcsResizeServerGroupModal extends React.Component<
91
+ IEcsResizeServerGroupModalProps,
92
+ IEcsResizeServerGroupModalState
93
+ > {
94
+ public static contextType = DeckRuntimeContext;
95
+ public declare context: React.ContextType<typeof DeckRuntimeContext>;
96
+
97
+ public static defaultProps: Partial<IEcsResizeServerGroupModalProps> = {
98
+ closeModal: noop,
99
+ dismissModal: noop,
100
+ };
101
+
102
+ public static show(
103
+ props: IEcsResizeServerGroupModalProps,
104
+ runtimeServices: DeckRuntimeServices,
105
+ ): Promise<IEcsResizeServerGroupJob> {
106
+ return ReactModal.show(EcsResizeServerGroupModal, props, undefined, runtimeServices);
107
+ }
108
+
109
+ public constructor(props: IEcsResizeServerGroupModalProps) {
110
+ super(props);
111
+ const { application, serverGroup } = props;
112
+ const attributes = application.attributes || {};
113
+ this.state = {
114
+ initialValues: {
115
+ capacity: {
116
+ desired: Number(serverGroup.capacity.desired),
117
+ max: Number(serverGroup.capacity.max),
118
+ min: Number(serverGroup.capacity.min),
119
+ },
120
+ interestingHealthProviderNames:
121
+ attributes.platformHealthOnlyShowOverride && attributes.platformHealthOnly ? ['Ecs'] : undefined,
122
+ },
123
+ taskMonitor: new TaskMonitor({
124
+ application,
125
+ title: `Resizing ${serverGroup.name}`,
126
+ onDismiss: () => this.props.dismissModal(),
127
+ onTaskComplete: () => application.serverGroups.refresh(),
128
+ }),
129
+ verified: false,
130
+ };
131
+ }
132
+
133
+ private close = (): void => this.props.dismissModal();
134
+
135
+ private submit = (values: IEcsResizeServerGroupValues): void => {
136
+ if (!this.state.verified || Object.keys(validateEcsResizeValues(values)).length > 0) {
137
+ return;
138
+ }
139
+
140
+ const { application, serverGroup } = this.props;
141
+ const command: IEcsResizeServerGroupJob = {
142
+ capacity: values.capacity,
143
+ interestingHealthProviderNames: values.interestingHealthProviderNames,
144
+ reason: values.reason,
145
+ };
146
+ this.state.taskMonitor.submit(() =>
147
+ this.context.services.serverGroupWriter.resizeServerGroup(serverGroup, application, command),
148
+ );
149
+ };
150
+
151
+ private capacityError(errors: IEcsResizeServerGroupErrors): string | undefined {
152
+ const capacity = errors.capacity;
153
+ return capacity && ([capacity.min, capacity.max, capacity.desired].find(Boolean) as string);
154
+ }
155
+
156
+ public render(): JSX.Element {
157
+ const { application, serverGroup } = this.props;
158
+
159
+ return (
160
+ <>
161
+ <TaskMonitorWrapper monitor={this.state.taskMonitor} />
162
+ <SpinFormik<IEcsResizeServerGroupValues>
163
+ initialValues={this.state.initialValues}
164
+ onSubmit={this.submit}
165
+ validate={validateEcsResizeValues}
166
+ render={(formik) => {
167
+ const error = this.capacityError(formik.errors as IEcsResizeServerGroupErrors);
168
+ return (
169
+ <>
170
+ <ModalClose dismiss={this.close} />
171
+ <Modal.Header>
172
+ <Modal.Title>Resize {serverGroup.name}</Modal.Title>
173
+ </Modal.Header>
174
+ <Modal.Body>
175
+ <form className="form-horizontal">
176
+ <div className="form-group bold">
177
+ <div className="col-sm-2 col-sm-offset-3">Min</div>
178
+ <div className="col-sm-2">Max</div>
179
+ <div className="col-sm-2">Desired</div>
180
+ </div>
181
+ <div className="form-group">
182
+ <div className="col-sm-3 sm-label-right">Current</div>
183
+ {(['min', 'max', 'desired'] as Array<keyof IEcsResizeCapacity>).map((field) => (
184
+ <div className="col-sm-2" key={field}>
185
+ <input
186
+ className="form-control input-sm"
187
+ disabled={true}
188
+ type="number"
189
+ value={serverGroup.capacity[field]}
190
+ />
191
+ </div>
192
+ ))}
193
+ </div>
194
+ <div className="form-group">
195
+ <div className="col-sm-3 sm-label-right">Resize to</div>
196
+ {(['min', 'max', 'desired'] as Array<keyof IEcsResizeCapacity>).map((field) => (
197
+ <div className="col-sm-2" key={field}>
198
+ <FormikFormField
199
+ name={`capacity.${field}`}
200
+ input={(props) => <NumberInput {...props} min={0} />}
201
+ layout={({ input }) => <>{input}</>}
202
+ touched={true}
203
+ />
204
+ </div>
205
+ ))}
206
+ </div>
207
+ {error && (
208
+ <div className="col-sm-9 col-sm-offset-3">
209
+ <ValidationMessage message={error} type="error" />
210
+ </div>
211
+ )}
212
+
213
+ <div className="form-group">
214
+ <div className="col-sm-3 sm-label-right">Changes</div>
215
+ <div className="col-sm-9 sm-control-field">
216
+ <MinMaxDesiredChanges current={serverGroup.capacity} next={formik.values.capacity} />
217
+ </div>
218
+ </div>
219
+
220
+ {application.attributes?.platformHealthOnlyShowOverride && (
221
+ <div className="form-group">
222
+ <div className="col-sm-10 col-sm-offset-1">
223
+ <PlatformHealthOverride
224
+ interestingHealthProviderNames={formik.values.interestingHealthProviderNames}
225
+ onChange={(names) => formik.setFieldValue('interestingHealthProviderNames', names)}
226
+ platformHealthType="Ecs"
227
+ showHelpDetails={true}
228
+ />
229
+ </div>
230
+ </div>
231
+ )}
232
+
233
+ <TaskReason
234
+ reason={formik.values.reason}
235
+ onChange={(reason) => formik.setFieldValue('reason', reason)}
236
+ />
237
+ </form>
238
+ </Modal.Body>
239
+ <Modal.Footer>
240
+ <UserVerification
241
+ account={serverGroup.account}
242
+ onValidChange={(verified) => this.setState({ verified })}
243
+ />
244
+ <button className="btn btn-default" onClick={this.close} type="button">
245
+ Cancel
246
+ </button>
247
+ <button
248
+ className="btn btn-primary"
249
+ disabled={!this.state.verified || !formik.isValid}
250
+ onClick={() => this.submit(formik.values)}
251
+ type="submit"
252
+ >
253
+ Submit
254
+ </button>
255
+ </Modal.Footer>
256
+ </>
257
+ );
258
+ }}
259
+ />
260
+ </>
261
+ );
262
+ }
263
+ }
@@ -0,0 +1 @@
1
+ export * from './EcsResizeServerGroupModal';
@@ -0,0 +1,173 @@
1
+ import { shallow } from 'enzyme';
2
+ import React from 'react';
3
+
4
+ import {
5
+ PlatformHealthOverride,
6
+ ReactModal,
7
+ SpinFormik,
8
+ TaskMonitorWrapper,
9
+ TaskReason,
10
+ UserVerification,
11
+ } from '@spinnaker/core';
12
+
13
+ import { EcsRollbackServerGroupModal, getEcsRollbackTargets, validateEcsRollbackValues } from './index';
14
+
15
+ describe('EcsRollbackServerGroupModal', () => {
16
+ const serverGroup = {
17
+ account: 'test-account',
18
+ capacity: { desired: 4, max: 8, min: 2 },
19
+ cluster: 'fnord-main',
20
+ moniker: { app: 'fnord', cluster: 'fnord-main' },
21
+ name: 'fnord-main-v004',
22
+ region: 'eu-west-1',
23
+ };
24
+
25
+ function application(attributes: any = {}) {
26
+ return {
27
+ attributes,
28
+ name: 'fnord',
29
+ serverGroups: {
30
+ data: [
31
+ target('fnord-main-v003'),
32
+ target('fnord-main-v002', { isDisabled: false }),
33
+ target('other-main-v003', { cluster: 'other-main', moniker: { app: 'fnord', cluster: 'other-main' } }),
34
+ target('fnord-main-v003-other-account', { account: 'other-account' }),
35
+ target('fnord-main-v003-other-region', { region: 'us-east-1' }),
36
+ target('fnord-main-v003-other-app', { moniker: { app: 'other', cluster: 'fnord-main' } }),
37
+ ],
38
+ refresh: jasmine.createSpy('refresh'),
39
+ },
40
+ } as any;
41
+ }
42
+
43
+ function target(name: string, overrides: any = {}) {
44
+ return {
45
+ account: 'test-account',
46
+ cluster: 'fnord-main',
47
+ isDisabled: true,
48
+ moniker: { app: 'fnord', cluster: 'fnord-main' },
49
+ name,
50
+ region: 'eu-west-1',
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function props(app = application()) {
56
+ return {
57
+ application: app,
58
+ closeModal: jasmine.createSpy('closeModal'),
59
+ dismissModal: jasmine.createSpy('dismissModal'),
60
+ serverGroup: serverGroup as any,
61
+ };
62
+ }
63
+
64
+ it('finds only disabled rollback targets in the same application, cluster, account, and region', () => {
65
+ expect(getEcsRollbackTargets(application(), serverGroup as any).map((candidate) => candidate.name)).toEqual([
66
+ 'fnord-main-v003',
67
+ ]);
68
+ });
69
+
70
+ it('requires a target and a health threshold from 0 through 100 while keeping reason optional', () => {
71
+ expect(validateEcsRollbackValues({ restoreServerGroupName: '', targetHealthyRollbackPercentage: 100 })).toEqual({
72
+ restoreServerGroupName: 'Select a server group to restore',
73
+ });
74
+ expect(
75
+ validateEcsRollbackValues({ restoreServerGroupName: 'fnord-main-v003', targetHealthyRollbackPercentage: -1 }),
76
+ ).toEqual({ targetHealthyRollbackPercentage: 'Healthy threshold must be between 0 and 100' });
77
+ expect(
78
+ validateEcsRollbackValues({ restoreServerGroupName: 'fnord-main-v003', targetHealthyRollbackPercentage: 101 }),
79
+ ).toEqual({ targetHealthyRollbackPercentage: 'Healthy threshold must be between 0 and 100' });
80
+ expect(
81
+ validateEcsRollbackValues({ restoreServerGroupName: 'fnord-main-v003', targetHealthyRollbackPercentage: 95 }),
82
+ ).toEqual({});
83
+ expect(
84
+ validateEcsRollbackValues({ restoreServerGroupName: 'fnord-main-v999', targetHealthyRollbackPercentage: 95 }, [
85
+ 'fnord-main-v003',
86
+ ]),
87
+ ).toEqual({ restoreServerGroupName: 'Select an eligible server group to restore' });
88
+ });
89
+
90
+ it('does not submit a rollback target outside the eligible disabled set', () => {
91
+ const app = application();
92
+ const writer = { rollbackServerGroup: jasmine.createSpy('rollbackServerGroup') };
93
+ const component = new EcsRollbackServerGroupModal(props(app));
94
+ (component as any).context = { services: { serverGroupWriter: writer } };
95
+ component.state.verified = true;
96
+ spyOn(component.state.taskMonitor, 'submit');
97
+
98
+ (component as any).submit({
99
+ restoreServerGroupName: 'fnord-main-v999',
100
+ targetHealthyRollbackPercentage: 95,
101
+ });
102
+
103
+ expect(component.state.taskMonitor.submit).not.toHaveBeenCalled();
104
+ expect(writer.rollbackServerGroup).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it('submits the exact shared rollback writer contract through its task monitor', () => {
108
+ const app = application({ platformHealthOnly: true, platformHealthOnlyShowOverride: true });
109
+ const writer = { rollbackServerGroup: jasmine.createSpy('rollbackServerGroup').and.returnValue(Promise.resolve()) };
110
+ const component = new EcsRollbackServerGroupModal(props(app));
111
+ (component as any).context = { services: { serverGroupWriter: writer } };
112
+ component.setState = ((update: any) => {
113
+ component.state = { ...component.state, ...(typeof update === 'function' ? update(component.state) : update) };
114
+ }) as any;
115
+ component.setState({ verified: true });
116
+ spyOn(component.state.taskMonitor, 'submit').and.callFake((submitMethod: any) => submitMethod());
117
+
118
+ (component as any).submit({
119
+ interestingHealthProviderNames: ['Ecs'],
120
+ reason: ' preserve this reason exactly ',
121
+ restoreServerGroupName: 'fnord-main-v003',
122
+ targetHealthyRollbackPercentage: 95,
123
+ });
124
+
125
+ expect(component.state.taskMonitor.submit).toHaveBeenCalled();
126
+ expect(writer.rollbackServerGroup).toHaveBeenCalledOnceWith(serverGroup, app, {
127
+ interestingHealthProviderNames: ['Ecs'],
128
+ platformHealthOnlyShowOverride: true,
129
+ reason: ' preserve this reason exactly ',
130
+ rollbackContext: {
131
+ restoreServerGroupName: 'fnord-main-v003',
132
+ rollbackServerGroupName: 'fnord-main-v004',
133
+ targetHealthyRollbackPercentage: 95,
134
+ },
135
+ rollbackType: 'EXPLICIT',
136
+ });
137
+ });
138
+
139
+ it('renders task, verification, reason, threshold, and ECS platform-health controls', () => {
140
+ const app = application({ platformHealthOnly: true, platformHealthOnlyShowOverride: true });
141
+ const wrapper = shallow(<EcsRollbackServerGroupModal {...props(app)} />);
142
+ const formik = wrapper.find(SpinFormik);
143
+ expect(wrapper.find(TaskMonitorWrapper).prop('monitor')).toBe((wrapper.instance() as any).state.taskMonitor);
144
+
145
+ const content = shallow(
146
+ <div>
147
+ {(formik.prop('render') as any)({
148
+ errors: {},
149
+ isValid: true,
150
+ setFieldValue: jasmine.createSpy('setFieldValue'),
151
+ values: formik.prop('initialValues'),
152
+ })}
153
+ </div>,
154
+ );
155
+
156
+ expect(content.find(UserVerification).prop('account')).toBe('test-account');
157
+ expect(content.find(TaskReason).exists()).toBe(true);
158
+ expect(content.find('input[name="targetHealthyRollbackPercentage"]').exists()).toBe(true);
159
+ expect(content.find(PlatformHealthOverride).props()).toEqual(
160
+ jasmine.objectContaining({ interestingHealthProviderNames: ['Ecs'], platformHealthType: 'Ecs' }),
161
+ );
162
+ });
163
+
164
+ it('exports a show primitive for later actions integration', () => {
165
+ const show = spyOn(ReactModal, 'show').and.returnValue(Promise.resolve() as any);
166
+ const modalProps = props();
167
+ const runtimeServices = {} as any;
168
+
169
+ EcsRollbackServerGroupModal.show(modalProps, runtimeServices);
170
+
171
+ expect(show).toHaveBeenCalledOnceWith(EcsRollbackServerGroupModal, modalProps, undefined, runtimeServices);
172
+ });
173
+ });