Skip to content

Commit

Permalink
Calculate IPv6 subnet CIDR based on cluster CIDR
Browse files Browse the repository at this point in the history
  • Loading branch information
hakman committed May 13, 2021
1 parent 830e91d commit c1d3249
Show file tree
Hide file tree
Showing 14 changed files with 632 additions and 9 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
github.com/MakeNowJust/heredoc/v2 v2.0.1
github.com/Masterminds/sprig/v3 v3.2.2
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1059
github.com/apparentlymart/go-cidr v1.1.0
github.com/aws/amazon-ec2-instance-selector/v2 v2.0.2
github.com/aws/aws-sdk-go v1.38.29
github.com/blang/semver/v4 v4.0.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.61.1059/go.mod h1:pUKYbK5JQ+1Dfxk80P0q
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
Expand Down
1 change: 1 addition & 0 deletions pkg/apis/kops/validation/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 18 additions & 4 deletions pkg/apis/kops/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -329,11 +330,24 @@ func validateCIDR(cidr string, fieldPath *field.Path) field.ErrorList {
}

func validateIPv6CIDR(cidr string, fieldPath *field.Path) field.ErrorList {
allErrs := validateCIDR(cidr, fieldPath)
allErrs := field.ErrorList{}

if strings.HasPrefix(cidr, "cidrsubnet-") {
newBits, _, err := utils.ParseCidrSubnet(cidr)
if err == nil {
allErrs = append(allErrs, field.Invalid(fieldPath, cidr, fmt.Sprintf("IPv6 CIDR subnet is not parsable: %v", err)))
return allErrs
}
if newBits < 1 || newBits > 128 {
allErrs = append(allErrs, field.Invalid(fieldPath, cidr, "IPv6 CIDR subnet newBits must be a value between 1 and 128"))
}
} else {
allErrs = append(allErrs, validateCIDR(cidr, fieldPath)...)

ip, _, err := net.ParseCIDR(cidr)
if err == nil && ip.To4() != nil {
allErrs = append(allErrs, field.Invalid(fieldPath, cidr, "Network is not an IPv6 CIDR"))
ip, _, err := net.ParseCIDR(cidr)
if err == nil && ip.To4() != nil {
allErrs = append(allErrs, field.Invalid(fieldPath, cidr, "Network is not an IPv6 CIDR"))
}
}

return allErrs
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
Expand Down
58 changes: 53 additions & 5 deletions upup/pkg/fi/cloudup/awstasks/subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package awstasks

import (
"fmt"
"strings"

"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"
Expand Down Expand Up @@ -76,6 +78,21 @@ func (e *Subnet) Find(c *fi.Context) (*Subnet, error) {
return nil, nil
}

ipv6CIDR := e.IPv6CIDR
if strings.HasPrefix(aws.StringValue(ipv6CIDR), "cidrsubnet-") {
newbits, netnum, err := utils.ParseCidrSubnet(aws.StringValue(ipv6CIDR))
if err != nil {
return nil, fmt.Errorf("error parsing IPv6 CIDR subnet: %v", err)
}

cidr, err := utils.CidrSubnet(aws.StringValue(e.VPC.IPv6CIDR), newbits, netnum)
if err != nil {
return nil, fmt.Errorf("error calculating IPv6 CIDR subnet: %v", err)
}

ipv6CIDR = aws.String(cidr)
}

actual := &Subnet{
ID: subnet.SubnetId,
AvailabilityZone: subnet.AvailabilityZone,
Expand All @@ -91,7 +108,11 @@ func (e *Subnet) Find(c *fi.Context) (*Subnet, error) {
if state == nil || fi.StringValue(state.State) == ec2.SubnetCidrBlockStateCodeDisassociated {
continue
}
actual.IPv6CIDR = association.Ipv6CidrBlock
if aws.StringValue(association.Ipv6CidrBlock) == aws.StringValue(ipv6CIDR) {
actual.IPv6CIDR = e.IPv6CIDR
} else {
actual.IPv6CIDR = association.Ipv6CidrBlock
}
}

klog.V(2).Infof("found matching subnet %q", *actual.ID)
Expand Down Expand Up @@ -190,14 +211,29 @@ func (_ *Subnet) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Subnet) error {
}
}

ipv6CIDR := e.IPv6CIDR
if strings.HasPrefix(aws.StringValue(ipv6CIDR), "cidrsubnet-") {
newbits, netnum, err := utils.ParseCidrSubnet(aws.StringValue(ipv6CIDR))
if err != nil {
return fmt.Errorf("error parsing IPv6 CIDR subnet: %v", err)
}

cidr, err := utils.CidrSubnet(aws.StringValue(e.VPC.IPv6CIDR), newbits, netnum)
if err != nil {
return fmt.Errorf("error calculating IPv6 CIDR subnet: %v", err)
}

ipv6CIDR = aws.String(cidr)
}

if a == nil {
klog.V(2).Infof("Creating Subnet with CIDR: %q", *e.CIDR)

request := &ec2.CreateSubnetInput{
CidrBlock: e.CIDR,
Ipv6CidrBlock: e.IPv6CIDR,
AvailabilityZone: e.AvailabilityZone,
VpcId: e.VPC.ID,
AvailabilityZone: e.AvailabilityZone,
CidrBlock: e.CIDR,
Ipv6CidrBlock: ipv6CIDR,
TagSpecifications: awsup.EC2TagSpecification(ec2.ResourceTypeSubnet, e.Tags),
}

Expand All @@ -210,8 +246,8 @@ func (_ *Subnet) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Subnet) error {
} else {
if changes.IPv6CIDR != nil {
request := &ec2.AssociateSubnetCidrBlockInput{
Ipv6CidrBlock: e.IPv6CIDR,
SubnetId: e.ID,
Ipv6CidrBlock: ipv6CIDR,
}

_, err := t.Cloud.EC2().AssociateSubnetCidrBlock(request)
Expand Down Expand Up @@ -267,6 +303,12 @@ func (_ *Subnet) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Su
return t.AddOutputVariableArray("subnet_ids", terraformWriter.LiteralFromStringValue(*e.ID))
}

if strings.HasPrefix(aws.StringValue(e.IPv6CIDR), "cidrsubnet-") {
// TODO: Implement using "cidrsubnet"
// https://www.terraform.io/docs/language/functions/cidrsubnet.html
return fmt.Errorf("<cidrsubnet> in not upported with Terraform target: %q", aws.StringValue(e.IPv6CIDR))
}

tf := &terraformSubnet{
VPCID: e.VPC.TerraformLink(),
CIDR: e.CIDR,
Expand Down Expand Up @@ -308,6 +350,12 @@ func (_ *Subnet) RenderCloudformation(t *cloudformation.CloudformationTarget, a,
return nil
}

if strings.HasPrefix(aws.StringValue(e.IPv6CIDR), "cidrsubnet-") {
// TODO: Implement using "Fn::Cidr"
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-cidr.html
return fmt.Errorf("<cidrsubnet> in not upported with CloudFormation target: %q", aws.StringValue(e.IPv6CIDR))
}

cf := &cloudformationSubnet{
VPCID: e.VPC.CloudformationLink(),
CIDR: e.CIDR,
Expand Down
180 changes: 180 additions & 0 deletions upup/pkg/fi/cloudup/awstasks/subnet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,186 @@ func TestSubnetCreate(t *testing.T) {
}
}

func TestSubnetCreateIPv6(t *testing.T) {
cloud := awsup.BuildMockAWSCloud("us-east-1", "abc")
c := &mockec2.MockEC2{}
cloud.MockEC2 = c

// We define a function so we can rebuild the tasks, because we modify in-place when running
buildTasks := func() map[string]fi.Task {
vpc1 := &VPC{
Name: s("vpc1"),
CIDR: s("172.20.0.0/16"),
IPv6CIDR: s("2001:db8::/56"),
Tags: map[string]string{"Name": "vpc1"},
}
cidr1 := &VPCAmazonIPv6CIDRBlock{
VPC: vpc1,
CIDRBlock: s("2001:db8::/56"),
}
subnet1 := &Subnet{
Name: s("subnet1"),
VPC: vpc1,
CIDR: s("172.20.1.0/24"),
IPv6CIDR: s("2001:db8:0:1::/64"),
Tags: map[string]string{"Name": "subnet1"},
}

return map[string]fi.Task{
"vpc1": vpc1,
"cidr1": cidr1,
"subnet1": subnet1,
}
}

{
allTasks := buildTasks()
subnet1 := allTasks["subnet1"].(*Subnet)

target := &awsup.AWSAPITarget{
Cloud: cloud,
}

context, err := fi.NewContext(target, nil, cloud, nil, nil, nil, true, allTasks)
if err != nil {
t.Fatalf("error building context: %v", err)
}
defer context.Close()

if err := context.RunTasks(testRunTasksOptions); err != nil {
t.Fatalf("unexpected error during Run: %v", err)
}

if fi.StringValue(subnet1.ID) == "" {
t.Fatalf("ID not set after create")
}

if len(c.SubnetIds()) != 1 {
t.Fatalf("Expected exactly one Subnet; found %v", c.SubnetIds())
}

expected := &ec2.Subnet{
CidrBlock: aws.String("172.20.1.0/24"),
Ipv6CidrBlockAssociationSet: []*ec2.SubnetIpv6CidrBlockAssociation{
{
AssociationId: aws.String("subnet-cidr-assoc-ipv6-subnet-1"),
Ipv6CidrBlock: aws.String("2001:db8:0:1::/64"),
Ipv6CidrBlockState: &ec2.SubnetCidrBlockState{
State: aws.String(ec2.SubnetCidrBlockStateCodeAssociated),
},
},
},
SubnetId: aws.String("subnet-1"),
VpcId: aws.String("vpc-1"),
Tags: buildTags(map[string]string{
"Name": "subnet1",
}),
}
actual := c.FindSubnet(*subnet1.ID)
if actual == nil {
t.Fatalf("Subnet created but then not found")
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("Unexpected Subnet: expected=%v actual=%v", expected, actual)
}
}

{
allTasks := buildTasks()
checkNoChanges(t, cloud, allTasks)
}
}

func TestSubnetCreateIPv6NetNum(t *testing.T) {
cloud := awsup.BuildMockAWSCloud("us-east-1", "abc")
c := &mockec2.MockEC2{}
cloud.MockEC2 = c

// We define a function so we can rebuild the tasks, because we modify in-place when running
buildTasks := func() map[string]fi.Task {
vpc1 := &VPC{
Name: s("vpc1"),
CIDR: s("172.20.0.0/16"),
IPv6CIDR: s("2001:db8::/56"),
Tags: map[string]string{"Name": "vpc1"},
}
cidr1 := &VPCAmazonIPv6CIDRBlock{
VPC: vpc1,
CIDRBlock: s("2001:db8::/56"),
}
subnet1 := &Subnet{
Name: s("subnet1"),
VPC: vpc1,
CIDR: s("172.20.1.0/24"),
IPv6CIDR: s("cidrsubnet-8-1"),
Tags: map[string]string{"Name": "subnet1"},
}

return map[string]fi.Task{
"vpc1": vpc1,
"cidr1": cidr1,
"subnet1": subnet1,
}
}

{
allTasks := buildTasks()
subnet1 := allTasks["subnet1"].(*Subnet)

target := &awsup.AWSAPITarget{
Cloud: cloud,
}

context, err := fi.NewContext(target, nil, cloud, nil, nil, nil, true, allTasks)
if err != nil {
t.Fatalf("error building context: %v", err)
}
defer context.Close()

if err := context.RunTasks(testRunTasksOptions); err != nil {
t.Fatalf("unexpected error during Run: %v", err)
}

if fi.StringValue(subnet1.ID) == "" {
t.Fatalf("ID not set after create")
}

if len(c.SubnetIds()) != 1 {
t.Fatalf("Expected exactly one Subnet; found %v", c.SubnetIds())
}

expected := &ec2.Subnet{
CidrBlock: aws.String("172.20.1.0/24"),
Ipv6CidrBlockAssociationSet: []*ec2.SubnetIpv6CidrBlockAssociation{
{
AssociationId: aws.String("subnet-cidr-assoc-ipv6-subnet-1"),
Ipv6CidrBlock: aws.String("2001:db8:0:1::/64"),
Ipv6CidrBlockState: &ec2.SubnetCidrBlockState{
State: aws.String(ec2.SubnetCidrBlockStateCodeAssociated),
},
},
},
SubnetId: aws.String("subnet-1"),
VpcId: aws.String("vpc-1"),
Tags: buildTags(map[string]string{
"Name": "subnet1",
}),
}
actual := c.FindSubnet(*subnet1.ID)
if actual == nil {
t.Fatalf("Subnet created but then not found")
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("Unexpected Subnet: expected=%v actual=%v", expected, actual)
}
}

{
allTasks := buildTasks()
checkNoChanges(t, cloud, allTasks)
}
}

func TestSharedSubnetCreateDoesNotCreateNew(t *testing.T) {
cloud := awsup.BuildMockAWSCloud("us-east-1", "abc")
c := &mockec2.MockEC2{}
Expand Down
2 changes: 2 additions & 0 deletions upup/pkg/fi/utils/BUILD.bazel

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c1d3249

Please sign in to comment.