diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 44f2227f38..8d892e96e2 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -2328,6 +2328,7 @@ func autoConvert_v1beta2_VPCSpec_To_v1beta1_VPCSpec(in *v1beta2.VPCSpec, out *VP // WARNING: in.PrivateDNSHostnameTypeOnLaunch requires manual conversion: does not exist in peer-type // WARNING: in.ElasticIPPool requires manual conversion: does not exist in peer-type // WARNING: in.SubnetSchema requires manual conversion: does not exist in peer-type + // WARNING: in.NATGatewayAvailabilityMode requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta2/network_types.go b/api/v1beta2/network_types.go index 26e38bc934..107db793f9 100644 --- a/api/v1beta2/network_types.go +++ b/api/v1beta2/network_types.go @@ -48,8 +48,16 @@ const ( ZoneTypeLocalZone ZoneType = "local-zone" // ZoneTypeWavelengthZone defines the AWS zone type in Wavelength infrastructure. ZoneTypeWavelengthZone ZoneType = "wavelength-zone" + + // NATGatewayAvailabilityModeZonal creates one NAT Gateway per Availability Zone. + NATGatewayAvailabilityModeZonal NATGatewayAvailabilityMode = "Zonal" + // NATGatewayAvailabilityModeRegional creates a single regional NAT Gateway. + NATGatewayAvailabilityModeRegional NATGatewayAvailabilityMode = "Regional" ) +// NATGatewayAvailabilityMode defines the availability mode for NAT Gateways. +type NATGatewayAvailabilityMode string + // NetworkStatus encapsulates AWS networking resources. type NetworkStatus struct { // SecurityGroups is a map from the role/kind of the security group to its unique name, if any. @@ -501,6 +509,28 @@ type VPCSpec struct { // +kubebuilder:default=PreferPrivate // +kubebuilder:validation:Enum=PreferPrivate;PreferPublic SubnetSchema *SubnetSchemaType `json:"subnetSchema,omitempty"` + + // NATGatewayAvailabilityMode specifies the availability mode for NAT Gateways in this VPC. + // Valid values are "zonal" and "regional". + // + // Zonal (default): Creates one NAT Gateway per Availability Zone in public subnets. + // Each private subnet routes traffic through the NAT Gateway in its own AZ. + // + // Regional: Creates a single NAT Gateway that automatically expands and contracts across + // all Availability Zones based on workload presence. Does not require public subnets. + // Provides automatic high availability with simplified setup and enhanced security. + // + // Regional NAT Gateways support up to 32 IP addresses per AZ (vs 8 for zonal) + // and are recommended for new deployments unless private connectivity is required. + // + // Note: Regional NAT Gateways are available in all commercial AWS Regions except + // AWS GovCloud (US) and China Regions. + // + // Defaults to Zonal for backward compatibility. + // +optional + // +kubebuilder:default=Zonal + // +kubebuilder:validation:Enum=Zonal;Regional + NATGatewayAvailabilityMode *NATGatewayAvailabilityMode `json:"natGatewayAvailabilityMode,omitempty"` } // String returns a string representation of the VPC. @@ -539,6 +569,19 @@ func (v *VPCSpec) GetPublicIpv4Pool() *string { return nil } +// IsRegionalNATGateway returns true if the NAT Gateway availability mode is Regional. +func (v *VPCSpec) IsRegionalNATGateway() bool { + return v.NATGatewayAvailabilityMode != nil && *v.NATGatewayAvailabilityMode == NATGatewayAvailabilityModeRegional +} + +// GetNATGatewayAvailabilityMode returns the NAT Gateway availability mode, defaulting to Zonal. +func (v *VPCSpec) GetNATGatewayAvailabilityMode() NATGatewayAvailabilityMode { + if v.NATGatewayAvailabilityMode == nil { + return NATGatewayAvailabilityModeZonal + } + return *v.NATGatewayAvailabilityMode +} + // SubnetSpec configures an AWS Subnet. type SubnetSpec struct { // ID defines a unique identifier to reference this resource. diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 1293074cb5..671bad929c 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -2463,6 +2463,11 @@ func (in *VPCSpec) DeepCopyInto(out *VPCSpec) { *out = new(SubnetSchemaType) **out = **in } + if in.NATGatewayAvailabilityMode != nil { + in, out := &in.NATGatewayAvailabilityMode, &out.NATGatewayAvailabilityMode + *out = new(NATGatewayAvailabilityMode) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCSpec. diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index 662022c257..e0a9bd2265 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -827,6 +827,30 @@ spec: Mutually exclusive with IPAMPool. type: string type: object + natGatewayAvailabilityMode: + default: Zonal + description: |- + NATGatewayAvailabilityMode specifies the availability mode for NAT Gateways in this VPC. + Valid values are "zonal" and "regional". + + Zonal (default): Creates one NAT Gateway per Availability Zone in public subnets. + Each private subnet routes traffic through the NAT Gateway in its own AZ. + + Regional: Creates a single NAT Gateway that automatically expands and contracts across + all Availability Zones based on workload presence. Does not require public subnets. + Provides automatic high availability with simplified setup and enhanced security. + + Regional NAT Gateways support up to 32 IP addresses per AZ (vs 8 for zonal) + and are recommended for new deployments unless private connectivity is required. + + Note: Regional NAT Gateways are available in all commercial AWS Regions except + AWS GovCloud (US) and China Regions. + + Defaults to Zonal for backward compatibility. + enum: + - Zonal + - Regional + type: string privateDnsHostnameTypeOnLaunch: description: |- PrivateDNSHostnameTypeOnLaunch is the type of hostname to assign to instances in the subnet at launch. @@ -3062,6 +3086,30 @@ spec: Mutually exclusive with IPAMPool. type: string type: object + natGatewayAvailabilityMode: + default: Zonal + description: |- + NATGatewayAvailabilityMode specifies the availability mode for NAT Gateways in this VPC. + Valid values are "zonal" and "regional". + + Zonal (default): Creates one NAT Gateway per Availability Zone in public subnets. + Each private subnet routes traffic through the NAT Gateway in its own AZ. + + Regional: Creates a single NAT Gateway that automatically expands and contracts across + all Availability Zones based on workload presence. Does not require public subnets. + Provides automatic high availability with simplified setup and enhanced security. + + Regional NAT Gateways support up to 32 IP addresses per AZ (vs 8 for zonal) + and are recommended for new deployments unless private connectivity is required. + + Note: Regional NAT Gateways are available in all commercial AWS Regions except + AWS GovCloud (US) and China Regions. + + Defaults to Zonal for backward compatibility. + enum: + - Zonal + - Regional + type: string privateDnsHostnameTypeOnLaunch: description: |- PrivateDNSHostnameTypeOnLaunch is the type of hostname to assign to instances in the subnet at launch. diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml index e0a9b811ce..9b54bbea5a 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml @@ -850,6 +850,30 @@ spec: Mutually exclusive with IPAMPool. type: string type: object + natGatewayAvailabilityMode: + default: Zonal + description: |- + NATGatewayAvailabilityMode specifies the availability mode for NAT Gateways in this VPC. + Valid values are "zonal" and "regional". + + Zonal (default): Creates one NAT Gateway per Availability Zone in public subnets. + Each private subnet routes traffic through the NAT Gateway in its own AZ. + + Regional: Creates a single NAT Gateway that automatically expands and contracts across + all Availability Zones based on workload presence. Does not require public subnets. + Provides automatic high availability with simplified setup and enhanced security. + + Regional NAT Gateways support up to 32 IP addresses per AZ (vs 8 for zonal) + and are recommended for new deployments unless private connectivity is required. + + Note: Regional NAT Gateways are available in all commercial AWS Regions except + AWS GovCloud (US) and China Regions. + + Defaults to Zonal for backward compatibility. + enum: + - Zonal + - Regional + type: string privateDnsHostnameTypeOnLaunch: description: |- PrivateDNSHostnameTypeOnLaunch is the type of hostname to assign to instances in the subnet at launch. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml index 869454a917..3ea861d96b 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -1774,6 +1774,30 @@ spec: Mutually exclusive with IPAMPool. type: string type: object + natGatewayAvailabilityMode: + default: Zonal + description: |- + NATGatewayAvailabilityMode specifies the availability mode for NAT Gateways in this VPC. + Valid values are "zonal" and "regional". + + Zonal (default): Creates one NAT Gateway per Availability Zone in public subnets. + Each private subnet routes traffic through the NAT Gateway in its own AZ. + + Regional: Creates a single NAT Gateway that automatically expands and contracts across + all Availability Zones based on workload presence. Does not require public subnets. + Provides automatic high availability with simplified setup and enhanced security. + + Regional NAT Gateways support up to 32 IP addresses per AZ (vs 8 for zonal) + and are recommended for new deployments unless private connectivity is required. + + Note: Regional NAT Gateways are available in all commercial AWS Regions except + AWS GovCloud (US) and China Regions. + + Defaults to Zonal for backward compatibility. + enum: + - Zonal + - Regional + type: string privateDnsHostnameTypeOnLaunch: description: |- PrivateDNSHostnameTypeOnLaunch is the type of hostname to assign to instances in the subnet at launch. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml index e4a0a6cf58..e08bcd85b5 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml @@ -1363,6 +1363,30 @@ spec: Mutually exclusive with IPAMPool. type: string type: object + natGatewayAvailabilityMode: + default: Zonal + description: |- + NATGatewayAvailabilityMode specifies the availability mode for NAT Gateways in this VPC. + Valid values are "zonal" and "regional". + + Zonal (default): Creates one NAT Gateway per Availability Zone in public subnets. + Each private subnet routes traffic through the NAT Gateway in its own AZ. + + Regional: Creates a single NAT Gateway that automatically expands and contracts across + all Availability Zones based on workload presence. Does not require public subnets. + Provides automatic high availability with simplified setup and enhanced security. + + Regional NAT Gateways support up to 32 IP addresses per AZ (vs 8 for zonal) + and are recommended for new deployments unless private connectivity is required. + + Note: Regional NAT Gateways are available in all commercial AWS Regions except + AWS GovCloud (US) and China Regions. + + Defaults to Zonal for backward compatibility. + enum: + - Zonal + - Regional + type: string privateDnsHostnameTypeOnLaunch: description: |- PrivateDNSHostnameTypeOnLaunch is the type of hostname to assign to instances in the subnet at launch. diff --git a/go.mod b/go.mod index ad5ab78087..60cdbc587b 100644 --- a/go.mod +++ b/go.mod @@ -10,14 +10,14 @@ require ( github.com/apparentlymart/go-cidr v1.1.0 github.com/aws/amazon-vpc-cni-k8s v1.15.5 github.com/aws/aws-lambda-go v1.41.0 - github.com/aws/aws-sdk-go-v2 v1.39.2 + github.com/aws/aws-sdk-go-v2 v1.40.1 github.com/aws/aws-sdk-go-v2/config v1.31.12 github.com/aws/aws-sdk-go-v2/credentials v1.18.16 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.12 github.com/aws/aws-sdk-go-v2/service/autoscaling v1.52.4 github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.52.0 github.com/aws/aws-sdk-go-v2/service/configservice v1.56.0 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.233.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.275.1 github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.36.0 github.com/aws/aws-sdk-go-v2/service/efs v1.39.0 github.com/aws/aws-sdk-go-v2/service/eks v1.64.0 @@ -29,7 +29,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.6 github.com/aws/aws-sdk-go-v2/service/ssm v1.59.1 github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 - github.com/aws/smithy-go v1.23.0 + github.com/aws/smithy-go v1.24.0 github.com/awslabs/goformation/v4 v4.19.5 github.com/blang/semver v3.5.1+incompatible github.com/coreos/ignition v0.35.0 @@ -115,15 +115,15 @@ require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 // indirect github.com/aws/aws-sdk-go-v2/service/cloudformation v1.50.0 github.com/aws/aws-sdk-go-v2/service/eventbridge v1.39.3 - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 // indirect github.com/aws/aws-sdk-go-v2/service/organizations v1.27.3 // indirect github.com/aws/aws-sdk-go-v2/service/servicequotas v1.21.4 diff --git a/go.sum b/go.sum index 65adb705a5..c795a8974c 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/aws/amazon-vpc-cni-k8s v1.15.5 h1:/mqTXB4HoGYg4CiU4Gco9iEvZ+V/309Na4H github.com/aws/amazon-vpc-cni-k8s v1.15.5/go.mod h1:jV4wNtmgT2Ra1/oZU99DPOFsCUKnf0mYfIyzDyAUVAY= github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= -github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= -github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2 v1.40.1 h1:difXb4maDZkRH0x//Qkwcfpdg1XQVXEAEs2DdXldFFc= +github.com/aws/aws-sdk-go-v2 v1.40.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00= github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= @@ -60,10 +60,10 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wg github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.12 h1:ofHawDLJTI6ytDIji+g4dXQ6u2idzTb04tDlN9AS614= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.12/go.mod h1:f5pL4iLDfbcxj1SZcdRdIokBB5eHbuYPS/Fs9DwUPRQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15 h1:Y5YXgygXwDI5P4RkteB5yF7v35neH7LfJKBG+hzIons= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.15/go.mod h1:K+/1EpG42dFSY7CBj+Fruzm8PsCGWTXJ3jdeJ659oGQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15 h1:AvltKnW9ewxX2hFmQS0FyJH93aSvJVUEFvXfU+HWtSE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.15/go.mod h1:3I4oCdZdmgrREhU74qS1dK9yZ62yumob+58AbFR4cQA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.9 h1:w9LnHqTq8MEdlnyhV4Bwfizd65lfNCNgdlNC6mM5paE= @@ -76,8 +76,8 @@ github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.52.0 h1:Wgjh6Igu7HS57d8AjRIG0 github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.52.0/go.mod h1:TSIIBxkIwUawJ9JyiymBksYZYsvIv8GIF2DkrlcTc5o= github.com/aws/aws-sdk-go-v2/service/configservice v1.56.0 h1:BFDPvTQk/+BM9T8I6uHhtmur8uaroCXoJ0AI2kpNO1U= github.com/aws/aws-sdk-go-v2/service/configservice v1.56.0/go.mod h1:46dDCtKXik+9IWU9oEOKBWzfQnyqn7EsmPnFUT7zqQw= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.233.0 h1:VxmOsv7MswuKQcSEIurxe4RK9tC6zYnosw9vBvv74lA= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.233.0/go.mod h1:35jGWx7ECvCwTsApqicFYzZ7JFEnBc6oHUuOQ3xIS54= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.275.1 h1:nEpHPUp2UKzxiLBoaLLTnIrWBmb1OL0vf8KHDHjNqcQ= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.275.1/go.mod h1:6xabBAflTTz4OO5f/P4QJrjzZ0WTYjRka+ZWXFqWw8U= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.36.0 h1:8GcatvIKYx5WkwjwY4H+K7egBHOddC3wwS6fIbpOUlQ= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.36.0/go.mod h1:yz4NeCWotlbHoT41Vc9NofCbKEyiNlKYZFT4SiqVQCY= github.com/aws/aws-sdk-go-v2/service/efs v1.39.0 h1:nxn7P1nAd7ThB1B0WASAKvjddJQcvLzaOo9iN4tp3ZU= @@ -92,12 +92,12 @@ github.com/aws/aws-sdk-go-v2/service/eventbridge v1.39.3 h1:T6L7fsONflMeXuvsT8qZ github.com/aws/aws-sdk-go-v2/service/eventbridge v1.39.3/go.mod h1:sIrUII6Z+hAVAgcpmsc2e9HvEr++m/v8aBPT7s4ZYUk= github.com/aws/aws-sdk-go-v2/service/iam v1.32.0 h1:ZNlfPdw849gBo/lvLFbEEvpTJMij0LXqiNWZ+lIamlU= github.com/aws/aws-sdk-go-v2/service/iam v1.32.0/go.mod h1:aXWImQV0uTW35LM0A/T4wEg6R1/ReXUu4SM6/lUHYK0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.0 h1:X0FveUndcZ3lKbSpIC6rMYGRiQTcUVRNH6X4yYtIrlU= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.0/go.mod h1:IWjQYlqw4EX9jw2g3qnEPPWvCE6bS8fKzhMed1OK7c8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15 h1:3/u/4yZOffg5jdNk1sDpOQ4Y+R6Xbh+GzpDrSZjuy3U= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.15/go.mod h1:4Zkjq0FKjE78NKjabuM4tRXKFzUJWXgP0ItEZK8l7JU= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9 h1:wuZ5uW2uhJR63zwNlqWH2W4aL4ZjeJP3o92/W+odDY4= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.9/go.mod h1:/G58M2fGszCrOzvJUkDdY8O9kycodunH4VdT5oBAqls= github.com/aws/aws-sdk-go-v2/service/organizations v1.27.3 h1:CnPWlONzFX9/yO6IGuKg9sWUE8WhKztYRFbhmOHXjJI= @@ -120,8 +120,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7U github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= -github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= -github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/awslabs/goformation/v4 v4.19.5 h1:Y+Tzh01tWg8gf//AgGKUamaja7Wx9NPiJf1FpZu4/iU= github.com/awslabs/goformation/v4 v4.19.5/go.mod h1:JoNpnVCBOUtEz9bFxc9sjy8uBUCLF5c4D1L7RhRTVM8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= diff --git a/pkg/cloud/services/network/natgateways.go b/pkg/cloud/services/network/natgateways.go index 78c041637d..c52cd7a134 100644 --- a/pkg/cloud/services/network/natgateways.go +++ b/pkg/cloud/services/network/natgateways.go @@ -61,7 +61,15 @@ func (s *Service) reconcileNatGateways() error { clusterv1beta1.ConditionSeverityWarning, "No private subnets available, skipping NAT gateways") return nil - } else if len(s.scope.Subnets().FilterPublic().FilterNonCni()) == 0 { + } + + // Check if Regional NAT Gateway is configured + if s.scope.VPC().IsRegionalNATGateway() { + return s.reconcileRegionalNatGateway() + } + + // Original zonal NAT gateway logic + if len(s.scope.Subnets().FilterPublic().FilterNonCni()) == 0 { s.scope.Debug("No public subnets available. Cannot create NAT gateways for private subnets, this might be a configuration error.") v1beta1conditions.MarkFalse( s.scope.InfraCluster(), @@ -106,6 +114,78 @@ func (s *Service) reconcileNatGateways() error { return nil } +func (s *Service) reconcileRegionalNatGateway() error { + s.scope.Debug("Reconciling regional NAT gateway") + + // Check if a regional NAT gateway already exists + existing, err := s.describeNatGatewaysBySubnet() + if err != nil { + return err + } + + // For regional NAT gateways, there should be only one + if len(existing) > 0 { + // Get the first (and should be only) NAT gateway + var ngw types.NatGateway + for _, gateway := range existing { + ngw = gateway + break + } + + s.scope.Info("Regional NAT gateway already exists", "nat-gateway-id", *ngw.NatGatewayId, "vpc-id", s.scope.VPC().ID) + + // Update tags if needed + if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { + buildParams := s.getNatGatewayTagParams(*ngw.NatGatewayId) + tagsBuilder := tags.New(&buildParams, tags.WithEC2(s.EC2Client)) + if err := tagsBuilder.Ensure(converters.TagsToMap(ngw.Tags)); err != nil { + return false, err + } + return true, nil + }, awserrors.ResourceNotFound); err != nil { + record.Warnf(s.scope.InfraCluster(), "FailedTagRegionalNATGateway", "Failed to tag regional NAT Gateway %q: %v", *ngw.NatGatewayId, err) + return errors.Wrapf(err, "failed to tag regional nat gateway %q", *ngw.NatGatewayId) + } + + // Store the regional NAT gateway ID in all private subnets for routing + subnets := s.scope.Subnets() + for i := range subnets { + if !subnets[i].IsPublic { + subnets[i].NatGatewayID = ngw.NatGatewayId + } + } + s.scope.SetSubnets(subnets) + + v1beta1conditions.MarkTrue(s.scope.InfraCluster(), infrav1.NatGatewaysReadyCondition) + return nil + } + + // No regional NAT gateway exists, create one + if !v1beta1conditions.Has(s.scope.InfraCluster(), infrav1.NatGatewaysReadyCondition) { + v1beta1conditions.MarkFalse(s.scope.InfraCluster(), infrav1.NatGatewaysReadyCondition, infrav1.NatGatewaysCreationStartedReason, clusterv1beta1.ConditionSeverityInfo, "") + if err := s.scope.PatchObject(); err != nil { + return errors.Wrap(err, "failed to patch conditions") + } + } + + ngw, err := s.createRegionalNatGateway() + if err != nil { + return err + } + + // Store the regional NAT gateway ID in all private subnets for routing + subnets := s.scope.Subnets() + for i := range subnets { + if !subnets[i].IsPublic { + subnets[i].NatGatewayID = ngw.NatGatewayId + } + } + s.scope.SetSubnets(subnets) + + v1beta1conditions.MarkTrue(s.scope.InfraCluster(), infrav1.NatGatewaysReadyCondition) + return nil +} + func (s *Service) updateNatGatewayIPs(updateTags bool) ([]string, error) { existing, err := s.describeNatGatewaysBySubnet() if err != nil { @@ -312,6 +392,37 @@ func (s *Service) createNatGateway(subnetID, ip string) (*types.NatGateway, erro return out.NatGateway, nil } +func (s *Service) createRegionalNatGateway() (*types.NatGateway, error) { + var out *ec2.CreateNatGatewayOutput + var err error + + s.scope.Info("Creating regional NAT gateway", "vpc-id", s.scope.VPC().ID) + + if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { + if out, err = s.EC2Client.CreateNatGateway(context.TODO(), &ec2.CreateNatGatewayInput{ + VpcId: aws.String(s.scope.VPC().ID), + AvailabilityMode: types.AvailabilityModeRegional, + TagSpecifications: []types.TagSpecification{tags.BuildParamsToTagSpecification(types.ResourceTypeNatgateway, s.getNatGatewayTagParams(services.TemporaryResourceID))}, + }); err != nil { + return false, err + } + return true, nil + }, awserrors.InvalidSubnet); err != nil { + record.Warnf(s.scope.InfraCluster(), "FailedCreateRegionalNATGateway", "Failed to create regional NAT Gateway: %v", err) + return nil, errors.Wrapf(err, "failed to create regional NAT gateway for VPC %q", s.scope.VPC().ID) + } + record.Eventf(s.scope.InfraCluster(), "SuccessfulCreateRegionalNATGateway", "Created regional NAT Gateway %q", *out.NatGateway.NatGatewayId) + + if err := ec2.NewNatGatewayAvailableWaiter(s.EC2Client).Wait(context.TODO(), &ec2.DescribeNatGatewaysInput{ + NatGatewayIds: []string{aws.ToString(out.NatGateway.NatGatewayId)}, + }, time.Minute*2); err != nil { + return nil, errors.Wrapf(err, "failed to wait for regional nat gateway %q", *out.NatGateway.NatGatewayId) + } + + s.scope.Info("Created regional NAT gateway", "nat-gateway-id", *out.NatGateway.NatGatewayId, "vpc-id", s.scope.VPC().ID) + return out.NatGateway, nil +} + func (s *Service) deleteNatGateway(id string) error { _, err := s.EC2Client.DeleteNatGateway(context.TODO(), &ec2.DeleteNatGatewayInput{ NatGatewayId: aws.String(id), @@ -361,11 +472,23 @@ func (s *Service) deleteNatGateway(id string) error { // NAT gateways in edge zones (Local Zones) are not globally supported, // private subnets in those locations uses Nat Gateways from the // Parent Zone or, when not available, the first zone in the Region. +// For regional NAT gateways, returns the single regional NAT gateway ID. func (s *Service) getNatGatewayForSubnet(sn *infrav1.SubnetSpec) (string, error) { if sn.IsPublic { return "", errors.Errorf("cannot get NAT gateway for a public subnet, got id %q", sn.GetResourceID()) } + // If regional NAT gateway is configured, return the NAT gateway ID stored in the subnet + if s.scope.VPC().IsRegionalNATGateway() { + if sn.NatGatewayID != nil && *sn.NatGatewayID != "" { + s.scope.Debug("Using regional NAT gateway for subnet", "nat-gateway-id", *sn.NatGatewayID, "subnet-id", sn.GetResourceID()) + return *sn.NatGatewayID, nil + } + s.scope.Debug("Regional NAT gateway not yet assigned to subnet", "subnet-id", sn.GetResourceID()) + return "", errors.Errorf("regional NAT gateway not found for private subnet %q", sn.GetResourceID()) + } + + // Original zonal NAT gateway logic // Check if public edge subnet in the edge zone has nat gateway azGateways := make(map[string]string) azNames := []string{} diff --git a/pkg/cloud/services/network/natgateways_test.go b/pkg/cloud/services/network/natgateways_test.go index 7c5eee7e39..a0b2a89dcc 100644 --- a/pkg/cloud/services/network/natgateways_test.go +++ b/pkg/cloud/services/network/natgateways_test.go @@ -1100,3 +1100,434 @@ func TestGetdNatGatewayForEdgeSubnet(t *testing.T) { }) } } + +func TestReconcileRegionalNatGateway(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + testCases := []struct { + name string + input infrav1.NetworkSpec + expect func(m *mocks.MockEC2APIMockRecorder) + }{ + { + name: "no existing regional NAT gateway, should create one", + input: infrav1.NetworkSpec{ + VPC: infrav1.VPCSpec{ + ID: subnetsVPCID, + NATGatewayAvailabilityMode: ptr.To(infrav1.NATGatewayAvailabilityModeRegional), + Tags: infrav1.Tags{ + infrav1.ClusterTagKey("test-cluster"): "owned", + }, + }, + Subnets: []infrav1.SubnetSpec{ + { + ID: "subnet-private-1", + AvailabilityZone: "us-east-1a", + CidrBlock: "10.0.10.0/24", + IsPublic: false, + }, + { + ID: "subnet-private-2", + AvailabilityZone: "us-east-1b", + CidrBlock: "10.0.11.0/24", + IsPublic: false, + }, + }, + }, + expect: func(m *mocks.MockEC2APIMockRecorder) { + m.DescribeNatGateways(context.TODO(), + gomock.Eq(&ec2.DescribeNatGatewaysInput{ + Filter: []types.Filter{ + { + Name: aws.String("vpc-id"), + Values: []string{subnetsVPCID}, + }, + { + Name: aws.String("state"), + Values: []string{"pending", "available"}, + }, + }, + }), + gomock.Any()).Return(&ec2.DescribeNatGatewaysOutput{}, nil) + + m.CreateNatGateway(context.TODO(), &ec2.CreateNatGatewayInput{ + VpcId: aws.String(subnetsVPCID), + AvailabilityMode: types.AvailabilityModeRegional, + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeNatgateway, + Tags: []types.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("test-cluster-nat"), + }, + { + Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), + Value: aws.String("owned"), + }, + { + Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), + Value: aws.String("common"), + }, + }, + }, + }, + }).Return(&ec2.CreateNatGatewayOutput{ + NatGateway: &types.NatGateway{ + NatGatewayId: aws.String("regional-natgateway"), + VpcId: aws.String(subnetsVPCID), + }, + }, nil) + + m.DescribeNatGateways(gomock.Any(), &ec2.DescribeNatGatewaysInput{ + NatGatewayIds: []string{"regional-natgateway"}, + }, gomock.Any()).Return(&ec2.DescribeNatGatewaysOutput{ + NatGateways: []types.NatGateway{ + { + State: types.NatGatewayStateAvailable, + NatGatewayId: aws.String("regional-natgateway"), + VpcId: aws.String(subnetsVPCID), + }, + }, + }, nil) + }, + }, + { + name: "existing regional NAT gateway, should not create another", + input: infrav1.NetworkSpec{ + VPC: infrav1.VPCSpec{ + ID: subnetsVPCID, + NATGatewayAvailabilityMode: ptr.To(infrav1.NATGatewayAvailabilityModeRegional), + Tags: infrav1.Tags{ + infrav1.ClusterTagKey("test-cluster"): "owned", + }, + }, + Subnets: []infrav1.SubnetSpec{ + { + ID: "subnet-private-1", + AvailabilityZone: "us-east-1a", + CidrBlock: "10.0.10.0/24", + IsPublic: false, + }, + { + ID: "subnet-private-2", + AvailabilityZone: "us-east-1b", + CidrBlock: "10.0.11.0/24", + IsPublic: false, + }, + }, + }, + expect: func(m *mocks.MockEC2APIMockRecorder) { + m.DescribeNatGateways(context.TODO(), + gomock.Eq(&ec2.DescribeNatGatewaysInput{ + Filter: []types.Filter{ + { + Name: aws.String("vpc-id"), + Values: []string{subnetsVPCID}, + }, + { + Name: aws.String("state"), + Values: []string{"pending", "available"}, + }, + }, + }), + gomock.Any()).Return(&ec2.DescribeNatGatewaysOutput{ + NatGateways: []types.NatGateway{ + { + NatGatewayId: aws.String("existing-regional-natgateway"), + VpcId: aws.String(subnetsVPCID), + Tags: []types.Tag{ + { + Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), + Value: aws.String("common"), + }, + { + Key: aws.String("Name"), + Value: aws.String("test-cluster-nat"), + }, + { + Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), + Value: aws.String("owned"), + }, + }, + }, + }, + }, nil) + + m.CreateNatGateway(context.TODO(), gomock.Any()).Times(0) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ec2Mock := mocks.NewMockEC2API(mockCtrl) + + scheme := runtime.NewScheme() + _ = infrav1.AddToScheme(scheme) + _ = clusterv1.AddToScheme(scheme) + awsCluster := &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: infrav1.AWSClusterSpec{ + NetworkSpec: tc.input, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(awsCluster).WithStatusSubresource(awsCluster).Build() + + clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, + }, + AWSCluster: awsCluster, + Client: client, + }) + if err != nil { + t.Fatalf("Failed to create test context: %v", err) + } + + tc.expect(ec2Mock.EXPECT()) + + s := &Service{ + scope: clusterScope, + EC2Client: ec2Mock, + } + + err = s.reconcileRegionalNatGateway() + if err != nil { + t.Fatalf("got an unexpected error: %v", err) + } + }) + } +} + +func TestCreateRegionalNatGateway(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + testCases := []struct { + name string + vpcID string + expect func(m *mocks.MockEC2APIMockRecorder) + expectErr bool + }{ + { + name: "successfully create regional NAT gateway", + vpcID: subnetsVPCID, + expect: func(m *mocks.MockEC2APIMockRecorder) { + m.CreateNatGateway(context.TODO(), &ec2.CreateNatGatewayInput{ + VpcId: aws.String(subnetsVPCID), + AvailabilityMode: types.AvailabilityModeRegional, + TagSpecifications: []types.TagSpecification{ + { + ResourceType: types.ResourceTypeNatgateway, + Tags: []types.Tag{ + { + Key: aws.String("Name"), + Value: aws.String("test-cluster-nat"), + }, + { + Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/cluster/test-cluster"), + Value: aws.String("owned"), + }, + { + Key: aws.String("sigs.k8s.io/cluster-api-provider-aws/role"), + Value: aws.String("common"), + }, + }, + }, + }, + }).Return(&ec2.CreateNatGatewayOutput{ + NatGateway: &types.NatGateway{ + NatGatewayId: aws.String("regional-natgateway"), + VpcId: aws.String(subnetsVPCID), + }, + }, nil) + + m.DescribeNatGateways(gomock.Any(), &ec2.DescribeNatGatewaysInput{ + NatGatewayIds: []string{"regional-natgateway"}, + }, gomock.Any()).Return(&ec2.DescribeNatGatewaysOutput{ + NatGateways: []types.NatGateway{ + { + State: types.NatGatewayStateAvailable, + NatGatewayId: aws.String("regional-natgateway"), + VpcId: aws.String(subnetsVPCID), + }, + }, + }, nil) + }, + expectErr: false, + }, + { + name: "create regional NAT gateway fails", + vpcID: subnetsVPCID, + expect: func(m *mocks.MockEC2APIMockRecorder) { + m.CreateNatGateway(context.TODO(), gomock.Any()). + Return(nil, awserrors.NewFailedDependency("dependency-violation")) + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ec2Mock := mocks.NewMockEC2API(mockCtrl) + + scheme := runtime.NewScheme() + _ = infrav1.AddToScheme(scheme) + _ = clusterv1.AddToScheme(scheme) + awsCluster := &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: infrav1.AWSClusterSpec{ + NetworkSpec: infrav1.NetworkSpec{ + VPC: infrav1.VPCSpec{ + ID: tc.vpcID, + Tags: infrav1.Tags{ + infrav1.ClusterTagKey("test-cluster"): "owned", + }, + }, + }, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(awsCluster).WithStatusSubresource(awsCluster).Build() + + clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, + }, + AWSCluster: awsCluster, + Client: client, + }) + if err != nil { + t.Fatalf("Failed to create test context: %v", err) + } + + tc.expect(ec2Mock.EXPECT()) + + s := &Service{ + scope: clusterScope, + EC2Client: ec2Mock, + } + + ngw, err := s.createRegionalNatGateway() + if tc.expectErr && err == nil { + t.Fatal("expected error but got no error") + } + if !tc.expectErr && err != nil { + t.Fatalf("got an unexpected error: %v", err) + } + if !tc.expectErr && ngw == nil { + t.Fatal("expected NAT gateway but got nil") + } + }) + } +} + +func TestGetNatGatewayForSubnetRegional(t *testing.T) { + g := NewWithT(t) + + testCases := []struct { + name string + input infrav1.SubnetSpec + vpcSpec infrav1.VPCSpec + expect string + expectErr bool + expectErrMessage string + }{ + { + name: "regional NAT gateway assigned to private subnet", + input: infrav1.SubnetSpec{ + ID: "subnet-private-1", + CidrBlock: "10.0.10.0/24", + IsPublic: false, + NatGatewayID: aws.String("regional-natgateway"), + }, + vpcSpec: infrav1.VPCSpec{ + ID: subnetsVPCID, + NATGatewayAvailabilityMode: ptr.To(infrav1.NATGatewayAvailabilityModeRegional), + }, + expect: "regional-natgateway", + expectErr: false, + }, + { + name: "regional NAT gateway not assigned to private subnet", + input: infrav1.SubnetSpec{ + ID: "subnet-private-1", + CidrBlock: "10.0.10.0/24", + IsPublic: false, + NatGatewayID: nil, + }, + vpcSpec: infrav1.VPCSpec{ + ID: subnetsVPCID, + NATGatewayAvailabilityMode: ptr.To(infrav1.NATGatewayAvailabilityModeRegional), + }, + expect: "", + expectErr: true, + expectErrMessage: "regional NAT gateway not found for private subnet \"subnet-private-1\"", + }, + { + name: "public subnet with regional NAT gateway should fail", + input: infrav1.SubnetSpec{ + ID: "subnet-public-1", + CidrBlock: "10.0.1.0/24", + IsPublic: true, + NatGatewayID: aws.String("regional-natgateway"), + }, + vpcSpec: infrav1.VPCSpec{ + ID: subnetsVPCID, + NATGatewayAvailabilityMode: ptr.To(infrav1.NATGatewayAvailabilityModeRegional), + }, + expect: "", + expectErr: true, + expectErrMessage: "cannot get NAT gateway for a public subnet, got id \"subnet-public-1\"", + }, + } + + for idx, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + scheme := runtime.NewScheme() + _ = infrav1.AddToScheme(scheme) + awsCluster := &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: infrav1.AWSClusterSpec{ + NetworkSpec: infrav1.NetworkSpec{ + VPC: tc.vpcSpec, + }, + }, + } + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(awsCluster).WithStatusSubresource(awsCluster).Build() + + clusterScope, err := scope.NewClusterScope(scope.ClusterScopeParams{ + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test-cluster"}, + }, + AWSCluster: awsCluster, + Client: client, + }) + if err != nil { + t.Fatalf("Failed to create test context: %v", err) + return + } + + s := NewService(clusterScope) + + id, err := s.getNatGatewayForSubnet(&testCases[idx].input) + + if tc.expectErr && err == nil { + t.Fatal("expected error but got no error") + } + if err != nil && len(tc.expectErrMessage) > 0 { + if err.Error() != tc.expectErrMessage { + t.Fatalf("got an unexpected error message:\nwant: %v\n got: %v\n", tc.expectErrMessage, err.Error()) + } + } + if !tc.expectErr && err != nil { + t.Fatalf("got an unexpected error: %v", err) + } + if len(tc.expect) > 0 { + g.Expect(id).To(Equal(tc.expect)) + } + }) + } +}