@spinnaker/google 2026.3.2 → 2026.4.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.
Files changed (108) hide show
  1. package/dist/autoscalingPolicy/GceAutoScalingFieldLayout.d.ts +2 -2
  2. package/dist/domain/loadBalancer.d.ts +6 -3
  3. package/dist/image/ImageSelect.d.ts +1 -1
  4. package/dist/index.js +159 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/loadBalancer/configure/common/gceLoadBalancerModels.d.ts +13 -2
  7. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerEditor.d.ts +3 -0
  8. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerModal.d.ts +1 -1
  9. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.d.ts +30 -0
  10. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.d.ts +53 -0
  11. package/dist/loadBalancer/httpLoadBalancerUtils.service.d.ts +3 -1
  12. package/dist/serverGroup/configure/wizard/GceCloneServerGroupModal.d.ts +3 -0
  13. package/dist/serverGroup/configure/wizard/GceServerGroupWizard.types.d.ts +1 -0
  14. package/dist/serverGroup/configure/wizard/customInstance/CustomInstanceConfigurer.d.ts +1 -1
  15. package/dist/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.d.ts +2 -2
  16. package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
  17. package/dist/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.d.ts +1 -0
  18. package/dist/serverGroup/configure/wizard/pages/Policies.d.ts +21 -3
  19. package/dist/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.d.ts +1 -0
  20. package/dist/serverGroup/configure/wizard/pages/ServerGroupCapacity.d.ts +1 -0
  21. package/dist/serverGroup/configure/wizard/pages/ServerGroupImageSettings.d.ts +1 -0
  22. package/dist/serverGroup/configure/wizard/pages/ServerGroupInstanceType.d.ts +1 -0
  23. package/dist/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.d.ts +2 -2
  24. package/dist/serverGroup/details/GceInstanceFlexibilityPolicyDetails.d.ts +2 -2
  25. package/dist/serverGroup/details/stateful/MarkDiskStatefulButton.d.ts +2 -2
  26. package/dist/serverGroup/details/stateful/UpdateBootImageButton.d.ts +2 -2
  27. package/package.json +5 -5
  28. package/src/autoHealingPolicy/GceAutoHealingPolicyEditor.spec.tsx +68 -52
  29. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.spec.tsx +43 -50
  30. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.tsx +1 -1
  31. package/src/autoscalingPolicy/GceAutoscalingPolicyWriter.spec.ts +1 -1
  32. package/src/domain/loadBalancer.ts +6 -3
  33. package/src/gce.module.spec.ts +132 -153
  34. package/src/instance/details/GceInstanceDetails.spec.tsx +86 -83
  35. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.spec.tsx +162 -121
  36. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.tsx +13 -1
  37. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.spec.tsx +47 -30
  38. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.tsx +2 -1
  39. package/src/loadBalancer/configure/common/GceProxyLoadBalancerModal.spec.tsx +85 -57
  40. package/src/loadBalancer/configure/common/gceLoadBalancerData.spec.ts +17 -16
  41. package/src/loadBalancer/configure/common/gceLoadBalancerModels.spec.ts +230 -5
  42. package/src/loadBalancer/configure/common/gceLoadBalancerModels.ts +186 -22
  43. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.spec.ts +298 -2
  44. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.ts +35 -5
  45. package/src/loadBalancer/configure/common/useGceLoadBalancerData.spec.tsx +37 -46
  46. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.spec.tsx +399 -58
  47. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.tsx +148 -41
  48. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.spec.tsx +169 -36
  49. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.tsx +80 -12
  50. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.spec.tsx +224 -124
  51. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.tsx +8 -2
  52. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.spec.tsx +57 -22
  53. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.tsx +21 -0
  54. package/src/loadBalancer/configure/http/GceHttpLoadBalancerRoutingEditor.spec.tsx +24 -20
  55. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerEditor.spec.tsx +41 -32
  56. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerModal.spec.tsx +35 -30
  57. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.spec.tsx +443 -0
  58. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.tsx +401 -0
  59. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.spec.tsx +330 -0
  60. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.tsx +348 -0
  61. package/src/loadBalancer/details/gceLoadBalancerDetails.spec.tsx +439 -35
  62. package/src/loadBalancer/details/gceLoadBalancerDetails.tsx +104 -21
  63. package/src/loadBalancer/httpLoadBalancerUtils.service.spec.ts +126 -0
  64. package/src/loadBalancer/httpLoadBalancerUtils.service.ts +27 -11
  65. package/src/loadBalancer/loadBalancer.setTransformer.spec.ts +165 -0
  66. package/src/loadBalancer/loadBalancer.setTransformer.ts +15 -2
  67. package/src/pipeline/stages/googleStageConfig.spec.js +22 -16
  68. package/src/securityGroup/configure/GceSecurityGroupModal.spec.tsx +177 -193
  69. package/src/securityGroup/configure/GceSecurityGroupModal.tsx +14 -2
  70. package/src/securityGroup/details/GceSecurityGroupDetails.spec.tsx +69 -78
  71. package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.js +35 -15
  72. package/src/serverGroup/configure/serverGroupConfiguration.service.js +7 -1
  73. package/src/serverGroup/configure/serverGroupConfiguration.service.spec.js +74 -4
  74. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.spec.tsx +593 -855
  75. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.tsx +120 -38
  76. package/src/serverGroup/configure/wizard/GceServerGroupWizard.types.ts +1 -0
  77. package/src/serverGroup/configure/wizard/GceServerGroupWizardPage.spec.tsx +53 -50
  78. package/src/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.spec.tsx +65 -49
  79. package/src/serverGroup/configure/wizard/hiddenMetadataKeys.value.js +1 -0
  80. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.spec.tsx +101 -108
  81. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +32 -28
  82. package/src/serverGroup/configure/wizard/pages/GceServerGroupFirewalls.spec.tsx +60 -59
  83. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.spec.tsx +240 -114
  84. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.tsx +152 -63
  85. package/src/serverGroup/configure/wizard/pages/Policies.spec.tsx +93 -48
  86. package/src/serverGroup/configure/wizard/pages/Policies.tsx +80 -55
  87. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.spec.tsx +123 -94
  88. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +24 -18
  89. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.spec.tsx +152 -133
  90. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.tsx +40 -37
  91. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.spec.tsx +66 -60
  92. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.tsx +8 -2
  93. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.spec.tsx +131 -101
  94. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +29 -28
  95. package/src/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.spec.tsx +104 -81
  96. package/src/serverGroup/details/GceInstanceFlexibilityPolicyDetails.spec.tsx +10 -11
  97. package/src/serverGroup/details/autoHealingPolicy/GceAutoHealingPolicyDetails.spec.tsx +51 -59
  98. package/src/serverGroup/details/autoHealingPolicy/modal/GceUpsertAutoHealingPolicyModal.spec.tsx +3 -3
  99. package/src/serverGroup/details/autoscalingPolicy/GceAutoscalingPolicyDetails.spec.tsx +49 -58
  100. package/src/serverGroup/details/autoscalingPolicy/modal/GceUpsertAutoscalingPolicyModal.spec.tsx +3 -3
  101. package/src/serverGroup/details/gceServerGroupDetails.spec.tsx +162 -141
  102. package/src/serverGroup/details/resize/GceResizeServerGroupModal.spec.tsx +102 -86
  103. package/src/serverGroup/details/resize/GceResizeServerGroupModal.tsx +4 -0
  104. package/src/serverGroup/details/rollback/GceRollbackServerGroupModal.spec.tsx +40 -31
  105. package/src/serverGroup/serverGroup.transformer.js +1 -0
  106. package/src/serverGroup/serverGroup.transformer.spec.js +48 -0
  107. package/src/validation/ApplicationNameValidator.spec.ts +1 -1
  108. /package/dist/{gce.logo2ae85d8704b1d6b8.png → gce.logoChI3w_A5.png} +0 -0
@@ -0,0 +1,330 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import React from 'react';
3
+
4
+ import { validateGceRegionalExternalNetworkLoadBalancerCommand } from './GceRegionalExternalNetworkLoadBalancerEditor';
5
+ import {
6
+ GceRegionalExternalNetworkLoadBalancerModal,
7
+ normalizeGceRegionalExternalNetworkLoadBalancerCommand,
8
+ serializeGceRegionalExternalNetworkLoadBalancerCommand,
9
+ submitGceRegionalExternalNetworkLoadBalancerCommand,
10
+ } from './GceRegionalExternalNetworkLoadBalancerModal';
11
+
12
+ describe('GceRegionalExternalNetworkLoadBalancerModal', () => {
13
+ const application = { name: 'app' } as any;
14
+
15
+ it('materializes the same valid defaults for create and pipeline modes', () => {
16
+ const defaults = { credentials: 'account-a', region: 'europe-west1' };
17
+ const create = normalizeGceRegionalExternalNetworkLoadBalancerCommand({}, 'create', 'app-main', defaults);
18
+ const pipeline = normalizeGceRegionalExternalNetworkLoadBalancerCommand({}, 'pipeline', 'app-main', defaults);
19
+
20
+ expect({ ...create, mode: undefined }).toEqual({ ...pipeline, mode: undefined });
21
+ expect(create).toEqual(
22
+ expect.objectContaining({
23
+ credentials: 'account-a',
24
+ name: 'app-main',
25
+ networkTier: 'PREMIUM',
26
+ ports: ['80'],
27
+ region: 'europe-west1',
28
+ }),
29
+ );
30
+ expect(create.listeners).toEqual([{ name: 'app-main', portRange: '80', protocol: 'TCP' }]);
31
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(create)).toEqual([]);
32
+ });
33
+
34
+ it('normalizes persisted REGIONAL_EXTERNAL_NETWORK details for edit', () => {
35
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
36
+ {
37
+ account: 'account-a',
38
+ backendService: {
39
+ healthCheck: {
40
+ healthCheckType: 'TCP',
41
+ name: 'tcp-check',
42
+ port: 80,
43
+ selfLink: 'projects/test/regions/europe-west1/healthChecks/tcp-check',
44
+ },
45
+ name: 'app-main',
46
+ sessionAffinity: 'CLIENT_IP',
47
+ },
48
+ ipAddress: '35.1.2.3',
49
+ ipProtocol: 'tcp',
50
+ loadBalancerName: 'app-main',
51
+ networkTier: 'PREMIUM',
52
+ ports: ['80', '443'],
53
+ region: 'europe-west1',
54
+ },
55
+ 'edit',
56
+ );
57
+
58
+ expect(command).toEqual(
59
+ expect.objectContaining({
60
+ credentials: 'account-a',
61
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
62
+ mode: 'edit',
63
+ name: 'app-main',
64
+ networkTier: 'PREMIUM',
65
+ ports: ['80', '443'],
66
+ region: 'europe-west1',
67
+ }),
68
+ );
69
+ expect(command.listeners).toEqual([
70
+ {
71
+ address: { name: '35.1.2.3' },
72
+ name: 'app-main',
73
+ portRange: '80,443',
74
+ protocol: 'TCP',
75
+ },
76
+ ]);
77
+ expect(command.backendServices[0]).toEqual(
78
+ expect.objectContaining({
79
+ healthCheck: expect.objectContaining({ name: 'tcp-check', port: 80 }),
80
+ name: 'app-main',
81
+ sessionAffinity: 'CLIENT_IP',
82
+ }),
83
+ );
84
+ });
85
+
86
+ it('serializes the exact Clouddriver REGIONAL_EXTERNAL_NETWORK contract', () => {
87
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
88
+ {
89
+ account: 'account-a',
90
+ backendService: {
91
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
92
+ name: 'app-main',
93
+ sessionAffinity: 'CLIENT_IP',
94
+ },
95
+ ipAddress: '35.1.2.3',
96
+ ipProtocol: 'TCP',
97
+ loadBalancerName: 'app-main',
98
+ networkTier: 'PREMIUM',
99
+ ports: ['80', '443'],
100
+ region: 'europe-west1',
101
+ },
102
+ 'pipeline',
103
+ );
104
+
105
+ const payload = serializeGceRegionalExternalNetworkLoadBalancerCommand(command);
106
+
107
+ expect(payload).toEqual({
108
+ backendService: {
109
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
110
+ name: 'app-main',
111
+ sessionAffinity: 'CLIENT_IP',
112
+ },
113
+ cloudProvider: 'gce',
114
+ credentials: 'account-a',
115
+ ipAddress: '35.1.2.3',
116
+ ipProtocol: 'TCP',
117
+ loadBalancerName: 'app-main',
118
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
119
+ name: 'app-main',
120
+ networkTier: 'PREMIUM',
121
+ ports: ['80', '443'],
122
+ provider: 'gce',
123
+ region: 'europe-west1',
124
+ type: 'upsertLoadBalancer',
125
+ });
126
+ expect((payload as any).portRange).toBeUndefined();
127
+ expect((payload as any).backendServices).toBeUndefined();
128
+ expect((payload as any).healthChecks).toBeUndefined();
129
+ expect((payload as any).listeners).toBeUndefined();
130
+ });
131
+
132
+ it('returns a named health check in pipeline mode when the persisted name is omitted', () => {
133
+ const executeTask = vi.fn();
134
+ const persisted = {
135
+ account: 'account-a',
136
+ backendService: {
137
+ healthCheck: { healthCheckType: 'TCP', port: 80 },
138
+ name: 'app-main',
139
+ sessionAffinity: 'NONE',
140
+ },
141
+ ipProtocol: 'TCP',
142
+ loadBalancerName: 'app-main',
143
+ ports: ['80'],
144
+ region: 'europe-west1',
145
+ };
146
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(persisted, 'pipeline');
147
+
148
+ const result = submitGceRegionalExternalNetworkLoadBalancerCommand(command, { application, executeTask });
149
+
150
+ expect(result).toEqual(serializeGceRegionalExternalNetworkLoadBalancerCommand(command));
151
+ expect((result as any).backendService.healthCheck.name).toBe('app-main');
152
+ expect(command.backendServices[0].healthCheck).toBe(command.healthChecks[0]);
153
+ expect(persisted.backendService.healthCheck).toEqual({ healthCheckType: 'TCP', port: 80 });
154
+ expect(executeTask).not.toHaveBeenCalled();
155
+ });
156
+
157
+ it('serializes the referenced pipeline health check through the backend service', () => {
158
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
159
+ {
160
+ account: 'account-a',
161
+ backendService: {
162
+ healthCheck: 'tcp-check',
163
+ name: 'app-main',
164
+ sessionAffinity: 'NONE',
165
+ },
166
+ healthChecks: [{ healthCheckType: 'TCP', name: 'tcp-check', port: 80 }],
167
+ ipProtocol: 'TCP',
168
+ loadBalancerName: 'app-main',
169
+ ports: ['80'],
170
+ region: 'europe-west1',
171
+ },
172
+ 'pipeline',
173
+ );
174
+
175
+ const payload = serializeGceRegionalExternalNetworkLoadBalancerCommand(command);
176
+
177
+ expect(command.backendServices[0].healthCheck).toBe(command.healthChecks[0]);
178
+ expect(payload.backendService.healthCheck).toEqual({
179
+ healthCheckType: 'TCP',
180
+ name: 'tcp-check',
181
+ port: 80,
182
+ });
183
+ });
184
+
185
+ it('serializes a named health check for direct edit when the persisted name is omitted', () => {
186
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
187
+ {
188
+ account: 'account-a',
189
+ backendService: {
190
+ healthCheck: { healthCheckType: 'TCP', port: 80 },
191
+ name: 'app-main',
192
+ sessionAffinity: 'NONE',
193
+ },
194
+ ipProtocol: 'TCP',
195
+ loadBalancerName: 'app-main',
196
+ ports: ['80'],
197
+ region: 'europe-west1',
198
+ },
199
+ 'edit',
200
+ );
201
+
202
+ const payload = serializeGceRegionalExternalNetworkLoadBalancerCommand(command);
203
+
204
+ expect(payload.backendService.healthCheck.name).toBe('app-main');
205
+ });
206
+
207
+ it('preserves ipAddress and networkTier during edit serialization', () => {
208
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
209
+ {
210
+ account: 'account-a',
211
+ backendService: {
212
+ healthCheck: { healthCheckType: 'UDP', name: 'udp-check', port: 53 },
213
+ name: 'app-main',
214
+ sessionAffinity: 'CLIENT_IP_PROTO',
215
+ },
216
+ ipAddress: '35.1.2.3',
217
+ ipProtocol: 'UDP',
218
+ loadBalancerName: 'app-main',
219
+ networkTier: 'PREMIUM',
220
+ ports: ['53'],
221
+ region: 'europe-west1',
222
+ },
223
+ 'edit',
224
+ );
225
+
226
+ const payload = serializeGceRegionalExternalNetworkLoadBalancerCommand(command);
227
+
228
+ expect(payload.ipAddress).toBe('35.1.2.3');
229
+ expect(payload.networkTier).toBe('PREMIUM');
230
+ expect(payload.ipProtocol).toBe('UDP');
231
+ });
232
+
233
+ it('rejects a destructive TCP to UDP protocol edit', () => {
234
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
235
+ {
236
+ account: 'account-a',
237
+ backendService: {
238
+ healthCheck: { healthCheckType: 'TCP', name: 'tcp-check', port: 80 },
239
+ name: 'app-main',
240
+ sessionAffinity: 'NONE',
241
+ },
242
+ ipAddress: '35.1.2.3',
243
+ ipProtocol: 'TCP',
244
+ loadBalancerName: 'app-main',
245
+ networkTier: 'PREMIUM',
246
+ ports: ['80'],
247
+ region: 'europe-west1',
248
+ },
249
+ 'edit',
250
+ );
251
+ command.listeners[0] = {
252
+ ...command.listeners[0],
253
+ portRange: '53',
254
+ protocol: 'UDP',
255
+ };
256
+ command.ports = ['53'];
257
+
258
+ expect(validateGceRegionalExternalNetworkLoadBalancerCommand(command)).toContain(
259
+ 'Protocol and ports cannot be changed while editing a REGIONAL_EXTERNAL_NETWORK load balancer.',
260
+ );
261
+ });
262
+
263
+ it('executes the direct normalized job in infrastructure create mode', () => {
264
+ const task = Promise.resolve({ id: 'task' });
265
+ const executeTask = vi.fn().mockReturnValue(task);
266
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
267
+ {
268
+ account: 'account-a',
269
+ backendService: {
270
+ healthCheck: { healthCheckType: 'TCP', port: 80 },
271
+ name: 'app-main',
272
+ sessionAffinity: 'NONE',
273
+ },
274
+ ipAddress: '35.1.2.3',
275
+ ipProtocol: 'TCP',
276
+ loadBalancerName: 'app-main',
277
+ networkTier: 'PREMIUM',
278
+ ports: ['80'],
279
+ region: 'europe-west1',
280
+ },
281
+ 'create',
282
+ );
283
+
284
+ const result = submitGceRegionalExternalNetworkLoadBalancerCommand(command, { application, executeTask });
285
+
286
+ expect(command.backendServices[0].healthCheck).toBe(command.healthChecks[0]);
287
+ expect((command.backendServices[0].healthCheck as any).name).toBe('app-main');
288
+ expect(result).toBe(task);
289
+ expect(executeTask).toHaveBeenCalledExactlyOnceWith({
290
+ application,
291
+ description: 'Create Load Balancer: app-main',
292
+ job: [serializeGceRegionalExternalNetworkLoadBalancerCommand(command)],
293
+ });
294
+ });
295
+
296
+ it('exposes pipeline support and passes edit mode to the editor', () => {
297
+ expect(GceRegionalExternalNetworkLoadBalancerModal.supportsPipelineConfig).toBe(true);
298
+ render(
299
+ <GceRegionalExternalNetworkLoadBalancerModal
300
+ app={application}
301
+ closeModal={vi.fn()}
302
+ data={emptyData()}
303
+ dismissModal={vi.fn()}
304
+ isNew={false}
305
+ loadBalancer={{ account: 'account-a', name: 'app-main', region: 'europe-west1' } as any}
306
+ />,
307
+ );
308
+
309
+ expect(screen.getByRole('heading', { name: 'Edit app-main' })).toBeInTheDocument();
310
+ ['Account', 'Region', 'IP address', 'Network tier', 'Protocol', 'Ports'].forEach((label) =>
311
+ expect(screen.getByLabelText(label), label).toBeDisabled(),
312
+ );
313
+ expect(screen.getAllByLabelText('Name')[0]).toBeDisabled();
314
+ expect(screen.getByLabelText('Session affinity')).toBeEnabled();
315
+ expect(screen.getByRole('button', { name: /Update/ })).toBeInTheDocument();
316
+ });
317
+ });
318
+
319
+ function emptyData(): any {
320
+ return {
321
+ accounts: [],
322
+ addresses: [],
323
+ backendServices: [],
324
+ certificates: [],
325
+ healthChecks: [],
326
+ networks: [],
327
+ regions: [],
328
+ subnets: [],
329
+ };
330
+ }
@@ -0,0 +1,348 @@
1
+ import React from 'react';
2
+
3
+ import type { Application, IModalComponentProps, ITask, ITaskCommand } from '@spinnaker/core';
4
+ import {
5
+ InfrastructureCaches,
6
+ ModalClose,
7
+ ReactModal,
8
+ SubmitButton,
9
+ TaskExecutor,
10
+ TaskMonitor,
11
+ TaskMonitorWrapper,
12
+ } from '@spinnaker/core';
13
+
14
+ import type { IGceRegionalExternalNetworkLoadBalancerCommand } from './GceRegionalExternalNetworkLoadBalancerEditor';
15
+ import {
16
+ createGceRegionalExternalNetworkHealthCheck,
17
+ GceRegionalExternalNetworkLoadBalancerEditor,
18
+ validateGceRegionalExternalNetworkLoadBalancerCommand,
19
+ } from './GceRegionalExternalNetworkLoadBalancerEditor';
20
+ import type {
21
+ GceLoadBalancerEditorMode,
22
+ IGceLoadBalancerData,
23
+ IGceLoadBalancerDataReaders,
24
+ IGceLoadBalancerDataState,
25
+ IGceLoadBalancerHealthCheck,
26
+ IGceResourceReference,
27
+ IGceSerializedLoadBalancerCommand,
28
+ } from '../common';
29
+ import {
30
+ GceLoadBalancerDataController,
31
+ normalizeGceLoadBalancerCommand,
32
+ serializeGceLoadBalancerCommand,
33
+ } from '../common';
34
+ import { GCEProviderSettings } from '../../../gce.settings';
35
+
36
+ type UnknownRecord = Record<string, any>;
37
+
38
+ const EMPTY_DATA: IGceLoadBalancerData = {
39
+ accounts: [],
40
+ addresses: [],
41
+ backendServices: [],
42
+ certificates: [],
43
+ healthChecks: [],
44
+ networks: [],
45
+ regions: [],
46
+ subnets: [],
47
+ };
48
+
49
+ export interface IGceRegionalExternalNetworkLoadBalancerPayload extends IGceSerializedLoadBalancerCommand {
50
+ backendService: UnknownRecord;
51
+ ipAddress?: string;
52
+ ipProtocol: string;
53
+ networkTier?: string;
54
+ ports: string[];
55
+ }
56
+
57
+ export interface IGceRegionalExternalNetworkLoadBalancerModalProps extends IModalComponentProps {
58
+ app?: Application;
59
+ application?: Application;
60
+ credentials?: string;
61
+ data?: IGceLoadBalancerData;
62
+ dataReaders?: IGceLoadBalancerDataReaders;
63
+ executeTask?: (taskCommand: ITaskCommand) => PromiseLike<ITask>;
64
+ forPipelineConfig?: boolean;
65
+ isNew?: boolean;
66
+ loadBalancer?: UnknownRecord;
67
+ mode?: GceLoadBalancerEditorMode;
68
+ }
69
+
70
+ interface IGceRegionalExternalNetworkLoadBalancerModalState extends IGceLoadBalancerDataState {
71
+ command: IGceRegionalExternalNetworkLoadBalancerCommand;
72
+ taskMonitor: TaskMonitor;
73
+ }
74
+
75
+ interface IGceRegionalExternalNetworkSubmissionDependencies {
76
+ application: Application;
77
+ executeTask?: (taskCommand: ITaskCommand) => PromiseLike<ITask>;
78
+ }
79
+
80
+ export function normalizeGceRegionalExternalNetworkLoadBalancerCommand(
81
+ persisted: UnknownRecord | null | undefined = {},
82
+ mode: GceLoadBalancerEditorMode,
83
+ applicationName = '',
84
+ defaults: Partial<Record<'credentials' | 'region', unknown>> = {},
85
+ ): IGceRegionalExternalNetworkLoadBalancerCommand {
86
+ const source = persisted || {};
87
+ const backendService = source.backendService || source.backendServices?.[0];
88
+ const materializeDefaults = mode !== 'edit';
89
+ const normalizedSource: UnknownRecord = {
90
+ ...defaults,
91
+ ...source,
92
+ backendServices: backendService ? [{ ...backendService }] : source.backendServices,
93
+ credentials: source.credentials || source.account || defaults.credentials,
94
+ ipProtocol: source.ipProtocol || (materializeDefaults ? 'TCP' : undefined),
95
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
96
+ name: source.name || source.loadBalancerName || applicationName,
97
+ networkTier: source.networkTier || (materializeDefaults ? 'PREMIUM' : undefined),
98
+ ports: source.ports ?? source.portRange ?? (materializeDefaults ? ['80'] : undefined),
99
+ };
100
+
101
+ const command = normalizeGceLoadBalancerCommand(
102
+ normalizedSource,
103
+ mode,
104
+ ) as IGceRegionalExternalNetworkLoadBalancerCommand;
105
+ const listener = command.listeners[0];
106
+ const ports = splitPorts(listener?.portRange);
107
+ let backendServices = command.backendServices;
108
+ let healthChecks = command.healthChecks;
109
+
110
+ if (materializeDefaults) {
111
+ if (backendService && !backendServices.length) {
112
+ backendServices = [
113
+ {
114
+ name: String(backendService.name || ''),
115
+ sessionAffinity: backendService.sessionAffinity || 'NONE',
116
+ ...(backendService.healthCheck ? { healthCheck: backendService.healthCheck } : {}),
117
+ },
118
+ ];
119
+ } else if (!backendServices.length) {
120
+ const defaultHealthCheck = createGceRegionalExternalNetworkHealthCheck(command.name);
121
+ backendServices = [
122
+ {
123
+ healthCheck: (defaultHealthCheck as unknown) as IGceResourceReference,
124
+ name: command.name,
125
+ sessionAffinity: 'NONE',
126
+ },
127
+ ];
128
+ healthChecks = [defaultHealthCheck];
129
+ }
130
+ }
131
+
132
+ const backend = backendServices[0];
133
+ const backendHealthCheck =
134
+ typeof backend?.healthCheck === 'object' ? (backend.healthCheck as IGceLoadBalancerHealthCheck) : undefined;
135
+ const referencedHealthCheck = backendHealthCheck?.name
136
+ ? healthChecks.find(({ name }) => name === backendHealthCheck.name)
137
+ : undefined;
138
+ const healthCheck =
139
+ (backendHealthCheck && Object.keys(backendHealthCheck).some((key) => key !== 'name' && key !== 'selfLink')
140
+ ? backendHealthCheck
141
+ : referencedHealthCheck || backendHealthCheck || healthChecks[0]) || undefined;
142
+ if (backend && healthCheck) {
143
+ const namedHealthCheck = {
144
+ ...healthCheck,
145
+ name: healthCheck.name?.trim() || command.name,
146
+ };
147
+ backendServices = [
148
+ {
149
+ ...backend,
150
+ healthCheck: (namedHealthCheck as unknown) as IGceResourceReference,
151
+ },
152
+ ];
153
+ healthChecks = [namedHealthCheck];
154
+ }
155
+
156
+ return {
157
+ ...command,
158
+ backendServices,
159
+ healthChecks,
160
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
161
+ ports,
162
+ };
163
+ }
164
+
165
+ export function serializeGceRegionalExternalNetworkLoadBalancerCommand(
166
+ command: IGceRegionalExternalNetworkLoadBalancerCommand,
167
+ ): IGceRegionalExternalNetworkLoadBalancerPayload {
168
+ const syncedCommand: IGceRegionalExternalNetworkLoadBalancerCommand = {
169
+ ...command,
170
+ listeners: [
171
+ {
172
+ ...(command.listeners[0] || { name: command.name, portRange: '', protocol: 'TCP' }),
173
+ portRange: (command.ports?.length ? command.ports : splitPorts(command.listeners[0]?.portRange)).join(','),
174
+ },
175
+ ],
176
+ };
177
+
178
+ return serializeGceLoadBalancerCommand(syncedCommand) as IGceRegionalExternalNetworkLoadBalancerPayload;
179
+ }
180
+
181
+ export function submitGceRegionalExternalNetworkLoadBalancerCommand(
182
+ command: IGceRegionalExternalNetworkLoadBalancerCommand,
183
+ { application, executeTask = TaskExecutor.executeTask }: IGceRegionalExternalNetworkSubmissionDependencies,
184
+ ): IGceRegionalExternalNetworkLoadBalancerPayload | Promise<ITask> {
185
+ const payload = serializeGceRegionalExternalNetworkLoadBalancerCommand(command);
186
+ if (command.mode === 'pipeline') {
187
+ return payload;
188
+ }
189
+
190
+ return Promise.resolve(
191
+ executeTask({
192
+ application,
193
+ description: `${command.mode === 'edit' ? 'Update' : 'Create'} Load Balancer: ${command.name}`,
194
+ job: [payload],
195
+ }),
196
+ );
197
+ }
198
+
199
+ class GceRegionalExternalNetworkLoadBalancerModalComponent extends React.Component<
200
+ IGceRegionalExternalNetworkLoadBalancerModalProps,
201
+ IGceRegionalExternalNetworkLoadBalancerModalState
202
+ > {
203
+ private dataController?: GceLoadBalancerDataController;
204
+ private unsubscribe?: () => void;
205
+
206
+ public constructor(props: IGceRegionalExternalNetworkLoadBalancerModalProps) {
207
+ super(props);
208
+ const application = this.application(props);
209
+ const mode = this.mode(props);
210
+ const command = normalizeGceRegionalExternalNetworkLoadBalancerCommand(
211
+ props.loadBalancer,
212
+ mode,
213
+ application?.name,
214
+ {
215
+ credentials: props.credentials || GCEProviderSettings.defaults.account,
216
+ region: GCEProviderSettings.defaults.region,
217
+ },
218
+ );
219
+ this.state = {
220
+ command,
221
+ data: props.data || EMPTY_DATA,
222
+ status: props.data ? 'ready' : 'idle',
223
+ taskMonitor: new TaskMonitor({
224
+ application,
225
+ title: `${mode === 'edit' ? 'Updating' : 'Creating'} your load balancer`,
226
+ onDismiss: () => props.dismissModal?.(),
227
+ onTaskComplete: () => {
228
+ InfrastructureCaches.clearCache('healthChecks');
229
+ application?.loadBalancers?.refresh?.();
230
+ props.closeModal?.();
231
+ },
232
+ }),
233
+ };
234
+ }
235
+
236
+ public componentDidMount(): void {
237
+ if (this.props.data) return;
238
+ this.dataController = new GceLoadBalancerDataController(this.props.dataReaders);
239
+ this.unsubscribe = this.dataController.subscribe((state) => this.setState(state));
240
+ this.dataController.load(this.state.command.credentials);
241
+ }
242
+
243
+ public componentWillUnmount(): void {
244
+ this.unsubscribe?.();
245
+ this.dataController?.dispose();
246
+ }
247
+
248
+ public render(): JSX.Element {
249
+ const { command, data, status, taskMonitor } = this.state;
250
+ const errors = validateGceRegionalExternalNetworkLoadBalancerCommand(command);
251
+ const heading =
252
+ command.mode === 'create'
253
+ ? 'Create Regional External Network Load Balancer'
254
+ : command.mode === 'pipeline'
255
+ ? 'Configure Regional External Network Load Balancer'
256
+ : `Edit ${command.name}`;
257
+
258
+ return (
259
+ <div className="modal-content">
260
+ <TaskMonitorWrapper monitor={taskMonitor} />
261
+ <ModalClose dismiss={this.props.dismissModal} />
262
+ <div className="modal-header">
263
+ <h3>{heading}</h3>
264
+ </div>
265
+ <div className="modal-body">
266
+ {status === 'loading' && <div className="horizontal center middle">Loading...</div>}
267
+ {status === 'error' && <div className="alert alert-danger">Unable to load GCE resources.</div>}
268
+ <GceRegionalExternalNetworkLoadBalancerEditor command={command} data={data} onChange={this.updateCommand} />
269
+ {!!errors.length && (
270
+ <div className="alert alert-danger gce-regional-external-network-validation-errors">
271
+ {errors.map((error) => (
272
+ <div key={error}>{error}</div>
273
+ ))}
274
+ </div>
275
+ )}
276
+ </div>
277
+ <div className="modal-footer">
278
+ <button
279
+ className="btn btn-default"
280
+ disabled={taskMonitor.submitting}
281
+ onClick={this.props.dismissModal}
282
+ type="button"
283
+ >
284
+ Cancel
285
+ </button>
286
+ <SubmitButton
287
+ isDisabled={!!errors.length || status === 'loading' || taskMonitor.submitting}
288
+ isFormSubmit={false}
289
+ label={command.mode === 'pipeline' ? 'Done' : command.mode === 'edit' ? 'Update' : 'Create'}
290
+ onClick={this.submit}
291
+ submitting={taskMonitor.submitting}
292
+ />
293
+ </div>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ private application(props = this.props): Application {
299
+ return (props.application || props.app) as Application;
300
+ }
301
+
302
+ private mode(props = this.props): GceLoadBalancerEditorMode {
303
+ if (props.forPipelineConfig) return 'pipeline';
304
+ if (props.mode) return props.mode;
305
+ return props.isNew === false || props.loadBalancer ? 'edit' : 'create';
306
+ }
307
+
308
+ private updateCommand = (command: IGceRegionalExternalNetworkLoadBalancerCommand): void => {
309
+ const accountChanged = command.credentials !== this.state.command.credentials;
310
+ this.setState({ command });
311
+ if (accountChanged) this.dataController?.load(command.credentials);
312
+ };
313
+
314
+ private submit = (): void => {
315
+ if (validateGceRegionalExternalNetworkLoadBalancerCommand(this.state.command).length) {
316
+ return;
317
+ }
318
+ if (this.state.command.mode === 'pipeline') {
319
+ this.props.closeModal?.(
320
+ submitGceRegionalExternalNetworkLoadBalancerCommand(this.state.command, {
321
+ application: this.application(),
322
+ executeTask: this.props.executeTask,
323
+ }),
324
+ );
325
+ return;
326
+ }
327
+ this.state.taskMonitor.submit(
328
+ () =>
329
+ submitGceRegionalExternalNetworkLoadBalancerCommand(this.state.command, {
330
+ application: this.application(),
331
+ executeTask: this.props.executeTask,
332
+ }) as PromiseLike<ITask>,
333
+ );
334
+ };
335
+ }
336
+
337
+ export const GceRegionalExternalNetworkLoadBalancerModal = Object.assign(
338
+ GceRegionalExternalNetworkLoadBalancerModalComponent,
339
+ {
340
+ show: (props: IGceRegionalExternalNetworkLoadBalancerModalProps) =>
341
+ ReactModal.show(GceRegionalExternalNetworkLoadBalancerModalComponent, props, { dialogClassName: 'modal-lg' }),
342
+ supportsPipelineConfig: true,
343
+ },
344
+ );
345
+
346
+ function splitPorts(value: string | undefined): string[] {
347
+ return String(value || '').split(',');
348
+ }