diff --git a/docs/guide/service/annotations.md b/docs/guide/service/annotations.md index c9dc89f9b..c7ab9bc07 100644 --- a/docs/guide/service/annotations.md +++ b/docs/guide/service/annotations.md @@ -54,6 +54,8 @@ | [service.beta.kubernetes.io/aws-load-balancer-inbound-sg-rules-on-private-link-traffic](#update-security-settings) | string | | | [service.beta.kubernetes.io/aws-load-balancer-listener-attributes.${Protocol}-${Port}](#listener-attributes) | stringMap | | | [service.beta.kubernetes.io/aws-load-balancer-multi-cluster-target-group](#multi-cluster-target-group) | boolean | false | If specified, the controller will only operate on targets that exist within the cluster, ignoring targets from other sources. | +| [service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat](#enable-prefix-for-ipv6-source-nat) | string | off | Optional annotation. dualstack lb only. Allowed values - on and off | +| [service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes](#source-nat-ipv6-prefixes) | stringList | | Optional annotation. dualstack lb only. This annotation is only applicable when user has to set the service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat to "on". Length must match the number of subnets | ## Traffic Routing Traffic Routing can be controlled with following annotations: @@ -199,6 +201,41 @@ Traffic Listening can be controlled with following annotations: service.beta.kubernetes.io/aws-load-balancer-ip-address-type: ipv4 ``` +## Support UDP-based services over IPv6 +You can configure dualstack NLB to support UDP-based services over IPv6 via the following annotations: + +- service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat specifies whether Prefix for IPv6 source NAT is enabled or not. UDP-based support can be enabled for dualstack NLBs only if Prefix for IPv6 source NAT is enabled. + + !!!note "" + - Applicable to Network Load Balancers using dualstack IP address type. + - This configuration is optional, and you can use it to enable UDP support over IPv6. + - Allowed values are either “on” or “off” + - Once the source prefix for source NATing is enabled, it cannot be disabled if load balancer has a UDP listener attached. + - Steps to disable the aws-load-balancer-enable-prefix-for-ipv6-source-nat after it is enabled and UDP listeners already attached. + - You will have to first remove the UDP listeners and apply the manifest. + - Update the manifest to set source NATing to "off" and then apply the manifest again. + + !!!example + - Enable prefix for IPv6 Source NAT + ``` + service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat: "on" + ``` + +- service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes specifies a list of IPv6 prefixes that should be used for IPv6 source NATing. + + !!!note "" + - Applicable to Network Load Balancers using dualstack IP address type. + - This annotation can be specified only if service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat annotation is set to “on”. + - This configuration is optional and it can be used to specify custom IPv6 prefixes for IPv6 source NATing to support UDP based services routing in Network Load Balancers using dualstack IP address type. + - If service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat annotation is set to “on”, and you don’t specify this annotation, then IPv6 prefix/CIDR for source NATing will be auto-assigned to each subnet. + - If you are specifying this annotation, you must specify the same number of items in the list as the load balancer subnets annotation and following the same order. Each item in the list can have value of either “auto_assigned” or a valid IPv6 prefix/CIDR with prefix length of 80 and it should be in range of the corresponding subnet CIDR. + - Once the source NAT IPv6 prefixes are set, the IPv6 prefixes cannot be updated if the load balancer has a UDP listener attached. + + !!!example + ``` + service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes: 1025:0223:0009:6487:0001::/80, auto_assigned, 1025:0223:0010:6487:0001::/80 + ``` + ## Resource attributes NLB resource attributes can be controlled via the following annotations: diff --git a/go.mod b/go.mod index 2eeae10d6..a04229159 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,22 @@ module sigs.k8s.io/aws-load-balancer-controller -go 1.22.7 +go 1.22.8 require ( - github.com/aws/aws-sdk-go-v2 v1.30.5 + github.com/aws/aws-sdk-go v1.55.5 + github.com/aws/aws-sdk-go-v2 v1.32.3 github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 github.com/aws/aws-sdk-go-v2/service/acm v1.28.4 github.com/aws/aws-sdk-go-v2/service/appmesh v1.27.7 github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.36.0 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.41.0 github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.23.3 github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.31.7 github.com/aws/aws-sdk-go-v2/service/shield v1.27.3 github.com/aws/aws-sdk-go-v2/service/wafregional v1.23.3 github.com/aws/aws-sdk-go-v2/service/wafv2 v1.51.4 - github.com/aws/smithy-go v1.21.0 + github.com/aws/smithy-go v1.22.0 github.com/evanphx/json-patch v5.7.0+incompatible github.com/gavv/httpexpect/v2 v2.9.0 github.com/go-logr/logr v1.4.1 @@ -56,8 +57,8 @@ require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect diff --git a/go.sum b/go.sum index bfb19e52e..c8386e14d 100644 --- a/go.sum +++ b/go.sum @@ -36,18 +36,20 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/aws/aws-sdk-go-v2 v1.30.5 h1:mWSRTwQAb0aLE17dSzztCVJWI9+cRMgqebndjwDyK0g= -github.com/aws/aws-sdk-go-v2 v1.30.5/go.mod h1:CT+ZPWXbYrci8chcARI3OmI/qgd+f6WtuLOoaIA8PR0= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.32.3 h1:T0dRlFBKcdaUPGNtkBSwHZxrtis8CQU17UpNBZYd0wk= +github.com/aws/aws-sdk-go-v2 v1.32.3/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 h1:pI7Bzt0BJtYA0N/JEC6B8fJ4RBrEMi1LBrkMdFYNSnQ= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17/go.mod h1:Dh5zzJYMtxfIjYW+/evjQ8uj2OyR/ve2KROHGHlSFqE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 h1:Mqr/V5gvrhA2gvgnF42Zh5iMiQNcOYthFYwCyrnuWlc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17/go.mod h1:aLJpZlCmjE+V+KtN1q1uyZkfnUWpQGpbsn89XPKyzfU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22 h1:Jw50LwEkVjuVzE1NzkhNKkBf9cRN7MtE1F/b2cOKTUM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.22/go.mod h1:Y/SmAyPcOTmpeVaWSzSKiILfXTVJwrGmYZhcRbhWuEY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22 h1:981MHwBaRZM7+9QSR6XamDzF/o7ouUGxFzr+nVSIhrs= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.22/go.mod h1:1RA1+aBEfn+CAB/Mh0MB6LsdCYCnjZm7tKXtnk499ZQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/acm v1.28.4 h1:wiW1Y6/1lysA0eJZRq0I53YYKuV9MNAzL15z2eZRlEE= @@ -56,8 +58,8 @@ github.com/aws/aws-sdk-go-v2/service/appmesh v1.27.7 h1:q44a6kysAfej9zZwRnraOg9s github.com/aws/aws-sdk-go-v2/service/appmesh v1.27.7/go.mod h1:ZYSmrgAMp0rTCHH+SGsoxZo+PPbgsDqBzewTp3tSJ60= github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0 h1:ta62lid9JkIpKZtZZXSj6rP2AqY5x1qYGq53ffxqD9Q= github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0/go.mod h1:o6QDjdVKpP5EF0dp/VlvqckzuSDATr1rLdHt3A5m0YY= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.36.0 h1:3t8g6wmPA9hr69qzDraI1umO2An7jKNe75dBsxbI30E= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.36.0/go.mod h1:jk+iid9R4MN7UVDwSTK/ZDDO8WNhxnO2WVzfYOMLh+4= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.41.0 h1:W+xNfPS8dQ8YoszdkHqTDYIgCrWJvyUU/ZgdJ0frCRE= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.41.0/go.mod h1:6WuvTcPjB9gff93p/2LNBg09d8xK99jpVO6+fRSCKEU= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= @@ -78,8 +80,8 @@ github.com/aws/aws-sdk-go-v2/service/wafregional v1.23.3 h1:7dr6En0/6KRFoz8VmnYk github.com/aws/aws-sdk-go-v2/service/wafregional v1.23.3/go.mod h1:24TtlRsv4LKAE3VnRJQhpatr8cpX0yj8NSzg8/lxOCw= github.com/aws/aws-sdk-go-v2/service/wafv2 v1.51.4 h1:1khBA5uryBRJoCb4G2iR5RT06BkfPEjjDCHAiRb8P3Q= github.com/aws/aws-sdk-go-v2/service/wafv2 v1.51.4/go.mod h1:QpFImaPGKNwa+MiZ+oo6LbV1PVQBapc0CnrAMRScoxM= -github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= -github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/pkg/annotations/constants.go b/pkg/annotations/constants.go index 997034dfb..3bad8a76b 100644 --- a/pkg/annotations/constants.go +++ b/pkg/annotations/constants.go @@ -99,4 +99,6 @@ const ( SvcLBSuffixSecurityGroupPrefixLists = "aws-load-balancer-security-group-prefix-lists" SvcLBSuffixlsAttsAnnotationPrefix = "aws-load-balancer-listener-attributes" SvcLBSuffixMultiClusterTargetGroup = "aws-load-balancer-multi-cluster-target-group" + ScvLBSuffixEnablePrefixForIpv6SourceNat = "aws-load-balancer-enable-prefix-for-ipv6-source-nat" + ScvLBSuffixSourceNatIpv6Prefixes = "aws-load-balancer-source-nat-ipv6-prefixes" ) diff --git a/pkg/deploy/elbv2/load_balancer_manager.go b/pkg/deploy/elbv2/load_balancer_manager.go index a7ecc4ef2..70df7f226 100644 --- a/pkg/deploy/elbv2/load_balancer_manager.go +++ b/pkg/deploy/elbv2/load_balancer_manager.go @@ -154,20 +154,36 @@ func (m *defaultLoadBalancerManager) updateSDKLoadBalancerWithIPAddressType(ctx func (m *defaultLoadBalancerManager) updateSDKLoadBalancerWithSubnetMappings(ctx context.Context, resLB *elbv2model.LoadBalancer, sdkLB LoadBalancerWithTags) error { desiredSubnets := sets.NewString() + desiredSubnetsSourceNATPrefixes := sets.NewString() + currentSubnetsSourceNATPrefixes := sets.NewString() for _, mapping := range resLB.Spec.SubnetMappings { desiredSubnets.Insert(mapping.SubnetID) + if mapping.SourceNatIpv6Prefix != nil { + desiredSubnetsSourceNATPrefixes.Insert(awssdk.ToString(mapping.SourceNatIpv6Prefix)) + } } currentSubnets := sets.NewString() for _, az := range sdkLB.LoadBalancer.AvailabilityZones { currentSubnets.Insert(awssdk.ToString(az.SubnetId)) + if len(az.SourceNatIpv6Prefixes) != 0 { + currentSubnetsSourceNATPrefixes.Insert(az.SourceNatIpv6Prefixes[0]) + } } - if desiredSubnets.Equal(currentSubnets) { + sdkLBEnablePrefixForIpv6SourceNatValue := string(elbv2model.EnablePrefixForIpv6SourceNatOff) + resLBEnablePrefixForIpv6SourceNatValue := string(elbv2model.EnablePrefixForIpv6SourceNatOff) + + sdkLBEnablePrefixForIpv6SourceNatValue = string(sdkLB.LoadBalancer.EnablePrefixForIpv6SourceNat) + + resLBEnablePrefixForIpv6SourceNatValue = string(resLB.Spec.EnablePrefixForIpv6SourceNat) + + if desiredSubnets.Equal(currentSubnets) && desiredSubnetsSourceNATPrefixes.Equal(currentSubnetsSourceNATPrefixes) && sdkLBEnablePrefixForIpv6SourceNatValue == resLBEnablePrefixForIpv6SourceNatValue { return nil } req := &elbv2sdk.SetSubnetsInput{ - LoadBalancerArn: sdkLB.LoadBalancer.LoadBalancerArn, - SubnetMappings: buildSDKSubnetMappings(resLB.Spec.SubnetMappings), + LoadBalancerArn: sdkLB.LoadBalancer.LoadBalancerArn, + SubnetMappings: buildSDKSubnetMappings(resLB.Spec.SubnetMappings), + EnablePrefixForIpv6SourceNat: elbv2types.EnablePrefixForIpv6SourceNatEnum(resLBEnablePrefixForIpv6SourceNatValue), } changeDesc := fmt.Sprintf("%v => %v", currentSubnets.List(), desiredSubnets.List()) m.logger.Info("modifying loadBalancer subnetMappings", @@ -261,6 +277,10 @@ func buildSDKCreateLoadBalancerInput(lbSpec elbv2model.LoadBalancerSpec) (*elbv2 sdkObj.SecurityGroups = sdkSecurityGroups } + if lbSpec.EnablePrefixForIpv6SourceNat != "" { + sdkObj.EnablePrefixForIpv6SourceNat = elbv2types.EnablePrefixForIpv6SourceNatEnum(lbSpec.EnablePrefixForIpv6SourceNat) + } + sdkObj.CustomerOwnedIpv4Pool = lbSpec.CustomerOwnedIPv4Pool return sdkObj, nil } @@ -294,10 +314,11 @@ func buildSDKSecurityGroups(modelSecurityGroups []coremodel.StringToken) ([]stri func buildSDKSubnetMapping(modelSubnetMapping elbv2model.SubnetMapping) elbv2types.SubnetMapping { return elbv2types.SubnetMapping{ - AllocationId: modelSubnetMapping.AllocationID, - PrivateIPv4Address: modelSubnetMapping.PrivateIPv4Address, - IPv6Address: modelSubnetMapping.IPv6Address, - SubnetId: awssdk.String(modelSubnetMapping.SubnetID), + AllocationId: modelSubnetMapping.AllocationID, + PrivateIPv4Address: modelSubnetMapping.PrivateIPv4Address, + IPv6Address: modelSubnetMapping.IPv6Address, + SubnetId: awssdk.String(modelSubnetMapping.SubnetID), + SourceNatIpv6Prefix: modelSubnetMapping.SourceNatIpv6Prefix, } } diff --git a/pkg/deploy/elbv2/load_balancer_manager_test.go b/pkg/deploy/elbv2/load_balancer_manager_test.go index 8014c6149..e35f75346 100644 --- a/pkg/deploy/elbv2/load_balancer_manager_test.go +++ b/pkg/deploy/elbv2/load_balancer_manager_test.go @@ -3,11 +3,13 @@ package elbv2 import ( "context" elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + "github.com/go-logr/logr" + "sigs.k8s.io/aws-load-balancer-controller/pkg/aws/services" "testing" awssdk "github.com/aws/aws-sdk-go-v2/aws" elbv2sdk "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/go-logr/logr" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" coremodel "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core" elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2" @@ -17,6 +19,8 @@ import ( func Test_buildSDKCreateLoadBalancerInput(t *testing.T) { schemeInternetFacing := elbv2model.LoadBalancerSchemeInternetFacing addressTypeDualStack := elbv2model.IPAddressTypeDualStack + enablePrefixForIpv6SourceNatOn := elbv2model.EnablePrefixForIpv6SourceNatOn + enablePrefixForIpv6SourceNatOff := elbv2model.EnablePrefixForIpv6SourceNatOff type args struct { lbSpec elbv2model.LoadBalancerSpec } @@ -97,6 +101,76 @@ func Test_buildSDKCreateLoadBalancerInput(t *testing.T) { }, }, }, + { + name: "network loadBalancer - Dualstack UDP Support over IPv6 - on", + args: args{ + lbSpec: elbv2model.LoadBalancerSpec{ + Name: "my-nlb", + Type: elbv2model.LoadBalancerTypeNetwork, + Scheme: schemeInternetFacing, + IPAddressType: addressTypeDualStack, + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + }, + { + SubnetID: "subnet-B", + }, + }, + EnablePrefixForIpv6SourceNat: enablePrefixForIpv6SourceNatOn, + }, + }, + want: &elbv2sdk.CreateLoadBalancerInput{ + Name: awssdk.String("my-nlb"), + Type: elbv2types.LoadBalancerTypeEnumNetwork, + IpAddressType: elbv2types.IpAddressTypeDualstack, + Scheme: elbv2types.LoadBalancerSchemeEnumInternetFacing, + SubnetMappings: []elbv2types.SubnetMapping{ + { + SubnetId: awssdk.String("subnet-A"), + }, + { + SubnetId: awssdk.String("subnet-B"), + }, + }, + EnablePrefixForIpv6SourceNat: elbv2types.EnablePrefixForIpv6SourceNatEnumOn, + }, + }, + { + name: "network loadBalancer - Dualstack UDP Support over IPv6 - off", + args: args{ + lbSpec: elbv2model.LoadBalancerSpec{ + Name: "my-nlb", + Type: elbv2model.LoadBalancerTypeNetwork, + Scheme: elbv2model.LoadBalancerSchemeInternetFacing, + IPAddressType: elbv2model.IPAddressTypeDualStack, + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + }, + { + SubnetID: "subnet-B", + }, + }, + EnablePrefixForIpv6SourceNat: enablePrefixForIpv6SourceNatOff, + }, + }, + want: &elbv2sdk.CreateLoadBalancerInput{ + Name: awssdk.String("my-nlb"), + Type: elbv2types.LoadBalancerTypeEnumNetwork, + IpAddressType: elbv2types.IpAddressTypeDualstack, + Scheme: elbv2types.LoadBalancerSchemeEnumInternetFacing, + SubnetMappings: []elbv2types.SubnetMapping{ + { + SubnetId: awssdk.String("subnet-A"), + }, + { + SubnetId: awssdk.String("subnet-B"), + }, + }, + EnablePrefixForIpv6SourceNat: elbv2types.EnablePrefixForIpv6SourceNatEnumOff, + }, + }, { name: "application loadBalancer - with CoIP pool", args: args{ @@ -152,6 +226,7 @@ func Test_buildSDKCreateLoadBalancerInput(t *testing.T) { } func Test_buildSDKSubnetMappings(t *testing.T) { + sourceNatIpv6PrefixAutoAssigned := elbv2model.SourceNatIpv6PrefixAutoAssigned type args struct { modelSubnetMappings []elbv2model.SubnetMapping } @@ -181,6 +256,23 @@ func Test_buildSDKSubnetMappings(t *testing.T) { }, }, }, + { + name: "subnet mappings with sourceNAT prefix", + args: args{ + modelSubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-a", + SourceNatIpv6Prefix: &sourceNatIpv6PrefixAutoAssigned, + }, + }, + }, + want: []elbv2types.SubnetMapping{ + { + SubnetId: awssdk.String("subnet-a"), + SourceNatIpv6Prefix: awssdk.String("auto_assigned"), + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -415,3 +507,128 @@ func Test_defaultLoadBalancerManager_checkSDKLoadBalancerWithCOIPv4Pool(t *testi }) } } + +func Test_defaultLoadBalancerManager_updateSDKLoadBalancerWithSubnetMappings(t *testing.T) { + type setSubnetsWithContextCall struct { + req *elbv2sdk.SetSubnetsInput + resp *elbv2sdk.SetSubnetsOutput + err error + } + type fields struct { + setSubnetsWithContextCall setSubnetsWithContextCall + } + enablePrefixForIpv6SourceNatOn := elbv2model.EnablePrefixForIpv6SourceNatOn + enablePrefixForIpv6SourceNatOff := elbv2model.EnablePrefixForIpv6SourceNatOff + sourceNatIpv6PrefixAutoAssigned := elbv2model.SourceNatIpv6PrefixAutoAssigned + stack := coremodel.NewDefaultStack(coremodel.StackID{Namespace: "namespace", Name: "name"}) + type args struct { + resLB *elbv2model.LoadBalancer + sdkLB LoadBalancerWithTags + } + tests := []struct { + name string + args args + wantErr error + fields fields + }{ + { + name: "should set the updated sourceNAT SourceNatIpv6Prefix", + fields: fields{ + setSubnetsWithContextCall: setSubnetsWithContextCall{ + req: &elbv2sdk.SetSubnetsInput{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + EnablePrefixForIpv6SourceNat: elbv2types.EnablePrefixForIpv6SourceNatEnumOn, + SubnetMappings: []elbv2types.SubnetMapping{ + { + SubnetId: awssdk.String("subnet-A"), + SourceNatIpv6Prefix: &sourceNatIpv6PrefixAutoAssigned, + }, + }, + }, + resp: &elbv2sdk.SetSubnetsOutput{}, + }, + }, + args: args{ + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + EnablePrefixForIpv6SourceNat: enablePrefixForIpv6SourceNatOn, + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + SourceNatIpv6Prefix: &sourceNatIpv6PrefixAutoAssigned, + }, + }, + }, + }, + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + EnablePrefixForIpv6SourceNat: elbv2types.EnablePrefixForIpv6SourceNatEnumOn, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A"), SourceNatIpv6Prefixes: []string{"1024:004:003::/80"}}}, + }, + }, + }, + wantErr: nil, + }, + { + name: "should set the updated enablePrefixForIpv6SourceNat value", + fields: fields{ + setSubnetsWithContextCall: setSubnetsWithContextCall{ + req: &elbv2sdk.SetSubnetsInput{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + EnablePrefixForIpv6SourceNat: elbv2types.EnablePrefixForIpv6SourceNatEnumOff, + SubnetMappings: []elbv2types.SubnetMapping{ + { + SubnetId: awssdk.String("subnet-A"), + }, + }, + }, + resp: &elbv2sdk.SetSubnetsOutput{}, + }, + }, + args: args{ + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + }, + }, + EnablePrefixForIpv6SourceNat: enablePrefixForIpv6SourceNatOff, + }, + }, + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + EnablePrefixForIpv6SourceNat: elbv2types.EnablePrefixForIpv6SourceNatEnumOn, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A"), SourceNatIpv6Prefixes: []string{"1024:004:003::/80"}}}, + }, + }, + }, + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + elbv2Client := services.NewMockELBV2(ctrl) + m := &defaultLoadBalancerManager{ + logger: logr.New(&log.NullLogSink{}), + elbv2Client: elbv2Client, + } + + elbv2Client.EXPECT().SetSubnetsWithContext(gomock.Any(), tt.fields.setSubnetsWithContextCall.req).Return(tt.fields.setSubnetsWithContextCall.resp, tt.fields.setSubnetsWithContextCall.err) + + err := m.updateSDKLoadBalancerWithSubnetMappings(context.Background(), tt.args.resLB, tt.args.sdkLB) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/model/elbv2/load_balancer.go b/pkg/model/elbv2/load_balancer.go index 139a2b79b..4e394f5a0 100644 --- a/pkg/model/elbv2/load_balancer.go +++ b/pkg/model/elbv2/load_balancer.go @@ -9,6 +9,9 @@ import ( var _ core.Resource = &LoadBalancer{} +const ON = "on" +const OFF = "off" + // LoadBalancer represents a ELBV2 LoadBalancer. type LoadBalancer struct { core.ResourceMeta `json:"-"` @@ -88,6 +91,13 @@ const ( IPAddressTypeDualStackWithoutPublicIPV4 IPAddressType = "dualstack-without-public-ipv4" ) +type EnablePrefixForIpv6SourceNat string + +const ( + EnablePrefixForIpv6SourceNatOn EnablePrefixForIpv6SourceNat = ON + EnablePrefixForIpv6SourceNatOff EnablePrefixForIpv6SourceNat = OFF +) + type SecurityGroupsInboundRulesOnPrivateLinkStatus string const ( @@ -102,6 +112,8 @@ const ( LoadBalancerSchemeInternetFacing LoadBalancerScheme = "internet-facing" ) +const SourceNatIpv6PrefixAutoAssigned = "auto_assigned" + // Information about a subnet mapping. type SubnetMapping struct { // [Network Load Balancers] The allocation ID of the Elastic IP address for @@ -114,6 +126,9 @@ type SubnetMapping struct { // [Network Load Balancers] The IPv6 address. IPv6Address *string `json:"ipv6Address,omitempty"` + // [Network Load Balancers] the SourceNatIpv6Prefix + SourceNatIpv6Prefix *string `json:"sourceNatIpv6Prefix,omitempty"` + // The ID of the subnet. SubnetID string `json:"subnetID"` } @@ -144,6 +159,10 @@ type LoadBalancerSpec struct { // +optional IPAddressType IPAddressType `json:"ipAddressType,omitempty"` + // Tells whether Prefix for Source NAT is enabled or not. + // +optional + EnablePrefixForIpv6SourceNat EnablePrefixForIpv6SourceNat `json:"enablePrefixForIpv6SourceNat,omitempty"` + // The IDs of the public subnets. You can specify only one subnet per Availability Zone. // +optional SubnetMappings []SubnetMapping `json:"subnetMapping,omitempty"` diff --git a/pkg/networking/utils.go b/pkg/networking/utils.go index c3a7aac9c..6cec2fd3b 100644 --- a/pkg/networking/utils.go +++ b/pkg/networking/utils.go @@ -1,9 +1,13 @@ package networking import ( + "fmt" awssdk "github.com/aws/aws-sdk-go-v2/aws" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/pkg/errors" "net/netip" + elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2" + "strings" ) // ParseCIDRs will parse CIDRs in string format into parsed IPPrefix @@ -72,3 +76,83 @@ func GetSubnetAssociatedIPv6CIDRs(subnet ec2types.Subnet) ([]netip.Prefix, error } return ipv6CIDRs, nil } + +// ValidateEnablePrefixForIpv6SourceNat function returns the validation error if error exists for EnablePrefixForIpv6SourceNat annotation value +func ValidateEnablePrefixForIpv6SourceNat(EnablePrefixForIpv6SourceNat string, ipAddressType elbv2model.IPAddressType, ec2Subnets []ec2types.Subnet) error { + if EnablePrefixForIpv6SourceNat != string(elbv2model.EnablePrefixForIpv6SourceNatOn) && EnablePrefixForIpv6SourceNat != string(elbv2model.EnablePrefixForIpv6SourceNatOff) { + return errors.Errorf(fmt.Sprintf("Invalid enable-prefix-for-ipv6-source-nat value: %v. Valid values are ['on', 'off'].", EnablePrefixForIpv6SourceNat)) + } + + if EnablePrefixForIpv6SourceNat != string(elbv2model.EnablePrefixForIpv6SourceNatOn) { + return nil + } + + if ipAddressType == elbv2model.IPAddressTypeIPV4 { + return errors.Errorf(fmt.Sprintf("enable-prefix-for-ipv6-source-nat annotation is only applicable to Network Load Balancers using Dualstack IP address type.")) + } + var subnetsWithoutIPv6CIDR []string + + for _, subnet := range ec2Subnets { + subnetIPv6CIDRs, err := GetSubnetAssociatedIPv6CIDRs(subnet) + if err != nil { + return errors.Errorf(fmt.Sprintf("%v", err)) + } + if len(subnetIPv6CIDRs) < 1 { + subnetsWithoutIPv6CIDR = append(subnetsWithoutIPv6CIDR, awssdk.ToString(subnet.SubnetId)) + + } + } + if len(subnetsWithoutIPv6CIDR) > 0 { + return errors.Errorf(fmt.Sprintf("To enable prefix for source NAT, all associated subnets must have an IPv6 CIDR. Subnets without IPv6 CIDR: %v.", subnetsWithoutIPv6CIDR)) + } + + return nil +} + +// ValidateSourceNatPrefixes function returns the validation error if error exists for sourceNatIpv6Prefixes annotation value +func ValidateSourceNatPrefixes(sourceNatIpv6Prefixes []string, ipAddressType elbv2model.IPAddressType, isPrefixForIpv6SourceNatEnabled bool, ec2Subnets []ec2types.Subnet) error { + const requiredPrefixLengthForSourceNatCidr = "80" + if ipAddressType != elbv2model.IPAddressTypeDualStack { + return errors.Errorf("source-nat-ipv6-prefixes annotation can only be set for Network Load Balancers using Dualstack IP address type.") + } + if !isPrefixForIpv6SourceNatEnabled { + return errors.Errorf("source-nat-ipv6-prefixes annotation is only applicable if enable-prefix-for-ipv6-source-nat annotation is set to on.") + } + + if len(sourceNatIpv6Prefixes) != len(ec2Subnets) { + return errors.Errorf(fmt.Sprintf("Number of values in source-nat-ipv6-prefixes (%d) must match the number of subnets (%d).", len(sourceNatIpv6Prefixes), len(ec2Subnets))) + } + for idx, sourceNatIpv6Prefix := range sourceNatIpv6Prefixes { + var subnet = ec2Subnets[idx] + var sourceNatIpv6PrefixParsedList []netip.Addr + if sourceNatIpv6Prefix != elbv2model.SourceNatIpv6PrefixAutoAssigned { + subStrings := strings.Split(sourceNatIpv6Prefix, "/") + if len(subStrings) < 2 { + return errors.Errorf(fmt.Sprintf("Invalid value in source-nat-ipv6-prefixes: %v.", sourceNatIpv6Prefix)) + } + var ipAddressPart = subStrings[0] + var prefixLengthPart = subStrings[1] + if prefixLengthPart != requiredPrefixLengthForSourceNatCidr { + return errors.Errorf(fmt.Sprintf("Invalid value in source-nat-ipv6-prefixes: %v. Prefix length must be %v, but %v is specified.", sourceNatIpv6Prefix, requiredPrefixLengthForSourceNatCidr, prefixLengthPart)) + } + sourceNatIpv6PrefixNetIpParsed, err := netip.ParseAddr(ipAddressPart) + if err != nil { + return errors.Errorf(fmt.Sprintf("Invalid value in source-nat-ipv6-prefixes: %v. Value must be a valid IPv6 CIDR.", sourceNatIpv6Prefix)) + } + sourceNatIpv6PrefixParsedList = append(sourceNatIpv6PrefixParsedList, sourceNatIpv6PrefixNetIpParsed) + if !sourceNatIpv6PrefixNetIpParsed.Is6() { + return errors.Errorf(fmt.Sprintf("Invalid value in source-nat-ipv6-prefixes: %v. Value must be a valid IPv6 CIDR.", sourceNatIpv6Prefix)) + } + subnetIPv6CIDRs, err := GetSubnetAssociatedIPv6CIDRs(subnet) + if err != nil { + return errors.Errorf(fmt.Sprintf("Subnet has invalid IPv6 CIDRs: %v. Subnet must have valid IPv6 CIDRs.", subnetIPv6CIDRs)) + } + sourceNatIpv6PrefixWithinSubnet := FilterIPsWithinCIDRs(sourceNatIpv6PrefixParsedList, subnetIPv6CIDRs) + if len(sourceNatIpv6PrefixWithinSubnet) != 1 { + return errors.Errorf(fmt.Sprintf("Invalid value in source-nat-ipv6-prefixes: %v. Value must be within subnet CIDR range: %v.", sourceNatIpv6Prefix, subnetIPv6CIDRs)) + } + } + } + + return nil +} diff --git a/pkg/service/model_build_load_balancer.go b/pkg/service/model_build_load_balancer.go index 6657a4188..8142de535 100644 --- a/pkg/service/model_build_load_balancer.go +++ b/pkg/service/model_build_load_balancer.go @@ -57,6 +57,10 @@ func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, schem if err != nil { return elbv2model.LoadBalancerSpec{}, err } + enablePrefixForIpv6SourceNat, err := t.buildLoadBalancerEnablePrefixForIpv6SourceNat(ctx, ipAddressType, t.ec2Subnets) + if err != nil { + return elbv2model.LoadBalancerSpec{}, err + } lbAttributes, err := t.buildLoadBalancerAttributes(ctx) if err != nil { return elbv2model.LoadBalancerSpec{}, err @@ -69,7 +73,7 @@ func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, schem if err != nil { return elbv2model.LoadBalancerSpec{}, err } - subnetMappings, err := t.buildLoadBalancerSubnetMappings(ctx, ipAddressType, scheme, t.ec2Subnets) + subnetMappings, err := t.buildLoadBalancerSubnetMappings(ctx, ipAddressType, scheme, t.ec2Subnets, enablePrefixForIpv6SourceNat) if err != nil { return elbv2model.LoadBalancerSpec{}, err } @@ -83,14 +87,15 @@ func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, schem } spec := elbv2model.LoadBalancerSpec{ - Name: name, - Type: elbv2model.LoadBalancerTypeNetwork, - Scheme: scheme, - IPAddressType: ipAddressType, - SecurityGroups: securityGroups, - SubnetMappings: subnetMappings, - LoadBalancerAttributes: lbAttributes, - Tags: tags, + Name: name, + Type: elbv2model.LoadBalancerTypeNetwork, + Scheme: scheme, + IPAddressType: ipAddressType, + EnablePrefixForIpv6SourceNat: enablePrefixForIpv6SourceNat, + SecurityGroups: securityGroups, + SubnetMappings: subnetMappings, + LoadBalancerAttributes: lbAttributes, + Tags: tags, } if securityGroupsInboundRulesOnPrivateLink != nil { @@ -187,6 +192,20 @@ func (t *defaultModelBuildTask) buildLoadBalancerIPAddressType(_ context.Context } } +func (t *defaultModelBuildTask) buildLoadBalancerEnablePrefixForIpv6SourceNat(_ context.Context, ipAddressType elbv2model.IPAddressType, ec2Subnets []ec2types.Subnet) (elbv2model.EnablePrefixForIpv6SourceNat, error) { + rawEnablePrefixForIpv6SourceNat := "" + if exists := t.annotationParser.ParseStringAnnotation(annotations.ScvLBSuffixEnablePrefixForIpv6SourceNat, &rawEnablePrefixForIpv6SourceNat, t.service.Annotations); !exists { + return elbv2model.EnablePrefixForIpv6SourceNatOff, nil + } + + validationError := networking.ValidateEnablePrefixForIpv6SourceNat(rawEnablePrefixForIpv6SourceNat, ipAddressType, ec2Subnets) + if validationError != nil { + return "", validationError + } + + return elbv2model.EnablePrefixForIpv6SourceNat(rawEnablePrefixForIpv6SourceNat), nil +} + func (t *defaultModelBuildTask) buildSecurityGroupsInboundRulesOnPrivateLink(_ context.Context) (*elbv2model.SecurityGroupsInboundRulesOnPrivateLinkStatus, error) { var rawSecurityGroupsInboundRulesOnPrivateLink string if exists := t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixEnforceSGInboundRulesOnPrivateLinkTraffic, &rawSecurityGroupsInboundRulesOnPrivateLink, t.service.Annotations); !exists { @@ -297,7 +316,7 @@ func (t *defaultModelBuildTask) buildLoadBalancerTags(ctx context.Context) (map[ return t.buildAdditionalResourceTags(ctx) } -func (t *defaultModelBuildTask) buildLoadBalancerSubnetMappings(_ context.Context, ipAddressType elbv2model.IPAddressType, scheme elbv2model.LoadBalancerScheme, ec2Subnets []ec2types.Subnet) ([]elbv2model.SubnetMapping, error) { +func (t *defaultModelBuildTask) buildLoadBalancerSubnetMappings(_ context.Context, ipAddressType elbv2model.IPAddressType, scheme elbv2model.LoadBalancerScheme, ec2Subnets []ec2types.Subnet, enablePrefixForIpv6SourceNat elbv2model.EnablePrefixForIpv6SourceNat) ([]elbv2model.SubnetMapping, error) { var eipAllocation []string eipConfigured := t.annotationParser.ParseStringSliceAnnotation(annotations.SvcLBSuffixEIPAllocations, &eipAllocation, t.service.Annotations) if eipConfigured { @@ -355,6 +374,17 @@ func (t *defaultModelBuildTask) buildLoadBalancerSubnetMappings(_ context.Contex } } + var isPrefixForIpv6SourceNatEnabled = enablePrefixForIpv6SourceNat == elbv2model.EnablePrefixForIpv6SourceNatOn + + var sourceNatIpv6Prefixes []string + sourceNatIpv6PrefixesConfigured := t.annotationParser.ParseStringSliceAnnotation(annotations.ScvLBSuffixSourceNatIpv6Prefixes, &sourceNatIpv6Prefixes, t.service.Annotations) + if sourceNatIpv6PrefixesConfigured { + sourceNatIpv6PrefixesError := networking.ValidateSourceNatPrefixes(sourceNatIpv6Prefixes, ipAddressType, isPrefixForIpv6SourceNatEnabled, ec2Subnets) + if sourceNatIpv6PrefixesError != nil { + return nil, sourceNatIpv6PrefixesError + } + } + subnetMappings := make([]elbv2model.SubnetMapping, 0, len(ec2Subnets)) for idx, subnet := range ec2Subnets { mapping := elbv2model.SubnetMapping{ @@ -374,6 +404,11 @@ func (t *defaultModelBuildTask) buildLoadBalancerSubnetMappings(_ context.Contex } mapping.PrivateIPv4Address = awssdk.String(ipv4AddressesWithinSubnet[0].String()) } + + if isPrefixForIpv6SourceNatEnabled && sourceNatIpv6PrefixesConfigured { + mapping.SourceNatIpv6Prefix = awssdk.String(sourceNatIpv6Prefixes[idx]) + } + if ipv6AddrConfigured { subnetIPv6CIDRs, err := networking.GetSubnetAssociatedIPv6CIDRs(subnet) if err != nil { diff --git a/pkg/service/model_build_load_balancer_test.go b/pkg/service/model_build_load_balancer_test.go index 65551cce1..8b3370e74 100644 --- a/pkg/service/model_build_load_balancer_test.go +++ b/pkg/service/model_build_load_balancer_test.go @@ -5,6 +5,7 @@ import ( "errors" ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + "github.com/aws/aws-sdk-go/service/ec2" "testing" "k8s.io/apimachinery/pkg/util/sets" @@ -200,13 +201,14 @@ func Test_defaultModelBuilderTask_buildLBAttributes(t *testing.T) { func Test_defaultModelBuilderTask_buildSubnetMappings(t *testing.T) { tests := []struct { - name string - ipAddressType elbv2.IPAddressType - scheme elbv2.LoadBalancerScheme - subnets []ec2types.Subnet - want []elbv2.SubnetMapping - svc *corev1.Service - wantErr error + name string + ipAddressType elbv2.IPAddressType + enablePrefixForIpv6SourceNat elbv2.EnablePrefixForIpv6SourceNat + scheme elbv2.LoadBalancerScheme + subnets []ec2types.Subnet + want []elbv2.SubnetMapping + svc *corev1.Service + wantErr error }{ { name: "ipv4 - with auto-assigned addresses", @@ -908,6 +910,223 @@ func Test_defaultModelBuilderTask_buildSubnetMappings(t *testing.T) { }, }, }, + { + name: "dualstack - source-nat-ipv6-prefixes - should throw error if enable-prefix-for-ipv6-source-nat is not provided or is off, but still source-nat-ipv6-prefixes is provided", + ipAddressType: elbv2.IPAddressTypeDualStack, + scheme: elbv2.LoadBalancerSchemeInternal, + enablePrefixForIpv6SourceNat: elbv2.EnablePrefixForIpv6SourceNatOff, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes": "2600:1f13:837:8504::1/80", + }, + }, + }, + want: nil, + wantErr: errors.New("source-nat-ipv6-prefixes annotation is only applicable if enable-prefix-for-ipv6-source-nat annotation is set to on."), + }, + { + name: "dualstack - source-nat-ipv6-prefixes - should throw error if its not dualstack nlb, but source-nat-ipv6-prefixes is set", + ipAddressType: elbv2.IPAddressTypeIPV4, + scheme: elbv2.LoadBalancerSchemeInternal, + enablePrefixForIpv6SourceNat: elbv2.EnablePrefixForIpv6SourceNatOn, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes": "2600:1f13:837:8504::1/80", + }, + }, + }, + want: nil, + wantErr: errors.New("source-nat-ipv6-prefixes annotation can only be set for Network Load Balancers using Dualstack IP address type."), + }, + { + name: "dualstack - source-nat-ipv6-prefixes - should throw error if source-nat-ipv6-prefix is not a valid IPv6 CIDR -1", + ipAddressType: elbv2.IPAddressTypeDualStack, + scheme: elbv2.LoadBalancerSchemeInternal, + enablePrefixForIpv6SourceNat: elbv2.EnablePrefixForIpv6SourceNatOn, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes": "2600:1f13:837:8766:6766:7987:6666:1:999/80", + }, + }, + }, + want: nil, + wantErr: errors.New("Invalid value in source-nat-ipv6-prefixes: 2600:1f13:837:8766:6766:7987:6666:1:999/80. Value must be a valid IPv6 CIDR."), + }, + { + name: "dualstack - source-nat-ipv6-prefixes - should throw error if source-nat-ipv6-prefix is not a valid IPv6 CIDR -2", + ipAddressType: elbv2.IPAddressTypeDualStack, + scheme: elbv2.LoadBalancerSchemeInternal, + enablePrefixForIpv6SourceNat: elbv2.EnablePrefixForIpv6SourceNatOn, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes": "2600:1f13:837:87667::/80", + }, + }, + }, + want: nil, + wantErr: errors.New("Invalid value in source-nat-ipv6-prefixes: 2600:1f13:837:87667::/80. Value must be a valid IPv6 CIDR."), + }, + { + name: "dualstack - source-nat-ipv6-prefixes - should throw error if source-nat-ipv6-prefix is not a valid IPv6 CIDR -3", + ipAddressType: elbv2.IPAddressTypeDualStack, + scheme: elbv2.LoadBalancerSchemeInternal, + enablePrefixForIpv6SourceNat: elbv2.EnablePrefixForIpv6SourceNatOn, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes": "2600:1f13:837:8766:77:789:9:9::/80", + }, + }, + }, + want: nil, + wantErr: errors.New("Invalid value in source-nat-ipv6-prefixes: 2600:1f13:837:8766:77:789:9:9::/80. Value must be a valid IPv6 CIDR."), + }, + { + name: "dualstack - source-nat-ipv6-prefixes - should throw error if source-nat-ipv6-prefix within subnet CIDR range", + ipAddressType: elbv2.IPAddressTypeDualStack, + scheme: elbv2.LoadBalancerSchemeInternal, + enablePrefixForIpv6SourceNat: elbv2.EnablePrefixForIpv6SourceNatOn, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes": "2601:1f13:837:8500:009::/80", + }, + }, + }, + want: nil, + wantErr: errors.New("Invalid value in source-nat-ipv6-prefixes: 2601:1f13:837:8500:009::/80. Value must be within subnet CIDR range: [2600:1f13:837:8500::/64]."), + }, + { + name: "dualstack - source-nat-ipv6-prefixes - should throw error if source-nat-ipv6-prefix doesnt have allowed prefix length of 80", + ipAddressType: elbv2.IPAddressTypeDualStack, + scheme: elbv2.LoadBalancerSchemeInternal, + enablePrefixForIpv6SourceNat: elbv2.EnablePrefixForIpv6SourceNatOn, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }, + }, + svc: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "service.beta.kubernetes.io/aws-load-balancer-source-nat-ipv6-prefixes": "2600:1f13:837:8500:9::/70", + }, + }, + }, + want: nil, + wantErr: errors.New("Invalid value in source-nat-ipv6-prefixes: 2600:1f13:837:8500:9::/70. Prefix length must be 80, but 70 is specified."), + }, } for _, tt := range tests { @@ -917,7 +1136,7 @@ func Test_defaultModelBuilderTask_buildSubnetMappings(t *testing.T) { annotationParser := annotations.NewSuffixAnnotationParser("service.beta.kubernetes.io") builder := &defaultModelBuildTask{service: tt.svc, annotationParser: annotationParser} - got, err := builder.buildLoadBalancerSubnetMappings(context.Background(), tt.ipAddressType, tt.scheme, tt.subnets) + got, err := builder.buildLoadBalancerSubnetMappings(context.Background(), tt.ipAddressType, tt.scheme, tt.subnets, tt.enablePrefixForIpv6SourceNat) if tt.wantErr != nil { assert.EqualError(t, err, tt.wantErr.Error()) } else { @@ -1247,6 +1466,144 @@ func Test_defaultModelBuildTask_buildLoadBalancerIPAddressType(t *testing.T) { } } +func Test_defaultModelBuildTask_buildLoadBalancerEnablePrefixForIpv6SourceNat(t *testing.T) { + tests := []struct { + name string + subnets []ec2types.Subnet + ipAddressType elbv2.IPAddressType + service *corev1.Service + want elbv2.EnablePrefixForIpv6SourceNat + wantErr error + }{ + { + name: "should error out if EnablePrefixForIpv6SourceNat is set to on for ipv4 address Type NLB", + ipAddressType: elbv2.IPAddressTypeIPV4, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + }}, + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat": elbv2.ON}, + }, + }, + want: "", + wantErr: errors.New("enable-prefix-for-ipv6-source-nat annotation is only applicable to Network Load Balancers using Dualstack IP address type."), + }, + { + name: "should error out if EnablePrefixForIpv6SourceNat is set to on for dualstack NLB which doesnt have ipv6 Cidr subnet", + ipAddressType: elbv2.IPAddressTypeDualStack, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + }}, + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat": elbv2.ON}, + }, + }, + want: "", + wantErr: errors.New("To enable prefix for source NAT, all associated subnets must have an IPv6 CIDR. Subnets without IPv6 CIDR: [subnet-1]."), + }, + { + name: "should error out if EnablePrefixForIpv6SourceNat value is set to something else than allowed values on or off", + ipAddressType: elbv2.IPAddressTypeDualStack, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + }}, + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat": "randomValue"}, + }, + }, + want: "", + wantErr: errors.New("Invalid enable-prefix-for-ipv6-source-nat value: randomValue. Valid values are ['on', 'off']."), + }, + { + name: "should return EnablePrefixForIpv6SourceNat as on if annotation value is on", + ipAddressType: elbv2.IPAddressTypeDualStack, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }}, + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat": elbv2.ON}, + }, + }, + want: elbv2.ON, + wantErr: nil, + }, + + { + name: "should return EnablePrefixForIpv6SourceNat as off if annotation value is off", + ipAddressType: elbv2.IPAddressTypeDualStack, + subnets: []ec2types.Subnet{ + { + SubnetId: aws.String("subnet-1"), + AvailabilityZone: aws.String("us-west-2a"), + VpcId: aws.String("vpc-1"), + CidrBlock: aws.String("192.168.1.0/24"), + Ipv6CidrBlockAssociationSet: []ec2types.SubnetIpv6CidrBlockAssociation{ + { + Ipv6CidrBlock: aws.String("2600:1f13:837:8500::/64"), + Ipv6CidrBlockState: &ec2types.SubnetCidrBlockState{ + State: ec2.SubnetCidrBlockStateCodeAssociated, + }, + }, + }, + }}, + service: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"service.beta.kubernetes.io/aws-load-balancer-enable-prefix-for-ipv6-source-nat": elbv2.OFF}, + }, + }, + want: elbv2.OFF, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := annotations.NewSuffixAnnotationParser("service.beta.kubernetes.io") + builder := &defaultModelBuildTask{ + annotationParser: parser, + service: tt.service, + defaultIPAddressType: elbv2.IPAddressTypeIPV4, + } + + got, err := builder.buildLoadBalancerEnablePrefixForIpv6SourceNat(context.Background(), tt.ipAddressType, tt.subnets) + if err != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + return + } + if got != tt.want { + t.Errorf("buildLoadBalancerIPAddressType() got = %v, want %v", got, tt.want) + } + }) + } +} + func Test_defaultModelBuildTask_buildAdditionalResourceTags(t *testing.T) { type fields struct { service *corev1.Service diff --git a/pkg/service/model_builder_test.go b/pkg/service/model_builder_test.go index 998d46c1a..d0a0ca0b4 100644 --- a/pkg/service/model_builder_test.go +++ b/pkg/service/model_builder_test.go @@ -188,6 +188,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type":"network", "scheme":"internal", "securityGroupsInboundRulesOnPrivateLink":"on", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -339,6 +340,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "scheme":"internet-facing", "securityGroupsInboundRulesOnPrivateLink":"on", "ipAddressType":"dualstack", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping":[ { "subnetID":"subnet-1" @@ -525,6 +527,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type":"network", "scheme":"internal", "securityGroupsInboundRulesOnPrivateLink":"off", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -851,6 +854,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-default-nlbipsvc-33e41aa671", "type":"network", "scheme":"internet-facing", + "enablePrefixForIpv6SourceNat":"off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -1213,6 +1217,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type":"network", "scheme":"internal", "ipAddressType":"ipv4", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping":[ { "subnetID":"subnet-1" @@ -1486,6 +1491,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type":"network", "scheme":"internet-facing", "ipAddressType":"ipv4", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping":[ { "subnetID":"subnet-1" @@ -1777,6 +1783,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type":"network", "scheme":"internal", "ipAddressType":"ipv4", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping":[ { "subnetID":"subnet-1" @@ -2001,6 +2008,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "spec": { "ipAddressType": "ipv4", "name": "k8s-default-iptarget-b44ef5a42d", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping": [ { "subnetID": "subnet-1" @@ -2169,6 +2177,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "LoadBalancer": { "spec": { "ipAddressType": "ipv4", + "enablePrefixForIpv6SourceNat": "off", "name": "k8s-default-defaulti-b44ef5a42d", "subnetMapping": [ { @@ -2447,6 +2456,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type": "network", "scheme": "internal", "ipAddressType": "dualstack", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping": [ { "subnetID": "subnet-1" @@ -2597,6 +2607,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type": "network", "scheme": "internet-facing", "ipAddressType": "dualstack", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping": [ { "subnetID": "subnet-1" @@ -2800,6 +2811,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type": "network", "scheme": "internal", "ipAddressType": "ipv4", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping": [ { "subnetID": "subnet-1" @@ -2967,6 +2979,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-default-manualsg-7af4592f28", "type":"network", "scheme":"internal", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -3115,6 +3128,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-default-nlbipsvc-6b0ba8ff70", "type":"network", "scheme":"internal", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -3299,6 +3313,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type":"network", "scheme":"internet-facing", "ipAddressType":"dualstack", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping":[ { "subnetID":"subnet-1" @@ -3518,6 +3533,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-default-nlbipsvc-518cdfc227", "type":"network", "scheme":"internal", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -3837,6 +3853,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-default-nlbipsvc-33e41aa671", "type":"network", "scheme":"internet-facing", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -4168,6 +4185,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-default-instance-7ca1de7e6c", "type":"network", "scheme":"internal", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -4500,6 +4518,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-app-trafficl-2af705447d", "type":"network", "scheme":"internet-facing", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -4850,6 +4869,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name":"k8s-app-trafficl-2af705447d", "type":"network", "scheme":"internet-facing", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType":"ipv4", "subnetMapping":[ { @@ -5117,6 +5137,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type":"network", "scheme":"internal", "ipAddressType":"ipv4", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping":[ { "subnetID":"subnet-1" @@ -5354,6 +5375,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "LoadBalancer": { "spec": { "ipAddressType": "ipv4", + "enablePrefixForIpv6SourceNat": "off", "name": "k8s-default-iptarget-b44ef5a42d", "subnetMapping": [ { @@ -5512,6 +5534,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name": "k8s-default-trafficl-6652458428", "type": "network", "scheme": "internal", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType": "dualstack", "subnetMapping": [ { @@ -5697,6 +5720,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type": "network", "scheme": "internet-facing", "ipAddressType": "dualstack", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping": [ { "subnetID": "subnet-1" @@ -5880,6 +5904,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type": "network", "scheme": "internal", "ipAddressType": "ipv4", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping": [ { "subnetID": "subnet-1" @@ -6076,6 +6101,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "type": "network", "scheme": "internal", "ipAddressType": "ipv4", + "enablePrefixForIpv6SourceNat": "off", "subnetMapping": [ { "subnetID": "subnet-1" @@ -6214,6 +6240,7 @@ func Test_defaultModelBuilderTask_Build(t *testing.T) { "name": "k8s-default-manualse-6b0ba8ff70", "type": "network", "scheme": "internal", + "enablePrefixForIpv6SourceNat": "off", "ipAddressType": "ipv4", "subnetMapping": [ {