@spinnaker/google 2026.3.2 → 2026.4.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 (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
@@ -24,6 +24,22 @@ function selectedReference(name: string, options: IGceLoadBalancerDataItem[]): I
24
24
  return (options.find((option) => option.name === name) || { name }) as IGceResourceReference;
25
25
  }
26
26
 
27
+ function certificateManagerReference(resourceUrl: string): IGceResourceReference | undefined {
28
+ const selfLink = resourceUrl.trim();
29
+ if (!selfLink) {
30
+ return undefined;
31
+ }
32
+ return {
33
+ name: selfLink.split('/').filter(Boolean).pop() || selfLink,
34
+ selfLink,
35
+ };
36
+ }
37
+
38
+ function certificateManagerUrl(certificate: IGceResourceReference | undefined): string {
39
+ const selfLink = certificate?.selfLink || '';
40
+ return selfLink.includes('/sslCertificates/') ? '' : selfLink;
41
+ }
42
+
27
43
  export function GceHttpLoadBalancerListenerEditor({
28
44
  addresses,
29
45
  certificates,
@@ -34,6 +50,24 @@ export function GceHttpLoadBalancerListenerEditor({
34
50
  subnets,
35
51
  }: IGceHttpLoadBalancerListenerEditorProps): JSX.Element {
36
52
  const protocols = ['HTTP', 'HTTPS'];
53
+ const externalManaged = loadBalancerType === 'EXTERNAL_MANAGED';
54
+ const reservedNetworkTier = listener.address?.networkTier as string | undefined;
55
+
56
+ const updateAddress = (name: string): void => {
57
+ const address = selectedReference(name, addresses);
58
+ if (externalManaged) {
59
+ const selectedAddress = addresses.find((option) => option.name === name);
60
+ const networkTier = (selectedAddress as { networkTier?: string } | undefined)?.networkTier;
61
+ onChange({
62
+ ...listener,
63
+ address,
64
+ ...(networkTier ? { networkTier } : {}),
65
+ });
66
+ return;
67
+ }
68
+ const { networkTier: _networkTier, ...listenerWithoutTier } = listener;
69
+ onChange({ ...listenerWithoutTier, address });
70
+ };
37
71
 
38
72
  return (
39
73
  <fieldset className="gce-http-listener">
@@ -99,7 +133,7 @@ export function GceHttpLoadBalancerListenerEditor({
99
133
  <select
100
134
  data-testid="listener-address"
101
135
  value={listener.address?.name || ''}
102
- onChange={(event) => onChange({ ...listener, address: selectedReference(event.target.value, addresses) })}
136
+ onChange={(event) => updateAddress(event.target.value)}
103
137
  >
104
138
  <option value="">Ephemeral</option>
105
139
  {addresses.map((address) => (
@@ -132,19 +166,53 @@ export function GceHttpLoadBalancerListenerEditor({
132
166
  ))}
133
167
  </select>
134
168
  </label>
135
- <label>
136
- Certificate map
137
- <input
138
- data-testid="listener-certificate-map"
139
- type="text"
140
- value={listener.certificateMap || ''}
141
- onChange={(event) =>
142
- onChange({ ...listener, certificate: undefined, certificateMap: event.target.value || undefined })
143
- }
144
- />
145
- </label>
169
+ {externalManaged && (
170
+ <label>
171
+ Certificate Manager resource URL
172
+ <input
173
+ data-testid="listener-certificate-manager-url"
174
+ placeholder="//certificatemanager.googleapis.com/projects/.../locations/.../certificates/..."
175
+ type="text"
176
+ value={certificateManagerUrl(listener.certificate)}
177
+ onChange={(event) =>
178
+ onChange({
179
+ ...listener,
180
+ certificate: certificateManagerReference(event.target.value),
181
+ certificateMap: undefined,
182
+ })
183
+ }
184
+ />
185
+ </label>
186
+ )}
187
+ {!externalManaged && (
188
+ <label>
189
+ Certificate map
190
+ <input
191
+ data-testid="listener-certificate-map"
192
+ type="text"
193
+ value={listener.certificateMap || ''}
194
+ onChange={(event) =>
195
+ onChange({ ...listener, certificate: undefined, certificateMap: event.target.value || undefined })
196
+ }
197
+ />
198
+ </label>
199
+ )}
146
200
  </>
147
201
  )}
202
+ {externalManaged && (
203
+ <label>
204
+ Network tier
205
+ <select
206
+ data-testid="listener-network-tier"
207
+ disabled={Boolean(reservedNetworkTier)}
208
+ value={reservedNetworkTier || listener.networkTier || 'PREMIUM'}
209
+ onChange={(event) => onChange({ ...listener, networkTier: event.target.value })}
210
+ >
211
+ <option value="PREMIUM">Premium</option>
212
+ <option value="STANDARD">Standard</option>
213
+ </select>
214
+ </label>
215
+ )}
148
216
  <button type="button" className="btn btn-sm btn-default" onClick={onRemove}>
149
217
  Remove listener
150
218
  </button>
@@ -1,9 +1,11 @@
1
1
  import React from 'react';
2
- import { shallow } from 'enzyme';
2
+ import { fireEvent, screen } from '@testing-library/react';
3
3
 
4
4
  import { TaskExecutor } from '@spinnaker/core';
5
+ import { renderWithRouter } from '../../../../../core/src/utils/testUtils/rtl';
5
6
 
6
7
  import { buildGceLoadBalancerJobs } from '../common';
8
+ import { validateGceHttpLoadBalancerCommand } from './GceHttpLoadBalancerEditor';
7
9
  import { GceHttpLoadBalancerModal, initializeGceHttpLoadBalancerCommand } from './GceHttpLoadBalancerModal';
8
10
 
9
11
  describe('GceHttpLoadBalancerModal', () => {
@@ -75,71 +77,79 @@ describe('GceHttpLoadBalancerModal', () => {
75
77
  });
76
78
 
77
79
  it('returns exact operations without executing a task in pipeline-edit mode', () => {
78
- const closeModal = jasmine.createSpy('closeModal');
79
- const executeTask = spyOn(TaskExecutor, 'executeTask');
80
- const modal = new GceHttpLoadBalancerModal({
81
- app: application,
82
- closeModal,
83
- dismissModal: jasmine.createSpy('dismissModal'),
84
- forPipelineConfig: true,
85
- isNew: false,
86
- loadBalancer: {
87
- account: 'account-a',
88
- backendServices: [{ healthCheck: 'check-a', name: 'backend-a', portName: 'http' }],
89
- defaultService: 'backend-a',
90
- healthChecks: [{ healthCheckType: 'HTTP', name: 'check-a', port: 80, requestPath: '/health' }],
91
- hostRules: [
92
- {
93
- hostPatterns: ['api.example.com'],
94
- pathMatcher: {
95
- defaultService: 'backend-a',
96
- pathRules: [{ backendService: 'backend-a', paths: ['/v1'] }],
97
- },
80
+ const closeModal = vi.fn();
81
+ const executeTask = vi.spyOn(TaskExecutor, 'executeTask').mockReturnValue(undefined);
82
+ const loadBalancer = {
83
+ account: 'account-a',
84
+ backendServices: [{ healthCheck: 'check-a', name: 'backend-a', portName: 'http' }],
85
+ defaultService: 'backend-a',
86
+ healthChecks: [{ healthCheckType: 'HTTP', name: 'check-a', port: 80, requestPath: '/health' }],
87
+ hostRules: [
88
+ {
89
+ hostPatterns: ['api.example.com'],
90
+ pathMatcher: {
91
+ defaultService: 'backend-a',
92
+ pathRules: [{ backendService: 'backend-a', paths: ['/v1'] }],
98
93
  },
99
- ],
100
- listeners: [{ ipAddress: 'address-a', name: 'frontend', port: 80, protocol: 'HTTP' }],
101
- loadBalancerType: 'HTTP',
102
- name: 'test-app-main',
103
- },
104
- mode: 'edit',
105
- } as any);
106
- const expectedOperations = buildGceLoadBalancerJobs((modal as any).state.command);
94
+ },
95
+ ],
96
+ listeners: [{ ipAddress: 'address-a', name: 'frontend', port: 80, protocol: 'HTTP' }],
97
+ loadBalancerType: 'HTTP',
98
+ name: 'test-app-main',
99
+ };
100
+ const expectedOperations = buildGceLoadBalancerJobs(
101
+ initializeGceHttpLoadBalancerCommand(loadBalancer, 'pipeline', application),
102
+ );
103
+ renderWithRouter(
104
+ <GceHttpLoadBalancerModal
105
+ app={application}
106
+ closeModal={closeModal}
107
+ data={emptyData}
108
+ dismissModal={vi.fn()}
109
+ forPipelineConfig={true}
110
+ isNew={false}
111
+ loadBalancer={loadBalancer}
112
+ />,
113
+ );
107
114
 
108
- (modal as any).submit();
115
+ fireEvent.click(screen.getByRole('button', { name: 'Done' }));
109
116
 
110
- expect((modal as any).state.command.mode).toBe('pipeline');
111
117
  expect(executeTask).not.toHaveBeenCalled();
112
- expect(closeModal).toHaveBeenCalledOnceWith(expectedOperations);
118
+ expect(closeModal).toHaveBeenCalledExactlyOnceWith(expectedOperations);
113
119
  expect(expectedOperations.map(({ loadBalancerName }) => loadBalancerName)).toEqual(['frontend']);
114
120
  });
115
121
 
116
122
  it('executes an update task instead of returning operations in infrastructure-edit mode', () => {
117
- const closeModal = jasmine.createSpy('closeModal');
118
- const executeTask = spyOn(TaskExecutor, 'executeTask').and.returnValue(Promise.resolve({ id: 'task' }) as any);
119
- const modal = new GceHttpLoadBalancerModal({
120
- app: application,
121
- closeModal,
122
- dismissModal: jasmine.createSpy('dismissModal'),
123
- forPipelineConfig: false,
124
- isNew: false,
125
- loadBalancer: {
126
- account: 'account-a',
127
- backendServices: [{ healthCheck: 'check-a', name: 'backend-a', portName: 'http' }],
128
- defaultService: 'backend-a',
129
- healthChecks: [{ healthCheckType: 'HTTP', name: 'check-a', port: 80, requestPath: '/health' }],
130
- listeners: [{ ipAddress: 'address-a', name: 'frontend', port: 80, protocol: 'HTTP' }],
131
- loadBalancerType: 'HTTP',
132
- name: 'test-app-main',
133
- },
134
- mode: 'edit',
135
- } as any);
136
- const expectedOperations = buildGceLoadBalancerJobs((modal as any).state.command);
123
+ const closeModal = vi.fn();
124
+ const executeTask = vi.fn().mockReturnValue(Promise.resolve({ id: 'task' }));
125
+ const loadBalancer = {
126
+ account: 'account-a',
127
+ backendServices: [{ healthCheck: 'check-a', name: 'backend-a', portName: 'http' }],
128
+ defaultService: 'backend-a',
129
+ healthChecks: [{ healthCheckType: 'HTTP', name: 'check-a', port: 80, requestPath: '/health' }],
130
+ listeners: [{ ipAddress: 'address-a', name: 'frontend', port: 80, protocol: 'HTTP' }],
131
+ loadBalancerType: 'HTTP',
132
+ name: 'test-app-main',
133
+ };
134
+ const expectedOperations = buildGceLoadBalancerJobs(
135
+ initializeGceHttpLoadBalancerCommand(loadBalancer, 'edit', application),
136
+ );
137
+ renderWithRouter(
138
+ <GceHttpLoadBalancerModal
139
+ app={application}
140
+ closeModal={closeModal}
141
+ data={emptyData}
142
+ dismissModal={vi.fn()}
143
+ executeTask={executeTask}
144
+ isNew={false}
145
+ loadBalancer={loadBalancer}
146
+ />,
147
+ );
137
148
 
138
- (modal as any).submit();
149
+ fireEvent.click(screen.getByRole('button', { name: 'Update' }));
139
150
 
140
- expect((modal as any).state.command.mode).toBe('edit');
141
151
  expect(closeModal).not.toHaveBeenCalled();
142
- expect(executeTask).toHaveBeenCalledOnceWith({
152
+ expect(executeTask).toHaveBeenCalledExactlyOnceWith({
143
153
  application,
144
154
  description: 'Update Load Balancer: test-app-main',
145
155
  job: expectedOperations,
@@ -147,37 +157,40 @@ describe('GceHttpLoadBalancerModal', () => {
147
157
  });
148
158
 
149
159
  it('executes normalized listener jobs in infrastructure mode', () => {
150
- const executeTask = jasmine.createSpy('executeTask').and.returnValue(Promise.resolve({ id: 'task' }));
151
- const modal = new GceHttpLoadBalancerModal({
152
- app: application,
153
- closeModal: jasmine.createSpy('closeModal'),
154
- dismissModal: jasmine.createSpy('dismissModal'),
155
- executeTask,
156
- loadBalancer: {
157
- account: 'account-a',
158
- backendServices: [{ healthCheck: 'check-a', name: 'backend-a', portName: 'http' }],
159
- defaultService: 'backend-a',
160
- healthChecks: [{ healthCheckType: 'HTTP', name: 'check-a', port: 80, requestPath: '/health' }],
161
- listeners: [
162
- { ipAddress: 'address-a', name: 'frontend-a', port: 80, protocol: 'HTTP' },
163
- { ipAddress: 'address-b', name: 'frontend-b', port: 8080, protocol: 'HTTP' },
164
- ],
165
- loadBalancerType: 'INTERNAL_MANAGED',
166
- name: 'test-app-main',
167
- network: 'network-a',
168
- region: 'europe-west1',
169
- subnet: 'subnet-a',
170
- },
171
- mode: 'create',
172
- } as any);
160
+ const executeTask = vi.fn().mockReturnValue(Promise.resolve({ id: 'task' }));
161
+ renderWithRouter(
162
+ <GceHttpLoadBalancerModal
163
+ app={application}
164
+ closeModal={vi.fn()}
165
+ data={emptyData}
166
+ dismissModal={vi.fn()}
167
+ executeTask={executeTask}
168
+ loadBalancer={{
169
+ account: 'account-a',
170
+ backendServices: [{ healthCheck: 'check-a', name: 'backend-a', portName: 'http' }],
171
+ defaultService: 'backend-a',
172
+ healthChecks: [{ healthCheckType: 'HTTP', name: 'check-a', port: 80, requestPath: '/health' }],
173
+ listeners: [
174
+ { ipAddress: 'address-a', name: 'frontend-a', port: 80, protocol: 'HTTP' },
175
+ { ipAddress: 'address-b', name: 'frontend-b', port: 8080, protocol: 'HTTP' },
176
+ ],
177
+ loadBalancerType: 'INTERNAL_MANAGED',
178
+ name: 'test-app-main',
179
+ network: 'network-a',
180
+ region: 'europe-west1',
181
+ subnet: 'subnet-a',
182
+ }}
183
+ mode="create"
184
+ />,
185
+ );
173
186
 
174
- (modal as any).submit();
187
+ fireEvent.click(screen.getByRole('button', { name: 'Create' }));
175
188
 
176
189
  expect(executeTask).toHaveBeenCalled();
177
- const jobs = executeTask.calls.mostRecent().args[0].job;
190
+ const jobs = executeTask.mock.lastCall[0].job;
178
191
  expect(jobs.map(({ name }: any) => name)).toEqual(['frontend-a', 'frontend-b']);
179
192
  expect(jobs[0]).toEqual(
180
- jasmine.objectContaining({
193
+ expect.objectContaining({
181
194
  ipAddress: 'address-a',
182
195
  ipProtocol: 'TCP',
183
196
  loadBalancerName: 'frontend-a',
@@ -189,11 +202,56 @@ describe('GceHttpLoadBalancerModal', () => {
189
202
  });
190
203
 
191
204
  it('serializes INTERNAL_MANAGED HTTPS listeners with certificates', () => {
192
- const closeModal = jasmine.createSpy('closeModal');
205
+ const closeModal = vi.fn();
206
+ renderWithRouter(
207
+ <GceHttpLoadBalancerModal
208
+ app={application}
209
+ closeModal={closeModal}
210
+ data={emptyData}
211
+ dismissModal={vi.fn()}
212
+ forPipelineConfig={true}
213
+ loadBalancer={{
214
+ account: 'account-a',
215
+ backendServices: [{ healthCheck: 'check-a', name: 'backend-a', portName: 'http' }],
216
+ defaultService: 'backend-a',
217
+ healthChecks: [{ healthCheckType: 'HTTPS', name: 'check-a', port: 443, requestPath: '/health' }],
218
+ listeners: [
219
+ {
220
+ certificate: 'regional-cert',
221
+ name: 'internal-https',
222
+ port: 443,
223
+ protocol: 'HTTPS',
224
+ subnet: 'subnet-a',
225
+ },
226
+ ],
227
+ loadBalancerType: 'INTERNAL_MANAGED',
228
+ name: 'test-app-internal',
229
+ network: 'network-a',
230
+ region: 'europe-west1',
231
+ subnet: 'subnet-a',
232
+ }}
233
+ />,
234
+ );
235
+
236
+ fireEvent.click(screen.getByRole('button', { name: 'Done' }));
237
+
238
+ expect(closeModal).toHaveBeenCalledWith([
239
+ expect.objectContaining({
240
+ certificate: 'regional-cert',
241
+ loadBalancerName: 'internal-https',
242
+ portRange: '443',
243
+ }),
244
+ ]);
245
+ });
246
+
247
+ it('flattens EXTERNAL_MANAGED pipeline payloads with regional network tier and certificate URL', () => {
248
+ const closeModal = vi.fn();
249
+ const regionalCertUrl =
250
+ '//certificatemanager.googleapis.com/projects/test/locations/europe-west1/certificates/regional-cert';
193
251
  const modal = new GceHttpLoadBalancerModal({
194
252
  app: application,
195
253
  closeModal,
196
- dismissModal: jasmine.createSpy('dismissModal'),
254
+ dismissModal: vi.fn(),
197
255
  forPipelineConfig: true,
198
256
  loadBalancer: {
199
257
  account: 'account-a',
@@ -202,42 +260,86 @@ describe('GceHttpLoadBalancerModal', () => {
202
260
  healthChecks: [{ healthCheckType: 'HTTPS', name: 'check-a', port: 443, requestPath: '/health' }],
203
261
  listeners: [
204
262
  {
205
- certificate: 'regional-cert',
206
- name: 'internal-https',
263
+ certificate: regionalCertUrl,
264
+ ipAddress: '203.0.113.10',
265
+ name: 'external-https',
266
+ networkTier: 'STANDARD',
207
267
  port: 443,
208
268
  protocol: 'HTTPS',
209
- subnet: 'subnet-a',
210
269
  },
211
270
  ],
212
- loadBalancerType: 'INTERNAL_MANAGED',
213
- name: 'test-app-internal',
271
+ loadBalancerType: 'EXTERNAL_MANAGED',
272
+ name: 'test-app-external',
214
273
  network: 'network-a',
215
274
  region: 'europe-west1',
216
- subnet: 'subnet-a',
217
275
  },
218
276
  } as any);
219
277
 
220
278
  (modal as any).submit();
221
279
 
222
280
  expect(closeModal).toHaveBeenCalledWith([
223
- jasmine.objectContaining({
224
- certificate: 'regional-cert',
225
- loadBalancerName: 'internal-https',
281
+ expect.objectContaining({
282
+ certificate: regionalCertUrl,
283
+ loadBalancerName: 'external-https',
284
+ loadBalancerType: 'EXTERNAL_MANAGED',
285
+ network: 'network-a',
286
+ networkTier: 'STANDARD',
226
287
  portRange: '443',
288
+ region: 'europe-west1',
289
+ urlMapName: 'test-app-external',
227
290
  }),
228
291
  ]);
292
+ const operations = closeModal.mock.lastCall[0];
293
+ expect((operations as any[])[0].certificateMap).toBeUndefined();
294
+ expect((operations as any[])[0].listeners).toBeUndefined();
295
+ });
296
+
297
+ it('initializes persisted EXTERNAL_MANAGED data with raw listener identity and regional certificate URL', () => {
298
+ const regionalCertUrl =
299
+ '//certificatemanager.googleapis.com/projects/test/locations/europe-west1/certificates/regional-cert';
300
+ const command = initializeGceHttpLoadBalancerCommand(
301
+ {
302
+ account: 'account-a',
303
+ listeners: [
304
+ {
305
+ certificate: regionalCertUrl,
306
+ ipAddress: '203.0.113.10',
307
+ name: 'external-https',
308
+ networkTier: 'STANDARD',
309
+ port: 443,
310
+ protocol: 'HTTPS',
311
+ },
312
+ ],
313
+ loadBalancerType: 'EXTERNAL_MANAGED',
314
+ urlMapName: 'test-app-external',
315
+ },
316
+ 'edit',
317
+ application,
318
+ );
319
+
320
+ expect(command.name).toBe('test-app-external');
321
+ expect(command.loadBalancerType).toBe('EXTERNAL_MANAGED');
322
+ expect(command.listeners[0]).toEqual({
323
+ address: { name: '203.0.113.10' },
324
+ certificate: { name: 'regional-cert', selfLink: regionalCertUrl },
325
+ name: 'external-https',
326
+ networkTier: 'STANDARD',
327
+ portRange: '443',
328
+ protocol: 'HTTPS',
329
+ });
330
+ expect(command.listeners[0].certificateMap).toBeUndefined();
229
331
  });
230
332
 
231
333
  (['HTTP', 'INTERNAL_MANAGED'] as const).forEach((loadBalancerType) => {
232
334
  ['pipeline', 'infrastructure'].forEach((submissionMode) => {
233
335
  it(`rejects ${loadBalancerType} ${submissionMode} commands with an unresolved path default and non-443 HTTPS port`, () => {
234
- const closeModal = jasmine.createSpy('closeModal');
235
- const executeTask = jasmine.createSpy('executeTask');
336
+ const closeModal = vi.fn();
337
+ const executeTask = vi.fn();
236
338
  const props = {
237
339
  app: application,
238
340
  closeModal,
239
341
  data: emptyData,
240
- dismissModal: jasmine.createSpy('dismissModal'),
342
+ dismissModal: vi.fn(),
241
343
  executeTask,
242
344
  forPipelineConfig: submissionMode === 'pipeline',
243
345
  loadBalancer: {
@@ -255,38 +357,37 @@ describe('GceHttpLoadBalancerModal', () => {
255
357
  },
256
358
  mode: submissionMode === 'infrastructure' ? 'create' : undefined,
257
359
  } as any;
258
- const wrapper = shallow(<GceHttpLoadBalancerModal {...props} />);
259
- const modal = wrapper.instance() as any;
260
- modal.setState({
261
- command: {
262
- ...modal.state.command,
263
- listeners: [{ ...modal.state.command.listeners[0], portRange: '8443' }],
264
- },
265
- });
266
- wrapper.update();
267
-
268
- modal.submit();
360
+ const command = initializeGceHttpLoadBalancerCommand(
361
+ props.loadBalancer,
362
+ submissionMode === 'pipeline' ? 'pipeline' : 'create',
363
+ application,
364
+ );
365
+ command.listeners = [{ ...command.listeners[0], portRange: '8443' }];
366
+ expect(validateGceHttpLoadBalancerCommand(command)).toEqual(
367
+ expect.arrayContaining([
368
+ 'Path matcher default backend service is required.',
369
+ 'HTTPS listeners must use port 443.',
370
+ ]),
371
+ );
372
+ renderWithRouter(<GceHttpLoadBalancerModal {...props} />);
269
373
 
270
374
  expect(executeTask).not.toHaveBeenCalled();
271
375
  expect(closeModal).not.toHaveBeenCalled();
272
- expect(wrapper.find('.gce-http-validation-errors').text()).toContain(
273
- 'Path matcher default backend service is required.',
274
- );
275
- expect(wrapper.find('.gce-http-validation-errors').text()).toContain('HTTPS listeners must use port 443.');
276
- expect(wrapper.find('.btn-primary').prop('disabled')).toBe(true);
376
+ expect(screen.getByText('Path matcher default backend service is required.')).toBeInTheDocument();
377
+ expect(screen.getByRole('button', { name: submissionMode === 'pipeline' ? 'Done' : 'Create' })).toBeDisabled();
277
378
  });
278
379
  });
279
380
  });
280
381
 
281
382
  ['pipeline', 'infrastructure'].forEach((submissionMode) => {
282
383
  it(`does not submit invalid ${submissionMode} commands`, () => {
283
- const closeModal = jasmine.createSpy('closeModal');
284
- const executeTask = jasmine.createSpy('executeTask');
384
+ const closeModal = vi.fn();
385
+ const executeTask = vi.fn();
285
386
  const props = {
286
387
  app: application,
287
388
  closeModal,
288
389
  data: emptyData,
289
- dismissModal: jasmine.createSpy('dismissModal'),
390
+ dismissModal: vi.fn(),
290
391
  executeTask,
291
392
  forPipelineConfig: submissionMode === 'pipeline',
292
393
  loadBalancer: {
@@ -297,30 +398,29 @@ describe('GceHttpLoadBalancerModal', () => {
297
398
  },
298
399
  mode: submissionMode === 'infrastructure' ? 'create' : undefined,
299
400
  } as any;
300
- const modal = new GceHttpLoadBalancerModal(props);
301
- const wrapper = shallow(<GceHttpLoadBalancerModal {...props} />);
302
-
303
- (modal as any).submit();
401
+ renderWithRouter(<GceHttpLoadBalancerModal {...props} />);
304
402
 
305
403
  expect(executeTask).not.toHaveBeenCalled();
306
404
  expect(closeModal).not.toHaveBeenCalled();
307
- expect(wrapper.find('.gce-http-validation-errors').exists()).toBe(true);
308
- expect(wrapper.find('.btn-primary').prop('disabled')).toBe(true);
405
+ expect(screen.getByText('Name is required.')).toBeInTheDocument();
406
+ expect(screen.getByRole('button', { name: submissionMode === 'pipeline' ? 'Done' : 'Create' })).toBeDisabled();
309
407
  });
310
408
  });
311
409
 
312
410
  it('renders modal controls as non-submit buttons', () => {
313
- const wrapper = shallow(
411
+ const { container } = renderWithRouter(
314
412
  <GceHttpLoadBalancerModal
315
413
  app={application}
316
- closeModal={jasmine.createSpy('closeModal')}
414
+ closeModal={vi.fn()}
317
415
  data={emptyData}
318
- dismissModal={jasmine.createSpy('dismissModal')}
416
+ dismissModal={vi.fn()}
319
417
  loadBalancer={{ account: 'account-a', loadBalancerType: 'HTTP', name: 'test-app-main' } as any}
320
418
  mode="edit"
321
419
  />,
322
420
  );
323
421
 
324
- expect(wrapper.find('button').everyWhere((button) => button.prop('type') === 'button')).toBe(true);
422
+ expect(
423
+ Array.from(container.querySelectorAll('button')).every((button) => button.getAttribute('type') === 'button'),
424
+ ).toBe(true);
325
425
  });
326
426
  });
@@ -43,7 +43,7 @@ export interface IGceHttpLoadBalancerModalProps extends IModalComponentProps {
43
43
  forPipelineConfig?: boolean;
44
44
  isNew?: boolean;
45
45
  loadBalancer?: Record<string, unknown>;
46
- loadBalancerType?: Extract<GceLoadBalancerType, 'HTTP' | 'INTERNAL_MANAGED'>;
46
+ loadBalancerType?: Extract<GceLoadBalancerType, 'HTTP' | 'INTERNAL_MANAGED' | 'EXTERNAL_MANAGED'>;
47
47
  mode?: GceLoadBalancerEditorMode;
48
48
  }
49
49
 
@@ -171,7 +171,13 @@ export class GceHttpLoadBalancerModal extends React.Component<
171
171
  const validationErrors = validateGceHttpLoadBalancerCommand(command);
172
172
  const heading =
173
173
  command.mode === 'create'
174
- ? `Create ${command.loadBalancerType === 'HTTP' ? 'HTTP(S)' : 'Internal managed HTTP(S)'} load balancer`
174
+ ? `Create ${
175
+ command.loadBalancerType === 'INTERNAL_MANAGED'
176
+ ? 'Internal managed HTTP(S)'
177
+ : command.loadBalancerType === 'EXTERNAL_MANAGED'
178
+ ? 'External managed HTTP(S)'
179
+ : 'HTTP(S)'
180
+ } load balancer`
175
181
  : `Edit ${command.name}`;
176
182
 
177
183
  return (