diff --git a/pkg/apis/kops/validation/BUILD.bazel b/pkg/apis/kops/validation/BUILD.bazel index 4f5ca4939314f..f7b2f54d2c00b 100644 --- a/pkg/apis/kops/validation/BUILD.bazel +++ b/pkg/apis/kops/validation/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "//pkg/featureflag:go_default_library", "//pkg/model/components:go_default_library", "//pkg/model/iam:go_default_library", + "//pkg/util/subnet:go_default_library", "//upup/pkg/fi:go_default_library", "//upup/pkg/fi/cloudup/awsup:go_default_library", "//vendor/github.com/blang/semver:go_default_library", @@ -35,7 +36,6 @@ go_test( name = "go_default_test", srcs = [ "aws_test.go", - "helpers_test.go", "instancegroup_test.go", "validation_test.go", ], diff --git a/pkg/apis/kops/validation/helpers.go b/pkg/apis/kops/validation/helpers.go index 20da4d726a3c5..5931228b8690d 100644 --- a/pkg/apis/kops/validation/helpers.go +++ b/pkg/apis/kops/validation/helpers.go @@ -17,32 +17,11 @@ limitations under the License. package validation import ( - "net" "net/url" "k8s.io/apimachinery/pkg/util/validation/field" ) -// isSubnet checks if child is a subnet of parent -func isSubnet(parent *net.IPNet, child *net.IPNet) bool { - parentOnes, parentBits := parent.Mask.Size() - childOnes, childBits := child.Mask.Size() - if childBits != parentBits { - return false - } - if parentOnes > childOnes { - return false - } - childMasked := child.IP.Mask(parent.Mask) - parentMasked := parent.IP.Mask(parent.Mask) - return childMasked.Equal(parentMasked) -} - -// subnetsOverlap checks if two subnets overlap -func subnetsOverlap(l *net.IPNet, r *net.IPNet) bool { - return l.Contains(r.IP) || r.Contains(l.IP) -} - func isValidAPIServersURL(s string) bool { u, err := url.Parse(s) if err != nil { diff --git a/pkg/apis/kops/validation/helpers_test.go b/pkg/apis/kops/validation/helpers_test.go deleted file mode 100644 index c1a9130ebb3ce..0000000000000 --- a/pkg/apis/kops/validation/helpers_test.go +++ /dev/null @@ -1,75 +0,0 @@ -/* -Copyright 2016 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 validation - -import ( - "net" - "testing" -) - -func Test_isSubnet(t *testing.T) { - grid := []struct { - L string - R string - IsSubnet bool - }{ - { - L: "192.168.1.0/24", - R: "192.168.0.0/24", - IsSubnet: false, - }, - { - L: "192.168.0.0/16", - R: "192.168.0.0/24", - IsSubnet: true, - }, - { - L: "192.168.0.0/24", - R: "192.168.0.0/16", - IsSubnet: false, - }, - { - L: "192.168.0.0/16", - R: "192.168.0.0/16", - IsSubnet: true, // Not a strict subnet - }, - { - L: "192.168.0.1/16", - R: "192.168.0.0/24", - IsSubnet: true, - }, - { - L: "0.0.0.0/0", - R: "101.0.1.0/32", - IsSubnet: true, - }, - } - for _, g := range grid { - _, l, err := net.ParseCIDR(g.L) - if err != nil { - t.Fatalf("error parsing %q: %v", g.L, err) - } - _, r, err := net.ParseCIDR(g.R) - if err != nil { - t.Fatalf("error parsing %q: %v", g.R, err) - } - actual := isSubnet(l, r) - if actual != g.IsSubnet { - t.Errorf("isSubnet(%q, %q) = %v, expected %v", g.L, g.R, actual, g.IsSubnet) - } - } -} diff --git a/pkg/apis/kops/validation/legacy.go b/pkg/apis/kops/validation/legacy.go index 72195c5ff52f7..ce59692f93758 100644 --- a/pkg/apis/kops/validation/legacy.go +++ b/pkg/apis/kops/validation/legacy.go @@ -27,6 +27,7 @@ import ( "k8s.io/kops/pkg/apis/kops/util" "k8s.io/kops/pkg/featureflag" "k8s.io/kops/pkg/model/components" + "k8s.io/kops/pkg/util/subnet" "k8s.io/kops/upup/pkg/fi" "github.com/blang/semver" @@ -190,7 +191,7 @@ func ValidateCluster(c *kops.Cluster, strict bool) *field.Error { return field.Invalid(fieldSpec.Child("NonMasqueradeCIDR"), nonMasqueradeCIDRString, "Cluster had an invalid NonMasqueradeCIDR") } - if networkCIDR != nil && subnetsOverlap(nonMasqueradeCIDR, networkCIDR) && c.Spec.Networking != nil && c.Spec.Networking.AmazonVPC == nil { + if networkCIDR != nil && subnet.Overlap(nonMasqueradeCIDR, networkCIDR) && c.Spec.Networking != nil && c.Spec.Networking.AmazonVPC == nil { return field.Invalid(fieldSpec.Child("NonMasqueradeCIDR"), nonMasqueradeCIDRString, fmt.Sprintf("NonMasqueradeCIDR %q cannot overlap with NetworkCIDR %q", nonMasqueradeCIDRString, c.Spec.NetworkCIDR)) } @@ -220,7 +221,7 @@ func ValidateCluster(c *kops.Cluster, strict bool) *field.Error { return field.Invalid(fieldSpec.Child("ServiceClusterIPRange"), serviceClusterIPRangeString, "Cluster had an invalid ServiceClusterIPRange") } - if !isSubnet(nonMasqueradeCIDR, serviceClusterIPRange) { + if !subnet.BelongsTo(nonMasqueradeCIDR, serviceClusterIPRange) { return field.Invalid(fieldSpec.Child("ServiceClusterIPRange"), serviceClusterIPRangeString, fmt.Sprintf("ServiceClusterIPRange %q must be a subnet of NonMasqueradeCIDR %q", serviceClusterIPRangeString, c.Spec.NonMasqueradeCIDR)) } @@ -266,7 +267,7 @@ func ValidateCluster(c *kops.Cluster, strict bool) *field.Error { return field.Invalid(fieldSpec.Child("KubeControllerManager", "ClusterCIDR"), clusterCIDRString, "Cluster had an invalid KubeControllerManager.ClusterCIDR") } - if !isSubnet(nonMasqueradeCIDR, clusterCIDR) { + if !subnet.BelongsTo(nonMasqueradeCIDR, clusterCIDR) { return field.Invalid(fieldSpec.Child("KubeControllerManager", "ClusterCIDR"), clusterCIDRString, fmt.Sprintf("KubeControllerManager.ClusterCIDR %q must be a subnet of NonMasqueradeCIDR %q", clusterCIDRString, c.Spec.NonMasqueradeCIDR)) } } @@ -625,12 +626,12 @@ func ValidateCluster(c *kops.Cluster, strict bool) *field.Error { // validateSubnetCIDR is responsible for validating subnets are part of the CIRDs assigned to the cluster. func validateSubnetCIDR(networkCIDR *net.IPNet, additionalNetworkCIDRs []*net.IPNet, subnetCIDR *net.IPNet) bool { - if isSubnet(networkCIDR, subnetCIDR) { + if subnet.BelongsTo(networkCIDR, subnetCIDR) { return true } for _, additionalNetworkCIDR := range additionalNetworkCIDRs { - if isSubnet(additionalNetworkCIDR, subnetCIDR) { + if subnet.BelongsTo(additionalNetworkCIDR, subnetCIDR) { return true } } diff --git a/pkg/util/subnet/BUILD.bazel b/pkg/util/subnet/BUILD.bazel new file mode 100644 index 0000000000000..a34f27752eaa1 --- /dev/null +++ b/pkg/util/subnet/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["subnet.go"], + importpath = "k8s.io/kops/pkg/util/subnet", + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["subnet_test.go"], + embed = [":go_default_library"], +) diff --git a/pkg/util/subnet/subnet.go b/pkg/util/subnet/subnet.go new file mode 100644 index 0000000000000..b15651ceacbd5 --- /dev/null +++ b/pkg/util/subnet/subnet.go @@ -0,0 +1,69 @@ +/* +Copyright 2016 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 subnet + +import ( + "encoding/binary" + "fmt" + "net" +) + +// Overlap checks if two subnets overlap +func Overlap(l, r *net.IPNet) bool { + return l.Contains(r.IP) || r.Contains(l.IP) +} + +// BelongsTo checks if child is a subnet of parent +func BelongsTo(parent *net.IPNet, child *net.IPNet) bool { + parentOnes, parentBits := parent.Mask.Size() + childOnes, childBits := child.Mask.Size() + if childBits != parentBits { + return false + } + if parentOnes > childOnes { + return false + } + childMasked := child.IP.Mask(parent.Mask) + parentMasked := parent.IP.Mask(parent.Mask) + return childMasked.Equal(parentMasked) +} + +// SplitInto8 splits the parent IPNet into 8 subnets +func SplitInto8(parent *net.IPNet) ([]*net.IPNet, error) { + networkLength, _ := parent.Mask.Size() + networkLength += 3 + + var subnets []*net.IPNet + for i := 0; i < 8; i++ { + ip4 := parent.IP.To4() + if ip4 != nil { + n := binary.BigEndian.Uint32(ip4) + n += uint32(i) << uint(32-networkLength) + subnetIP := make(net.IP, len(ip4)) + binary.BigEndian.PutUint32(subnetIP, n) + + subnets = append(subnets, &net.IPNet{ + IP: subnetIP, + Mask: net.CIDRMask(networkLength, 32), + }) + } else { + return nil, fmt.Errorf("Unexpected IP address type: %s", parent) + } + } + + return subnets, nil +} diff --git a/pkg/util/subnet/subnet_test.go b/pkg/util/subnet/subnet_test.go new file mode 100644 index 0000000000000..1b3e58b8b6cfa --- /dev/null +++ b/pkg/util/subnet/subnet_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2016 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 subnet + +import ( + "net" + "reflect" + "testing" +) + +func Test_BelongsTo(t *testing.T) { + grid := []struct { + L string + R string + Belongs bool + }{ + { + L: "192.168.1.0/24", + R: "192.168.0.0/24", + Belongs: false, + }, + { + L: "192.168.0.0/16", + R: "192.168.0.0/24", + Belongs: true, + }, + { + L: "192.168.0.0/24", + R: "192.168.0.0/16", + Belongs: false, + }, + { + L: "192.168.0.0/16", + R: "192.168.0.0/16", + Belongs: true, // Not a strict subnet + }, + { + L: "192.168.0.1/16", + R: "192.168.0.0/24", + Belongs: true, + }, + { + L: "0.0.0.0/0", + R: "101.0.1.0/32", + Belongs: true, + }, + } + for _, g := range grid { + _, l, err := net.ParseCIDR(g.L) + if err != nil { + t.Fatalf("error parsing %q: %v", g.L, err) + } + _, r, err := net.ParseCIDR(g.R) + if err != nil { + t.Fatalf("error parsing %q: %v", g.R, err) + } + actual := BelongsTo(l, r) + if actual != g.Belongs { + t.Errorf("isSubnet(%q, %q) = %v, expected %v", g.L, g.R, actual, g.Belongs) + } + } +} + +func Test_SplitInto8(t *testing.T) { + tests := []struct { + parent string + expected []string + }{ + { + parent: "1.2.3.0/24", + expected: []string{"1.2.3.0/27", "1.2.3.32/27", "1.2.3.64/27", "1.2.3.96/27", "1.2.3.128/27", "1.2.3.160/27", "1.2.3.192/27", "1.2.3.224/27"}, + }, + { + parent: "1.2.3.0/27", + expected: []string{"1.2.3.0/30", "1.2.3.4/30", "1.2.3.8/30", "1.2.3.12/30", "1.2.3.16/30", "1.2.3.20/30", "1.2.3.24/30", "1.2.3.28/30"}, + }, + } + for _, test := range tests { + _, parent, err := net.ParseCIDR(test.parent) + if err != nil { + t.Fatalf("error parsing parent cidr %q: %v", test.parent, err) + } + + subnets, err := SplitInto8(parent) + if err != nil { + t.Fatalf("error splitting parent cidr %q: %v", parent, err) + } + + var actual []string + for _, subnet := range subnets { + actual = append(actual, subnet.String()) + } + if !reflect.DeepEqual(actual, test.expected) { + t.Fatalf("unexpected result of split: actual=%v, expected=%v", actual, test.expected) + } + } +} diff --git a/upup/pkg/fi/cloudup/BUILD.bazel b/upup/pkg/fi/cloudup/BUILD.bazel index 8d70ce10b60d9..9219a3244b38e 100644 --- a/upup/pkg/fi/cloudup/BUILD.bazel +++ b/upup/pkg/fi/cloudup/BUILD.bazel @@ -53,6 +53,7 @@ go_library( "//pkg/resources/digitalocean:go_default_library", "//pkg/resources/spotinst:go_default_library", "//pkg/templates:go_default_library", + "//pkg/util/subnet:go_default_library", "//upup/models:go_default_library", "//upup/pkg/fi:go_default_library", "//upup/pkg/fi/assettasks:go_default_library", diff --git a/upup/pkg/fi/cloudup/subnets.go b/upup/pkg/fi/cloudup/subnets.go index 36278bb563db5..4ca4f01eed68e 100644 --- a/upup/pkg/fi/cloudup/subnets.go +++ b/upup/pkg/fi/cloudup/subnets.go @@ -17,13 +17,13 @@ limitations under the License. package cloudup import ( - "encoding/binary" "fmt" "net" "sort" "github.com/golang/glog" "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/pkg/util/subnet" "k8s.io/kops/upup/pkg/fi" ) @@ -109,7 +109,7 @@ func assignCIDRsToSubnets(c *kops.Cluster) error { // TODO: Does this make sense on GCE? // TODO: Should we limit this to say 1000 IPs per subnet? (any reason to?) - bigCIDRs, err := splitInto8Subnets(cidr) + bigCIDRs, err := subnet.SplitInto8(cidr) if err != nil { return err } @@ -147,7 +147,7 @@ func assignCIDRsToSubnets(c *kops.Cluster) error { for _, c := range bigCIDRs { overlapped := false for _, r := range reserved { - if cidrsOverlap(r, c) { + if subnet.Overlap(r, c) { overlapped = true } } @@ -162,7 +162,7 @@ func assignCIDRsToSubnets(c *kops.Cluster) error { return fmt.Errorf("could not find any non-overlapping CIDRs in parent NetworkCIDR; cannot automatically assign CIDR to subnet") } - littleCIDRs, err := splitInto8Subnets(bigCIDRs[0]) + littleCIDRs, err := subnet.SplitInto8(bigCIDRs[0]) if err != nil { return err } @@ -203,32 +203,6 @@ func assignCIDRsToSubnets(c *kops.Cluster) error { return nil } -// splitInto8Subnets splits the parent IPNet into 8 subnets -func splitInto8Subnets(parent *net.IPNet) ([]*net.IPNet, error) { - networkLength, _ := parent.Mask.Size() - networkLength += 3 - - var subnets []*net.IPNet - for i := 0; i < 8; i++ { - ip4 := parent.IP.To4() - if ip4 != nil { - n := binary.BigEndian.Uint32(ip4) - n += uint32(i) << uint(32-networkLength) - subnetIP := make(net.IP, len(ip4)) - binary.BigEndian.PutUint32(subnetIP, n) - - subnets = append(subnets, &net.IPNet{ - IP: subnetIP, - Mask: net.CIDRMask(networkLength, 32), - }) - } else { - return nil, fmt.Errorf("Unexpected IP address type: %s", parent) - } - } - - return subnets, nil -} - // allSubnetsHaveCIDRs returns true iff each subnet in the cluster has a non-empty CIDR func allSubnetsHaveCIDRs(c *kops.Cluster) bool { for i := range c.Spec.Subnets { @@ -240,8 +214,3 @@ func allSubnetsHaveCIDRs(c *kops.Cluster) bool { return true } - -// cidrsOverlap returns true iff the two CIDRs are non-disjoint -func cidrsOverlap(l, r *net.IPNet) bool { - return l.Contains(r.IP) || r.Contains(l.IP) -} diff --git a/upup/pkg/fi/cloudup/subnets_test.go b/upup/pkg/fi/cloudup/subnets_test.go index c41e1ce08138c..27e496de0ba6d 100644 --- a/upup/pkg/fi/cloudup/subnets_test.go +++ b/upup/pkg/fi/cloudup/subnets_test.go @@ -17,48 +17,12 @@ limitations under the License. package cloudup import ( - "net" "reflect" "testing" "k8s.io/kops/pkg/apis/kops" ) -func Test_Split_Subnet(t *testing.T) { - tests := []struct { - parent string - expected []string - }{ - { - parent: "1.2.3.0/24", - expected: []string{"1.2.3.0/27", "1.2.3.32/27", "1.2.3.64/27", "1.2.3.96/27", "1.2.3.128/27", "1.2.3.160/27", "1.2.3.192/27", "1.2.3.224/27"}, - }, - { - parent: "1.2.3.0/27", - expected: []string{"1.2.3.0/30", "1.2.3.4/30", "1.2.3.8/30", "1.2.3.12/30", "1.2.3.16/30", "1.2.3.20/30", "1.2.3.24/30", "1.2.3.28/30"}, - }, - } - for _, test := range tests { - _, parent, err := net.ParseCIDR(test.parent) - if err != nil { - t.Fatalf("error parsing parent cidr %q: %v", test.parent, err) - } - - subnets, err := splitInto8Subnets(parent) - if err != nil { - t.Fatalf("error splitting parent cidr %q: %v", parent, err) - } - - var actual []string - for _, subnet := range subnets { - actual = append(actual, subnet.String()) - } - if !reflect.DeepEqual(actual, test.expected) { - t.Fatalf("unexpected result of split: actual=%v, expected=%v", actual, test.expected) - } - } -} - func Test_AssignSubnets(t *testing.T) { tests := []struct { subnets []kops.ClusterSubnetSpec