@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
@@ -1,5 +1,6 @@
1
+ import type { Mock } from 'vitest';
1
2
  import React from 'react';
2
- import { mount as enzymeMount, shallow } from 'enzyme';
3
+ import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
3
4
 
4
5
  import { AccountService, ConfirmationModalService, DeckRuntimeContext, SecurityGroupWriter } from '@spinnaker/core';
5
6
 
@@ -11,11 +12,11 @@ describe('GceSecurityGroupDetails', () => {
11
12
  const RuntimeWrapper = ({ children }: React.PropsWithChildren<{}>) => (
12
13
  <DeckRuntimeContext.Provider value={{ services: runtimeServices } as any}>{children}</DeckRuntimeContext.Provider>
13
14
  );
14
- const mount = (component: React.ReactElement) => enzymeMount(component, { wrappingComponent: RuntimeWrapper });
15
+ const renderWithRuntime = (component: React.ReactElement) => render(<RuntimeWrapper>{component}</RuntimeWrapper>);
15
16
 
16
17
  beforeEach(() => {
17
18
  runtimeServices = {};
18
- spyOn(AccountService, 'challengeDestructiveActions').and.resolveTo(false);
19
+ vi.spyOn(AccountService, 'challengeDestructiveActions').mockResolvedValue(false);
19
20
  });
20
21
 
21
22
  const firstRoute = {
@@ -48,83 +49,72 @@ describe('GceSecurityGroupDetails', () => {
48
49
  };
49
50
  }
50
51
 
51
- async function flush(): Promise<void> {
52
- await Promise.resolve();
53
- await Promise.resolve();
54
- }
55
-
56
52
  it('reloads the current firewall details after the security group data source refreshes', async () => {
57
53
  let refreshDetails: (() => void) | undefined;
58
54
  const app = {
59
55
  securityGroups: {
60
- onRefresh: jasmine.createSpy('onRefresh').and.callFake((callback: () => void) => {
56
+ onRefresh: vi.fn().mockImplementation((callback: () => void) => {
61
57
  refreshDetails = callback;
62
- return jasmine.createSpy('unsubscribe');
58
+ return vi.fn();
63
59
  }),
64
60
  },
65
61
  };
66
62
  const reader = {
67
- getSecurityGroupDetails: jasmine
68
- .createSpy('getSecurityGroupDetails')
69
- .and.returnValues(
70
- Promise.resolve(details('first-firewall', 'before refresh')),
71
- Promise.resolve(details('first-firewall', 'after refresh')),
72
- ),
63
+ getSecurityGroupDetails: vi
64
+ .fn()
65
+ .mockReturnValueOnce(Promise.resolve(details('first-firewall', 'before refresh')))
66
+ .mockReturnValueOnce(Promise.resolve(details('first-firewall', 'after refresh'))),
73
67
  };
74
68
  runtimeServices.securityGroupReader = reader;
75
- const wrapper = mount(<GceSecurityGroupDetails app={app as any} resolvedSecurityGroup={firstRoute} />);
69
+ const rendered = renderWithRuntime(<GceSecurityGroupDetails app={app as any} resolvedSecurityGroup={firstRoute} />);
76
70
 
77
- await flush();
78
- wrapper.update();
79
- expect(wrapper.text()).toContain('before refresh');
71
+ await waitFor(() => expect(screen.getByText('before refresh')).toBeInTheDocument());
80
72
  expect(refreshDetails).toBeDefined();
81
73
 
82
- refreshDetails?.();
83
- await flush();
84
- wrapper.update();
74
+ await act(async () => refreshDetails?.());
85
75
 
86
76
  expect(reader.getSecurityGroupDetails).toHaveBeenCalledTimes(2);
87
- expect(wrapper.text()).toContain('after refresh');
88
- wrapper.unmount();
77
+ await waitFor(() => expect(screen.getByText('after refresh')).toBeInTheDocument());
78
+ rendered.unmount();
89
79
  });
90
80
 
91
81
  it('clears prior details and actions on route change and ignores an out-of-order response', async () => {
92
82
  const secondRequest = deferred<any>();
93
83
  const thirdRequest = deferred<any>();
94
84
  const reader = {
95
- getSecurityGroupDetails: jasmine
96
- .createSpy('getSecurityGroupDetails')
97
- .and.returnValues(
98
- Promise.resolve(details('first-firewall', 'first details')),
99
- secondRequest.promise,
100
- thirdRequest.promise,
101
- ),
85
+ getSecurityGroupDetails: vi
86
+ .fn()
87
+ .mockReturnValueOnce(Promise.resolve(details('first-firewall', 'first details')))
88
+ .mockReturnValueOnce(secondRequest.promise)
89
+ .mockReturnValueOnce(thirdRequest.promise),
102
90
  };
103
91
  runtimeServices.securityGroupReader = reader;
104
- const app = { securityGroups: { onRefresh: () => jasmine.createSpy('unsubscribe') } };
105
- const wrapper = mount(<GceSecurityGroupDetails app={app as any} resolvedSecurityGroup={firstRoute} />);
106
-
107
- await flush();
108
- wrapper.update();
109
- expect(wrapper.find(GceSecurityGroupActions).length).toBe(1);
110
- expect(wrapper.text()).toContain('first details');
111
-
112
- wrapper.setProps({ resolvedSecurityGroup: { ...firstRoute, name: 'second-firewall' } });
113
- expect(wrapper.find(GceSecurityGroupActions).length).toBe(0);
114
- expect(wrapper.text()).not.toContain('first details');
115
-
116
- wrapper.setProps({ resolvedSecurityGroup: { ...firstRoute, name: 'third-firewall' } });
117
- secondRequest.resolve(details('second-firewall', 'stale second details'));
118
- await flush();
119
- wrapper.update();
120
- expect(wrapper.text()).not.toContain('stale second details');
121
-
122
- thirdRequest.resolve(details('third-firewall', 'current third details'));
123
- await flush();
124
- wrapper.update();
125
- expect(wrapper.text()).toContain('current third details');
126
- expect(wrapper.find(GceSecurityGroupActions).length).toBe(1);
127
- wrapper.unmount();
92
+ const app = { securityGroups: { onRefresh: () => vi.fn() } };
93
+ const rendered = renderWithRuntime(<GceSecurityGroupDetails app={app as any} resolvedSecurityGroup={firstRoute} />);
94
+
95
+ await waitFor(() => expect(screen.getByText('first details')).toBeInTheDocument());
96
+ expect(screen.getByRole('button', { name: /Firewall Actions/ })).toBeInTheDocument();
97
+
98
+ rendered.rerender(
99
+ <RuntimeWrapper>
100
+ <GceSecurityGroupDetails app={app as any} resolvedSecurityGroup={{ ...firstRoute, name: 'second-firewall' }} />
101
+ </RuntimeWrapper>,
102
+ );
103
+ expect(screen.queryByRole('button', { name: /Firewall Actions/ })).not.toBeInTheDocument();
104
+ expect(screen.queryByText('first details')).not.toBeInTheDocument();
105
+
106
+ rendered.rerender(
107
+ <RuntimeWrapper>
108
+ <GceSecurityGroupDetails app={app as any} resolvedSecurityGroup={{ ...firstRoute, name: 'third-firewall' }} />
109
+ </RuntimeWrapper>,
110
+ );
111
+ await act(async () => secondRequest.resolve(details('second-firewall', 'stale second details')));
112
+ expect(screen.queryByText('stale second details')).not.toBeInTheDocument();
113
+
114
+ await act(async () => thirdRequest.resolve(details('third-firewall', 'current third details')));
115
+ await waitFor(() => expect(screen.getByText('current third details')).toBeInTheDocument());
116
+ expect(screen.getByRole('button', { name: /Firewall Actions/ })).toBeInTheDocument();
117
+ rendered.unmount();
128
118
  });
129
119
  });
130
120
 
@@ -133,11 +123,11 @@ describe('GceSecurityGroupActions', () => {
133
123
  const RuntimeWrapper = ({ children }: React.PropsWithChildren<{}>) => (
134
124
  <DeckRuntimeContext.Provider value={{ services: runtimeServices }}>{children}</DeckRuntimeContext.Provider>
135
125
  );
136
- const mountActions = (component: React.ReactElement) => enzymeMount(component, { wrappingComponent: RuntimeWrapper });
126
+ const renderActions = (component: React.ReactElement) => render(<RuntimeWrapper>{component}</RuntimeWrapper>);
137
127
 
138
128
  const app = {
139
129
  name: 'my-app',
140
- securityGroups: { refresh: jasmine.createSpy('refresh') },
130
+ securityGroups: { refresh: vi.fn() },
141
131
  };
142
132
  const resolvedSecurityGroup = {
143
133
  accountId: 'my-account',
@@ -156,24 +146,26 @@ describe('GceSecurityGroupActions', () => {
156
146
  };
157
147
 
158
148
  it('opens edit and clone modals and confirms deletion with complete firewall identity', () => {
159
- spyOn(GceSecurityGroupModal, 'show');
160
- spyOn(ConfirmationModalService, 'confirm');
161
- spyOn(SecurityGroupWriter, 'deleteSecurityGroup').and.returnValue(Promise.resolve({} as any));
162
- const wrapper = mountActions(
149
+ vi.spyOn(GceSecurityGroupModal, 'show').mockReturnValue(undefined);
150
+ vi.spyOn(ConfirmationModalService, 'confirm').mockReturnValue(undefined);
151
+ vi.spyOn(SecurityGroupWriter, 'deleteSecurityGroup').mockReturnValue(Promise.resolve({} as any));
152
+ const rendered = renderActions(
163
153
  <GceSecurityGroupActions
164
154
  app={app as any}
165
155
  resolvedSecurityGroup={resolvedSecurityGroup}
166
156
  securityGroup={securityGroup}
167
157
  />,
168
158
  );
169
- const items = wrapper.find('button[data-action]');
159
+ const items = Array.from(rendered.container.querySelectorAll<HTMLButtonElement>('button[data-action]'));
170
160
 
171
- expect(items.map((item) => item.text())).toEqual(['Edit Inbound Rules', 'Clone Firewall', 'Delete Firewall']);
172
- items.at(0).prop('onClick')({} as any);
173
- items.at(1).prop('onClick')({} as any);
174
- items.at(2).prop('onClick')({} as any);
161
+ expect(items.map((item) => item.textContent?.trim())).toEqual([
162
+ 'Edit Inbound Rules',
163
+ 'Clone Firewall',
164
+ 'Delete Firewall',
165
+ ]);
166
+ items.forEach((item) => fireEvent.click(item));
175
167
 
176
- const firewallWithIdentity = jasmine.objectContaining({
168
+ const firewallWithIdentity = expect.objectContaining({
177
169
  accountId: 'my-account',
178
170
  id: 'my-firewall',
179
171
  name: 'my-firewall',
@@ -197,25 +189,25 @@ describe('GceSecurityGroupActions', () => {
197
189
  runtimeServices,
198
190
  );
199
191
  expect(ConfirmationModalService.confirm).toHaveBeenCalledWith(
200
- jasmine.objectContaining({
192
+ expect.objectContaining({
201
193
  account: 'my-account',
202
194
  buttonText: 'Delete my-firewall',
203
195
  header: 'Really delete my-firewall?',
204
- taskMonitorConfig: jasmine.objectContaining({ application: app as any, title: 'Deleting my-firewall' }),
196
+ taskMonitorConfig: expect.objectContaining({ application: app as any, title: 'Deleting my-firewall' }),
205
197
  }),
206
198
  );
207
199
 
208
- const confirmation = (ConfirmationModalService.confirm as jasmine.Spy).calls.mostRecent().args[0];
200
+ const confirmation = (ConfirmationModalService.confirm as Mock).mock.lastCall[0];
209
201
  confirmation.submitMethod();
210
202
  expect(SecurityGroupWriter.deleteSecurityGroup).toHaveBeenCalledWith(
211
203
  firewallWithIdentity,
212
204
  app as any,
213
- jasmine.objectContaining({ cloudProvider: 'gce', securityGroupName: 'my-firewall' }),
205
+ expect.objectContaining({ cloudProvider: 'gce', securityGroupName: 'my-firewall' }),
214
206
  );
215
207
  });
216
208
 
217
209
  it('disables host-project shared-VPC actions and explains why they are read-only', () => {
218
- const wrapper = mountActions(
210
+ const rendered = renderActions(
219
211
  <GceSecurityGroupActions
220
212
  app={app as any}
221
213
  resolvedSecurityGroup={resolvedSecurityGroup}
@@ -223,10 +215,9 @@ describe('GceSecurityGroupActions', () => {
223
215
  />,
224
216
  );
225
217
 
226
- expect(wrapper.find('button[data-action]').length).toBe(3);
227
- wrapper.find('button[data-action]').forEach((item) => expect(item.prop('disabled')).toBe(true));
228
- expect(wrapper.find('.shared-vpc-warning').text()).toContain(
229
- 'You cannot modify shared VPC host project firewall rules.',
230
- );
218
+ const items = Array.from(rendered.container.querySelectorAll<HTMLButtonElement>('button[data-action]'));
219
+ expect(items).toHaveLength(3);
220
+ items.forEach((item) => expect(item).toBeDisabled());
221
+ expect(screen.getByText('You cannot modify shared VPC host project firewall rules.')).toBeInTheDocument();
231
222
  });
232
223
  });
@@ -11,25 +11,25 @@ describe('GceServerGroupCommandBuilder', () => {
11
11
  let xpnNamingService;
12
12
 
13
13
  beforeEach(() => {
14
- spyOn(AccountService, 'listAccounts').and.resolveTo([{ name: 'test-account' }]);
15
- spyOn(ExpectedArtifactService, 'getExpectedArtifactsAvailableToStage').and.returnValue([]);
16
- instanceTypeService = jasmine.createSpyObj('instanceTypeService', [
17
- 'getCategories',
18
- 'getInstanceTypeDetails',
19
- 'getCategoryForInstanceType',
20
- ]);
21
- instanceTypeService.getCategories.and.resolveTo([
14
+ vi.spyOn(AccountService, 'listAccounts').mockResolvedValue([{ name: 'test-account' }]);
15
+ vi.spyOn(ExpectedArtifactService, 'getExpectedArtifactsAvailableToStage').mockReturnValue([]);
16
+ instanceTypeService = {
17
+ getCategories: vi.fn(),
18
+ getInstanceTypeDetails: vi.fn(),
19
+ getCategoryForInstanceType: vi.fn(),
20
+ };
21
+ instanceTypeService.getCategories.mockResolvedValue([
22
22
  { type: 'custom', families: [{ instanceTypes: [{ name: 'n1-standard-1' }] }] },
23
23
  ]);
24
- instanceTypeService.getInstanceTypeDetails.and.resolveTo({
24
+ instanceTypeService.getInstanceTypeDetails.mockResolvedValue({
25
25
  storage: { localSSDSupported: false, size: 10, count: 1 },
26
26
  });
27
- instanceTypeService.getCategoryForInstanceType.and.resolveTo('custom');
28
- customInstanceBuilder = jasmine.createSpyObj('customInstanceBuilder', ['parseInstanceTypeString']);
29
- customInstanceBuilder.parseInstanceTypeString.and.returnValue({});
30
- xpnNamingService = jasmine.createSpyObj('xpnNamingService', ['deriveProjectId', 'decorateXpnResourceIfNecessary']);
31
- xpnNamingService.deriveProjectId.and.returnValue('test-project');
32
- xpnNamingService.decorateXpnResourceIfNecessary.and.callFake((_projectId, resource) => resource);
27
+ instanceTypeService.getCategoryForInstanceType.mockResolvedValue('custom');
28
+ customInstanceBuilder = { parseInstanceTypeString: vi.fn() };
29
+ customInstanceBuilder.parseInstanceTypeString.mockReturnValue({});
30
+ xpnNamingService = { deriveProjectId: vi.fn(), decorateXpnResourceIfNecessary: vi.fn() };
31
+ xpnNamingService.deriveProjectId.mockReturnValue('test-project');
32
+ xpnNamingService.decorateXpnResourceIfNecessary.mockImplementation((_projectId, resource) => resource);
33
33
 
34
34
  const promiseService = { all: (values) => Promise.all(values), resolve: (value) => Promise.resolve(value) };
35
35
  builder = new GceServerGroupCommandBuilder(
@@ -153,6 +153,26 @@ describe('GceServerGroupCommandBuilder', () => {
153
153
  expect(serverGroup.instanceFlexibilityPolicy.instanceSelections.preferred.machineTypes).toEqual(['n2-standard-8']);
154
154
  });
155
155
 
156
+ it('keeps Clouddriver-managed load balancer metadata out of clone instance metadata', async () => {
157
+ const promiseService = { all: (values) => Promise.all(values), resolve: (value) => Promise.resolve(value) };
158
+ const defaultHiddenKeysBuilder = new GceServerGroupCommandBuilder(
159
+ promiseService,
160
+ instanceTypeService,
161
+ customInstanceBuilder,
162
+ xpnNamingService,
163
+ );
164
+ const serverGroup = buildServerGroup();
165
+ serverGroup.launchConfig.instanceTemplate.properties.metadata.items = [
166
+ { key: 'load-balancer-names', value: 'listener-a,listener-b' },
167
+ { key: 'regional-external-load-balancer-names', value: 'listener-a,listener-b' },
168
+ { key: 'team', value: 'payments' },
169
+ ];
170
+
171
+ const command = await defaultHiddenKeysBuilder.buildServerGroupCommandFromExisting(application(), serverGroup);
172
+
173
+ expect(command.instanceMetadata).toEqual({ team: 'payments' });
174
+ });
175
+
156
176
  it('preserves absent versus explicitly empty clone flexibility policies', async () => {
157
177
  const absent = buildServerGroup();
158
178
  const empty = buildServerGroup();
@@ -428,6 +428,7 @@ export class GceServerGroupConfigurationService {
428
428
  command.loadBalancers,
429
429
  command.credentials,
430
430
  newLoadBalancerObjects,
431
+ command.region,
431
432
  );
432
433
  const matched = _.intersection(command.backingData.filtered.loadBalancers, command.loadBalancers);
433
434
  const removed = _.xor(matched, command.loadBalancers);
@@ -452,7 +453,12 @@ export class GceServerGroupConfigurationService {
452
453
  const lbIndex = command.backingData.filtered.loadBalancerIndex;
453
454
 
454
455
  const backendServices = command.loadBalancers.reduce((backendServices, lbName) => {
455
- if (gceHttpLoadBalancerUtils.isHttpLoadBalancer(lbIndex[lbName])) {
456
+ // Clouddriver attaches every EXTERNAL_MANAGED backend service; its regional names must not
457
+ // enter the global backend-service-names metadata.
458
+ if (
459
+ gceHttpLoadBalancerUtils.isHttpLoadBalancer(lbIndex[lbName]) &&
460
+ lbIndex[lbName].loadBalancerType !== 'EXTERNAL_MANAGED'
461
+ ) {
456
462
  backendServices[lbName] = _.intersection(lbIndex[lbName].backendServices, backendsFromMetadata);
457
463
  }
458
464
  return backendServices;
@@ -1,9 +1,79 @@
1
1
  'use strict';
2
2
 
3
- import { GCE_DISTRIBUTION_POLICY_TARGET_SHAPES } from './serverGroupConfiguration.service';
3
+ import { nativePromiseService } from '@spinnaker/core';
4
4
 
5
- describe('gceServerGroupConfigurationService target shapes', function () {
6
- it('exposes BALANCED and ANY_SINGLE_ZONE alongside ANY and EVEN', function () {
7
- expect(GCE_DISTRIBUTION_POLICY_TARGET_SHAPES).toEqual(['ANY', 'EVEN', 'BALANCED', 'ANY_SINGLE_ZONE']);
5
+ import {
6
+ GCE_DISTRIBUTION_POLICY_TARGET_SHAPES,
7
+ GceServerGroupConfigurationService,
8
+ } from './serverGroupConfiguration.service';
9
+
10
+ describe('GceServerGroupConfigurationService', () => {
11
+ let service;
12
+
13
+ beforeEach(() => {
14
+ service = new GceServerGroupConfigurationService(nativePromiseService, {
15
+ securityGroupReader: {},
16
+ loadBalancerReader: {},
17
+ });
18
+ });
19
+
20
+ describe('configureLoadBalancerOptions', () => {
21
+ it('scopes EXTERNAL_MANAGED listener normalization by account and region without seeding global backends', () => {
22
+ const command = {
23
+ credentials: 'account-a',
24
+ region: 'europe-west1',
25
+ loadBalancers: ['external-listener'],
26
+ backendServiceMetadata: ['europe-backend'],
27
+ backingData: {
28
+ loadBalancers: [
29
+ {
30
+ accounts: [
31
+ {
32
+ name: 'account-a',
33
+ regions: [
34
+ {
35
+ loadBalancers: [
36
+ {
37
+ account: 'account-a',
38
+ listeners: [{ name: 'external-listener' }],
39
+ loadBalancerType: 'EXTERNAL_MANAGED',
40
+ name: 'external-listener',
41
+ provider: 'gce',
42
+ region: 'us-central1',
43
+ urlMapName: 'app-main',
44
+ backendServices: ['central-backend', 'other-backend'],
45
+ },
46
+ {
47
+ account: 'account-a',
48
+ listeners: [{ name: 'external-listener' }],
49
+ loadBalancerType: 'EXTERNAL_MANAGED',
50
+ name: 'external-listener',
51
+ provider: 'gce',
52
+ region: 'europe-west1',
53
+ urlMapName: 'app-main',
54
+ backendServices: ['europe-backend'],
55
+ },
56
+ ],
57
+ },
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ filtered: {},
64
+ },
65
+ };
66
+
67
+ service.configureLoadBalancerOptions(command);
68
+
69
+ expect(command.loadBalancers).toEqual(['app-main (account-a/europe-west1/EXTERNAL_MANAGED)']);
70
+ expect(command.backendServices).toBeUndefined();
71
+ });
72
+ });
73
+
74
+ describe('target shapes', () => {
75
+ it('exposes BALANCED and ANY_SINGLE_ZONE alongside ANY and EVEN', () => {
76
+ expect(GCE_DISTRIBUTION_POLICY_TARGET_SHAPES).toEqual(['ANY', 'EVEN', 'BALANCED', 'ANY_SINGLE_ZONE']);
77
+ });
8
78
  });
9
79
  });