diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 376336cb0..9fda6c4d0 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,14 +1,17 @@ # v0.1.0 -* IP conflicts: By default, Cloud Filestore creation will pick an unused IP subnet to allocate its +* IP conflicts: By default, Cloud Filestore creation will pick an unused IP range to allocate its service in. This may conflict with other GCP services that do not explicitly - reserve IP subnets, such as GKE non-IP alias clusters or GKE TPUs. To avoid - IP conflicts, it is recommended to explicitly allocate IP subnets to each GCP - service, and this plugin. - * IP reservation for this driver is a future enhancement. - * GKE Pod and Service CIDRs can be reserved during cluster creation using the - `--cluster-ipv4-cidr` and `--services-ipv4-cidr` flags. - * GKE TPU CIDRs can be reserved during cluster creation using the - `--tpu-ipv4-cidr` flag. + reserve IP ranges, such as GKE non-IP alias clusters or GKE TPUs. To avoid + IP conflicts, it is recommended to either: + * Use a GKE cluster with [Alias IPs](https://cloud.google.com/kubernetes-engine/docs/how-to/alias-ips) + * This will prevent IP conflicts with GKE Pod and Service IPs, but not TPUs. + * Explicitly allocate IP ranges to each GCP service, and this plugin. + * IP range reservation for this driver is a future enhancement. + * GKE Pod and Service CIDRs can be reserved during [cluster creation](https://cloud.google.com/sdk/gcloud/reference/container/clusters/create) + using the `--cluster-ipv4-cidr` flag. + * GKE TPU CIDRs can be reserved during [cluster + creation](https://cloud.google.com/sdk/gcloud/reference/beta/container/clusters/create) + using the `--tpu-ipv4-cidr` flag. * Locality of CSI driver and Cloud Filestore instances: If no location is specified in the CreateVolume parameters, then by default the driver will pick the zone that it is currently running in. This could result in CreateVolume failures if diff --git a/pkg/cloud_provider/file/fake.go b/pkg/cloud_provider/file/fake.go index 4c60cd0af..8d4852fd6 100644 --- a/pkg/cloud_provider/file/fake.go +++ b/pkg/cloud_provider/file/fake.go @@ -72,3 +72,27 @@ func (manager *fakeServiceManager) GetInstance(ctx context.Context, obj *Service }, } } + +func (manager *fakeServiceManager) ListInstances(ctx context.Context, obj *ServiceInstance) ([]*ServiceInstance, error) { + instances := []*ServiceInstance{ + { + Project: "test-project", + Location: "test-location", + Name: "test", + Tier: "test_tier", + Network: Network{ + ReservedIpRange: "192.168.92.32/29", + }, + }, + { + Project: "test-project", + Location: "test-location", + Name: "test", + Tier: "test_tier", + Network: Network{ + ReservedIpRange: "192.168.92.40/29", + }, + }, + } + return instances, nil +} diff --git a/pkg/cloud_provider/file/file.go b/pkg/cloud_provider/file/file.go index ff403df00..91f7be497 100644 --- a/pkg/cloud_provider/file/file.go +++ b/pkg/cloud_provider/file/file.go @@ -56,6 +56,7 @@ type Service interface { CreateInstance(ctx context.Context, obj *ServiceInstance) (*ServiceInstance, error) DeleteInstance(ctx context.Context, obj *ServiceInstance) error GetInstance(ctx context.Context, obj *ServiceInstance) (*ServiceInstance, error) + ListInstances(ctx context.Context, obj *ServiceInstance) ([]*ServiceInstance, error) } type gcfsServiceManager struct { @@ -231,6 +232,25 @@ func (manager *gcfsServiceManager) DeleteInstance(ctx context.Context, obj *Serv return nil } +// ListInstances returns a list of active instances in a project at a specific location +func (manager *gcfsServiceManager) ListInstances(ctx context.Context, obj *ServiceInstance) ([]*ServiceInstance, error) { + // Calling cloud provider service to get list of active instances. - indicates we are looking for instances in all the locations for a project + instances, err := manager.instancesService.List(locationURI(obj.Project, "-")).Context(ctx).Do() + if err != nil { + return nil, err + } + + var activeInstances []*ServiceInstance + for _, activeInstance := range instances.Instances { + serviceInstance, err := cloudInstanceToServiceInstance(activeInstance) + if err != nil { + return nil, err + } + activeInstances = append(activeInstances, serviceInstance) + } + return activeInstances, nil +} + func (manager *gcfsServiceManager) waitForOp(ctx context.Context, op *beta.Operation) error { return wait.Poll(5*time.Second, 5*time.Minute, func() (bool, error) { pollOp, err := manager.operationsService.Get(op.Name).Context(ctx).Do() diff --git a/pkg/csi_driver/controller.go b/pkg/csi_driver/controller.go index a5c4d1044..eb5c893aa 100644 --- a/pkg/csi_driver/controller.go +++ b/pkg/csi_driver/controller.go @@ -48,9 +48,10 @@ const ( // CreateVolume parameters const ( - paramTier = "tier" - paramLocation = "location" - paramNetwork = "network" + paramTier = "tier" + paramLocation = "location" + paramNetwork = "network" + paramReservedIPV4CIDR = "reserved-ipv4-cidr" ) // controllerServer handles volume provisioning @@ -62,9 +63,11 @@ type controllerServerConfig struct { driver *GCFSDriver fileService file.Service metaService metadata.Service + ipAllocator *util.IPAllocator } func newControllerServer(config *controllerServerConfig) csi.ControllerServer { + config.ipAllocator = util.NewIPAllocator(make(map[string]bool)) return &controllerServer{config: config} } @@ -101,6 +104,23 @@ func (s *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolu return nil, status.Error(codes.AlreadyExists, err.Error()) } } else { + // If we are creating a new instance, we need pick an unused /29 range from reserved-ipv4-cidr + // If the param was not provided, we default reservedIPRange to "" and cloud provider takes care of the allocation + if reservedIPV4CIDR, ok := req.GetParameters()[paramReservedIPV4CIDR]; ok { + reservedIPRange, err := s.reserveIPRange(ctx, newFiler, reservedIPV4CIDR) + + // Possible cases are 1) CreateInstanceAborted, 2)CreateInstance running in background + // The ListInstances response will contain the reservedIPRange if the operation was started + // In case of abort, the /29 IP is released and available for reservation + defer s.config.ipAllocator.ReleaseIPRange(reservedIPRange) + if err != nil { + return nil, err + } + + // Adding the reserved IP range to the instance object + newFiler.Network.ReservedIpRange = reservedIPRange + } + // Create the instance filer, err = s.config.fileService.CreateInstance(ctx, newFiler) if err != nil { @@ -110,6 +130,33 @@ func (s *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolu return &csi.CreateVolumeResponse{Volume: fileInstanceToCSIVolume(filer, modeInstance)}, nil } +// reserveIPRange returns the available IP in the cidr +func (s *controllerServer) reserveIPRange(ctx context.Context, filer *file.ServiceInstance, cidr string) (string, error) { + cloudInstancesReservedIPRanges, err := s.getCloudInstancesReservedIPRanges(ctx, filer) + if err != nil { + return "", err + } + unreservedIPBlock, err := s.config.ipAllocator.GetUnreservedIPRange(cidr, cloudInstancesReservedIPRanges) + if err != nil { + return "", err + } + return unreservedIPBlock, nil +} + +// getCloudInstancesReservedIPRanges gets the list of reservedIPRanges from cloud instances +func (s *controllerServer) getCloudInstancesReservedIPRanges(ctx context.Context, filer *file.ServiceInstance) (map[string]bool, error) { + instances, err := s.config.fileService.ListInstances(ctx, filer) + if err != nil { + return nil, status.Error(codes.Aborted, err.Error()) + } + // Initialize an empty reserved list. It will be populated with all the reservedIPRanges obtained from the cloud instances + cloudInstancesReservedIPRanges := make(map[string]bool) + for _, instance := range instances { + cloudInstancesReservedIPRanges[instance.Network.ReservedIpRange] = true + } + return cloudInstancesReservedIPRanges, nil +} + // DeleteVolume deletes a GCFS instance func (s *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) { glog.V(4).Infof("DeleteVolume called with request %v", *req) @@ -211,7 +258,6 @@ func (s *controllerServer) generateNewFileInstance(name string, capBytes int64, tier := defaultTier network := defaultNetwork location := s.config.metaService.GetZone() - // Validate parameters (case-insensitive). for k, v := range params { switch strings.ToLower(k) { @@ -222,7 +268,11 @@ func (s *controllerServer) generateNewFileInstance(name string, capBytes int64, location = v case paramNetwork: network = v - // Unused + + // Ignore the cidr flag as it is not passed to the cloud provider + // It will be used to get unreserved IP in the reserveIPV4Range function + case paramReservedIPV4CIDR: + continue case "csiprovisionersecretname", "csiprovisionersecretnamespace": default: return nil, fmt.Errorf("invalid parameter %q", k) @@ -235,7 +285,6 @@ func (s *controllerServer) generateNewFileInstance(name string, capBytes int64, Tier: tier, Network: file.Network{ Name: network, - // ReservedIpRange: "10.3.0.0/29", // TODO }, Volume: file.Volume{ Name: newInstanceVolume, diff --git a/pkg/csi_driver/controller_test.go b/pkg/csi_driver/controller_test.go index cf6ca4af3..7c2692c5a 100644 --- a/pkg/csi_driver/controller_test.go +++ b/pkg/csi_driver/controller_test.go @@ -28,12 +28,13 @@ import ( ) const ( - testProject = "test-project" - testLocation = "test-location" - testIp = "test-ip" - testCSIVolume = "test-csi" - testVolumeId = "modeInstance/test-location/test-csi/vol1" - testBytes = 1 * util.Tb + testProject = "test-project" + testLocation = "test-location" + testIp = "test-ip" + testCSIVolume = "test-csi" + testVolumeId = "modeInstance/test-location/test-csi/vol1" + testReservedIPV4CIDR = "192.168.92.0/26" + testBytes = 1 * util.Tb ) func initTestController(t *testing.T) csi.ControllerServer { diff --git a/pkg/util/ip_reservation.go b/pkg/util/ip_reservation.go new file mode 100644 index 000000000..9ac321a3b --- /dev/null +++ b/pkg/util/ip_reservation.go @@ -0,0 +1,202 @@ +/* +Copyright 2018 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 util + +import ( + "fmt" + "math" + "net" + "sync" +) + +const ( + // Size of the network address of the IPRange we intend to reserve + ipRangeSize = 29 + // Maximum value of a byte + byteMax = 255 + // Total number of bits in an IPV4 address + ipV4Bits = 32 +) + +var ( + // step size for IP range increment + incrementStep29IPRange = (byte)(math.Exp2(ipV4Bits - ipRangeSize)) + // mask for IP range + ipRangeMask = net.CIDRMask(ipRangeSize, ipV4Bits) +) + +// IPAllocator struct consists of shared resources that are used to keep track of the /29 IPRanges currently reserved by service instances +type IPAllocator struct { + // pendingIPRanges set maintains the set of IP ranges that have been reserved by the service instances but pending reservation in the cloud instances + // The key is a IP range currently reserved by a service instance e.g(192.168.92.0/29). Value is a bool to implement map as a set + pendingIPRanges map[string]bool + + // pendingIPRangesMutex is used to synchronize access to the pendingIPRanges set to prevent data races + pendingIPRangesMutex sync.Mutex +} + +// NewIPAllocator is the constructor to initialize the IPAllocator object +// Argument pendingIPRanges map[string]bool is a set of IP ranges currently reserved by service instances but pending reservation in the cloud instances +func NewIPAllocator(pendingIPRanges map[string]bool) *IPAllocator { + // Make a copy of the pending IP ranges and set it in the IPAllocator so that the caller cannot mutate this map outside the library + pendingIPRangesCopy := make(map[string]bool) + for pendingIPRange := range pendingIPRanges { + pendingIPRangesCopy[pendingIPRange] = true + } + return &IPAllocator{ + pendingIPRanges: pendingIPRangesCopy, + } +} + +// holdIPRange adds a particular IP range in the pendingIPRanges set +// Argument ipRange string is an IPV4 range which needs put in pendingIPRanges +func (ipAllocator *IPAllocator) holdIPRange(ipRange string) { + ipAllocator.pendingIPRanges[ipRange] = true +} + +// ReleaseIPRange releases the pending IPRange +// Argument ipRange string is an IPV4 range which needs to be released +func (ipAllocator *IPAllocator) ReleaseIPRange(ipRange string) { + ipAllocator.pendingIPRangesMutex.Lock() + defer ipAllocator.pendingIPRangesMutex.Unlock() + delete(ipAllocator.pendingIPRanges, ipRange) +} + +// GetUnreservedIPRange returns an unreserved /29 IP block. +// cidr: Provided cidr address in which we need to look for an unreserved /29 IP range +// cloudInstancesReservedIPRanges: All the used IP ranges in the cloud instances +// All the used IP ranges in the service instances not updated in cloud instances is extracted from the pendingIPRanges list in the IPAllocator +// Finally a final reservedIPRange list is created by merging these two lists +// Potential error cases: +// 1) No /29 IP range in the CIDR is unreserved +// 2) Parsing the CIDR resulted in an error +func (ipAllocator *IPAllocator) GetUnreservedIPRange(cidr string, cloudInstancesReservedIPRanges map[string]bool) (string, error) { + ip, ipnet, err := ipAllocator.parseCIDR(cidr) + if err != nil { + return "", err + } + var reservedIPRanges = make(map[string]bool) + + // The final reserved list is obtained by combining the cloudInstancesReservedIPRanges list and the pendingIPRanges list in the ipAllocator + for cloudInstancesReservedIPRange := range cloudInstancesReservedIPRanges { + reservedIPRanges[cloudInstancesReservedIPRange] = true + } + + // Lock is placed here so that the pendingIPRanges list captures all the IPs pending reservation in the cloud instances + ipAllocator.pendingIPRangesMutex.Lock() + defer ipAllocator.pendingIPRangesMutex.Unlock() + for reservedIPRange := range ipAllocator.pendingIPRanges { + reservedIPRanges[reservedIPRange] = true + } + + for cidrIP := cloneIP(ip.Mask(ipnet.Mask)); ipnet.Contains(cidrIP) && err == nil; cidrIP, err = incrementIP(cidrIP, incrementStep29IPRange) { + overLap := false + for reservedIPRange := range reservedIPRanges { + _, reservedIPNet, err := net.ParseCIDR(reservedIPRange) + if err != nil { + return "", err + } + // Creating IPnet object using IP and mask + cidrIPNet := &net.IPNet{ + IP: cidrIP, + Mask: ipRangeMask, + } + + // Find if the current IP range in the CIDR overlaps with any of the reserved IP ranges. If not, this can be returned + overLap, err = isOverlap(cidrIPNet, reservedIPNet) + + // Error while processing ipnet + if err != nil { + return "", err + } + if overLap { + break + } + } + if !overLap { + ipRange := fmt.Sprint(cidrIP.String(), "/", ipRangeSize) + ipAllocator.holdIPRange(ipRange) + return ipRange, nil + } + } + + // No unreserved IP range available in the entire CIDR range since we did not return + return "", fmt.Errorf("all of the /29 IP ranges in the cidr %s are reserved", cidr) +} + +// isOverlap checks if two ipnets have any overlapping IPs +func isOverlap(ipnet1 *net.IPNet, ipnet2 *net.IPNet) (bool, error) { + if ipnet1 == nil || ipnet2 == nil { + return true, fmt.Errorf("invalid ipnet object provided for cidr overlap check") + } + return ipnet1.Contains(ipnet2.IP) || ipnet2.Contains(ipnet1.IP), nil +} + +// ParseCIDR function parses the CIDR and returns the ip and ipnet object if the cidr is valid +// For a CIDR to be valid it must satisfy the following properties +// 1) Network address bits must be less than 30 +// 2) The IP in the CIDR must be 'aligned' i.e we must have 8 available IPs before byte overflow occurs +func (ipAllocator *IPAllocator) parseCIDR(cidr string) (net.IP, *net.IPNet, error) { + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, nil, err + } + // The cidr network address needs to be maximum 29 bits + cidrSize, _ := ipnet.Mask.Size() + if cidrSize > ipRangeSize { + return nil, nil, fmt.Errorf("the maximum size of network address in the cidr can be /%d", ipRangeSize) + } + + // The IP specified in the reserved-ipv4-cidr must be aligned to cover /29 IP Range + if ip.String() != ip.Mask(ipRangeMask).String() { + return nil, nil, fmt.Errorf("the IP specified in the reserved-ipv4-cidr must be aligned to cover /29 IP Range") + } + return ip, ipnet, nil +} + +// Increment the given IP value by the provided step. The step is a byte with maximum value maximum byte value +func incrementIP(ip net.IP, step byte) (net.IP, error) { + incrementedIP := cloneIP(ip) + incrementedIP = incrementedIP.To4() + + // Step can be added directly to the Least Significant Byte and we can return the result + if incrementedIP[3] < byteMax-step { + incrementedIP[3] += step + return incrementedIP, nil + } + + // Step addition in the Least Significant Byte resulted in overflow + // Propogating the carry addition to the higher order bytes and calculating value of the current byte + incrementedIP[3] = incrementedIP[3] - byteMax + step - 1 + + for ipByte := 2; ipByte >= 0; ipByte-- { + // Rollover occurs when value changes from maximum byte value to 0 as maximum propagated carry is 1 + if incrementedIP[ipByte] != byteMax { + incrementedIP[ipByte]++ + return incrementedIP, nil + } + incrementedIP[ipByte] = 0 + } + return nil, fmt.Errorf("ip range overflowed while incrementing IP %s by step %d", ip.String(), step) +} + +// Clone the provided IP and return the copy +func cloneIP(ip net.IP) net.IP { + clone := make(net.IP, len(ip)) + copy(clone, ip) + return clone +} diff --git a/pkg/util/ip_reservation_test.go b/pkg/util/ip_reservation_test.go new file mode 100644 index 000000000..16855ef2f --- /dev/null +++ b/pkg/util/ip_reservation_test.go @@ -0,0 +1,373 @@ +/* +Copyright 2018 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 util + +import ( + "fmt" + "net" + "testing" +) + +func initTestIPAllocator() *IPAllocator { + pendingIPRanges := make(map[string]bool) + return &IPAllocator{ + pendingIPRanges: pendingIPRanges, + } +} + +func TestParseCIDR(t *testing.T) { + cases := []struct { + name string + cidr string + ipExpected string + errorExpected bool + }{ + { + name: "Valid /29 CIDR format", + cidr: "192.168.92.192/29", + errorExpected: false, + }, + { + name: "Invalid CIDR format", + cidr: "192.168.92.192", + errorExpected: true, + }, + { + name: "Invalid CIDR format with network address greater than 29 bits", + cidr: "192.168.92.249/30", + errorExpected: true, + }, + { + name: "Misaligned CIDR with network address less than 29 bits", + cidr: "192.168.92.249/28", + errorExpected: true, + }, + { + name: "Valid CIDR format with network address less than 29 bits", + cidr: "192.168.92.248/28", + errorExpected: false, + }, + } + + ipAllocator := initTestIPAllocator() + for _, test := range cases { + ip, ipnet, err := ipAllocator.parseCIDR(test.cidr) + if test.errorExpected && err == nil { + t.Errorf("error while validating cidr %s, expected error while validating, got response as valid", test.cidr) + } else if !test.errorExpected && err != nil { + t.Errorf("error while validating cidr %s, expected valid response, got error %s", test.cidr, err.Error()) + } else if !test.errorExpected { + ipExpected, ipnetExpected, err := net.ParseCIDR(test.cidr) + // If parsing fails at this point, it implies test input is invalid + if err != nil { + t.Errorf("invalid CIDR %s provided as test input", test.cidr) + } + if !ipExpected.Equal(ipExpected) { + t.Errorf("test %q failed, expected ip %s but got %s", test.name, ipExpected.String(), ip.String()) + } + if ipnetExpected.String() != ipnet.String() { + t.Errorf("test %q failed, expected ipnet %s but got ipnet %s", test.name, ipnetExpected.String(), ipnet.String()) + } + } + } +} + +func TestGetUnReservedIPRange(t *testing.T) { + // Using IPs which are not the beginning IPs of /29 CIDRs to evaluate the edge case + ips := [8]string{"192.168.92.3/29", "192.168.92.10/29", "192.168.92.20/29", "192.168.92.28/29"} + cases := []struct { + name string + cidr string + pendingIPRanges map[string]bool + cloudProviderReservedIPRanges map[string]bool + expected string + errorExpected bool + }{ + { + name: "0 Pending, 0 Used", + cidr: "192.168.92.0/27", + pendingIPRanges: make(map[string]bool), + cloudProviderReservedIPRanges: make(map[string]bool), + expected: "192.168.92.0/29", + errorExpected: false, + }, + { + name: "0 Pending, 1 Used", + cidr: "192.168.92.0/27", + pendingIPRanges: make(map[string]bool), + cloudProviderReservedIPRanges: map[string]bool{ + ips[0]: true, + }, + expected: "192.168.92.8/29", + errorExpected: false, + }, + { + name: "1 Pending 0 Used", + cidr: "192.168.92.0/27", + pendingIPRanges: make(map[string]bool), + cloudProviderReservedIPRanges: map[string]bool{ + ips[0]: true, + }, + expected: "192.168.92.8/29", + errorExpected: false, + }, + { + name: "1 Pending 1 Used", + cidr: "192.168.92.0/27", + pendingIPRanges: map[string]bool{ + ips[0]: true, + }, + cloudProviderReservedIPRanges: map[string]bool{ + ips[1]: true, + }, + expected: "192.168.92.16/29", + errorExpected: false, + }, + { + name: "2 Pending 1 Used", + cidr: "192.168.92.0/27", + pendingIPRanges: map[string]bool{ + ips[0]: true, + ips[2]: true, + }, + cloudProviderReservedIPRanges: map[string]bool{ + ips[1]: true, + }, + expected: "192.168.92.24/29", + errorExpected: false, + }, + { + name: "Pending and used IPs out of CIDR range", + cidr: "192.168.92.0/27", + pendingIPRanges: map[string]bool{ + "192.168.33.33/29": true, + "192.168.44.44/29": true, + }, + cloudProviderReservedIPRanges: map[string]bool{ + "192.255.255.0/29": true, + "192.168.255.255/29": true, + }, + expected: "192.168.92.0/29", + errorExpected: false, + }, + { + name: "Unreserved IP Range obtained with carry over to significant bytes", + cidr: "192.168.0.0/16", + // Using a function for this case as we reserve 32 IP ranges + pendingIPRanges: getIPRanges("192.168.0.0/16", 32, t), + // Reserving IP ranges from 192.168.0.0/29 to 192.168.1.248 + cloudProviderReservedIPRanges: getIPRanges("192.168.1.0/16", 32, t), + expected: "192.168.2.0/29", + errorExpected: false, + }, + { + name: "2 Pending 2 Used. Unreserved IPRange unavailable", + cidr: "192.168.92.0/27", + pendingIPRanges: map[string]bool{ + ips[0]: true, + ips[2]: true, + }, + cloudProviderReservedIPRanges: map[string]bool{ + ips[1]: true, + ips[3]: true, + }, + errorExpected: true, + }, + } + + for _, test := range cases { + ipAllocator := initTestIPAllocator() + ipAllocator.pendingIPRanges = test.pendingIPRanges + ipRange, err := ipAllocator.GetUnreservedIPRange(test.cidr, test.cloudProviderReservedIPRanges) + if err != nil && !test.errorExpected { + t.Errorf("test %q failed: got error %s, expected %s", test.name, err.Error(), test.expected) + } else if err == nil && test.errorExpected { + t.Errorf("test %q failed: got reserved IP range %s, expected error", test.name, ipRange) + } else if ipRange != test.expected { + t.Errorf("test %q failed: got reserved IP range %s, expected %s", test.name, ipRange, test.expected) + } + } +} + +func getIPRanges(cidr string, ipRangesCount int, t *testing.T) map[string]bool { + ip, ipnet, err := net.ParseCIDR(cidr) + ipRangeMask := net.CIDRMask(ipRangeSize, ipV4Bits) + i := 0 + ipRanges := make(map[string]bool) + // Break out of the loop if + // 1) We have the required number of IP ranges in the set + // 2) IP range overflow occurs and IP increment is not possible + // 3) The incremented IP range is not contained in the cidr + for cidrIP := ip.Mask(ipRangeMask); ipnet.Contains(cidrIP) && err == nil && i < ipRangesCount; cidrIP, err = incrementIP(cidrIP, incrementStep29IPRange) { + i++ + ipRangeString := fmt.Sprint(cidrIP.String(), "/", ipRangeSize) + ipRanges[ipRangeString] = true + } + if err != nil { + t.Fatalf(err.Error()) + } else if i != ipRangesCount { + t.Fatalf("The required number of IP ranges %d are not available in the CIDR %s", ipRangesCount, cidr) + } + return ipRanges +} + +func TestValidateCIDROverlap(t *testing.T) { + cases := []struct { + name string + cidr1 string + cidr2 string + expected bool + errorExpected bool + }{ + { + name: "Overlapping CIDRs", + cidr1: "192.168.92.0/29", + cidr2: "192.168.92.48/26", + expected: true, + errorExpected: false, + }, + { + name: "Non overlapping CIDRs", + cidr1: "192.168.92.0/29", + cidr2: "192.168.22.67/26", + expected: false, + errorExpected: false, + }, + { + name: "Non overlapping CIDRs with same cidr size", + cidr1: "192.168.92.247/29", + cidr2: "192.168.92.248/29", + expected: false, + errorExpected: false, + }, + { + name: "Overlapping CIDRs with same cidr size", + cidr1: "192.168.92.249/29", + cidr2: "192.168.92.255/29", + expected: true, + errorExpected: false, + }, + { + name: "Invalid CIDR provided", + cidr1: "192.168.92.0", + cidr2: "192.168.22.67/26", + errorExpected: true, + }, + } + + for _, test := range cases { + _, ipnet1, _ := net.ParseCIDR(test.cidr1) + _, ipnet2, _ := net.ParseCIDR(test.cidr2) + overlap, err := isOverlap(ipnet1, ipnet2) + if err != nil && !test.errorExpected { + t.Errorf("test %q failed: got error %s, expected cidr overlap between %s and %s to be %t", test.name, err.Error(), test.cidr1, test.cidr2, test.expected) + } else if err == nil && test.errorExpected { + t.Errorf("test %q failed: got cidr overlap value %t, expected error", test.name, overlap) + } else if !test.errorExpected && overlap != test.expected { + t.Errorf("test %q failed: got overlap for cidr %s and %s as %t, expected %t", test.name, test.cidr1, test.cidr2, test.expected, test.expected) + } + } +} + +func TestIncrementIP(t *testing.T) { + + cases := []struct { + name string + currentIP string + step byte + expected string + errorExpected bool + }{ + { + name: "Valid IP increment with step size 143 without carry forward to significant bytes", + currentIP: "192.168.92.32", + step: 143, + expected: "192.168.92.175", + errorExpected: false, + }, + { + name: "Valid increment with step size 255 and carry forward to significant bytes with maximum step size", + currentIP: "192.255.255.253", + step: 255, + expected: "193.0.0.252", + errorExpected: false, + }, + { + name: "Valid increment with step size 8 without carry forward to significant bytes", + currentIP: "0.255.255.106", + step: 8, + expected: "0.255.255.114", + errorExpected: false, + }, + { + name: "Valid increment with step size 8 and carry forward uptil 3rd byte", + currentIP: "0.255.106.255", + step: 8, + expected: "0.255.107.7", + errorExpected: false, + }, + { + name: "Valid increment with step size 8 and carry forward uptil 2nd byte bytes", + currentIP: "255.106.255.255", + step: 8, + expected: "255.107.0.7", + errorExpected: false, + }, + { + name: "Valid increment with step size 8 and carry forward uptil 1st byte", + currentIP: "106.255.255.255", + step: 8, + expected: "107.0.0.7", + errorExpected: false, + }, + { + name: "Invalid increment with step size 3", + currentIP: "255.255.255.253", + step: 3, + errorExpected: true, + }, + { + name: "Invalid increment with step size 8", + currentIP: "255.255.255.253", + step: 8, + errorExpected: true, + }, + } + + for _, test := range cases { + currentIP := net.ParseIP(test.currentIP) + incrementedIP, err := incrementIP(currentIP, test.step) + + if err != nil && !test.errorExpected { + t.Errorf("test %q failed: got error %s, expected %s", test.name, err.Error(), test.expected) + } else if err == nil && test.errorExpected { + t.Errorf("test %q failed: got reserved IP range %s, expected error", test.name, incrementedIP.String()) + } else if !test.errorExpected && incrementedIP.String() != test.expected { + t.Errorf("test %q failed: got incremented IP %s, expected %s", test.name, incrementedIP.String(), test.expected) + } + } +} +func TestCloneIP(t *testing.T) { + originalIP := net.ParseIP("192.168.92.32") + cloneIP := cloneIP(originalIP) + if cloneIP.String() != originalIP.String() { + t.Errorf("error while cloning IP %s", originalIP.String()) + } + if &originalIP == &cloneIP { + t.Errorf("clone function returned the original object") + } +}