Skip to content

Commit 9639164

Browse files
committed
feat(clusters): add GCP static egress configuration for NAT gateway
1 parent 73c0d81 commit 9639164

15 files changed

Lines changed: 708 additions & 39 deletions

File tree

libs/domains/clusters/feature/src/lib/cluster-card-feature/cluster-card-feature.spec.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,56 @@ describe('ClusterCardFeature', () => {
5656
const toggle = screen.getByTestId('feature')
5757
expect(toggle).toBeInTheDocument()
5858
})
59+
60+
it('should show NAT_GATEWAY as false when gcp nested static_ips_enabled is false', () => {
61+
renderWithProviders(
62+
wrapWithReactHookForm(
63+
<ClusterCardFeature
64+
cloudProvider={CloudProviderEnum.GCP}
65+
feature={{
66+
id: 'NAT_GATEWAY',
67+
title: 'Static IP / Nat Gateways',
68+
value_object: {
69+
value: {
70+
nat_gateway_type: {
71+
provider: 'gcp',
72+
static_ips_enabled: false,
73+
static_ips_count: 2,
74+
},
75+
},
76+
},
77+
}}
78+
disabled
79+
/>
80+
)
81+
)
82+
83+
expect(screen.getByDisplayValue('false')).toBeInTheDocument()
84+
})
85+
86+
it('should read NAT_GATEWAY nested nat_gateway_type static_ips_enabled', () => {
87+
renderWithProviders(
88+
wrapWithReactHookForm(
89+
<ClusterCardFeature
90+
cloudProvider={CloudProviderEnum.GCP}
91+
feature={{
92+
id: 'NAT_GATEWAY',
93+
title: 'Static IP / Nat Gateways',
94+
value_object: {
95+
value: {
96+
nat_gateway_type: {
97+
provider: 'gcp',
98+
static_ips_enabled: true,
99+
static_ips_count: 2,
100+
},
101+
},
102+
},
103+
}}
104+
disabled
105+
/>
106+
)
107+
)
108+
109+
expect(screen.getByDisplayValue('true')).toBeInTheDocument()
110+
})
59111
})

libs/domains/clusters/feature/src/lib/cluster-card-feature/cluster-card-feature.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type CloudVendorEnum, type ClusterFeatureResponse } from 'qovery-typesc
22
import { type PropsWithChildren, type ReactNode, useEffect, useState } from 'react'
33
import { type Control, Controller, type FieldValues, type UseFormSetValue, type UseFormWatch } from 'react-hook-form'
44
import { ExternalLink, Icon, InputSelect, InputToggle, Tooltip } from '@qovery/shared/ui'
5+
import { getGcpNatGatewaySettings } from '../utils/get-gcp-nat-gateway-settings'
56

67
export interface ClusterCardFeatureProps extends PropsWithChildren {
78
feature: ClusterFeatureResponse
@@ -27,11 +28,21 @@ export function ClusterCardFeature({
2728

2829
const name = watch && watch(`features.${feature.id}.value`)
2930

30-
const getValue = (value: boolean | string) => {
31+
const getFeatureToggleValue = (feature: ClusterFeatureResponse) => {
32+
const value = feature.value_object?.value
33+
34+
if (feature.id === 'NAT_GATEWAY') {
35+
const gcpNatGatewaySettings = getGcpNatGatewaySettings(value)
36+
if (gcpNatGatewaySettings) {
37+
return gcpNatGatewaySettings.static_ips_enabled
38+
}
39+
}
40+
3141
if (typeof value === 'string') {
3242
return true
3343
}
34-
return value
44+
45+
return Boolean(value)
3546
}
3647

3748
useEffect(() => {
@@ -69,12 +80,7 @@ export function ClusterCardFeature({
6980
) : (
7081
<Tooltip content={tooltip} disabled={!tooltip}>
7182
<span>
72-
<InputToggle
73-
disabled
74-
small
75-
className="relative top-[2px]"
76-
value={getValue(Boolean(feature?.value_object?.value) || false)}
77-
/>
83+
<InputToggle disabled small className="relative top-[2px]" value={getFeatureToggleValue(feature)} />
7884
</span>
7985
</Tooltip>
8086
)}

libs/domains/clusters/feature/src/lib/cluster-creation-flow/step-features/step-features.spec.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe('StepFeatures', () => {
108108
})
109109
})
110110

111-
it('should hide NAT_GATEWAY feature for GCP cluster creation', async () => {
111+
it('should merge STATIC_IP and NAT_GATEWAY in GCP network configuration', async () => {
112112
useCloudProviderFeaturesMockSpy.mockReturnValue({
113113
data: [
114114
{
@@ -119,7 +119,12 @@ describe('StepFeatures', () => {
119119
{
120120
id: 'NAT_GATEWAY',
121121
title: 'NAT Gateway',
122-
value_object: { value: false },
122+
value_object: {
123+
value: {
124+
static_ips_enabled: false,
125+
static_ips_count: 2,
126+
},
127+
},
123128
},
124129
{
125130
id: 'PRIVATE_CLUSTER',
@@ -145,9 +150,11 @@ describe('StepFeatures', () => {
145150
renderWithProviders(<StepFeatures {...defaultProps} />, { wrapper: getWrapper(gcpContextValue) })
146151

147152
await waitFor(() => {
153+
expect(screen.getAllByText('Static IP / Nat Gateways').length).toBeGreaterThan(0)
154+
expect(screen.getByText('Enable static egress IPs')).toBeInTheDocument()
155+
expect(screen.queryByText('Static IP count')).not.toBeInTheDocument()
148156
expect(screen.getByText('Private Cluster')).toBeInTheDocument()
149-
expect(screen.getByText('Static IP')).toBeInTheDocument()
150-
expect(screen.queryByText('NAT Gateway')).not.toBeInTheDocument()
157+
expect(screen.queryByText(/^NAT Gateway$/)).not.toBeInTheDocument()
151158
})
152159
})
153160
})

libs/domains/clusters/feature/src/lib/cluster-creation-flow/step-features/step-features.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ import {
2525
} from '@qovery/shared/ui'
2626
import { twMerge } from '@qovery/shared/util-js'
2727
import { ClusterCardFeature } from '../../cluster-card-feature/cluster-card-feature'
28+
import { GcpStaticIp } from '../../gcp-static-ip/gcp-static-ip'
2829
import { ScalewayStaticIp } from '../../scaleway-static-ip/scaleway-static-ip'
2930
import { steps, useClusterContainerCreateContext } from '../cluster-creation-flow'
3031
import AWSVpcFeature from './aws-vpc-feature/aws-vpc-feature'
3132
import GCPVpcFeature from './gcp-vpc-feature/gcp-vpc-feature'
3233

3334
const Qovery = '/assets/logos/logo-icon.svg'
34-
const GCP_HIDDEN_FEATURE_IDS = new Set(['NAT_GATEWAY'])
3535
const removeEmptySubnet = (objects?: Subnets[]) =>
3636
objects?.filter((field) => field.A !== '' || field.B !== '' || field.C !== '')
3737

@@ -212,11 +212,32 @@ function StepFeaturesForm({
212212
{cloudProvider === 'GCP' && (
213213
<div>
214214
{match(watchVpcMode)
215-
.with('DEFAULT', () =>
216-
features && features.length > 0 ? (
217-
features
218-
.filter((feature) => !GCP_HIDDEN_FEATURE_IDS.has(feature.id ?? ''))
219-
.map((feature) => (
215+
.with('DEFAULT', () => {
216+
if (!features || features.length === 0) {
217+
return (
218+
<div className="mt-2 flex justify-center">
219+
<LoaderSpinner className="w-4" />
220+
</div>
221+
)
222+
}
223+
224+
const staticIpFeature = features.find(({ id }) => id === 'STATIC_IP')
225+
const natGatewayFeature = features.find(({ id }) => id === 'NAT_GATEWAY')
226+
const hasMergedStaticIpNatGateway = Boolean(staticIpFeature && natGatewayFeature)
227+
const remainingFeatures = hasMergedStaticIpNatGateway
228+
? features.filter(({ id }) => id !== 'STATIC_IP' && id !== 'NAT_GATEWAY')
229+
: features
230+
231+
return (
232+
<>
233+
{hasMergedStaticIpNatGateway && (
234+
<GcpStaticIp
235+
staticIpFeature={staticIpFeature}
236+
natGatewayFeature={natGatewayFeature}
237+
production={isProduction || false}
238+
/>
239+
)}
240+
{remainingFeatures.map((feature) => (
220241
<ClusterCardFeature
221242
key={feature.id}
222243
feature={feature}
@@ -225,13 +246,10 @@ function StepFeaturesForm({
225246
watch={clusterCardFeatureFormBindings.watch}
226247
setValue={clusterCardFeatureFormBindings.setValue}
227248
/>
228-
))
229-
) : (
230-
<div className="mt-2 flex justify-center">
231-
<LoaderSpinner className="w-4" />
232-
</div>
249+
))}
250+
</>
233251
)
234-
)
252+
})
235253
.with('EXISTING_VPC', () => <GCPVpcFeature />)
236254
.otherwise(() => null)}
237255
</div>

libs/domains/clusters/feature/src/lib/cluster-creation-flow/step-summary/step-summary-presentation.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ function SubnetsList({ title, index, subnets }: { title: string; index: string;
4545
)
4646
}
4747

48+
function formatFeatureValue(feature: ClusterFeaturesData['features'][string]) {
49+
if (typeof feature.extendedValue === 'string') {
50+
return feature.extendedValue
51+
}
52+
53+
if (feature.extendedValue && typeof feature.extendedValue === 'object') {
54+
const staticIpsEnabled = feature.extendedValue.static_ips_enabled
55+
const staticIpsCount = feature.extendedValue.static_ips_count
56+
return `static_ips_enabled=${staticIpsEnabled}, static_ips_count=${staticIpsCount}`
57+
}
58+
59+
return feature.value.toString()
60+
}
61+
4862
export function StepSummaryPresentation(props: StepSummaryPresentationProps) {
4963
const clusterBackup = props.resourcesData.infrastructure_charts_parameters?.eks_anywhere_parameters?.cluster_backup
5064
const showClusterBackup = Boolean(clusterBackup?.enabled)
@@ -595,7 +609,7 @@ export function StepSummaryPresentation(props: StepSummaryPresentationProps) {
595609
return (
596610
<li key={id}>
597611
<strong className="font-medium">{currentFeature.title}: </strong>
598-
{currentFeature.extendedValue ? currentFeature.extendedValue : currentFeature.value.toString()}
612+
{formatFeatureValue(currentFeature)}
599613
</li>
600614
)
601615
})}

libs/domains/clusters/feature/src/lib/cluster-creation-flow/step-summary/step-summary.spec.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,66 @@ describe('StepSummary', () => {
143143
expect(mockNavigate).toHaveBeenCalledWith({ to: '/organization/org-123/clusters' })
144144
})
145145
})
146+
147+
it('should send GCP NAT_GATEWAY using nat_gateway_type format on create', async () => {
148+
mockContextValue.generalData = {
149+
name: 'test-gcp-cluster',
150+
description: 'description',
151+
cloud_provider: CloudProviderEnum.GCP,
152+
region: 'europe-west1',
153+
installation_type: 'MANAGED',
154+
production: false,
155+
credentials: 'cred-id',
156+
credentials_name: 'cred-name',
157+
}
158+
mockContextValue.featuresData = {
159+
vpc_mode: 'DEFAULT',
160+
features: {
161+
STATIC_IP: {
162+
id: 'STATIC_IP',
163+
title: 'Static IP / Nat Gateways',
164+
value: true,
165+
},
166+
NAT_GATEWAY: {
167+
id: 'NAT_GATEWAY',
168+
title: 'NAT Gateway',
169+
value: true,
170+
extendedValue: {
171+
static_ips_enabled: false,
172+
static_ips_count: 2,
173+
},
174+
},
175+
},
176+
}
177+
178+
mockCreateCluster.mockResolvedValue({ id: 'cluster-123' })
179+
mockEditCloudProviderInfo.mockResolvedValue({})
180+
181+
const { userEvent } = renderWithProviders(<StepSummary {...defaultProps} />, { wrapper: Wrapper })
182+
183+
await userEvent.click(screen.getByTestId('button-create'))
184+
185+
await waitFor(() => {
186+
expect(mockCreateCluster).toHaveBeenCalledWith(
187+
expect.objectContaining({
188+
organizationId: 'org-123',
189+
clusterRequest: expect.objectContaining({
190+
cloud_provider: 'GCP',
191+
features: expect.arrayContaining([
192+
expect.objectContaining({
193+
id: 'NAT_GATEWAY',
194+
value: {
195+
nat_gateway_type: {
196+
provider: 'gcp',
197+
static_ips_enabled: false,
198+
static_ips_count: 2,
199+
},
200+
},
201+
}),
202+
]),
203+
}),
204+
})
205+
)
206+
})
207+
})
146208
})

libs/domains/clusters/feature/src/lib/cluster-creation-flow/step-summary/step-summary.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,33 @@ export function StepSummary({ organizationId }: StepSummaryProps) {
180180
if (generalData.cloud_provider === 'AWS' || generalData.cloud_provider === 'GCP') {
181181
if (featuresData && featuresData.vpc_mode === 'DEFAULT') {
182182
formatFeatures = Object.keys(featuresData.features)
183-
.map(
184-
(id: string) =>
185-
featuresData.features[id]?.value && {
183+
.map((id: string) => {
184+
const feature = featuresData.features[id]
185+
186+
if (!feature?.value) return null
187+
188+
if (generalData.cloud_provider === 'GCP' && id === 'NAT_GATEWAY') {
189+
const gcpNatGatewayType =
190+
feature.extendedValue && typeof feature.extendedValue === 'object'
191+
? feature.extendedValue
192+
: { static_ips_enabled: false, static_ips_count: 2 }
193+
194+
return {
186195
id,
187-
value: featuresData.features[id].extendedValue || featuresData.features[id].value,
196+
value: {
197+
nat_gateway_type: {
198+
provider: 'gcp',
199+
...gcpNatGatewayType,
200+
},
201+
} as unknown as ClusterRequestFeaturesInner['value'],
188202
}
189-
)
203+
}
204+
205+
return {
206+
id,
207+
value: feature.extendedValue || feature.value,
208+
}
209+
})
190210
.filter(Boolean) as ClusterRequestFeaturesInner[]
191211
} else if (generalData.cloud_provider === 'AWS') {
192212
formatFeatures = [
@@ -274,15 +294,17 @@ export function StepSummary({ organizationId }: StepSummaryProps) {
274294
Object.keys(featuresData.features).forEach((featureId) => {
275295
if (featureId === SCW_CONTROL_PLANE_FEATURE_ID) return
276296
const featureData = featuresData.features[featureId]
277-
if (featureId === 'NAT_GATEWAY' && featureData.extendedValue) {
297+
if (featureId === 'NAT_GATEWAY' && typeof featureData.extendedValue === 'string') {
278298
scwFeatures.push({
279299
id: featureId,
280300
value: {
281301
nat_gateway_type: { provider: 'scaleway', type: featureData.extendedValue },
282302
} as unknown as ClusterRequestFeaturesInner['value'],
283303
})
284304
} else if (featureData.value) {
285-
scwFeatures.push({ id: featureId, value: featureData.extendedValue || featureData.value })
305+
const scwFeatureValue =
306+
typeof featureData.extendedValue === 'string' ? featureData.extendedValue : featureData.value
307+
scwFeatures.push({ id: featureId, value: scwFeatureValue })
286308
}
287309
})
288310
}

0 commit comments

Comments
 (0)