diff --git a/cloudmock/aws/mockec2/subnets.go b/cloudmock/aws/mockec2/subnets.go index 0196914e94..18a312ec1d 100644 --- a/cloudmock/aws/mockec2/subnets.go +++ b/cloudmock/aws/mockec2/subnets.go @@ -74,6 +74,18 @@ func (m *MockEC2) CreateSubnetWithId(request *ec2.CreateSubnetInput, id string) AvailabilityZone: request.AvailabilityZone, } + if request.Ipv6CidrBlock != nil { + subnet.Ipv6CidrBlockAssociationSet = []*ec2.SubnetIpv6CidrBlockAssociation{ + { + AssociationId: aws.String("subnet-cidr-assoc-ipv6-" + id), + Ipv6CidrBlock: request.Ipv6CidrBlock, + Ipv6CidrBlockState: &ec2.SubnetCidrBlockState{ + State: aws.String(ec2.SubnetCidrBlockStateCodeAssociated), + }, + }, + } + } + if m.subnets == nil { m.subnets = make(map[string]*subnetInfo) } diff --git a/cloudmock/aws/mockec2/vpcs.go b/cloudmock/aws/mockec2/vpcs.go index 04fea37445..1c8179244f 100644 --- a/cloudmock/aws/mockec2/vpcs.go +++ b/cloudmock/aws/mockec2/vpcs.go @@ -243,18 +243,33 @@ func (m *MockEC2) AssociateVpcCidrBlock(request *ec2.AssociateVpcCidrBlockInput) if !ok { return nil, fmt.Errorf("VPC %q not found", id) } - association := &ec2.VpcCidrBlockAssociation{ - CidrBlock: request.CidrBlock, - AssociationId: aws.String(fmt.Sprintf("%v-%v", id, len(vpc.main.CidrBlockAssociationSet))), - CidrBlockState: &ec2.VpcCidrBlockState{ - State: aws.String(ec2.VpcCidrBlockStateCodeAssociated), - }, + var ipv4association *ec2.VpcCidrBlockAssociation + var ipv6association *ec2.VpcIpv6CidrBlockAssociation + if aws.BoolValue(request.AmazonProvidedIpv6CidrBlock) { + ipv6association = &ec2.VpcIpv6CidrBlockAssociation{ + Ipv6Pool: aws.String("Amazon"), + Ipv6CidrBlock: aws.String("2001:db8::/56"), + AssociationId: aws.String(fmt.Sprintf("%v-%v", id, len(vpc.main.Ipv6CidrBlockAssociationSet))), + Ipv6CidrBlockState: &ec2.VpcCidrBlockState{ + State: aws.String(ec2.VpcCidrBlockStateCodeAssociated), + }, + } + vpc.main.Ipv6CidrBlockAssociationSet = append(vpc.main.Ipv6CidrBlockAssociationSet, ipv6association) + } else { + ipv4association = &ec2.VpcCidrBlockAssociation{ + CidrBlock: request.CidrBlock, + AssociationId: aws.String(fmt.Sprintf("%v-%v", id, len(vpc.main.CidrBlockAssociationSet))), + CidrBlockState: &ec2.VpcCidrBlockState{ + State: aws.String(ec2.VpcCidrBlockStateCodeAssociated), + }, + } + vpc.main.CidrBlockAssociationSet = append(vpc.main.CidrBlockAssociationSet, ipv4association) } - vpc.main.CidrBlockAssociationSet = append(vpc.main.CidrBlockAssociationSet, association) return &ec2.AssociateVpcCidrBlockOutput{ - CidrBlockAssociation: association, - VpcId: request.VpcId, + CidrBlockAssociation: ipv4association, + Ipv6CidrBlockAssociation: ipv6association, + VpcId: request.VpcId, }, nil } diff --git a/k8s/crds/kops.k8s.io_clusters.yaml b/k8s/crds/kops.k8s.io_clusters.yaml index f42f78a89f..5caf6f5523 100644 --- a/k8s/crds/kops.k8s.io_clusters.yaml +++ b/k8s/crds/kops.k8s.io_clusters.yaml @@ -4103,6 +4103,7 @@ spec: items: properties: cidr: + description: CIDR is the IPv4 CIDR block assigned to the subnet. type: string egress: description: Egress defines the method of traffic egress for @@ -4112,6 +4113,10 @@ spec: description: ProviderID is the cloud provider id for the objects associated with the zone (the subnet on AWS) type: string + ipv6CIDR: + description: IPv6CIDR is the IPv6 CIDR block assigned to the + subnet. + type: string name: type: string publicIP: diff --git a/pkg/apis/kops/cluster.go b/pkg/apis/kops/cluster.go index 602dd6044a..6b11e91952 100644 --- a/pkg/apis/kops/cluster.go +++ b/pkg/apis/kops/cluster.go @@ -621,8 +621,10 @@ const ( type ClusterSubnetSpec struct { // Name is the name of the subnet Name string `json:"name,omitempty"` - // CIDR is the network cidr of the subnet + // CIDR is the IPv4 CIDR block assigned to the subnet. CIDR string `json:"cidr,omitempty"` + // IPv6CIDR is the IPv6 CIDR block assigned to the subnet. + IPv6CIDR string `json:"ipv6CIDR,omitempty"` // Zone is the zone the subnet is in, set for subnets that are zonally scoped Zone string `json:"zone,omitempty"` // Region is the region the subnet is in, set for subnets that are regionally scoped diff --git a/pkg/apis/kops/v1alpha2/cluster.go b/pkg/apis/kops/v1alpha2/cluster.go index 7a62d236c8..025736c6c0 100644 --- a/pkg/apis/kops/v1alpha2/cluster.go +++ b/pkg/apis/kops/v1alpha2/cluster.go @@ -606,7 +606,10 @@ type ClusterSubnetSpec struct { // Region is the region the subnet is in, set for subnets that are regionally scoped Region string `json:"region,omitempty"` + // CIDR is the IPv4 CIDR block assigned to the subnet. CIDR string `json:"cidr,omitempty"` + // IPv6CIDR is the IPv6 CIDR block assigned to the subnet. + IPv6CIDR string `json:"ipv6CIDR,omitempty"` // ProviderID is the cloud provider id for the objects associated with the zone (the subnet on AWS) ProviderID string `json:"id,omitempty"` diff --git a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go index c12fdb8012..33119dfb7f 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go @@ -2983,6 +2983,7 @@ func autoConvert_v1alpha2_ClusterSubnetSpec_To_kops_ClusterSubnetSpec(in *Cluste out.Zone = in.Zone out.Region = in.Region out.CIDR = in.CIDR + out.IPv6CIDR = in.IPv6CIDR out.ProviderID = in.ProviderID out.Egress = in.Egress out.Type = kops.SubnetType(in.Type) @@ -2998,6 +2999,7 @@ func Convert_v1alpha2_ClusterSubnetSpec_To_kops_ClusterSubnetSpec(in *ClusterSub func autoConvert_kops_ClusterSubnetSpec_To_v1alpha2_ClusterSubnetSpec(in *kops.ClusterSubnetSpec, out *ClusterSubnetSpec, s conversion.Scope) error { out.Name = in.Name out.CIDR = in.CIDR + out.IPv6CIDR = in.IPv6CIDR out.Zone = in.Zone out.Region = in.Region out.ProviderID = in.ProviderID diff --git a/pkg/apis/kops/validation/BUILD.bazel b/pkg/apis/kops/validation/BUILD.bazel index 10f0cb3975..90f9b0dbe5 100644 --- a/pkg/apis/kops/validation/BUILD.bazel +++ b/pkg/apis/kops/validation/BUILD.bazel @@ -24,6 +24,7 @@ go_library( "//pkg/util/subnet:go_default_library", "//upup/pkg/fi:go_default_library", "//upup/pkg/fi/cloudup/awsup:go_default_library", + "//upup/pkg/fi/utils:go_default_library", "//util/pkg/vfs:go_default_library", "//vendor/github.com/aws/aws-sdk-go/aws/arn:go_default_library", "//vendor/github.com/aws/aws-sdk-go/service/ec2:go_default_library", diff --git a/pkg/apis/kops/validation/validation.go b/pkg/apis/kops/validation/validation.go index df8e1c370b..4e4c0da48d 100644 --- a/pkg/apis/kops/validation/validation.go +++ b/pkg/apis/kops/validation/validation.go @@ -39,6 +39,7 @@ import ( "k8s.io/kops/pkg/model/components" "k8s.io/kops/pkg/model/iam" "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/utils" ) func newValidateCluster(cluster *kops.Cluster) field.ErrorList { @@ -78,7 +79,7 @@ func newValidateCluster(cluster *kops.Cluster) field.ErrorList { func validateClusterSpec(spec *kops.ClusterSpec, c *kops.Cluster, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - allErrs = append(allErrs, validateSubnets(spec.Subnets, fieldPath.Child("subnets"))...) + allErrs = append(allErrs, validateSubnets(spec, fieldPath.Child("subnets"))...) // SSHAccess for i, cidr := range spec.SSHAccess { @@ -312,7 +313,11 @@ func validateCIDR(cidr string, fieldPath *field.Path) field.ErrorList { if !strings.Contains(cidr, "/") { ip := net.ParseIP(cidr) if ip != nil { - detail += fmt.Sprintf(" (did you mean \"%s/32\")", cidr) + if ip.To4() != nil && !strings.Contains(cidr, ":") { + detail += fmt.Sprintf(" (did you mean \"%s/32\")", cidr) + } else { + detail += fmt.Sprintf(" (did you mean \"%s/64\")", cidr) + } } } allErrs = append(allErrs, field.Invalid(fieldPath, cidr, detail)) @@ -324,6 +329,16 @@ func validateCIDR(cidr string, fieldPath *field.Path) field.ErrorList { return allErrs } +func validateIPv6CIDR(cidr string, fieldPath *field.Path) field.ErrorList { + allErrs := validateCIDR(cidr, fieldPath) + + if !utils.IsIPv6CIDR(cidr) { + allErrs = append(allErrs, field.Invalid(fieldPath, cidr, "Network is not an IPv6 CIDR")) + } + + return allErrs +} + func validateTopology(topology *kops.TopologySpec, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -360,9 +375,11 @@ func validateTopology(topology *kops.TopologySpec, fieldPath *field.Path) field. return allErrs } -func validateSubnets(subnets []kops.ClusterSubnetSpec, fieldPath *field.Path) field.ErrorList { +func validateSubnets(cluster *kops.ClusterSpec, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} + subnets := cluster.Subnets + // cannot be empty if len(subnets) == 0 { allErrs = append(allErrs, field.Required(fieldPath, "")) @@ -395,6 +412,14 @@ func validateSubnets(subnets []kops.ClusterSubnetSpec, fieldPath *field.Path) fi } } + if kops.CloudProviderID(cluster.CloudProvider) != kops.CloudProviderAWS { + for i := range subnets { + if subnets[i].IPv6CIDR != "" { + allErrs = append(allErrs, field.Forbidden(fieldPath.Child("ipv6CIDR"), "ipv6CIDR can only be specified for AWS")) + } + } + } + return allErrs } @@ -410,6 +435,10 @@ func validateSubnet(subnet *kops.ClusterSubnetSpec, fieldPath *field.Path) field if subnet.CIDR != "" { allErrs = append(allErrs, validateCIDR(subnet.CIDR, fieldPath.Child("cidr"))...) } + // IPv6CIDR + if subnet.IPv6CIDR != "" { + allErrs = append(allErrs, validateIPv6CIDR(subnet.IPv6CIDR, fieldPath.Child("ipv6CIDR"))...) + } if subnet.Egress != "" { egressType := strings.Split(subnet.Egress, "-")[0] diff --git a/pkg/apis/kops/validation/validation_test.go b/pkg/apis/kops/validation/validation_test.go index 0596859ab9..79b03f7d44 100644 --- a/pkg/apis/kops/validation/validation_test.go +++ b/pkg/apis/kops/validation/validation_test.go @@ -155,9 +155,42 @@ func TestValidateSubnets(t *testing.T) { }, ExpectedErrors: []string{"Invalid value::subnets[0].cidr"}, }, + { + Input: []kops.ClusterSubnetSpec{ + {Name: "a", IPv6CIDR: "2001:db8::/56"}, + }, + }, + { + Input: []kops.ClusterSubnetSpec{ + {Name: "a", IPv6CIDR: "10.0.0.0/8"}, + }, + ExpectedErrors: []string{"Invalid value::subnets[0].ipv6CIDR"}, + }, + { + Input: []kops.ClusterSubnetSpec{ + {Name: "a", IPv6CIDR: "::ffff:10.128.0.0"}, + }, + ExpectedErrors: []string{"Invalid value::subnets[0].ipv6CIDR"}, + }, + { + Input: []kops.ClusterSubnetSpec{ + {Name: "a", IPv6CIDR: "::ffff:10.128.0.0/8"}, + }, + ExpectedErrors: []string{"Invalid value::subnets[0].ipv6CIDR"}, + }, + { + Input: []kops.ClusterSubnetSpec{ + {Name: "a", CIDR: "::ffff:10.128.0.0/8"}, + }, + ExpectedErrors: []string{"Invalid value::subnets[0].cidr"}, + }, } for _, g := range grid { - errs := validateSubnets(g.Input, field.NewPath("subnets")) + cluster := &kops.ClusterSpec{ + CloudProvider: "aws", + Subnets: g.Input, + } + errs := validateSubnets(cluster, field.NewPath("subnets")) testErrors(t, g.Input, errs, g.ExpectedErrors) } diff --git a/pkg/model/awsmodel/BUILD.bazel b/pkg/model/awsmodel/BUILD.bazel index 81c0a55cd8..0ff644c926 100644 --- a/pkg/model/awsmodel/BUILD.bazel +++ b/pkg/model/awsmodel/BUILD.bazel @@ -31,6 +31,7 @@ go_library( "//upup/pkg/fi/cloudup/awstasks:go_default_library", "//upup/pkg/fi/cloudup/awsup:go_default_library", "//upup/pkg/fi/cloudup/spotinsttasks:go_default_library", + "//upup/pkg/fi/utils:go_default_library", "//vendor/github.com/aws/aws-sdk-go/aws:go_default_library", "//vendor/github.com/aws/aws-sdk-go/aws/endpoints:go_default_library", "//vendor/github.com/aws/aws-sdk-go/service/ec2:go_default_library", diff --git a/pkg/model/awsmodel/api_loadbalancer.go b/pkg/model/awsmodel/api_loadbalancer.go index 8307b0c3da..8c92459124 100644 --- a/pkg/model/awsmodel/api_loadbalancer.go +++ b/pkg/model/awsmodel/api_loadbalancer.go @@ -27,6 +27,7 @@ import ( "k8s.io/kops/pkg/dns" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" + "k8s.io/kops/upup/pkg/fi/utils" ) // LoadBalancerDefaultIdleTimeout is the default idle time for the ELB @@ -301,40 +302,70 @@ func (b *APILoadBalancerBuilder) Build(c *fi.ModelBuilderContext) error { // Allow traffic from ELB to egress freely if b.APILoadBalancerClass() == kops.LoadBalancerClassClassic { - t := &awstasks.SecurityGroupRule{ - Name: fi.String("api-elb-egress"), - Lifecycle: b.SecurityLifecycle, - CIDR: fi.String("0.0.0.0/0"), - Egress: fi.Bool(true), - SecurityGroup: lbSG, + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("ipv4-api-elb-egress"), + Lifecycle: b.SecurityLifecycle, + CIDR: fi.String("0.0.0.0/0"), + Egress: fi.Bool(true), + SecurityGroup: lbSG, + } + AddDirectionalGroupRule(c, t) + } + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("ipv6-api-elb-egress"), + Lifecycle: b.SecurityLifecycle, + IPv6CIDR: fi.String("::/0"), + Egress: fi.Bool(true), + SecurityGroup: lbSG, + } + AddDirectionalGroupRule(c, t) } - AddDirectionalGroupRule(c, t) } // Allow traffic into the ELB from KubernetesAPIAccess CIDRs if b.APILoadBalancerClass() == kops.LoadBalancerClassClassic { for _, cidr := range b.Cluster.Spec.KubernetesAPIAccess { - t := &awstasks.SecurityGroupRule{ - Name: fi.String("https-api-elb-" + cidr), - Lifecycle: b.SecurityLifecycle, - CIDR: fi.String(cidr), - FromPort: fi.Int64(443), - Protocol: fi.String("tcp"), - SecurityGroup: lbSG, - ToPort: fi.Int64(443), + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("https-api-elb-" + cidr), + Lifecycle: b.SecurityLifecycle, + FromPort: fi.Int64(443), + Protocol: fi.String("tcp"), + SecurityGroup: lbSG, + ToPort: fi.Int64(443), + } + if utils.IsIPv6CIDR(cidr) { + t.IPv6CIDR = fi.String(cidr) + } else { + t.CIDR = fi.String(cidr) + } + AddDirectionalGroupRule(c, t) } - AddDirectionalGroupRule(c, t) // Allow ICMP traffic required for PMTU discovery - c.AddTask(&awstasks.SecurityGroupRule{ - Name: fi.String("icmp-pmtu-api-elb-" + cidr), - Lifecycle: b.SecurityLifecycle, - CIDR: fi.String(cidr), - FromPort: fi.Int64(3), - Protocol: fi.String("icmp"), - SecurityGroup: lbSG, - ToPort: fi.Int64(4), - }) + if utils.IsIPv6CIDR(cidr) { + c.AddTask(&awstasks.SecurityGroupRule{ + Name: fi.String("icmpv6-pmtu-api-elb-" + cidr), + Lifecycle: b.SecurityLifecycle, + IPv6CIDR: fi.String(cidr), + FromPort: fi.Int64(-1), + Protocol: fi.String("icmpv6"), + SecurityGroup: lbSG, + ToPort: fi.Int64(-1), + }) + } else { + c.AddTask(&awstasks.SecurityGroupRule{ + Name: fi.String("icmp-pmtu-api-elb-" + cidr), + Lifecycle: b.SecurityLifecycle, + CIDR: fi.String(cidr), + FromPort: fi.Int64(3), + Protocol: fi.String("icmp"), + SecurityGroup: lbSG, + ToPort: fi.Int64(4), + }) + } } } @@ -347,39 +378,62 @@ func (b *APILoadBalancerBuilder) Build(c *fi.ModelBuilderContext) error { for _, cidr := range b.Cluster.Spec.KubernetesAPIAccess { for _, masterGroup := range masterGroups { - t := &awstasks.SecurityGroupRule{ - Name: fi.String(fmt.Sprintf("https-api-elb-%s", cidr)), - Lifecycle: b.SecurityLifecycle, - CIDR: fi.String(cidr), - FromPort: fi.Int64(443), - Protocol: fi.String("tcp"), - SecurityGroup: masterGroup.Task, - ToPort: fi.Int64(443), + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String(fmt.Sprintf("https-api-elb-%s", cidr)), + Lifecycle: b.SecurityLifecycle, + FromPort: fi.Int64(443), + Protocol: fi.String("tcp"), + SecurityGroup: masterGroup.Task, + ToPort: fi.Int64(443), + } + if utils.IsIPv6CIDR(cidr) { + t.IPv6CIDR = fi.String(cidr) + } else { + t.CIDR = fi.String(cidr) + } + AddDirectionalGroupRule(c, t) } - AddDirectionalGroupRule(c, t) // Allow ICMP traffic required for PMTU discovery - c.AddTask(&awstasks.SecurityGroupRule{ - Name: fi.String("icmp-pmtu-api-elb-" + cidr), - Lifecycle: b.SecurityLifecycle, - CIDR: fi.String(cidr), - FromPort: fi.Int64(3), - Protocol: fi.String("icmp"), - SecurityGroup: masterGroup.Task, - ToPort: fi.Int64(4), - }) + if utils.IsIPv6CIDR(cidr) { + c.AddTask(&awstasks.SecurityGroupRule{ + Name: fi.String("icmpv6-pmtu-api-elb-" + cidr), + Lifecycle: b.SecurityLifecycle, + IPv6CIDR: fi.String(cidr), + FromPort: fi.Int64(-1), + Protocol: fi.String("icmpv6"), + SecurityGroup: masterGroup.Task, + ToPort: fi.Int64(-1), + }) + } else { + c.AddTask(&awstasks.SecurityGroupRule{ + Name: fi.String("icmp-pmtu-api-elb-" + cidr), + Lifecycle: b.SecurityLifecycle, + CIDR: fi.String(cidr), + FromPort: fi.Int64(3), + Protocol: fi.String("icmp"), + SecurityGroup: masterGroup.Task, + ToPort: fi.Int64(4), + }) + } if b.Cluster.Spec.API != nil && b.Cluster.Spec.API.LoadBalancer != nil && b.Cluster.Spec.API.LoadBalancer.SSLCertificate != "" { // Allow access to masters on secondary port through NLB - c.AddTask(&awstasks.SecurityGroupRule{ + t := &awstasks.SecurityGroupRule{ Name: fi.String(fmt.Sprintf("tcp-api-%s", cidr)), Lifecycle: b.SecurityLifecycle, - CIDR: fi.String(cidr), FromPort: fi.Int64(8443), Protocol: fi.String("tcp"), SecurityGroup: masterGroup.Task, ToPort: fi.Int64(8443), - }) + } + if utils.IsIPv6CIDR(cidr) { + t.IPv6CIDR = fi.String(cidr) + } else { + t.CIDR = fi.String(cidr) + } + c.AddTask(t) } } } diff --git a/pkg/model/awsmodel/autoscalinggroup.go b/pkg/model/awsmodel/autoscalinggroup.go index eed9647827..4411d6fa64 100644 --- a/pkg/model/awsmodel/autoscalinggroup.go +++ b/pkg/model/awsmodel/autoscalinggroup.go @@ -183,6 +183,7 @@ func (b *AutoscalingGroupModelBuilder) buildLaunchTemplateTask(c *fi.ModelBuilde InstanceInterruptionBehavior: ig.Spec.InstanceInterruptionBehavior, InstanceMonitoring: ig.Spec.DetailedInstanceMonitoring, InstanceType: fi.String(strings.Split(ig.Spec.MachineType, ",")[0]), + IPv6AddressCount: fi.Int64(0), RootVolumeIops: fi.Int64(int64(fi.Int32Value(ig.Spec.RootVolumeIops))), RootVolumeOptimization: ig.Spec.RootVolumeOptimization, RootVolumeSize: fi.Int64(int64(rootVolumeSize)), @@ -211,6 +212,14 @@ func (b *AutoscalingGroupModelBuilder) buildLaunchTemplateTask(c *fi.ModelBuilde case kops.SubnetTypePrivate: lt.AssociatePublicIP = fi.Bool(false) } + + // @step: add an IPv6 address + for _, subnet := range subnets { + if subnet.IPv6CIDR != "" { + lt.IPv6AddressCount = fi.Int64(1) + break + } + } } // @step: add any additional block devices diff --git a/pkg/model/awsmodel/bastion.go b/pkg/model/awsmodel/bastion.go index d8cb496aed..7e08bce83e 100644 --- a/pkg/model/awsmodel/bastion.go +++ b/pkg/model/awsmodel/bastion.go @@ -23,6 +23,7 @@ import ( "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" + "k8s.io/kops/upup/pkg/fi/utils" ) const ( @@ -77,14 +78,26 @@ func (b *BastionModelBuilder) Build(c *fi.ModelBuilderContext) error { for _, src := range bastionGroups { // Allow traffic from bastion instances to egress freely - t := &awstasks.SecurityGroupRule{ - Name: fi.String("bastion-egress" + src.Suffix), - Lifecycle: b.SecurityLifecycle, - SecurityGroup: src.Task, - Egress: fi.Bool(true), - CIDR: fi.String("0.0.0.0/0"), + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("ipv4-bastion-egress" + src.Suffix), + Lifecycle: b.SecurityLifecycle, + SecurityGroup: src.Task, + Egress: fi.Bool(true), + CIDR: fi.String("0.0.0.0/0"), + } + AddDirectionalGroupRule(c, t) + } + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("ipv6-bastion-egress" + src.Suffix), + Lifecycle: b.SecurityLifecycle, + SecurityGroup: src.Task, + Egress: fi.Bool(true), + IPv6CIDR: fi.String("::/0"), + } + AddDirectionalGroupRule(c, t) } - AddDirectionalGroupRule(c, t) } // Allow incoming SSH traffic to bastions, through the ELB @@ -137,9 +150,8 @@ func (b *BastionModelBuilder) Build(c *fi.ModelBuilderContext) error { // Create security group for bastion ELB { t := &awstasks.SecurityGroup{ - Name: fi.String(b.ELBSecurityGroupName(BastionELBSecurityGroupPrefix)), - Lifecycle: b.SecurityLifecycle, - + Name: fi.String(b.ELBSecurityGroupName(BastionELBSecurityGroupPrefix)), + Lifecycle: b.SecurityLifecycle, VPC: b.LinkToVPC(), Description: fi.String("Security group for bastion ELB"), RemoveExtraRules: []string{"port=22"}, @@ -151,29 +163,41 @@ func (b *BastionModelBuilder) Build(c *fi.ModelBuilderContext) error { // Allow traffic from ELB to egress freely { t := &awstasks.SecurityGroupRule{ - Name: fi.String("bastion-elb-egress"), - Lifecycle: b.SecurityLifecycle, - + Name: fi.String("ipv4-bastion-elb-egress"), + Lifecycle: b.SecurityLifecycle, SecurityGroup: b.LinkToELBSecurityGroup(BastionELBSecurityGroupPrefix), Egress: fi.Bool(true), CIDR: fi.String("0.0.0.0/0"), } - + AddDirectionalGroupRule(c, t) + } + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("ipv6-bastion-elb-egress"), + Lifecycle: b.SecurityLifecycle, + SecurityGroup: b.LinkToELBSecurityGroup(BastionELBSecurityGroupPrefix), + Egress: fi.Bool(true), + IPv6CIDR: fi.String("::/0"), + } AddDirectionalGroupRule(c, t) } // Allow external access to ELB for _, sshAccess := range b.Cluster.Spec.SSHAccess { t := &awstasks.SecurityGroupRule{ - Name: fi.String("ssh-external-to-bastion-elb-" + sshAccess), - Lifecycle: b.SecurityLifecycle, - + Name: fi.String("ssh-external-to-bastion-elb-" + sshAccess), + Lifecycle: b.SecurityLifecycle, SecurityGroup: b.LinkToELBSecurityGroup(BastionELBSecurityGroupPrefix), Protocol: fi.String("tcp"), FromPort: fi.Int64(22), ToPort: fi.Int64(22), CIDR: fi.String(sshAccess), } + if utils.IsIPv6CIDR(sshAccess) { + t.IPv6CIDR = fi.String(sshAccess) + } else { + t.CIDR = fi.String(sshAccess) + } AddDirectionalGroupRule(c, t) } diff --git a/pkg/model/awsmodel/external_access.go b/pkg/model/awsmodel/external_access.go index cca11aceb6..4144f90dfa 100644 --- a/pkg/model/awsmodel/external_access.go +++ b/pkg/model/awsmodel/external_access.go @@ -23,6 +23,7 @@ import ( "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" + "k8s.io/kops/upup/pkg/fi/utils" ) // ExternalAccessModelBuilder configures security group rules for external access @@ -69,7 +70,11 @@ func (b *ExternalAccessModelBuilder) Build(c *fi.ModelBuilderContext) error { Protocol: fi.String("tcp"), FromPort: fi.Int64(22), ToPort: fi.Int64(22), - CIDR: fi.String(sshAccess), + } + if utils.IsIPv6CIDR(sshAccess) { + t.IPv6CIDR = fi.String(sshAccess) + } else { + t.CIDR = fi.String(sshAccess) } AddDirectionalGroupRule(c, t) } @@ -83,7 +88,11 @@ func (b *ExternalAccessModelBuilder) Build(c *fi.ModelBuilderContext) error { Protocol: fi.String("tcp"), FromPort: fi.Int64(22), ToPort: fi.Int64(22), - CIDR: fi.String(sshAccess), + } + if utils.IsIPv6CIDR(sshAccess) { + t.IPv6CIDR = fi.String(sshAccess) + } else { + t.CIDR = fi.String(sshAccess) } AddDirectionalGroupRule(c, t) } @@ -98,27 +107,38 @@ func (b *ExternalAccessModelBuilder) Build(c *fi.ModelBuilderContext) error { for _, nodeGroup := range nodeGroups { suffix := nodeGroup.Suffix - t1 := &awstasks.SecurityGroupRule{ - Name: fi.String(fmt.Sprintf("nodeport-tcp-external-to-node-%s%s", nodePortAccess, suffix)), - Lifecycle: b.Lifecycle, - SecurityGroup: nodeGroup.Task, - Protocol: fi.String("tcp"), - FromPort: fi.Int64(int64(nodePortRange.Base)), - ToPort: fi.Int64(int64(nodePortRange.Base + nodePortRange.Size - 1)), - CIDR: fi.String(nodePortAccess), + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String(fmt.Sprintf("nodeport-tcp-external-to-node-%s%s", nodePortAccess, suffix)), + Lifecycle: b.Lifecycle, + SecurityGroup: nodeGroup.Task, + Protocol: fi.String("tcp"), + FromPort: fi.Int64(int64(nodePortRange.Base)), + ToPort: fi.Int64(int64(nodePortRange.Base + nodePortRange.Size - 1)), + } + if utils.IsIPv6CIDR(nodePortAccess) { + t.IPv6CIDR = fi.String(nodePortAccess) + } else { + t.CIDR = fi.String(nodePortAccess) + } + c.AddTask(t) } - c.AddTask(t1) - - t2 := &awstasks.SecurityGroupRule{ - Name: fi.String(fmt.Sprintf("nodeport-udp-external-to-node-%s%s", nodePortAccess, suffix)), - Lifecycle: b.Lifecycle, - SecurityGroup: nodeGroup.Task, - Protocol: fi.String("udp"), - FromPort: fi.Int64(int64(nodePortRange.Base)), - ToPort: fi.Int64(int64(nodePortRange.Base + nodePortRange.Size - 1)), - CIDR: fi.String(nodePortAccess), + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String(fmt.Sprintf("nodeport-udp-external-to-node-%s%s", nodePortAccess, suffix)), + Lifecycle: b.Lifecycle, + SecurityGroup: nodeGroup.Task, + Protocol: fi.String("udp"), + FromPort: fi.Int64(int64(nodePortRange.Base)), + ToPort: fi.Int64(int64(nodePortRange.Base + nodePortRange.Size - 1)), + } + if utils.IsIPv6CIDR(nodePortAccess) { + t.IPv6CIDR = fi.String(nodePortAccess) + } else { + t.CIDR = fi.String(nodePortAccess) + } + c.AddTask(t) } - c.AddTask(t2) } } @@ -138,7 +158,11 @@ func (b *ExternalAccessModelBuilder) Build(c *fi.ModelBuilderContext) error { Protocol: fi.String("tcp"), FromPort: fi.Int64(443), ToPort: fi.Int64(443), - CIDR: fi.String(apiAccess), + } + if utils.IsIPv6CIDR(apiAccess) { + t.IPv6CIDR = fi.String(apiAccess) + } else { + t.CIDR = fi.String(apiAccess) } AddDirectionalGroupRule(c, t) } diff --git a/pkg/model/awsmodel/firewall.go b/pkg/model/awsmodel/firewall.go index 4bd6a7972c..cbfd1d67c1 100644 --- a/pkg/model/awsmodel/firewall.go +++ b/pkg/model/awsmodel/firewall.go @@ -78,7 +78,7 @@ func (b *FirewallModelBuilder) buildNodeRules(c *fi.ModelBuilderContext) ([]Secu // Allow full egress { t := &awstasks.SecurityGroupRule{ - Name: fi.String("node-egress" + src.Suffix), + Name: fi.String("ipv4-node-egress" + src.Suffix), Lifecycle: b.Lifecycle, SecurityGroup: src.Task, Egress: fi.Bool(true), @@ -86,6 +86,16 @@ func (b *FirewallModelBuilder) buildNodeRules(c *fi.ModelBuilderContext) ([]Secu } AddDirectionalGroupRule(c, t) } + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("ipv6-node-egress" + src.Suffix), + Lifecycle: b.Lifecycle, + SecurityGroup: src.Task, + Egress: fi.Bool(true), + IPv6CIDR: fi.String("::/0"), + } + AddDirectionalGroupRule(c, t) + } // Nodes can talk to nodes for _, dest := range nodeGroups { @@ -238,7 +248,7 @@ func (b *FirewallModelBuilder) buildMasterRules(c *fi.ModelBuilderContext, nodeG // Allow full egress { t := &awstasks.SecurityGroupRule{ - Name: fi.String("master-egress" + src.Suffix), + Name: fi.String("ipv4-master-egress" + src.Suffix), Lifecycle: b.Lifecycle, SecurityGroup: src.Task, Egress: fi.Bool(true), @@ -246,6 +256,16 @@ func (b *FirewallModelBuilder) buildMasterRules(c *fi.ModelBuilderContext, nodeG } AddDirectionalGroupRule(c, t) } + { + t := &awstasks.SecurityGroupRule{ + Name: fi.String("ipv6-master-egress" + src.Suffix), + Lifecycle: b.Lifecycle, + SecurityGroup: src.Task, + Egress: fi.Bool(true), + IPv6CIDR: fi.String("::/0"), + } + AddDirectionalGroupRule(c, t) + } // Masters can talk to masters for _, dest := range masterGroups { @@ -420,8 +440,10 @@ func generateName(o *awstasks.SecurityGroupRule) string { var target, dst, src, direction, proto string if o.SourceGroup != nil { target = fi.StringValue(o.SourceGroup.Name) - } else if o.CIDR != nil && fi.StringValue(o.CIDR) != "" { + } else if o.CIDR != nil { target = fi.StringValue(o.CIDR) + } else if o.IPv6CIDR != nil { + target = fi.StringValue(o.IPv6CIDR) } else { target = "0.0.0.0/0" } diff --git a/pkg/model/awsmodel/network.go b/pkg/model/awsmodel/network.go index 47477b17df..0aa6ffb77a 100644 --- a/pkg/model/awsmodel/network.go +++ b/pkg/model/awsmodel/network.go @@ -71,8 +71,11 @@ func (b *NetworkModelBuilder) Build(c *fi.ModelBuilderContext) error { } else { // In theory we don't need to enable it for >= 1.5, // but seems safer to stick with existing behaviour - t.EnableDNSHostnames = fi.Bool(true) + + // Used only for Terraform rendering. + // Direct and CloudFormation rendering is handled via the VPCAmazonIPv6CIDRBlock task + t.AmazonIPv6 = fi.Bool(true) t.AssociateExtraCIDRBlocks = b.Cluster.Spec.AdditionalNetworkCIDRs } @@ -88,12 +91,21 @@ func (b *NetworkModelBuilder) Build(c *fi.ModelBuilderContext) error { } if !sharedVPC { + // Associate an Amazon-provided IPv6 CIDR block with the VPC + c.AddTask(&awstasks.VPCAmazonIPv6CIDRBlock{ + Name: fi.String("AmazonIPv6"), + Lifecycle: b.Lifecycle, + VPC: b.LinkToVPC(), + Shared: fi.Bool(false), + }) + + // Associate additional CIDR blocks with the VPC for _, cidr := range b.Cluster.Spec.AdditionalNetworkCIDRs { c.AddTask(&awstasks.VPCCIDRBlock{ Name: fi.String(cidr), Lifecycle: b.Lifecycle, VPC: b.LinkToVPC(), - Shared: fi.Bool(sharedVPC), + Shared: fi.Bool(false), CIDRBlock: fi.String(cidr), }) } @@ -184,6 +196,13 @@ func (b *NetworkModelBuilder) Build(c *fi.ModelBuilderContext) error { RouteTable: publicRouteTable, InternetGateway: igw, }) + c.AddTask(&awstasks.Route{ + Name: fi.String("::/0"), + Lifecycle: b.Lifecycle, + IPv6CIDR: fi.String("::/0"), + RouteTable: publicRouteTable, + InternetGateway: igw, + }) } } @@ -232,6 +251,9 @@ func (b *NetworkModelBuilder) Build(c *fi.ModelBuilderContext) error { Tags: tags, } + if subnetSpec.IPv6CIDR != "" { + subnet.IPv6CIDR = fi.String(subnetSpec.IPv6CIDR) + } if subnetSpec.ProviderID != "" { subnet.ID = fi.String(subnetSpec.ProviderID) } diff --git a/tests/integration/update_cluster/minimal-ipv6/in-v1alpha2.yaml b/tests/integration/update_cluster/minimal-ipv6/in-v1alpha2.yaml index 33e648d665..db81a4649e 100644 --- a/tests/integration/update_cluster/minimal-ipv6/in-v1alpha2.yaml +++ b/tests/integration/update_cluster/minimal-ipv6/in-v1alpha2.yaml @@ -10,8 +10,10 @@ spec: class: Network kubernetesApiAccess: - 0.0.0.0/0 + - ::/0 sshAccess: - 0.0.0.0/0 + - ::/0 channel: stable cloudProvider: aws configBase: memfs://clusters.example.com/minimal-ipv6.example.com @@ -39,6 +41,7 @@ spec: nodes: public subnets: - cidr: 172.20.32.0/19 + ipv6CIDR: 2001:db8:0:111::/64 name: us-test-1a type: Public zone: us-test-1a diff --git a/upup/pkg/fi/cloudup/awstasks/BUILD.bazel b/upup/pkg/fi/cloudup/awstasks/BUILD.bazel index 1797626e28..27394f4404 100644 --- a/upup/pkg/fi/cloudup/awstasks/BUILD.bazel +++ b/upup/pkg/fi/cloudup/awstasks/BUILD.bazel @@ -78,6 +78,8 @@ go_library( "vpc.go", "vpc_dhcpoptions_association.go", "vpc_fitask.go", + "vpcamazonipv6cidrblock.go", + "vpcamazonipv6cidrblock_fitask.go", "vpccidrblock.go", "vpccidrblock_fitask.go", "vpcdhcpoptionsassociation_fitask.go", diff --git a/upup/pkg/fi/cloudup/awstasks/launchtemplate.go b/upup/pkg/fi/cloudup/awstasks/launchtemplate.go index b8058e1bc3..e156d57dc8 100644 --- a/upup/pkg/fi/cloudup/awstasks/launchtemplate.go +++ b/upup/pkg/fi/cloudup/awstasks/launchtemplate.go @@ -56,6 +56,8 @@ type LaunchTemplate struct { InstanceMonitoring *bool // InstanceType is the type of instance we are using InstanceType *string + // Ipv6AddressCount is the number of IPv6 addresses to assign with the primary network interface. + IPv6AddressCount *int64 // RootVolumeIops is the provisioned IOPS when the volume type is io1, io2 or gp3 RootVolumeIops *int64 // RootVolumeOptimization enables EBS optimization for an instance diff --git a/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_api.go b/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_api.go index 5a7a3c7a6a..a63b90affb 100644 --- a/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_api.go +++ b/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_api.go @@ -51,6 +51,7 @@ func (t *LaunchTemplate) RenderAWS(c *awsup.AWSAPITarget, a, e, changes *LaunchT AssociatePublicIpAddress: t.AssociatePublicIP, DeleteOnTermination: aws.Bool(true), DeviceIndex: fi.Int64(0), + Ipv6AddressCount: t.IPv6AddressCount, }, }, } @@ -214,6 +215,7 @@ func (t *LaunchTemplate) Find(c *fi.Context) (*LaunchTemplate, error) { for _, id := range x.Groups { actual.SecurityGroups = append(actual.SecurityGroups, &SecurityGroup{ID: id}) } + actual.IPv6AddressCount = x.Ipv6AddressCount } // In older Kops versions, security groups were added to LaunchTemplateData.SecurityGroupIds for _, id := range lt.LaunchTemplateData.SecurityGroupIds { diff --git a/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_cloudformation.go b/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_cloudformation.go index 81892f2b1f..a5099940b5 100644 --- a/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_cloudformation.go +++ b/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_cloudformation.go @@ -32,6 +32,8 @@ type cloudformationLaunchTemplateNetworkInterface struct { DeleteOnTermination *bool `json:"DeleteOnTermination,omitempty"` // DeviceIndex is the device index for the network interface attachment. DeviceIndex *int `json:"DeviceIndex,omitempty"` + // Ipv6AddressCount is the number of IPv6 addresses to assign with the primary network interface. + Ipv6AddressCount *int64 `json:"Ipv6AddressCount,omitempty"` // SecurityGroups is a list of security group ids. SecurityGroups []*cloudformation.Literal `json:"Groups,omitempty"` } @@ -200,6 +202,7 @@ func (t *LaunchTemplate) RenderCloudformation(target *cloudformation.Cloudformat AssociatePublicIPAddress: e.AssociatePublicIP, DeleteOnTermination: fi.Bool(true), DeviceIndex: fi.Int(0), + Ipv6AddressCount: e.IPv6AddressCount, }, }, } diff --git a/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_terraform.go b/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_terraform.go index 41a0804f2e..b7a9926563 100644 --- a/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_terraform.go +++ b/upup/pkg/fi/cloudup/awstasks/launchtemplate_target_terraform.go @@ -31,6 +31,8 @@ type terraformLaunchTemplateNetworkInterface struct { AssociatePublicIPAddress *bool `json:"associate_public_ip_address,omitempty" cty:"associate_public_ip_address"` // DeleteOnTermination indicates whether the network interface should be destroyed on instance termination. DeleteOnTermination *bool `json:"delete_on_termination,omitempty" cty:"delete_on_termination"` + // Ipv6AddressCount is the number of IPv6 addresses to assign with the primary network interface. + Ipv6AddressCount *int64 `json:"ipv6_address_count,omitempty" cty:"ipv6_address_count"` // SecurityGroups is a list of security group ids. SecurityGroups []*terraformWriter.Literal `json:"security_groups,omitempty" cty:"security_groups"` } @@ -205,6 +207,7 @@ func (t *LaunchTemplate) RenderTerraform(target *terraform.TerraformTarget, a, e { AssociatePublicIPAddress: e.AssociatePublicIP, DeleteOnTermination: fi.Bool(true), + Ipv6AddressCount: e.IPv6AddressCount, }, }, } diff --git a/upup/pkg/fi/cloudup/awstasks/route.go b/upup/pkg/fi/cloudup/awstasks/route.go index fa02b5c2fd..c1d28e5896 100644 --- a/upup/pkg/fi/cloudup/awstasks/route.go +++ b/upup/pkg/fi/cloudup/awstasks/route.go @@ -37,6 +37,7 @@ type Route struct { RouteTable *RouteTable Instance *Instance CIDR *string + IPv6CIDR *string // Exactly one of the below fields // MUST be provided. @@ -48,7 +49,7 @@ type Route struct { func (e *Route) Find(c *fi.Context) (*Route, error) { cloud := c.Cloud.(awsup.AWSCloud) - if e.RouteTable == nil || e.CIDR == nil { + if e.RouteTable == nil || (e.CIDR == nil && e.IPv6CIDR == nil) { // TODO: Move to validate? return nil, nil } @@ -73,13 +74,15 @@ func (e *Route) Find(c *fi.Context) (*Route, error) { } rt := response.RouteTables[0] for _, r := range rt.Routes { - if aws.StringValue(r.DestinationCidrBlock) != *e.CIDR { + if (r.DestinationCidrBlock == nil || aws.StringValue(r.DestinationCidrBlock) != aws.StringValue(e.CIDR)) && + (r.DestinationIpv6CidrBlock == nil || aws.StringValue(r.DestinationIpv6CidrBlock) != aws.StringValue(e.IPv6CIDR)) { continue } actual := &Route{ Name: e.Name, RouteTable: &RouteTable{ID: rt.RouteTableId}, CIDR: r.DestinationCidrBlock, + IPv6CIDR: r.DestinationIpv6CidrBlock, } if r.GatewayId != nil { actual.InternetGateway = &InternetGateway{ID: r.GatewayId} @@ -105,7 +108,7 @@ func (e *Route) Find(c *fi.Context) (*Route, error) { // Prevent spurious changes actual.Lifecycle = e.Lifecycle - klog.V(2).Infof("found route matching cidr %s", *e.CIDR) + klog.V(2).Infof("found route matching CIDR=%q IPv6CIDR=%q", aws.StringValue(e.CIDR), aws.StringValue(e.IPv6CIDR)) return actual, nil } } @@ -123,8 +126,11 @@ func (s *Route) CheckChanges(a, e, changes *Route) error { if e.RouteTable == nil { return fi.RequiredField("RouteTable") } - if e.CIDR == nil { - return fi.RequiredField("CIDR") + if e.CIDR == nil && e.IPv6CIDR == nil { + return fi.RequiredField("CIDR/IPv6CIDR") + } + if e.CIDR != nil && e.IPv6CIDR != nil { + return fmt.Errorf("cannot set more than 1 CIDR or IPv6CIDR") } targetCount := 0 if e.InternetGateway != nil { @@ -143,7 +149,7 @@ func (s *Route) CheckChanges(a, e, changes *Route) error { return fmt.Errorf("InternetGateway, Instance, NatGateway, or TransitGateway is required") } if targetCount != 1 { - return fmt.Errorf("Cannot set more than 1 InternetGateway, Instance, NatGateway, or TransitGateway") + return fmt.Errorf("cannot set more than 1 InternetGateway, Instance, NatGateway, or TransitGateway") } } @@ -154,6 +160,9 @@ func (s *Route) CheckChanges(a, e, changes *Route) error { if changes.CIDR != nil { return fi.CannotChangeField("CIDR") } + if changes.IPv6CIDR != nil { + return fi.CannotChangeField("IPv6CIDR") + } } return nil } @@ -162,7 +171,13 @@ func (_ *Route) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Route) error { if a == nil { request := &ec2.CreateRouteInput{} request.RouteTableId = checkNotNil(e.RouteTable.ID) - request.DestinationCidrBlock = checkNotNil(e.CIDR) + + if e.CIDR != nil || e.IPv6CIDR != nil { + request.DestinationCidrBlock = e.CIDR + request.DestinationIpv6CidrBlock = e.IPv6CIDR + } else { + klog.Fatal("both CIDR and IPv6CIDR were unexpectedly nil") + } if e.InternetGateway == nil && e.NatGateway == nil && e.TransitGatewayID == nil { return fmt.Errorf("missing target for route") @@ -178,7 +193,8 @@ func (_ *Route) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Route) error { request.InstanceId = checkNotNil(e.Instance.ID) } - klog.V(2).Infof("Creating Route with RouteTable:%q CIDR:%q", *e.RouteTable.ID, *e.CIDR) + klog.V(2).Infof("Creating Route with RouteTable:%q CIDR:%q IPv6CIDR:%q", + aws.StringValue(e.RouteTable.ID), aws.StringValue(e.CIDR), aws.StringValue(e.IPv6CIDR)) response, err := t.Cloud.EC2().CreateRoute(request) if err != nil { @@ -197,7 +213,13 @@ func (_ *Route) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Route) error { } else { request := &ec2.ReplaceRouteInput{} request.RouteTableId = checkNotNil(e.RouteTable.ID) - request.DestinationCidrBlock = checkNotNil(e.CIDR) + + if e.CIDR != nil || e.IPv6CIDR != nil { + request.DestinationCidrBlock = e.CIDR + request.DestinationIpv6CidrBlock = e.IPv6CIDR + } else { + klog.Fatal("both CIDR and IPv6CIDR were unexpectedly nil") + } if e.InternetGateway == nil && e.NatGateway == nil && e.TransitGatewayID == nil { return fmt.Errorf("missing target for route") @@ -239,6 +261,7 @@ func checkNotNil(s *string) *string { type terraformRoute struct { RouteTableID *terraformWriter.Literal `json:"route_table_id" cty:"route_table_id"` CIDR *string `json:"destination_cidr_block,omitempty" cty:"destination_cidr_block"` + IPv6CIDR *string `json:"destination_ipv6_cidr_block,omitempty" cty:"destination_ipv6_cidr_block"` InternetGatewayID *terraformWriter.Literal `json:"gateway_id,omitempty" cty:"gateway_id"` NATGatewayID *terraformWriter.Literal `json:"nat_gateway_id,omitempty" cty:"nat_gateway_id"` TransitGatewayID *string `json:"transit_gateway_id,omitempty" cty:"transit_gateway_id"` @@ -247,8 +270,9 @@ type terraformRoute struct { func (_ *Route) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Route) error { tf := &terraformRoute{ - CIDR: e.CIDR, RouteTableID: e.RouteTable.TerraformLink(), + CIDR: e.CIDR, + IPv6CIDR: e.IPv6CIDR, } if e.InternetGateway == nil && e.NatGateway == nil && e.TransitGatewayID == nil { @@ -274,6 +298,7 @@ func (_ *Route) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Rou type cloudformationRoute struct { RouteTableID *cloudformation.Literal `json:"RouteTableId"` CIDR *string `json:"DestinationCidrBlock,omitempty"` + IPv6CIDR *string `json:"DestinationIpv6CidrBlock,omitempty"` InternetGatewayID *cloudformation.Literal `json:"GatewayId,omitempty"` NATGatewayID *cloudformation.Literal `json:"NatGatewayId,omitempty"` TransitGatewayID *string `json:"TransitGatewayId,omitempty"` @@ -282,8 +307,9 @@ type cloudformationRoute struct { func (_ *Route) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *Route) error { tf := &cloudformationRoute{ - CIDR: e.CIDR, RouteTableID: e.RouteTable.CloudformationLink(), + CIDR: e.CIDR, + IPv6CIDR: e.IPv6CIDR, } if e.InternetGateway == nil && e.NatGateway == nil && e.TransitGatewayID == nil { diff --git a/upup/pkg/fi/cloudup/awstasks/securitygroup.go b/upup/pkg/fi/cloudup/awstasks/securitygroup.go index e161e33ab5..dc58fdcc8d 100644 --- a/upup/pkg/fi/cloudup/awstasks/securitygroup.go +++ b/upup/pkg/fi/cloudup/awstasks/securitygroup.go @@ -325,6 +325,9 @@ func (d *deleteSecurityGroupRule) Item() string { for _, r := range p.IpRanges { s += fmt.Sprintf(" ip=%s", aws.StringValue(r.CidrIp)) } + for _, r := range p.Ipv6Ranges { + s += fmt.Sprintf(" ipv6=%s", aws.StringValue(r.CidrIpv6)) + } //permissionString := fi.DebugAsJsonString(d.permission) //s += permissionString @@ -347,6 +350,13 @@ func expandPermissions(sgID *string, permission *ec2.IpPermission, egress bool) rules = append(rules, a) } + for _, ipv6Range := range permission.Ipv6Ranges { + a := &ec2.IpPermission{} + *a = *master + a.Ipv6Ranges = []*ec2.Ipv6Range{ipv6Range} + rules = append(rules, a) + } + for _, ug := range permission.UserIdGroupPairs { a := &ec2.IpPermission{} *a = *master diff --git a/upup/pkg/fi/cloudup/awstasks/securitygrouprule.go b/upup/pkg/fi/cloudup/awstasks/securitygrouprule.go index 312831b665..11380fbd3e 100644 --- a/upup/pkg/fi/cloudup/awstasks/securitygrouprule.go +++ b/upup/pkg/fi/cloudup/awstasks/securitygrouprule.go @@ -39,6 +39,7 @@ type SecurityGroupRule struct { SecurityGroup *SecurityGroup CIDR *string + IPv6CIDR *string Protocol *string // FromPort is the lower-bound (inclusive) of the port-range @@ -113,6 +114,9 @@ func (e *SecurityGroupRule) Find(c *fi.Context) (*SecurityGroupRule, error) { if e.CIDR != nil { actual.CIDR = e.CIDR } + if e.IPv6CIDR != nil { + actual.IPv6CIDR = e.IPv6CIDR + } if e.SourceGroup != nil { actual.SourceGroup = &SecurityGroup{ID: e.SourceGroup.ID} } @@ -143,7 +147,6 @@ func (e *SecurityGroupRule) matches(rule *ec2.IpPermission) bool { } if e.CIDR != nil { - // TODO: Only if len 1? match := false for _, ipRange := range rule.IpRanges { if aws.StringValue(ipRange.CidrIp) == *e.CIDR { @@ -156,8 +159,20 @@ func (e *SecurityGroupRule) matches(rule *ec2.IpPermission) bool { } } + if e.IPv6CIDR != nil { + match := false + for _, ipv6Range := range rule.Ipv6Ranges { + if aws.StringValue(ipv6Range.CidrIpv6) == *e.IPv6CIDR { + match = true + break + } + } + if !match { + return false + } + } + if e.SourceGroup != nil { - // TODO: Only if len 1? match := false for _, spec := range rule.UserIdGroupPairs { if e.SourceGroup == nil { @@ -190,6 +205,9 @@ func (_ *SecurityGroupRule) CheckChanges(a, e, changes *SecurityGroupRule) error if e.SecurityGroup == nil { return field.Required(field.NewPath("SecurityGroup"), "") } + if e.CIDR != nil && e.IPv6CIDR != nil { + return field.Forbidden(field.NewPath("CIDR/IPv6CIDR"), "Cannot set more than 1 CIDR or IPv6CIDR") + } } if e.FromPort != nil && e.Protocol == nil { @@ -226,6 +244,10 @@ func (e *SecurityGroupRule) Description() string { description = append(description, fmt.Sprintf("cidr=%s", *e.CIDR)) } + if e.IPv6CIDR != nil { + description = append(description, fmt.Sprintf("ipv6cidr=%s", *e.IPv6CIDR)) + } + return strings.Join(description, " ") } @@ -250,12 +272,20 @@ func (_ *SecurityGroupRule) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Secu GroupId: e.SourceGroup.ID, }, } - } else { + } else if e.IPv6CIDR != nil { + IPv6CIDR := e.IPv6CIDR + ipPermission.Ipv6Ranges = []*ec2.Ipv6Range{ + {CidrIpv6: IPv6CIDR}, + } + } else if e.CIDR != nil { CIDR := e.CIDR - // Default to 0.0.0.0/0 ? ipPermission.IpRanges = []*ec2.IpRange{ {CidrIp: CIDR}, } + } else { + ipPermission.IpRanges = []*ec2.IpRange{ + {CidrIp: aws.String("0.0.0.0/0")}, + } } description := e.Description() @@ -300,8 +330,9 @@ type terraformSecurityGroupIngress struct { FromPort *int64 `json:"from_port,omitempty" cty:"from_port"` ToPort *int64 `json:"to_port,omitempty" cty:"to_port"` - Protocol *string `json:"protocol,omitempty" cty:"protocol"` - CIDRBlocks []string `json:"cidr_blocks,omitempty" cty:"cidr_blocks"` + Protocol *string `json:"protocol,omitempty" cty:"protocol"` + CIDRBlocks []string `json:"cidr_blocks,omitempty" cty:"cidr_blocks"` + IPv6CIDRBlocks []string `json:"ipv6_cidr_blocks,omitempty" cty:"ipv6_cidr_blocks"` } func (_ *SecurityGroupRule) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *SecurityGroupRule) error { @@ -338,6 +369,10 @@ func (_ *SecurityGroupRule) RenderTerraform(t *terraform.TerraformTarget, a, e, if e.CIDR != nil { tf.CIDRBlocks = append(tf.CIDRBlocks, *e.CIDR) } + if e.IPv6CIDR != nil { + tf.IPv6CIDRBlocks = append(tf.IPv6CIDRBlocks, *e.IPv6CIDR) + } + return t.RenderResource("aws_security_group_rule", *e.Name, tf) } @@ -386,11 +421,10 @@ func (_ *SecurityGroupRule) RenderCloudformation(t *cloudformation.Cloudformatio } if e.CIDR != nil { - if strings.Contains(fi.StringValue(e.CIDR), ":") { - tf.CidrIpv6 = e.CIDR - } else { - tf.CidrIp = e.CIDR - } + tf.CidrIp = e.CIDR + } + if e.IPv6CIDR != nil { + tf.CidrIpv6 = e.IPv6CIDR } return t.RenderResource(cfType, *e.Name, tf) diff --git a/upup/pkg/fi/cloudup/awstasks/subnet.go b/upup/pkg/fi/cloudup/awstasks/subnet.go index b1baf44e82..7ed5c4a3ee 100644 --- a/upup/pkg/fi/cloudup/awstasks/subnet.go +++ b/upup/pkg/fi/cloudup/awstasks/subnet.go @@ -19,6 +19,7 @@ package awstasks import ( "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/klog/v2" @@ -45,6 +46,7 @@ type Subnet struct { VPC *VPC AvailabilityZone *string CIDR *string + IPv6CIDR *string Shared *bool Tags map[string]string @@ -85,6 +87,19 @@ func (e *Subnet) Find(c *fi.Context) (*Subnet, error) { Tags: intersectTags(subnet.Tags, e.Tags), } + for _, association := range subnet.Ipv6CidrBlockAssociationSet { + if association == nil || association.Ipv6CidrBlockState == nil { + continue + } + + state := aws.StringValue(association.Ipv6CidrBlockState.State) + if state != ec2.SubnetCidrBlockStateCodeAssociated && state != ec2.SubnetCidrBlockStateCodeAssociating { + continue + } + + actual.IPv6CIDR = association.Ipv6CidrBlock + } + klog.V(2).Infof("found matching subnet %q", *actual.ID) e.ID = actual.ID @@ -155,10 +170,13 @@ func (s *Subnet) CheckChanges(a, e, changes *Subnet) error { errors = append(errors, fi.FieldIsImmutable(eID, aID, fieldPath.Child("VPC"))) } if changes.AvailabilityZone != nil { - errors = append(errors, fi.FieldIsImmutable(a.AvailabilityZone, e.AvailabilityZone, fieldPath.Child("AvailabilityZone"))) + errors = append(errors, fi.FieldIsImmutable(e.AvailabilityZone, a.AvailabilityZone, fieldPath.Child("AvailabilityZone"))) } if changes.CIDR != nil { - errors = append(errors, fi.FieldIsImmutable(a.CIDR, e.CIDR, fieldPath.Child("CIDR"))) + errors = append(errors, fi.FieldIsImmutable(e.CIDR, a.CIDR, fieldPath.Child("CIDR"))) + } + if changes.IPv6CIDR != nil && a.IPv6CIDR != nil { + errors = append(errors, fi.FieldIsImmutable(e.IPv6CIDR, a.IPv6CIDR, fieldPath.Child("IPv6CIDR"))) } } @@ -183,6 +201,7 @@ func (_ *Subnet) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Subnet) error { request := &ec2.CreateSubnetInput{ CidrBlock: e.CIDR, + Ipv6CidrBlock: e.IPv6CIDR, AvailabilityZone: e.AvailabilityZone, VpcId: e.VPC.ID, TagSpecifications: awsup.EC2TagSpecification(ec2.ResourceTypeSubnet, e.Tags), @@ -194,6 +213,18 @@ func (_ *Subnet) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Subnet) error { } e.ID = response.Subnet.SubnetId + } else { + if changes.IPv6CIDR != nil { + request := &ec2.AssociateSubnetCidrBlockInput{ + Ipv6CidrBlock: e.IPv6CIDR, + SubnetId: e.ID, + } + + _, err := t.Cloud.EC2().AssociateSubnetCidrBlock(request) + if err != nil { + return fmt.Errorf("error associating subnet cidr block: %v", err) + } + } } return t.AddAWSTags(*e.ID, e.Tags) @@ -218,6 +249,7 @@ func subnetSlicesEqualIgnoreOrder(l, r []*Subnet) bool { type terraformSubnet struct { VPCID *terraformWriter.Literal `json:"vpc_id" cty:"vpc_id"` CIDR *string `json:"cidr_block" cty:"cidr_block"` + IPv6CIDR *string `json:"ipv6_cidr_block" cty:"ipv6_cidr_block"` AvailabilityZone *string `json:"availability_zone" cty:"availability_zone"` Tags map[string]string `json:"tags,omitempty" cty:"tags"` } @@ -244,6 +276,7 @@ func (_ *Subnet) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Su tf := &terraformSubnet{ VPCID: e.VPC.TerraformLink(), CIDR: e.CIDR, + IPv6CIDR: e.IPv6CIDR, AvailabilityZone: e.AvailabilityZone, Tags: e.Tags, } @@ -268,6 +301,7 @@ func (e *Subnet) TerraformLink() *terraformWriter.Literal { type cloudformationSubnet struct { VPCID *cloudformation.Literal `json:"VpcId,omitempty"` CIDR *string `json:"CidrBlock,omitempty"` + IPv6CIDR *string `json:"Ipv6CidrBlock,omitempty"` AvailabilityZone *string `json:"AvailabilityZone,omitempty"` Tags []cloudformationTag `json:"Tags,omitempty"` } @@ -283,6 +317,7 @@ func (_ *Subnet) RenderCloudformation(t *cloudformation.CloudformationTarget, a, cf := &cloudformationSubnet{ VPCID: e.VPC.CloudformationLink(), CIDR: e.CIDR, + IPv6CIDR: e.IPv6CIDR, AvailabilityZone: e.AvailabilityZone, Tags: buildCloudformationTags(e.Tags), } @@ -303,3 +338,74 @@ func (e *Subnet) CloudformationLink() *cloudformation.Literal { return cloudformation.Ref("AWS::EC2::Subnet", *e.Name) } + +func (e *Subnet) FindDeletions(c *fi.Context) ([]fi.Deletion, error) { + if e.ID == nil || aws.BoolValue(e.Shared) { + return nil, nil + } + + subnet, err := e.findEc2Subnet(c) + if err != nil { + return nil, err + } + + if subnet == nil { + return nil, nil + } + + var removals []fi.Deletion + for _, association := range subnet.Ipv6CidrBlockAssociationSet { + // Skip when without state + if association == nil || association.Ipv6CidrBlockState == nil { + continue + } + + // Skip when already disassociated + state := aws.StringValue(association.Ipv6CidrBlockState.State) + if state == ec2.SubnetCidrBlockStateCodeDisassociated || state == ec2.SubnetCidrBlockStateCodeDisassociating { + continue + } + + // Skip when current IPv6CIDR + if aws.StringValue(e.IPv6CIDR) == aws.StringValue(association.Ipv6CidrBlock) { + continue + } + + removals = append(removals, &deleteSubnetIPv6CIDRBlock{ + vpcID: subnet.VpcId, + ipv6CidrBlock: association.Ipv6CidrBlock, + associationID: association.AssociationId, + }) + } + + return removals, nil +} + +type deleteSubnetIPv6CIDRBlock struct { + vpcID *string + ipv6CidrBlock *string + associationID *string +} + +var _ fi.Deletion = &deleteSubnetIPv6CIDRBlock{} + +func (d *deleteSubnetIPv6CIDRBlock) Delete(t fi.Target) error { + awsTarget, ok := t.(*awsup.AWSAPITarget) + if !ok { + return fmt.Errorf("unexpected target type for deletion: %T", t) + } + + request := &ec2.DisassociateSubnetCidrBlockInput{ + AssociationId: d.associationID, + } + _, err := awsTarget.Cloud.EC2().DisassociateSubnetCidrBlock(request) + return err +} + +func (d *deleteSubnetIPv6CIDRBlock) TaskName() string { + return "SubnetIPv6CIDRBlock" +} + +func (d *deleteSubnetIPv6CIDRBlock) Item() string { + return fmt.Sprintf("%v: ipv6cidr=%v", *d.vpcID, *d.ipv6CidrBlock) +} diff --git a/upup/pkg/fi/cloudup/awstasks/subnet_test.go b/upup/pkg/fi/cloudup/awstasks/subnet_test.go index 52624c7ad9..c59a61ab5b 100644 --- a/upup/pkg/fi/cloudup/awstasks/subnet_test.go +++ b/upup/pkg/fi/cloudup/awstasks/subnet_test.go @@ -58,7 +58,7 @@ func Test_Subnet_CannotChangeSubnet(t *testing.T) { if err == nil { t.Errorf("validation error was expected") } - if fmt.Sprintf("%v", err) != "Subnet.CIDR: Forbidden: field is immutable: old=\"192.168.0.1/16\" new=\"192.168.0.0/16\"" { + if fmt.Sprintf("%v", err) != "Subnet.CIDR: Forbidden: field is immutable: old=\"192.168.0.0/16\" new=\"192.168.0.1/16\"" { t.Errorf("unexpected error: %v", err) } } diff --git a/upup/pkg/fi/cloudup/awstasks/vpc.go b/upup/pkg/fi/cloudup/awstasks/vpc.go index c585e2855c..4bf4813600 100644 --- a/upup/pkg/fi/cloudup/awstasks/vpc.go +++ b/upup/pkg/fi/cloudup/awstasks/vpc.go @@ -36,8 +36,14 @@ type VPC struct { Name *string Lifecycle *fi.Lifecycle - ID *string - CIDR *string + ID *string + CIDR *string + + // AmazonIPv6 is used only for Terraform rendering. + // Direct and CloudFormation rendering is handled via the VPCAmazonIPv6CIDRBlock task + AmazonIPv6 *bool + IPv6CIDR *string + EnableDNSHostnames *bool EnableDNSSupport *bool @@ -83,14 +89,34 @@ func (e *VPC) Find(c *fi.Context) (*VPC, error) { } vpc := response.Vpcs[0] actual := &VPC{ - ID: vpc.VpcId, - CIDR: vpc.CidrBlock, - Name: findNameTag(vpc.Tags), - Tags: intersectTags(vpc.Tags, e.Tags), + ID: vpc.VpcId, + CIDR: vpc.CidrBlock, + AmazonIPv6: aws.Bool(false), + Name: findNameTag(vpc.Tags), + Tags: intersectTags(vpc.Tags, e.Tags), } klog.V(4).Infof("found matching VPC %v", actual) + for _, association := range vpc.Ipv6CidrBlockAssociationSet { + if association == nil || association.Ipv6CidrBlockState == nil { + continue + } + + state := aws.StringValue(association.Ipv6CidrBlockState.State) + if state != ec2.VpcCidrBlockStateCodeAssociated && state != ec2.VpcCidrBlockStateCodeAssociating { + continue + } + + pool := aws.StringValue(association.Ipv6Pool) + if pool == "Amazon" { + actual.AmazonIPv6 = aws.Bool(true) + actual.IPv6CIDR = association.Ipv6CidrBlock + e.IPv6CIDR = association.Ipv6CidrBlock + break + } + } + if actual.ID != nil { request := &ec2.DescribeVpcAttributeInput{VpcId: actual.ID, Attribute: aws.String(ec2.VpcAttributeNameEnableDnsSupport)} response, err := cloud.EC2().DescribeVpcAttribute(request) @@ -253,6 +279,7 @@ type terraformVPC struct { CIDR *string `json:"cidr_block,omitempty" cty:"cidr_block"` EnableDNSHostnames *bool `json:"enable_dns_hostnames,omitempty" cty:"enable_dns_hostnames"` EnableDNSSupport *bool `json:"enable_dns_support,omitempty" cty:"enable_dns_support"` + AmazonIPv6 *bool `json:"assign_generated_ipv6_cidr_block,omitempty" cty:"assign_generated_ipv6_cidr_block"` Tags map[string]string `json:"tags,omitempty" cty:"tags"` } @@ -278,6 +305,7 @@ func (_ *VPC) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *VPC) Tags: e.Tags, EnableDNSHostnames: e.EnableDNSHostnames, EnableDNSSupport: e.EnableDNSSupport, + AmazonIPv6: e.AmazonIPv6, } return t.RenderResource("aws_vpc", *e.Name, tf) diff --git a/upup/pkg/fi/cloudup/awstasks/vpcamazonipv6cidrblock.go b/upup/pkg/fi/cloudup/awstasks/vpcamazonipv6cidrblock.go new file mode 100644 index 0000000000..89042a2130 --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/vpcamazonipv6cidrblock.go @@ -0,0 +1,149 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awstasks + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/cloudup/awsup" + "k8s.io/kops/upup/pkg/fi/cloudup/cloudformation" + "k8s.io/kops/upup/pkg/fi/cloudup/terraform" +) + +// +kops:fitask +type VPCAmazonIPv6CIDRBlock struct { + Name *string + Lifecycle *fi.Lifecycle + + VPC *VPC + CIDRBlock *string + + // Shared is set if this is a shared VPC + Shared *bool +} + +func (e *VPCAmazonIPv6CIDRBlock) Find(c *fi.Context) (*VPCAmazonIPv6CIDRBlock, error) { + cloud := c.Cloud.(awsup.AWSCloud) + + vpc, err := cloud.DescribeVPC(aws.StringValue(e.VPC.ID)) + if err != nil { + return nil, err + } + + var cidr *string + for _, association := range vpc.Ipv6CidrBlockAssociationSet { + if association == nil || association.Ipv6CidrBlockState == nil { + continue + } + + state := aws.StringValue(association.Ipv6CidrBlockState.State) + if state != ec2.VpcCidrBlockStateCodeAssociated && state != ec2.VpcCidrBlockStateCodeAssociating { + continue + } + + if aws.StringValue(association.Ipv6Pool) == "Amazon" { + cidr = association.Ipv6CidrBlock + break + } + } + if cidr == nil { + return nil, nil + } + + actual := &VPCAmazonIPv6CIDRBlock{ + VPC: &VPC{ID: vpc.VpcId}, + CIDRBlock: cidr, + } + + // Expose the Amazon provided IPv6 CIDR block to other tasks + e.CIDRBlock = cidr + + // Prevent spurious changes + actual.Shared = e.Shared + actual.Name = e.Name + actual.Lifecycle = e.Lifecycle + + return actual, nil +} + +func (e *VPCAmazonIPv6CIDRBlock) Run(c *fi.Context) error { + return fi.DefaultDeltaRunMethod(e, c) +} + +func (s *VPCAmazonIPv6CIDRBlock) CheckChanges(a, e, changes *VPCAmazonIPv6CIDRBlock) error { + if e.VPC == nil { + return fi.RequiredField("VPC") + } + + if a != nil && changes != nil { + if changes.VPC != nil { + return fi.CannotChangeField("VPC") + } + } + + return nil +} + +func (_ *VPCAmazonIPv6CIDRBlock) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *VPCAmazonIPv6CIDRBlock) error { + shared := aws.BoolValue(e.Shared) + if shared && a == nil { + // VPC not owned by kOps, no changes will be applied + // Verify that the Amazon IPv6 provided CIDR block was found. + return fmt.Errorf("IPv6 CIDR block provided by Amazon not found") + } + + request := &ec2.AssociateVpcCidrBlockInput{ + VpcId: e.VPC.ID, + AmazonProvidedIpv6CidrBlock: aws.Bool(true), + } + + _, err := t.Cloud.EC2().AssociateVpcCidrBlock(request) + if err != nil { + return fmt.Errorf("error associating Amazon IPv6 provided CIDR block to VPC: %v", err) + } + + return nil // no tags +} + +func (_ *VPCAmazonIPv6CIDRBlock) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *VPCAmazonIPv6CIDRBlock) error { + // At the moment, this can only be done via the aws_vpc resource + return nil +} + +type cloudformationVPCAmazonIPv6CIDRBlock struct { + VPCID *cloudformation.Literal `json:"VpcId"` + AmazonIPv6 *bool `json:"AmazonProvidedIpv6CidrBlock"` +} + +func (_ *VPCAmazonIPv6CIDRBlock) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *VPCAmazonIPv6CIDRBlock) error { + shared := aws.BoolValue(e.Shared) + if shared && a == nil { + // VPC not owned by kOps, no changes will be applied + // Verify that the Amazon IPv6 provided CIDR block was found. + return fmt.Errorf("IPv6 CIDR block provided by Amazon not found") + } + + cf := &cloudformationVPCAmazonIPv6CIDRBlock{ + VPCID: e.VPC.CloudformationLink(), + AmazonIPv6: aws.Bool(true), + } + + return t.RenderResource("AWS::EC2::VPCCidrBlock", *e.Name, cf) +} diff --git a/upup/pkg/fi/cloudup/awstasks/vpcamazonipv6cidrblock_fitask.go b/upup/pkg/fi/cloudup/awstasks/vpcamazonipv6cidrblock_fitask.go new file mode 100644 index 0000000000..35e3debfd0 --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/vpcamazonipv6cidrblock_fitask.go @@ -0,0 +1,51 @@ +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by fitask. DO NOT EDIT. + +package awstasks + +import ( + "k8s.io/kops/upup/pkg/fi" +) + +// VPCAmazonIPv6CIDRBlock + +var _ fi.HasLifecycle = &VPCAmazonIPv6CIDRBlock{} + +// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle +func (o *VPCAmazonIPv6CIDRBlock) GetLifecycle() *fi.Lifecycle { + return o.Lifecycle +} + +// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle +func (o *VPCAmazonIPv6CIDRBlock) SetLifecycle(lifecycle fi.Lifecycle) { + o.Lifecycle = &lifecycle +} + +var _ fi.HasName = &VPCAmazonIPv6CIDRBlock{} + +// GetName returns the Name of the object, implementing fi.HasName +func (o *VPCAmazonIPv6CIDRBlock) GetName() *string { + return o.Name +} + +// String is the stringer function for the task, producing readable output using fi.TaskAsString +func (o *VPCAmazonIPv6CIDRBlock) String() string { + return fi.TaskAsString(o) +} diff --git a/upup/pkg/fi/cloudup/awstasks/vpccidrblock.go b/upup/pkg/fi/cloudup/awstasks/vpccidrblock.go index 66632fce23..d8b9c1948f 100644 --- a/upup/pkg/fi/cloudup/awstasks/vpccidrblock.go +++ b/upup/pkg/fi/cloudup/awstasks/vpccidrblock.go @@ -19,6 +19,7 @@ package awstasks import ( "fmt" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awsup" @@ -42,19 +43,28 @@ type VPCCIDRBlock struct { func (e *VPCCIDRBlock) Find(c *fi.Context) (*VPCCIDRBlock, error) { cloud := c.Cloud.(awsup.AWSCloud) - vpcID := e.VPC.ID - - vpc, err := cloud.DescribeVPC(*vpcID) + vpcID := aws.StringValue(e.VPC.ID) + vpc, err := cloud.DescribeVPC(vpcID) if err != nil { return nil, err } found := false - for _, cba := range vpc.CidrBlockAssociationSet { - if fi.StringValue(cba.CidrBlock) == fi.StringValue(e.CIDRBlock) && - cba.CidrBlockState != nil && fi.StringValue(cba.CidrBlockState.State) == ec2.VpcCidrBlockStateCodeAssociated { - found = true - break + if e.CIDRBlock != nil { + for _, cba := range vpc.CidrBlockAssociationSet { + if cba == nil || cba.CidrBlockState == nil { + continue + } + + state := aws.StringValue(cba.CidrBlockState.State) + if state != ec2.VpcCidrBlockStateCodeAssociated && state != ec2.VpcCidrBlockStateCodeAssociating { + continue + } + + if aws.StringValue(cba.CidrBlock) == aws.StringValue(e.CIDRBlock) { + found = true + break + } } } if !found { @@ -62,8 +72,8 @@ func (e *VPCCIDRBlock) Find(c *fi.Context) (*VPCCIDRBlock, error) { } actual := &VPCCIDRBlock{ - CIDRBlock: e.CIDRBlock, VPC: &VPC{ID: vpc.VpcId}, + CIDRBlock: e.CIDRBlock, } // Prevent spurious changes @@ -101,12 +111,11 @@ func (s *VPCCIDRBlock) CheckChanges(a, e, changes *VPCCIDRBlock) error { } func (_ *VPCCIDRBlock) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *VPCCIDRBlock) error { - shared := fi.BoolValue(e.Shared) - if shared { - // Verify the CIDR block was found. - if a == nil { - return fmt.Errorf("CIDR block %q not found", fi.StringValue(e.CIDRBlock)) - } + shared := aws.BoolValue(e.Shared) + if shared && a == nil { + // VPC not owned by kOps, no changes will be applied + // Verify that the CIDR block was found. + return fmt.Errorf("CIDR block %q not found", aws.StringValue(e.CIDRBlock)) } if changes.CIDRBlock != nil { @@ -130,6 +139,12 @@ type terraformVPCCIDRBlock struct { } func (_ *VPCCIDRBlock) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *VPCCIDRBlock) error { + shared := aws.BoolValue(e.Shared) + if shared && a == nil { + // VPC not owned by kOps, no changes will be applied + // Verify that the CIDR block was found. + return fmt.Errorf("CIDR block %q not found", aws.StringValue(e.CIDRBlock)) + } // When this has been enabled please fix test TestAdditionalCIDR in integration_test.go to run runTestAWS. tf := &terraformVPCCIDRBlock{ @@ -149,6 +164,13 @@ type cloudformationVPCCIDRBlock struct { } func (_ *VPCCIDRBlock) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *VPCCIDRBlock) error { + shared := aws.BoolValue(e.Shared) + if shared && a == nil { + // VPC not owned by kOps, no changes will be applied + // Verify that the CIDR block was found. + return fmt.Errorf("CIDR block %q not found", aws.StringValue(e.CIDRBlock)) + } + cf := &cloudformationVPCCIDRBlock{ VPCID: e.VPC.CloudformationLink(), CIDRBlock: e.CIDRBlock, diff --git a/upup/pkg/fi/utils/BUILD.bazel b/upup/pkg/fi/utils/BUILD.bazel index aa0ba5827c..5fd82c59ce 100644 --- a/upup/pkg/fi/utils/BUILD.bazel +++ b/upup/pkg/fi/utils/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "cidr.go", "equals.go", "gzip.go", "hash.go", diff --git a/upup/pkg/fi/utils/cidr.go b/upup/pkg/fi/utils/cidr.go new file mode 100644 index 0000000000..6a7fb8d435 --- /dev/null +++ b/upup/pkg/fi/utils/cidr.go @@ -0,0 +1,58 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "net" + "strings" +) + +func IsIPv4CIDR(cidr string) bool { + ip, _, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + + // Must convert to IPv4 + if ip.To4() == nil { + return false + } + // Must NOT contain ":" + if strings.Contains(cidr, ":") { + return false + } + + return true +} + +func IsIPv6CIDR(cidr string) bool { + ip, _, err := net.ParseCIDR(cidr) + if err != nil { + return false + } + + // Must NOT convert to IPv4 + if ip.To4() != nil { + return false + } + // Must contain ":" + if !strings.Contains(cidr, ":") { + return false + } + + return true +}