@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
@@ -6,6 +6,7 @@ import {
6
6
  CloudProviderRegistry,
7
7
  CollapsibleSection,
8
8
  ConfirmationModalService,
9
+ HelpField,
9
10
  InfrastructureCaches,
10
11
  LoadBalancerWriter,
11
12
  ManagedMenuItem,
@@ -36,14 +37,21 @@ function isSameVpc(candidate: any, params: any): boolean {
36
37
  }
37
38
 
38
39
  function findLoadBalancerInList(loadBalancers: any[], params: any): any {
39
- return (loadBalancers || []).find((candidate: any) => {
40
- return (
41
- candidate.name === params.name &&
40
+ const inScope = (loadBalancers || []).filter(
41
+ (candidate: any) =>
42
42
  candidate.account === params.accountId &&
43
43
  (candidate.region === params.region || candidate.region === 'global') &&
44
- isSameVpc(candidate, params)
45
- );
46
- });
44
+ isSameVpc(candidate, params),
45
+ );
46
+ // EXTERNAL_MANAGED rows carry a decorated display name; links built from the raw URL map name
47
+ // still resolve once no exact name matches.
48
+ return (
49
+ inScope.find((candidate: any) => candidate.name === params.name) ||
50
+ inScope.find(
51
+ (candidate: any) =>
52
+ gceHttpLoadBalancerUtils.isRegionalHttpLoadBalancer(candidate) && candidate.urlMapName === params.name,
53
+ )
54
+ );
47
55
  }
48
56
 
49
57
  function uniqBy<T>(items: T[], getKey: (item: T) => string): T[] {
@@ -58,11 +66,18 @@ function uniqBy<T>(items: T[], getKey: (item: T) => string): T[] {
58
66
  });
59
67
  }
60
68
 
61
- function getProtocol(loadBalancer: any): string {
69
+ function getProtocol(loadBalancer: any, httpFamily = false): string | undefined {
70
+ if (!httpFamily) {
71
+ return undefined;
72
+ }
62
73
  const port = loadBalancer?.listenerDescriptions?.[0]?.listener?.loadBalancerPort;
63
74
  return port === '443' ? 'https:' : 'http:';
64
75
  }
65
76
 
77
+ function formatDns(dns: { dnsname: string; protocol?: string }): string {
78
+ return dns.protocol ? `${dns.protocol}//${dns.dnsname}` : dns.dnsname;
79
+ }
80
+
66
81
  function getBackendServices(loadBalancer: any): any[] {
67
82
  let backendServices = [loadBalancer.defaultService].filter(Boolean);
68
83
 
@@ -83,6 +98,7 @@ function getBackendServices(loadBalancer: any): any[] {
83
98
  function getResourceTypes(loadBalancerType: string): string[] {
84
99
  switch (loadBalancerType) {
85
100
  case 'INTERNAL':
101
+ case 'REGIONAL_EXTERNAL_NETWORK':
86
102
  return ['gce_forwarding_rule', 'gce_backend_service'];
87
103
  case 'NETWORK':
88
104
  return ['gce_forwarding_rule', 'gce_target_pool', 'gce_health_check'];
@@ -90,16 +106,23 @@ function getResourceTypes(loadBalancerType: string): string[] {
90
106
  case 'TCP':
91
107
  return ['gce_forwarding_rule', 'gce_backend_service'];
92
108
  case 'INTERNAL_MANAGED':
109
+ case 'EXTERNAL_MANAGED':
93
110
  return ['http_load_balancer', 'gce_target_http_proxy', 'gce_url_map', 'gce_backend_service'];
94
111
  default:
95
112
  return ['http_load_balancer', 'gce_target_http_proxy', 'gce_url_map', 'gce_backend_service'];
96
113
  }
97
114
  }
98
115
 
116
+ function getLogSearchName(loadBalancer: any): string {
117
+ return gceHttpLoadBalancerUtils.isRegionalHttpLoadBalancer(loadBalancer)
118
+ ? loadBalancer.urlMapName || loadBalancer.name
119
+ : loadBalancer.name;
120
+ }
121
+
99
122
  function buildLogsLink(project: string, loadBalancer: any): string {
100
123
  return `https://console.developers.google.com/project/${project}/logs?advancedFilter=resource.type=(${getResourceTypes(
101
124
  loadBalancer.loadBalancerType,
102
- ).join(' OR ')})%0A"${loadBalancer.name}"`;
125
+ ).join(' OR ')})%0A"${getLogSearchName(loadBalancer)}"`;
103
126
  }
104
127
 
105
128
  async function loadDetails(loadBalancer: any, loadBalancerParams: any, loadBalancerReader: any): Promise<any[]> {
@@ -126,7 +149,7 @@ async function loadDetails(loadBalancer: any, loadBalancerParams: any, loadBalan
126
149
 
127
150
  representativeLoadBalancer.dns = uniqBy(
128
151
  loadBalancers
129
- .map((detail: any) => ({ dnsname: detail.dnsname, protocol: getProtocol(detail) }))
152
+ .map((detail: any) => ({ dnsname: detail.dnsname, protocol: getProtocol(detail, true) }))
130
153
  .filter((detail: any) => detail.dnsname),
131
154
  (detail: any) => detail.dnsname,
132
155
  );
@@ -143,7 +166,10 @@ async function loadDetails(loadBalancer: any, loadBalancerParams: any, loadBalan
143
166
  loadBalancer.name,
144
167
  );
145
168
  if (details[0]) {
146
- details[0].dns = { dnsname: details[0].dnsname, protocol: getProtocol(details[0]) };
169
+ details[0].dns = {
170
+ dnsname: details[0].dnsname,
171
+ protocol: getProtocol(details[0], loadBalancer.loadBalancerType !== 'REGIONAL_EXTERNAL_NETWORK'),
172
+ };
147
173
  }
148
174
  return details;
149
175
  }
@@ -175,6 +201,11 @@ export async function loadGceLoadBalancerDetails({
175
201
  loadBalancer.elb.backendServices.map((backendService: any) => backendService.healthCheck).filter(Boolean),
176
202
  (healthCheck: any) => healthCheck.name || healthCheck.selfLink || '',
177
203
  );
204
+ } else if (loadBalancer.loadBalancerType === 'REGIONAL_EXTERNAL_NETWORK' && loadBalancer.backendService) {
205
+ loadBalancer.elb.backendServices = [loadBalancer.backendService];
206
+ loadBalancer.elb.healthChecks = loadBalancer.backendService.healthCheck
207
+ ? [loadBalancer.backendService.healthCheck]
208
+ : [];
178
209
  }
179
210
  }
180
211
 
@@ -251,9 +282,9 @@ function deleteGceHttpLoadBalancer(loadBalancer: any, app: any, params: any = {}
251
282
 
252
283
  return TaskExecutor.executeTask({
253
284
  application: app,
254
- description: `Delete load balancer: ${loadBalancer.urlMapName || loadBalancer.name} in ${
255
- loadBalancer.account
256
- }:global`,
285
+ description: `Delete load balancer: ${loadBalancer.urlMapName || loadBalancer.name} in ${loadBalancer.account}:${
286
+ loadBalancer.loadBalancerType === 'EXTERNAL_MANAGED' ? region : 'global'
287
+ }`,
257
288
  job: [job],
258
289
  });
259
290
  }
@@ -288,7 +319,10 @@ function GceLoadBalancerDeleteOptions({
288
319
  deleteParams: { deleteHealthChecks: boolean };
289
320
  loadBalancer: any;
290
321
  }): JSX.Element {
291
- const hasHealthChecks = gceHttpLoadBalancerUtils.isHttpLoadBalancer(loadBalancer) || !!loadBalancer.healthCheck;
322
+ const hasHealthChecks =
323
+ gceHttpLoadBalancerUtils.isHttpLoadBalancer(loadBalancer) ||
324
+ !!loadBalancer.healthCheck ||
325
+ (loadBalancer.loadBalancerType === 'REGIONAL_EXTERNAL_NETWORK' && !!loadBalancer.backendService?.healthCheck);
292
326
  if (!hasHealthChecks) {
293
327
  return null;
294
328
  }
@@ -382,8 +416,8 @@ export function GceLoadBalancerInformationSection({ loadBalancer }: { app: any;
382
416
  <dt>DNS</dt>
383
417
  <dd>
384
418
  {Array.isArray(loadBalancer.elb.dns)
385
- ? loadBalancer.elb.dns.map((dns: any) => `${dns.protocol}//${dns.dnsname}`).join(', ')
386
- : `${loadBalancer.elb.dns.protocol}//${loadBalancer.elb.dns.dnsname}`}
419
+ ? loadBalancer.elb.dns.map(formatDns).join(', ')
420
+ : formatDns(loadBalancer.elb.dns)}
387
421
  </dd>
388
422
  </>
389
423
  )}
@@ -392,15 +426,64 @@ export function GceLoadBalancerInformationSection({ loadBalancer }: { app: any;
392
426
  );
393
427
  }
394
428
 
429
+ function formatListenerDescription(description: any): string {
430
+ const listener = description.listener || description;
431
+ return `${listener.protocol}:${listener.loadBalancerPort} → ${listener.instanceProtocol}:${listener.instancePort}`;
432
+ }
433
+
434
+ function formatNormalizedHttpListener(listener: any): string {
435
+ const port = listener.port || listener.portRange?.split('-')[0];
436
+ const protocol = listener.protocol || (port === '443' || listener.certificate ? 'HTTPS' : 'HTTP');
437
+ return `${protocol}:${port}`;
438
+ }
439
+
440
+ function getListenerDisplayRows(loadBalancer: any): string[] {
441
+ const listenerDescriptions = loadBalancer.elb?.listenerDescriptions;
442
+ if (listenerDescriptions?.length) {
443
+ return listenerDescriptions.map(formatListenerDescription);
444
+ }
445
+
446
+ if (gceHttpLoadBalancerUtils.isHttpLoadBalancer(loadBalancer) && loadBalancer.listeners?.length) {
447
+ return loadBalancer.listeners.map(formatNormalizedHttpListener);
448
+ }
449
+
450
+ return [];
451
+ }
452
+
395
453
  export function GceLoadBalancerListenersSection({ loadBalancer }: { app: any; loadBalancer: any }): JSX.Element {
454
+ if (
455
+ loadBalancer.loadBalancerType !== 'EXTERNAL_MANAGED' &&
456
+ loadBalancer.loadBalancerType !== 'REGIONAL_EXTERNAL_NETWORK'
457
+ ) {
458
+ return (
459
+ <CollapsibleSection heading="Listeners" defaultExpanded={true}>
460
+ {(loadBalancer.listeners || []).length ? (
461
+ <ul>
462
+ {loadBalancer.listeners.map((listener: any, index: number) => (
463
+ <li key={index}>{[listener.protocol, listener.port || listener.portRange].filter(Boolean).join(':')}</li>
464
+ ))}
465
+ </ul>
466
+ ) : (
467
+ <span>No listeners configured</span>
468
+ )}
469
+ </CollapsibleSection>
470
+ );
471
+ }
472
+
473
+ const listenerRows = getListenerDisplayRows(loadBalancer);
474
+
396
475
  return (
397
476
  <CollapsibleSection heading="Listeners" defaultExpanded={true}>
398
- {(loadBalancer.listeners || []).length ? (
399
- <ul>
400
- {loadBalancer.listeners.map((listener: any, index: number) => (
401
- <li key={index}>{[listener.protocol, listener.port || listener.portRange].filter(Boolean).join(':')}</li>
477
+ {listenerRows.length ? (
478
+ <dl>
479
+ <dt>
480
+ Load Balancer → Instance
481
+ {loadBalancer.loadBalancerType === 'EXTERNAL_MANAGED' && <HelpField id="gce.httpLoadBalancer.namedPort" />}
482
+ </dt>
483
+ {listenerRows.map((listenerRow, index) => (
484
+ <dd key={index}>{listenerRow}</dd>
402
485
  ))}
403
- </ul>
486
+ </dl>
404
487
  ) : (
405
488
  <span>No listeners configured</span>
406
489
  )}
@@ -0,0 +1,126 @@
1
+ import type { IGceLoadBalancer } from '../domain/loadBalancer';
2
+
3
+ import { GceHttpLoadBalancerUtils } from './httpLoadBalancerUtils.service';
4
+
5
+ describe('GceHttpLoadBalancerUtils', () => {
6
+ const utils = new GceHttpLoadBalancerUtils();
7
+
8
+ it('treats EXTERNAL_MANAGED as part of the HTTP load balancer family', () => {
9
+ const externalManaged = {
10
+ account: 'account-a',
11
+ listeners: [{ name: 'listener-443' }],
12
+ loadBalancerType: 'EXTERNAL_MANAGED',
13
+ provider: 'gce',
14
+ region: 'us-central1',
15
+ urlMapName: 'app-main',
16
+ } as IGceLoadBalancer;
17
+
18
+ expect(utils.isHttpLoadBalancer(externalManaged)).toBe(true);
19
+ expect(utils.isRegionalHttpLoadBalancer(externalManaged)).toBe(true);
20
+ });
21
+
22
+ it('keeps global HTTP and INTERNAL_MANAGED classification unchanged', () => {
23
+ const globalHttp = {
24
+ account: 'account-a',
25
+ listeners: [{ name: 'listener-80' }],
26
+ loadBalancerType: 'HTTP',
27
+ provider: 'gce',
28
+ region: 'global',
29
+ urlMapName: 'app-main',
30
+ } as IGceLoadBalancer;
31
+ const internalManaged = {
32
+ account: 'account-a',
33
+ listeners: [{ name: 'listener-internal' }],
34
+ loadBalancerType: 'INTERNAL_MANAGED',
35
+ provider: 'gce',
36
+ region: 'europe-west1',
37
+ urlMapName: 'app-internal',
38
+ } as IGceLoadBalancer;
39
+
40
+ expect(utils.isHttpLoadBalancer(globalHttp)).toBe(true);
41
+ expect(utils.isRegionalHttpLoadBalancer(globalHttp)).toBe(false);
42
+
43
+ expect(utils.isHttpLoadBalancer(internalManaged)).toBe(true);
44
+ expect(utils.isRegionalHttpLoadBalancer(internalManaged)).toBe(true);
45
+ });
46
+
47
+ it('scopes regional listener normalization by account and region', () => {
48
+ const loadBalancers = [
49
+ {
50
+ account: 'account-a',
51
+ listeners: [{ name: 'listener-a' }],
52
+ loadBalancerType: 'EXTERNAL_MANAGED',
53
+ name: 'app-main (account-a/us-central1/EXTERNAL_MANAGED)',
54
+ provider: 'gce',
55
+ region: 'us-central1',
56
+ urlMapName: 'app-main',
57
+ },
58
+ {
59
+ account: 'account-a',
60
+ listeners: [{ name: 'listener-b' }],
61
+ loadBalancerType: 'EXTERNAL_MANAGED',
62
+ name: 'app-main (account-a/europe-west1/EXTERNAL_MANAGED)',
63
+ provider: 'gce',
64
+ region: 'europe-west1',
65
+ urlMapName: 'app-main',
66
+ },
67
+ ] as IGceLoadBalancer[];
68
+
69
+ expect(
70
+ utils.normalizeLoadBalancerNamesForAccount(['listener-a'], 'account-a', loadBalancers, 'us-central1'),
71
+ ).toEqual(['app-main (account-a/us-central1/EXTERNAL_MANAGED)']);
72
+ expect(
73
+ utils.normalizeLoadBalancerNamesForAccount(['listener-b'], 'account-a', loadBalancers, 'europe-west1'),
74
+ ).toEqual(['app-main (account-a/europe-west1/EXTERNAL_MANAGED)']);
75
+ });
76
+
77
+ it('maps global HTTP listener names to url map names without requiring region', () => {
78
+ const loadBalancers = [
79
+ {
80
+ account: 'account-a',
81
+ listeners: [{ name: 'listener-80' }, { name: 'listener-443' }],
82
+ loadBalancerType: 'HTTP',
83
+ name: 'app-main',
84
+ provider: 'gce',
85
+ region: 'global',
86
+ urlMapName: 'app-main',
87
+ },
88
+ ] as IGceLoadBalancer[];
89
+
90
+ expect(utils.normalizeLoadBalancerNamesForAccount(['listener-443'], 'account-a', loadBalancers)).toEqual([
91
+ 'app-main',
92
+ ]);
93
+ });
94
+
95
+ it('keeps the first INTERNAL_MANAGED url map for a listener name regardless of region', () => {
96
+ const loadBalancers = ['us-central1', 'europe-west1'].map((region) => ({
97
+ account: 'account-a',
98
+ listeners: [{ name: 'shared-listener' }],
99
+ loadBalancerType: 'INTERNAL_MANAGED',
100
+ name: `app-internal-${region}`,
101
+ provider: 'gce',
102
+ region,
103
+ urlMapName: `app-internal-${region}`,
104
+ })) as IGceLoadBalancer[];
105
+
106
+ expect(
107
+ utils.normalizeLoadBalancerNamesForAccount(['shared-listener'], 'account-a', loadBalancers, 'europe-west1'),
108
+ ).toEqual(['app-internal-us-central1']);
109
+ });
110
+
111
+ it('leaves an EXTERNAL_MANAGED listener name unresolved when regions make it ambiguous', () => {
112
+ const loadBalancers = ['us-central1', 'europe-west1'].map((region) => ({
113
+ account: 'account-a',
114
+ listeners: [{ name: 'shared-listener' }],
115
+ loadBalancerType: 'EXTERNAL_MANAGED',
116
+ name: `app-main (account-a/${region}/EXTERNAL_MANAGED)`,
117
+ provider: 'gce',
118
+ region,
119
+ urlMapName: 'app-main',
120
+ })) as IGceLoadBalancer[];
121
+
122
+ expect(utils.normalizeLoadBalancerNamesForAccount(['shared-listener'], 'account-a', loadBalancers)).toEqual([
123
+ 'shared-listener',
124
+ ]);
125
+ });
126
+ });
@@ -4,35 +4,51 @@ import type { IGceHttpLoadBalancer, IGceLoadBalancer } from '../domain/loadBalan
4
4
 
5
5
  export class GceHttpLoadBalancerUtils {
6
6
  public static REGION = 'global';
7
+ public static HTTP_LOAD_BALANCER_TYPES = ['HTTP', 'INTERNAL_MANAGED', 'EXTERNAL_MANAGED'];
7
8
 
8
9
  public isHttpLoadBalancer(lb: IGceLoadBalancer): lb is IGceHttpLoadBalancer {
9
10
  const loadBalancer = lb as any;
10
11
  return (
11
12
  (loadBalancer.provider === 'gce' || loadBalancer.type === 'gce') &&
12
- (loadBalancer.loadBalancerType === 'HTTP' || loadBalancer.loadBalancerType === 'INTERNAL_MANAGED')
13
+ GceHttpLoadBalancerUtils.HTTP_LOAD_BALANCER_TYPES.includes(loadBalancer.loadBalancerType)
13
14
  );
14
15
  }
15
16
 
17
+ public isRegionalHttpLoadBalancer(lb: IGceLoadBalancer): lb is IGceHttpLoadBalancer {
18
+ return this.isHttpLoadBalancer(lb) && lb.loadBalancerType !== 'HTTP';
19
+ }
20
+
16
21
  public normalizeLoadBalancerNamesForAccount(
17
22
  loadBalancerNames: string[],
18
23
  account: string,
19
24
  loadBalancers: IGceLoadBalancer[],
25
+ region?: string,
20
26
  ): string[] {
21
27
  // Assume that loadBalancers is a list of all GCE load balancers in an application
22
28
  // (but possibly from several accounts), and has already been normalized (listener names mapped to URL map names).
23
29
  const normalizedLoadBalancerNames: string[] = [];
30
+ const hasListener = (loadBalancer: IGceLoadBalancer, loadBalancerName: string) =>
31
+ account === loadBalancer.account &&
32
+ this.isHttpLoadBalancer(loadBalancer) &&
33
+ loadBalancer.listeners.map((listener) => listener.name).includes(loadBalancerName);
24
34
  loadBalancerNames.forEach((loadBalancerName) => {
25
- const matchingUrlMap = loadBalancers.find((loadBalancer) => {
26
- return (
27
- account === loadBalancer.account &&
28
- this.isHttpLoadBalancer(loadBalancer) &&
29
- loadBalancer.listeners.map((listener) => listener.name).includes(loadBalancerName)
30
- );
31
- });
35
+ const matchingUrlMap = loadBalancers.find(
36
+ (loadBalancer) =>
37
+ loadBalancer.loadBalancerType !== 'EXTERNAL_MANAGED' && hasListener(loadBalancer, loadBalancerName),
38
+ );
39
+ // EXTERNAL_MANAGED listener names repeat across regions, so a raw alias maps only when the
40
+ // account and region identify one load balancer.
41
+ const externalMatches = matchingUrlMap
42
+ ? []
43
+ : loadBalancers.filter(
44
+ (loadBalancer) =>
45
+ loadBalancer.loadBalancerType === 'EXTERNAL_MANAGED' &&
46
+ (!region || loadBalancer.region === region) &&
47
+ hasListener(loadBalancer, loadBalancerName),
48
+ );
49
+ const match = matchingUrlMap || (externalMatches.length === 1 ? externalMatches[0] : undefined);
32
50
 
33
- matchingUrlMap
34
- ? normalizedLoadBalancerNames.push(matchingUrlMap.name)
35
- : normalizedLoadBalancerNames.push(loadBalancerName);
51
+ match ? normalizedLoadBalancerNames.push(match.name) : normalizedLoadBalancerNames.push(loadBalancerName);
36
52
  });
37
53
  return uniq(normalizedLoadBalancerNames);
38
54
  }
@@ -0,0 +1,165 @@
1
+ import { GceLoadBalancerSetTransformer } from './loadBalancer.setTransformer';
2
+
3
+ describe('GceLoadBalancerSetTransformer', () => {
4
+ const transformer = new GceLoadBalancerSetTransformer();
5
+
6
+ it('groups regional HTTP listeners by account, region, load balancer type, and raw urlMapName', () => {
7
+ const loadBalancers = [
8
+ {
9
+ account: 'test',
10
+ loadBalancerType: 'EXTERNAL_MANAGED',
11
+ name: 'listener-a',
12
+ portRange: '80-80',
13
+ provider: 'gce',
14
+ region: 'us-central1',
15
+ urlMapName: 'app',
16
+ },
17
+ {
18
+ account: 'test',
19
+ loadBalancerType: 'EXTERNAL_MANAGED',
20
+ name: 'listener-b',
21
+ portRange: '80-80',
22
+ provider: 'gce',
23
+ region: 'us-east1',
24
+ urlMapName: 'app',
25
+ },
26
+ ];
27
+
28
+ const normalized = transformer.normalizeLoadBalancerSet(loadBalancers as any);
29
+
30
+ expect(normalized.length).toBe(2);
31
+ expect(normalized.map((loadBalancer) => loadBalancer.region).sort()).toEqual(['us-central1', 'us-east1']);
32
+ expect(normalized.map((loadBalancer) => loadBalancer.name).sort()).toEqual([
33
+ 'app (test/us-central1/EXTERNAL_MANAGED)',
34
+ 'app (test/us-east1/EXTERNAL_MANAGED)',
35
+ ]);
36
+ expect(normalized.every((loadBalancer: any) => loadBalancer.urlMapName === 'app')).toBe(true);
37
+ });
38
+
39
+ it('folds same-scope regional external listeners into one normalized load balancer', () => {
40
+ const loadBalancers = [
41
+ {
42
+ account: 'test',
43
+ certificate: undefined,
44
+ ipAddress: '34.0.0.1',
45
+ loadBalancerType: 'EXTERNAL_MANAGED',
46
+ name: 'listener-http',
47
+ networkTier: 'STANDARD',
48
+ portRange: '80-80',
49
+ provider: 'gce',
50
+ region: 'us-central1',
51
+ urlMapName: 'app',
52
+ },
53
+ {
54
+ account: 'test',
55
+ certificate: '//certificatemanager.googleapis.com/projects/p/locations/us-central1/certificates/cert',
56
+ ipAddress: '34.0.0.2',
57
+ loadBalancerType: 'EXTERNAL_MANAGED',
58
+ name: 'listener-https',
59
+ networkTier: 'PREMIUM',
60
+ portRange: '443-443',
61
+ provider: 'gce',
62
+ region: 'us-central1',
63
+ urlMapName: 'app',
64
+ },
65
+ ];
66
+
67
+ const [normalized] = transformer.normalizeLoadBalancerSet(loadBalancers as any);
68
+
69
+ expect(normalized.name).toBe('app (test/us-central1/EXTERNAL_MANAGED)');
70
+ expect((normalized as any).urlMapName).toBe('app');
71
+ expect((normalized as any).listeners).toEqual([
72
+ expect.objectContaining({
73
+ certificate: undefined,
74
+ ipAddress: '34.0.0.1',
75
+ name: 'listener-http',
76
+ networkTier: 'STANDARD',
77
+ port: '80',
78
+ }),
79
+ expect.objectContaining({
80
+ certificate: '//certificatemanager.googleapis.com/projects/p/locations/us-central1/certificates/cert',
81
+ ipAddress: '34.0.0.2',
82
+ name: 'listener-https',
83
+ networkTier: 'PREMIUM',
84
+ port: '443',
85
+ }),
86
+ ]);
87
+ });
88
+
89
+ it('keeps global HTTP normalization unchanged', () => {
90
+ const result = transformer.normalizeLoadBalancerSet([
91
+ {
92
+ account: 'test',
93
+ loadBalancerType: 'HTTP',
94
+ name: 'forwarding-rule-80',
95
+ portRange: '80-80',
96
+ provider: 'gce',
97
+ region: 'global',
98
+ urlMapName: 'frontend-map',
99
+ },
100
+ {
101
+ account: 'test',
102
+ loadBalancerType: 'HTTP',
103
+ name: 'forwarding-rule-443',
104
+ portRange: '443-443',
105
+ provider: 'gce',
106
+ region: 'global',
107
+ urlMapName: 'frontend-map',
108
+ },
109
+ ] as any);
110
+
111
+ expect(result.length).toBe(1);
112
+ expect(result[0].name).toBe('frontend-map');
113
+ expect((result[0] as any).listeners.map((listener: any) => listener.port)).toEqual(['80', '443']);
114
+ });
115
+
116
+ it('keeps INTERNAL_MANAGED names as the raw url map name that Clouddriver attaches by', () => {
117
+ const [internalManaged, externalManaged] = transformer.normalizeLoadBalancerSet([
118
+ {
119
+ account: 'test',
120
+ loadBalancerType: 'INTERNAL_MANAGED',
121
+ name: 'internal-listener',
122
+ portRange: '80-80',
123
+ provider: 'gce',
124
+ region: 'us-central1',
125
+ urlMapName: 'shared-map',
126
+ },
127
+ {
128
+ account: 'test',
129
+ loadBalancerType: 'EXTERNAL_MANAGED',
130
+ name: 'external-listener',
131
+ portRange: '80-80',
132
+ provider: 'gce',
133
+ region: 'us-central1',
134
+ urlMapName: 'shared-map',
135
+ },
136
+ ] as any);
137
+
138
+ expect(internalManaged.name).toBe('shared-map');
139
+ expect(externalManaged.name).toBe('shared-map (test/us-central1/EXTERNAL_MANAGED)');
140
+ });
141
+
142
+ it('does not group REGIONAL_EXTERNAL_NETWORK load balancers with regional HTTP families', () => {
143
+ const networkLoadBalancer = {
144
+ account: 'test',
145
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
146
+ name: 'regional-network-lb',
147
+ provider: 'gce',
148
+ region: 'us-central1',
149
+ };
150
+ const httpListener = {
151
+ account: 'test',
152
+ loadBalancerType: 'EXTERNAL_MANAGED',
153
+ name: 'listener-http',
154
+ portRange: '80-80',
155
+ provider: 'gce',
156
+ region: 'us-central1',
157
+ urlMapName: 'app',
158
+ };
159
+
160
+ const normalized = transformer.normalizeLoadBalancerSet([httpListener, networkLoadBalancer] as any);
161
+
162
+ expect(normalized.length).toBe(2);
163
+ expect(normalized.find((loadBalancer) => loadBalancer.name === 'regional-network-lb')).toBe(networkLoadBalancer);
164
+ });
165
+ });
@@ -10,6 +10,9 @@ export class GceLoadBalancerSetTransformer {
10
10
  private static normalizeHttpLoadBalancerGroup(group: IGceHttpLoadBalancer[]): IGceHttpLoadBalancer {
11
11
  const normalized = cloneDeep(group[0]);
12
12
 
13
+ // Clouddriver returns one row per forwarding-rule listener. Deck presents the URL map as the
14
+ // logical HTTP(S) load balancer, so the normalized object keeps the URL-map identity and folds
15
+ // forwarding-rule-specific fields into listener rows.
13
16
  normalized.listeners = group.map((loadBalancer) => {
14
17
  const port = loadBalancer.portRange ? GceLoadBalancerSetTransformer.parsePortRange(loadBalancer.portRange) : null;
15
18
  return {
@@ -18,11 +21,17 @@ export class GceLoadBalancerSetTransformer {
18
21
  certificate: loadBalancer.certificate,
19
22
  certificateMap: loadBalancer.certificateMap,
20
23
  ipAddress: loadBalancer.ipAddress,
24
+ networkTier: loadBalancer.networkTier,
21
25
  subnet: loadBalancer.subnet,
22
26
  };
23
27
  });
24
28
 
25
- normalized.name = normalized.urlMapName;
29
+ normalized.name =
30
+ normalized.loadBalancerType === 'EXTERNAL_MANAGED'
31
+ ? // Regional external URL map names can repeat across accounts and regions, and match
32
+ // INTERNAL_MANAGED names; details routing can still use the raw urlMapName plus scope.
33
+ `${normalized.urlMapName} (${normalized.account}/${normalized.region}/${normalized.loadBalancerType})`
34
+ : normalized.urlMapName;
26
35
  delete normalized.subnet;
27
36
  return normalized;
28
37
  }
@@ -36,7 +45,11 @@ export class GceLoadBalancerSetTransformer {
36
45
  this.gceHttpLoadBalancerUtils.isHttpLoadBalancer(lb),
37
46
  );
38
47
 
39
- const groupedByUrlMap = groupBy(httpLoadBalancers, 'urlMapName');
48
+ const groupedByUrlMap = groupBy(httpLoadBalancers, (loadBalancer) =>
49
+ loadBalancer.loadBalancerType === 'EXTERNAL_MANAGED'
50
+ ? [loadBalancer.account, loadBalancer.region, loadBalancer.loadBalancerType, loadBalancer.urlMapName].join(':')
51
+ : loadBalancer.urlMapName,
52
+ );
40
53
  const normalizedElSevenLoadBalancers = map(
41
54
  groupedByUrlMap,
42
55
  GceLoadBalancerSetTransformer.normalizeHttpLoadBalancerGroup,