@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
@@ -10,6 +10,8 @@ import type {
10
10
  import { GceServerGroupWizardPage } from '../GceServerGroupWizardPage';
11
11
 
12
12
  interface ILoadBalancerOption {
13
+ loadBalancerType?: string;
14
+ listeners?: Array<{ name?: string }>;
13
15
  name: string;
14
16
  region?: string;
15
17
  }
@@ -91,53 +93,69 @@ function renderPolicyError(error: string | undefined, id: string): React.ReactNo
91
93
  );
92
94
  }
93
95
 
94
- export class GceServerGroupLoadBalancers extends GceServerGroupWizardPage<IGceServerGroupWizardPageProps> {
95
- public validate(values: IGceServerGroupCommand): IGceServerGroupCommandValidationErrors {
96
- const errors = super.validate(values) as IGceServerGroupCommandValidationErrors & {
97
- loadBalancingPolicy?: ILoadBalancingPolicyErrors;
98
- };
99
- const policy = values.loadBalancingPolicy as ILoadBalancingPolicy | undefined;
100
- if (!policy || !uniqueStrings(values.loadBalancers).length) {
101
- return errors;
102
- }
96
+ export function validateGceServerGroupLoadBalancers(
97
+ values: IGceServerGroupCommand,
98
+ ): IGceServerGroupCommandValidationErrors {
99
+ const errors = {} as IGceServerGroupCommandValidationErrors & {
100
+ loadBalancingPolicy?: ILoadBalancingPolicyErrors;
101
+ };
102
+ if (hasAmbiguousSelectedName(values)) {
103
+ errors.loadBalancers =
104
+ 'The selected load balancer name is ambiguous across regions or load balancer types. Rename it or select an unambiguous load balancer.';
105
+ } else if (hasMixedRegionalExternalNetworkSelection(values)) {
106
+ errors.loadBalancers =
107
+ 'REGIONAL_EXTERNAL_NETWORK load balancers cannot be combined with other load balancer families in this editor.';
108
+ }
109
+ const policy = values.loadBalancingPolicy as ILoadBalancingPolicy | undefined;
110
+ if (!policy || !getBalancingModes(values).length) {
111
+ return errors;
112
+ }
103
113
 
104
- const policyErrors: ILoadBalancingPolicyErrors = {};
105
- const balancingModes = getBalancingModes(values);
106
- if (!policy.balancingMode || !balancingModes.includes(policy.balancingMode)) {
107
- policyErrors.balancingMode = 'Select a balancing mode supported by the selected load balancers.';
114
+ const policyErrors: ILoadBalancingPolicyErrors = {};
115
+ const balancingModes = getBalancingModes(values);
116
+ if (!policy.balancingMode || !balancingModes.includes(policy.balancingMode)) {
117
+ policyErrors.balancingMode = 'Select a balancing mode supported by the selected load balancers.';
118
+ }
119
+ if (!isValidBoundedValue(policy.capacityScaler, values, 0, 1)) {
120
+ policyErrors.capacityScaler = 'Capacity must be between 0 and 100%.';
121
+ }
122
+
123
+ const namedPortErrors = (policy.namedPorts || []).map(({ name, port }) => {
124
+ const namedPortError: { name?: string; port?: string } = {};
125
+ if (!name?.trim()) {
126
+ namedPortError.name = 'Port name required.';
108
127
  }
109
- if (!isValidBoundedValue(policy.capacityScaler, values, 0, 1)) {
110
- policyErrors.capacityScaler = 'Capacity must be between 0 and 100%.';
128
+ if (!isValidInteger(port, values, 1, 65535)) {
129
+ namedPortError.port = 'Port must be an integer between 1 and 65535.';
111
130
  }
131
+ return namedPortError;
132
+ });
133
+ if (namedPortErrors.some((namedPortError) => Object.keys(namedPortError).length)) {
134
+ policyErrors.namedPorts = namedPortErrors;
135
+ }
112
136
 
113
- const namedPortErrors = (policy.namedPorts || []).map(({ name, port }) => {
114
- const namedPortError: { name?: string; port?: string } = {};
115
- if (!name?.trim()) {
116
- namedPortError.name = 'Port name required.';
117
- }
118
- if (!isValidInteger(port, values, 1, 65535)) {
119
- namedPortError.port = 'Port must be an integer between 1 and 65535.';
120
- }
121
- return namedPortError;
122
- });
123
- if (namedPortErrors.some((namedPortError) => Object.keys(namedPortError).length)) {
124
- policyErrors.namedPorts = namedPortErrors;
125
- }
137
+ if (policy.balancingMode === 'RATE' && !isValidMinimum(policy.maxRatePerInstance, values, 0)) {
138
+ policyErrors.maxRatePerInstance = 'Max rate must be a finite number greater than or equal to zero.';
139
+ }
140
+ if (policy.balancingMode === 'CONNECTION' && !isValidMinimum(policy.maxConnectionsPerInstance, values, 0)) {
141
+ policyErrors.maxConnectionsPerInstance = 'Max connections must be a finite number greater than or equal to zero.';
142
+ }
143
+ if (policy.balancingMode === 'UTILIZATION' && !isValidBoundedValue(policy.maxUtilization, values, 0, 1)) {
144
+ policyErrors.maxUtilization = 'Max utilization must be between 0 and 100%.';
145
+ }
126
146
 
127
- if (policy.balancingMode === 'RATE' && !isValidMinimum(policy.maxRatePerInstance, values, 0)) {
128
- policyErrors.maxRatePerInstance = 'Max rate must be a finite number greater than or equal to zero.';
129
- }
130
- if (policy.balancingMode === 'CONNECTION' && !isValidMinimum(policy.maxConnectionsPerInstance, values, 0)) {
131
- policyErrors.maxConnectionsPerInstance = 'Max connections must be a finite number greater than or equal to zero.';
132
- }
133
- if (policy.balancingMode === 'UTILIZATION' && !isValidBoundedValue(policy.maxUtilization, values, 0, 1)) {
134
- policyErrors.maxUtilization = 'Max utilization must be between 0 and 100%.';
135
- }
147
+ if (Object.keys(policyErrors).length) {
148
+ errors.loadBalancingPolicy = policyErrors;
149
+ }
150
+ return errors;
151
+ }
136
152
 
137
- if (Object.keys(policyErrors).length) {
138
- errors.loadBalancingPolicy = policyErrors;
139
- }
140
- return errors;
153
+ export class GceServerGroupLoadBalancers extends GceServerGroupWizardPage<IGceServerGroupWizardPageProps> {
154
+ public validate(values: IGceServerGroupCommand): IGceServerGroupCommandValidationErrors {
155
+ return {
156
+ ...super.validate(values),
157
+ ...validateGceServerGroupLoadBalancers(values),
158
+ } as IGceServerGroupCommandValidationErrors;
141
159
  }
142
160
 
143
161
  public render(): React.ReactElement {
@@ -145,9 +163,10 @@ export class GceServerGroupLoadBalancers extends GceServerGroupWizardPage<IGceSe
145
163
  const selectedLoadBalancers = uniqueStrings(values.loadBalancers);
146
164
  const selectedReferences = preserveReferences(getAvailableLoadBalancers(values), selectedLoadBalancers);
147
165
  const policy = (values.loadBalancingPolicy || {}) as ILoadBalancingPolicy;
148
- const policyErrors = (this.validate(values) as IGceServerGroupCommandValidationErrors & {
166
+ const validationErrors = this.validate(values) as IGceServerGroupCommandValidationErrors & {
149
167
  loadBalancingPolicy?: ILoadBalancingPolicyErrors;
150
- }).loadBalancingPolicy || { namedPorts: [] };
168
+ };
169
+ const policyErrors = validationErrors.loadBalancingPolicy || { namedPorts: [] };
151
170
  const balancingModes = getBalancingModes(values);
152
171
 
153
172
  return (
@@ -172,6 +191,11 @@ export class GceServerGroupLoadBalancers extends GceServerGroupWizardPage<IGceSe
172
191
  </option>
173
192
  ))}
174
193
  </select>
194
+ {validationErrors.loadBalancers && (
195
+ <span className="help-block" id="gce-server-group-load-balancers-error" role="alert">
196
+ {validationErrors.loadBalancers}
197
+ </span>
198
+ )}
175
199
  </div>
176
200
  <div className="col-md-1">
177
201
  <button
@@ -257,6 +281,11 @@ export class GceServerGroupLoadBalancers extends GceServerGroupWizardPage<IGceSe
257
281
  values: IGceServerGroupCommand,
258
282
  loadBalancerName: string,
259
283
  ): React.ReactElement | null {
284
+ // Clouddriver attaches every backend service of a regional external load balancer.
285
+ const loadBalancerType = getLoadBalancerIndex(values)[loadBalancerName]?.loadBalancerType;
286
+ if (loadBalancerType === 'REGIONAL_EXTERNAL_NETWORK' || loadBalancerType === 'EXTERNAL_MANAGED') {
287
+ return null;
288
+ }
260
289
  const availableBackendServices = getBackendServiceData(values, loadBalancerName).map(({ name }) => name);
261
290
  const selectedBackendServices = uniqueStrings(values.backendServices?.[loadBalancerName]);
262
291
  if (!availableBackendServices.length && !selectedBackendServices.length) {
@@ -578,17 +607,10 @@ export class GceServerGroupLoadBalancers extends GceServerGroupWizardPage<IGceSe
578
607
  }
579
608
 
580
609
  function getAvailableLoadBalancers(command: IGceServerGroupCommand): ILoadBalancerOption[] {
581
- const rawLoadBalancers = command.backingData?.loadBalancers;
582
- if (Array.isArray(rawLoadBalancers)) {
610
+ const scopedLoadBalancers = getScopedLoadBalancers(command);
611
+ if (scopedLoadBalancers) {
583
612
  return uniqueLoadBalancers(
584
- rawLoadBalancers
585
- .flatMap((provider: any) => (Array.isArray(provider?.accounts) ? provider.accounts : []))
586
- .filter((account: any) => account?.name === command.credentials)
587
- .flatMap((account: any) => (Array.isArray(account?.regions) ? account.regions : []))
588
- .flatMap((region: any) => (Array.isArray(region?.loadBalancers) ? region.loadBalancers : []))
589
- .filter((loadBalancer: any) => isLoadBalancerInRegion(loadBalancer, command))
590
- .map(toLoadBalancerOption)
591
- .filter((loadBalancer): loadBalancer is ILoadBalancerOption => Boolean(loadBalancer)),
613
+ scopedLoadBalancers.map((loadBalancer) => toIndexedExternalManagedOption(command, loadBalancer)),
592
614
  );
593
615
  }
594
616
 
@@ -603,6 +625,23 @@ function getAvailableLoadBalancers(command: IGceServerGroupCommand): ILoadBalanc
603
625
  );
604
626
  }
605
627
 
628
+ // Clouddriver summarizes EXTERNAL_MANAGED per listener, while the wizard indexes it by the URL-map
629
+ // display name that clone submission expands back into listener names.
630
+ function toIndexedExternalManagedOption(
631
+ command: IGceServerGroupCommand,
632
+ loadBalancer: ILoadBalancerOption,
633
+ ): ILoadBalancerOption {
634
+ if (loadBalancer.loadBalancerType !== 'EXTERNAL_MANAGED') {
635
+ return loadBalancer;
636
+ }
637
+ const indexed = Object.values(getLoadBalancerIndex(command)).find(
638
+ (candidate) =>
639
+ candidate.loadBalancerType === 'EXTERNAL_MANAGED' &&
640
+ (candidate.listeners || []).some(({ name }) => name === loadBalancer.name),
641
+ );
642
+ return indexed?.name ? { ...loadBalancer, name: indexed.name } : loadBalancer;
643
+ }
644
+
606
645
  function getLoadBalancerIndex(command: IGceServerGroupCommand): Record<string, ILoadBalancerData> {
607
646
  return (command.backingData?.filtered?.loadBalancerIndex || {}) as Record<string, ILoadBalancerData>;
608
647
  }
@@ -635,14 +674,19 @@ function getNamedPortNames(command: IGceServerGroupCommand): string[] {
635
674
 
636
675
  function getBalancingModes(command: IGceServerGroupCommand): string[] {
637
676
  const loadBalancerIndex = getLoadBalancerIndex(command);
638
- const modeSets = uniqueStrings(command.loadBalancers)
677
+ const loadBalancerTypes = uniqueStrings(command.loadBalancers)
639
678
  .map((loadBalancerName) => loadBalancerIndex[loadBalancerName]?.loadBalancerType)
640
- .filter((loadBalancerType): loadBalancerType is string => Boolean(loadBalancerType))
641
- .map((loadBalancerType) =>
642
- loadBalancerType === 'HTTP' || loadBalancerType === 'INTERNAL_MANAGED'
643
- ? HTTP_BALANCING_MODES
644
- : CONNECTION_BALANCING_MODES,
645
- );
679
+ .filter((loadBalancerType): loadBalancerType is string => Boolean(loadBalancerType));
680
+ // Clouddriver always attaches passthrough NLB backends with CONNECTION balancing and this page
681
+ // rejects mixing them with other families, so there is no balancing mode to choose.
682
+ if (loadBalancerTypes.includes('REGIONAL_EXTERNAL_NETWORK')) {
683
+ return [];
684
+ }
685
+ const modeSets = loadBalancerTypes.map((loadBalancerType) =>
686
+ loadBalancerType === 'HTTP' || loadBalancerType === 'INTERNAL_MANAGED' || loadBalancerType === 'EXTERNAL_MANAGED'
687
+ ? HTTP_BALANCING_MODES
688
+ : CONNECTION_BALANCING_MODES,
689
+ );
646
690
  if (!modeSets.length) {
647
691
  const persistedMode = (command.loadBalancingPolicy as ILoadBalancingPolicy | undefined)?.balancingMode;
648
692
  return persistedMode ? [persistedMode] : [];
@@ -706,7 +750,11 @@ function getLoadBalancerMetadataReference(
706
750
  if (!loadBalancer) {
707
751
  return undefined;
708
752
  }
709
- if (loadBalancer.loadBalancerType === 'HTTP' || loadBalancer.loadBalancerType === 'INTERNAL_MANAGED') {
753
+ if (
754
+ loadBalancer.loadBalancerType === 'HTTP' ||
755
+ loadBalancer.loadBalancerType === 'INTERNAL_MANAGED' ||
756
+ loadBalancer.loadBalancerType === 'EXTERNAL_MANAGED'
757
+ ) {
710
758
  const names = uniqueStrings((loadBalancer.listeners || []).map(({ name }) => name || ''));
711
759
  if (!names.length) {
712
760
  return undefined;
@@ -828,13 +876,54 @@ function isLoadBalancerInRegion(loadBalancer: any, command: IGceServerGroupComma
828
876
  return loadBalancer?.region === command.region || loadBalancer?.region === 'global';
829
877
  }
830
878
 
879
+ function getScopedLoadBalancers(command: IGceServerGroupCommand): ILoadBalancerOption[] | undefined {
880
+ const rawLoadBalancers = command.backingData?.loadBalancers;
881
+ if (!Array.isArray(rawLoadBalancers)) {
882
+ return undefined;
883
+ }
884
+ return rawLoadBalancers
885
+ .flatMap((provider: any) => (Array.isArray(provider?.accounts) ? provider.accounts : []))
886
+ .filter((account: any) => account?.name === command.credentials)
887
+ .flatMap((account: any) => (Array.isArray(account?.regions) ? account.regions : []))
888
+ .flatMap((region: any) => (Array.isArray(region?.loadBalancers) ? region.loadBalancers : []))
889
+ .filter((loadBalancer: any) => isLoadBalancerInRegion(loadBalancer, command))
890
+ .map(toLoadBalancerOption)
891
+ .filter((loadBalancer): loadBalancer is ILoadBalancerOption => Boolean(loadBalancer));
892
+ }
893
+
894
+ function hasAmbiguousSelectedName(command: IGceServerGroupCommand): boolean {
895
+ const selectedNames = new Set(uniqueStrings(command.loadBalancers));
896
+ const identitiesByName = (getScopedLoadBalancers(command) || []).reduce<Map<string, Set<string>>>(
897
+ (identities, loadBalancer) => {
898
+ const identity = `${loadBalancer.region || ''}:${loadBalancer.loadBalancerType || ''}`;
899
+ [loadBalancer.name, ...(loadBalancer.listeners || []).map(({ name }) => name)]
900
+ .filter((name): name is string => Boolean(name))
901
+ .forEach((name) => {
902
+ const values = identities.get(name) || new Set<string>();
903
+ values.add(identity);
904
+ identities.set(name, values);
905
+ });
906
+ return identities;
907
+ },
908
+ new Map(),
909
+ );
910
+ return Array.from(selectedNames).some((name) => (identitiesByName.get(name)?.size || 0) > 1);
911
+ }
912
+
913
+ function hasMixedRegionalExternalNetworkSelection(command: IGceServerGroupCommand): boolean {
914
+ const selectedTypes = uniqueStrings(command.loadBalancers)
915
+ .map((name) => getLoadBalancerIndex(command)[name]?.loadBalancerType)
916
+ .filter(Boolean);
917
+ return selectedTypes.includes('REGIONAL_EXTERNAL_NETWORK') && selectedTypes.length > 1;
918
+ }
919
+
831
920
  function toLoadBalancerOption(loadBalancer: unknown): ILoadBalancerOption | null {
832
921
  if (typeof loadBalancer === 'string') {
833
922
  return { name: loadBalancer };
834
923
  }
835
924
  if (loadBalancer && typeof (loadBalancer as ILoadBalancerOption).name === 'string') {
836
- const { name, region } = loadBalancer as ILoadBalancerOption;
837
- return { name, region };
925
+ const { listeners, loadBalancerType, name, region } = loadBalancer as ILoadBalancerOption;
926
+ return { listeners, loadBalancerType, name, region };
838
927
  }
839
928
  return null;
840
929
  }
@@ -1,10 +1,48 @@
1
- import { shallow } from 'enzyme';
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
2
  import React from 'react';
3
3
 
4
4
  import type { IGceServerGroupCommand } from '../GceServerGroupWizard.types';
5
- import { Policies } from './Policies';
6
- import { GceAutoHealingPolicyEditor } from '../../../../autoHealingPolicy';
7
- import { GceAutoscalingPolicyEditor } from '../../../../autoscalingPolicy';
5
+ import { Policies, validateGceServerGroupPolicies } from './Policies';
6
+
7
+ vi.mock('../../../../autoscalingPolicy', async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import('../../../../autoscalingPolicy')>();
9
+ return {
10
+ ...actual,
11
+ GceAutoscalingPolicyEditor: ({ onChange, policy }: any) => (
12
+ <div>
13
+ <output aria-label="Autoscaling policy">{JSON.stringify(policy)}</output>
14
+ <button
15
+ onClick={() =>
16
+ onChange({
17
+ minNumReplicas: 2,
18
+ maxNumReplicas: 6,
19
+ coolDownPeriodSec: 90,
20
+ cpuUtilization: { utilizationTarget: 0.7 },
21
+ })
22
+ }
23
+ type="button"
24
+ >
25
+ Edit autoscaling policy
26
+ </button>
27
+ </div>
28
+ ),
29
+ };
30
+ });
31
+
32
+ vi.mock('../../../../autoHealingPolicy', () => ({
33
+ GceAutoHealingPolicyEditor: ({ policy, reader }: any) => {
34
+ const [healthChecks, setHealthChecks] = React.useState<any[]>([]);
35
+ return (
36
+ <div>
37
+ <output aria-label="Autohealing policy">{JSON.stringify(policy)}</output>
38
+ <button onClick={() => reader.listHealthChecks().then(setHealthChecks)} type="button">
39
+ Load health checks
40
+ </button>
41
+ <output aria-label="Health checks">{JSON.stringify(healthChecks)}</output>
42
+ </div>
43
+ );
44
+ },
45
+ }));
8
46
 
9
47
  describe('GCE server group Policies page', () => {
10
48
  it('reuses the policy editors and supplies autohealing health checks from filtered backing data', async () => {
@@ -27,14 +65,16 @@ describe('GCE server group Policies page', () => {
27
65
  },
28
66
  },
29
67
  });
30
- const wrapper = shallow(<Policies app={{} as any} formik={formik(values)} />);
31
-
32
- expect(wrapper.find(GceAutoscalingPolicyEditor).prop('policy')).toBe(values.autoscalingPolicy);
33
- expect(wrapper.find(GceAutoHealingPolicyEditor).prop('policy')).toBe(values.autoHealingPolicy);
34
-
35
- const reader = wrapper.find(GceAutoHealingPolicyEditor).prop('reader') as any;
36
- expect(await reader.listHealthChecks()).toEqual([
37
- jasmine.objectContaining({ account: 'account', name: 'check', selfLink: 'https://compute/healthChecks/check' }),
68
+ renderPolicies(values);
69
+
70
+ expect(screen.getByLabelText('Autoscaling policy')).toHaveTextContent('unknownPolicyField');
71
+ expect(screen.getByLabelText('Autohealing policy')).toHaveTextContent('unknownPolicyField');
72
+ fireEvent.click(screen.getByRole('button', { name: 'Load health checks' }));
73
+ await waitFor(() =>
74
+ expect(screen.getByLabelText('Health checks')).toHaveTextContent('https://compute/healthChecks/check'),
75
+ );
76
+ expect(JSON.parse(screen.getByLabelText('Health checks').textContent || '[]')).toEqual([
77
+ expect.objectContaining({ account: 'account', name: 'check', selfLink: 'https://compute/healthChecks/check' }),
38
78
  ]);
39
79
  });
40
80
 
@@ -47,10 +87,9 @@ describe('GCE server group Policies page', () => {
47
87
  source: { region: 'us-central1', serverGroupName: 'app-v001', useSourceCapacity: true },
48
88
  viewState: { mode: 'clone', useSimpleCapacity: false, unrelated: 'keep' },
49
89
  });
50
- const persistedFormik = formik(persisted);
51
- const persistedPage = shallow(<Policies app={{} as any} formik={persistedFormik} />);
90
+ const page = renderPolicies(persisted);
52
91
 
53
- persistedPage.find('[data-testid="enable-autoscaling"]').simulate('change', { target: { checked: false } });
92
+ fireEvent.click(screen.getByTestId('enable-autoscaling'));
54
93
 
55
94
  expect(persisted.enableAutoScaling).toBe(false);
56
95
  expect(persisted.autoscalingPolicy).toBeNull();
@@ -63,8 +102,8 @@ describe('GCE server group Policies page', () => {
63
102
  expect(persisted.viewState).toEqual({ mode: 'clone', useSimpleCapacity: true, unrelated: 'keep' });
64
103
  expect(persisted.capacity).toEqual({ min: 2, max: 2, desired: 2 });
65
104
 
66
- persistedPage.setProps({ formik: persistedFormik });
67
- persistedPage.find('[data-testid="enable-autoscaling"]').simulate('change', { target: { checked: true } });
105
+ page.rerenderPage(persisted);
106
+ fireEvent.click(screen.getByTestId('enable-autoscaling'));
68
107
 
69
108
  expect(persisted.enableAutoScaling).toBe(true);
70
109
  expect(persisted.overwriteAncestorAutoscalingPolicy).toBe(false);
@@ -85,9 +124,9 @@ describe('GCE server group Policies page', () => {
85
124
  autoscalingPolicy: { minNumReplicas: 1, maxNumReplicas: 4 },
86
125
  viewState: { mode },
87
126
  });
88
- const wrapper = shallow(<Policies app={{} as any} formik={formik(values)} />);
127
+ renderPolicies(values);
89
128
 
90
- wrapper.find('[data-testid="enable-autoscaling"]').simulate('change', { target: { checked: false } });
129
+ fireEvent.click(screen.getByTestId('enable-autoscaling'));
91
130
 
92
131
  expect(values.overwriteAncestorAutoscalingPolicy).toBe(false);
93
132
  });
@@ -96,17 +135,18 @@ describe('GCE server group Policies page', () => {
96
135
  it('preserves autohealing policy fields and limits ancestor overwrite to clone disables', () => {
97
136
  const policy = { healthCheck: 'check', initialDelaySec: 300, unknownPolicyField: 'keep' };
98
137
  const clone = command({ enableAutoHealing: true, autoHealingPolicy: policy, viewState: { mode: 'clone' } });
99
- const clonePage = shallow(<Policies app={{} as any} formik={formik(clone)} />);
138
+ const page = renderPolicies(clone);
100
139
 
101
- clonePage.find('[data-testid="enable-autohealing"]').simulate('change', { target: { checked: false } });
140
+ fireEvent.click(screen.getByTestId('enable-autohealing'));
102
141
 
103
142
  expect(clone.enableAutoHealing).toBe(false);
104
143
  expect(clone.overwriteAncestorAutoHealingPolicy).toBe(true);
105
144
  expect(clone.autoHealingPolicy).toBe(policy);
106
145
 
107
146
  const create = command({ enableAutoHealing: true, autoHealingPolicy: policy, viewState: { mode: 'create' } });
108
- const createPage = shallow(<Policies app={{} as any} formik={formik(create)} />);
109
- createPage.find('[data-testid="enable-autohealing"]').simulate('change', { target: { checked: false } });
147
+ page.unmount();
148
+ renderPolicies(create);
149
+ fireEvent.click(screen.getByTestId('enable-autohealing'));
110
150
 
111
151
  expect(create.overwriteAncestorAutoHealingPolicy).toBe(false);
112
152
  });
@@ -122,10 +162,11 @@ describe('GCE server group Policies page', () => {
122
162
  source: { useSourceCapacity: true },
123
163
  viewState: { mode: 'clone' },
124
164
  });
125
- const wrapper = shallow(<Policies app={{} as any} formik={formik(values)} />);
165
+ const page = renderPolicies(values);
126
166
 
127
- wrapper.find('[data-testid="enable-autoscaling"]').simulate('change', { target: { checked: true } });
128
- wrapper.find('[data-testid="enable-autohealing"]').simulate('change', { target: { checked: true } });
167
+ fireEvent.click(screen.getByTestId('enable-autoscaling'));
168
+ page.rerenderPage(values);
169
+ fireEvent.click(screen.getByTestId('enable-autohealing'));
129
170
 
130
171
  expect(values.autoscalingPolicy).toEqual({
131
172
  minNumReplicas: 3,
@@ -141,18 +182,17 @@ describe('GCE server group Policies page', () => {
141
182
  });
142
183
 
143
184
  it('derives autoscaling rendering and validation from canonical policy presence', () => {
144
- const page = new Policies({ app: {} as any, formik: formik(command()) } as any);
145
185
  const canonical = command({
146
186
  enableAutoScaling: false,
147
187
  autoscalingPolicy: { minNumReplicas: 3, maxNumReplicas: 2, cpuUtilization: {} },
148
188
  });
149
- const canonicalWrapper = shallow(<Policies app={{} as any} formik={formik(canonical)} />);
189
+ renderPolicies(canonical);
150
190
 
151
- expect(canonicalWrapper.find('[data-testid="enable-autoscaling"]').prop('checked')).toBe(true);
152
- expect(canonicalWrapper.find(GceAutoscalingPolicyEditor).exists()).toBe(true);
191
+ expect(screen.getByTestId('enable-autoscaling')).toBeChecked();
192
+ expect(screen.getByLabelText('Autoscaling policy')).toBeInTheDocument();
153
193
 
154
194
  expect(
155
- page.validate({
195
+ validateGceServerGroupPolicies({
156
196
  ...canonical,
157
197
  enableAutoHealing: true,
158
198
  autoHealingPolicy: {
@@ -174,7 +214,7 @@ describe('GCE server group Policies page', () => {
174
214
  },
175
215
  });
176
216
  expect(
177
- page.validate(
217
+ validateGceServerGroupPolicies(
178
218
  command({
179
219
  enableAutoScaling: true,
180
220
  autoscalingPolicy: null,
@@ -198,17 +238,16 @@ describe('GCE server group Policies page', () => {
198
238
  source: { useSourceCapacity: true },
199
239
  viewState: { mode: 'editPipeline', useSimpleCapacity: true, unrelated: 'keep' },
200
240
  });
201
- const wrapper = shallow(<Policies app={{} as any} formik={formik(values)} />);
202
- const policy = {
241
+ renderPolicies(values);
242
+
243
+ fireEvent.click(screen.getByRole('button', { name: 'Edit autoscaling policy' }));
244
+
245
+ expect(values.autoscalingPolicy).toEqual({
203
246
  minNumReplicas: 2,
204
247
  maxNumReplicas: 6,
205
248
  coolDownPeriodSec: 90,
206
249
  cpuUtilization: { utilizationTarget: 0.7 },
207
- };
208
-
209
- wrapper.find(GceAutoscalingPolicyEditor).prop('onChange')(policy);
210
-
211
- expect(values.autoscalingPolicy).toBe(policy);
250
+ });
212
251
  expect(values.capacity).toEqual({ min: 2, max: 6, desired: 3 });
213
252
  expect(values.enableAutoScaling).toBe(true);
214
253
  expect(values.source.useSourceCapacity).toBe(false);
@@ -216,10 +255,8 @@ describe('GCE server group Policies page', () => {
216
255
  });
217
256
 
218
257
  it('requires autoscaling capacity, cooldown, and real metric values', () => {
219
- const page = new Policies({ app: {} as any, formik: formik(command()) } as any);
220
-
221
258
  expect(
222
- page.validate(
259
+ validateGceServerGroupPolicies(
223
260
  command({
224
261
  enableAutoScaling: true,
225
262
  autoscalingPolicy: {
@@ -240,7 +277,6 @@ describe('GCE server group Policies page', () => {
240
277
  });
241
278
 
242
279
  it('matches autoscaling schedule and scale-in validation from the completed modal', () => {
243
- const page = new Policies({ app: {} as any, formik: formik(command()) } as any);
244
280
  const values = command({
245
281
  enableAutoScaling: true,
246
282
  autoscalingPolicy: {
@@ -253,7 +289,7 @@ describe('GCE server group Policies page', () => {
253
289
  },
254
290
  });
255
291
 
256
- expect(page.validate(values)).toEqual({
292
+ expect(validateGceServerGroupPolicies(values)).toEqual({
257
293
  autoscalingPolicy: {
258
294
  scalingSchedules: 'Every scaling schedule must be complete and within supported bounds.',
259
295
  scaleInControl: 'Scale-in control values are outside supported bounds.',
@@ -262,10 +298,8 @@ describe('GCE server group Policies page', () => {
262
298
  });
263
299
 
264
300
  it('accepts complete policies at modal boundary values', () => {
265
- const page = new Policies({ app: {} as any, formik: formik(command()) } as any);
266
-
267
301
  expect(
268
- page.validate(
302
+ validateGceServerGroupPolicies(
269
303
  command({
270
304
  enableAutoScaling: true,
271
305
  autoscalingPolicy: {
@@ -297,6 +331,17 @@ describe('GCE server group Policies page', () => {
297
331
  });
298
332
  });
299
333
 
334
+ function renderPolicies(values: IGceServerGroupCommand) {
335
+ const pageProps = { app: {} as any, formik: formik(values) };
336
+ const rendered = render(<Policies {...pageProps} />);
337
+ return {
338
+ ...rendered,
339
+ rerenderPage(nextValues: IGceServerGroupCommand) {
340
+ rendered.rerender(<Policies {...pageProps} formik={formik(nextValues)} />);
341
+ },
342
+ };
343
+ }
344
+
300
345
  function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGroupCommand {
301
346
  return {
302
347
  credentials: 'account',
@@ -310,7 +355,7 @@ function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGro
310
355
  function formik(values: IGceServerGroupCommand): any {
311
356
  return {
312
357
  values,
313
- setFieldValue: jasmine.createSpy('setFieldValue').and.callFake((field: string, value: any) => {
358
+ setFieldValue: vi.fn().mockImplementation((field: string, value: any) => {
314
359
  values[field] = value;
315
360
  }),
316
361
  };