...

Source file src/edge-infra.dev/pkg/f8n/ipranger/ipranger.go

Documentation: edge-infra.dev/pkg/f8n/ipranger

     1  package ipranger
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/netip"
     8  	"strconv"
     9  	"strings"
    10  	"sync"
    11  
    12  	"go4.org/netipx"
    13  	"google.golang.org/api/compute/v1"
    14  )
    15  
    16  const Netmask = 21
    17  const rangePerSubnet = 30
    18  const rangePerRequest = 2
    19  const allIPs = "10.0.0.0/8"
    20  const gcpIPs = "10.128.0.0/9"
    21  
    22  type IPRanger struct {
    23  	mu        sync.Mutex
    24  	subnetSvc computeSubnetSvc
    25  	// stores maps gcp projects to subnet range information
    26  	stores map[string]*rangeStore
    27  }
    28  
    29  type Subnet struct {
    30  	Network         string
    31  	Name            string
    32  	Ref             string
    33  	region          string
    34  	aliasRangeQuota int
    35  	ipRange         string
    36  }
    37  
    38  type rangeStore struct {
    39  	// subnets map is e.g.:
    40  	// region0:
    41  	//   subnet0: {}
    42  	//   subnet1: {}
    43  	// region1:
    44  	//   subnet2: {}
    45  	subnets map[string]map[string]Subnet
    46  	ipset   *netipx.IPSet
    47  }
    48  
    49  func New(ctx context.Context) (*IPRanger, error) {
    50  	subnetSvc, err := newGCPSubnetService(ctx)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  	return &IPRanger{
    55  		subnetSvc: subnetSvc,
    56  	}, nil
    57  }
    58  
    59  // FindOrCreateSubnet returns an available subnet if one exists, otherwise
    60  // it creates and returns information about a new one. This is the primary
    61  // function that should be used by callers, as it controls concurrent access
    62  // to the underlying state and performs the entire sequence of operations
    63  // needed to get a suitable subnet
    64  func (r *IPRanger) FindOrCreateSubnet(project, region string) (Subnet, error) {
    65  	r.mu.Lock()
    66  	defer r.mu.Unlock()
    67  
    68  	// get current state from gcp
    69  	err := r.InitState(project)
    70  	if err != nil {
    71  		return Subnet{}, fmt.Errorf("failed to initialize state. err: %v", err)
    72  	}
    73  
    74  	// find subnet with range quota
    75  	ok, s := r.FindAvailableSubnet(project, region)
    76  	if ok {
    77  		return s, nil
    78  	}
    79  
    80  	// if no subnets have quota, create new one
    81  	s, err = r.CreateNewSubnet(project, region)
    82  	if err != nil {
    83  		return Subnet{}, fmt.Errorf("failed to create subnet. region: %v err: %v", region, err)
    84  	}
    85  	return s, nil
    86  }
    87  
    88  // CreateNewSubnet needs no introduction
    89  func (r *IPRanger) CreateNewSubnet(project, region string) (Subnet, error) {
    90  	// find unused subnet ip range
    91  	prefix, _, ok := r.stores[project].ipset.RemoveFreePrefix(Netmask)
    92  	if !ok {
    93  		return Subnet{}, errors.New("no ip ranges remaining")
    94  	}
    95  	name := r.nextSubnetName(project)
    96  	newSubnet, err := r.subnetSvc.Create(project, prefix.String(), region, name)
    97  	if err != nil {
    98  		return Subnet{}, err
    99  	}
   100  
   101  	// if successful, update store to use ipset with new range removed
   102  	// and include new subnet
   103  	// r.store.ipset = newSet
   104  	// r.store.subnets[region][subnet.Name] = subnet
   105  	return fromComputeSubnetwork(newSubnet), nil
   106  }
   107  
   108  // FindAvailableSubnet locates a subnet that has enough spare secondary ranges
   109  // to be suitable for use in the creation of a gke cluster
   110  func (r *IPRanger) FindAvailableSubnet(project, region string) (ok bool, s Subnet) {
   111  	// find existing subnet with available alias range quota
   112  	subnets := r.stores[project].subnets[region]
   113  	if subnets == nil {
   114  		return false, Subnet{}
   115  	}
   116  	for _, s := range subnets {
   117  		if s.aliasRangeQuota >= rangePerRequest {
   118  			return true, s
   119  		}
   120  	}
   121  
   122  	return false, Subnet{}
   123  }
   124  
   125  // nextSubnetName finds the next unused name that can be used to create
   126  // a new subnet
   127  func (r *IPRanger) nextSubnetName(project string) string {
   128  	var next int64
   129  	for _, v := range r.stores[project].subnets {
   130  		for name := range v {
   131  			snum := strings.TrimPrefix(name, "subnet")
   132  			if snum == name {
   133  				// name wasnt prefixed with "subnet", maybe "default", thats fine
   134  				// since it wont overlap with this naming scheme
   135  				continue
   136  			}
   137  			// if we cant parse a number, the name was malformed and thus wont
   138  			// collide with the new name we provide. thats also fine
   139  			num, _ := strconv.ParseInt(snum, 10, 64)
   140  			if num >= next {
   141  				// if max is N, next num is N+1
   142  				next = num + 1
   143  			}
   144  		}
   145  	}
   146  	return fmt.Sprintf("subnet%d", next)
   147  }
   148  
   149  func fromComputeSubnetwork(csn *compute.Subnetwork) Subnet {
   150  	return Subnet{
   151  		Network:         csn.Network,
   152  		Name:            csn.Name,
   153  		Ref:             csn.SelfLink,
   154  		region:          fromRegionURI(csn.Region),
   155  		aliasRangeQuota: rangePerSubnet - len(csn.SecondaryIpRanges),
   156  		ipRange:         csn.IpCidrRange,
   157  	}
   158  }
   159  
   160  // buildRangeStore converts gcp types to a local representation with additional
   161  // information, like a processable ipset
   162  func buildRangeStore(subnets computeSubnetAggregatedList) (*rangeStore, error) {
   163  	store := &rangeStore{
   164  		subnets: map[string]map[string]Subnet{},
   165  	}
   166  
   167  	var existingIPs netipx.IPSetBuilder
   168  	for regionRegion, v := range subnets {
   169  		// aggregated list keys are e.g. regions/us-east1
   170  		rr := strings.Split(regionRegion, "/")
   171  		if len(rr) != 2 {
   172  			return nil, fmt.Errorf("invalid region key %v. expected 'regions/$region'", regionRegion)
   173  		}
   174  		region := rr[1]
   175  		store.subnets[region] = map[string]Subnet{}
   176  		// each region key in region->subnets
   177  		for _, sn := range v.Subnetworks {
   178  			subnet := fromComputeSubnetwork(sn)
   179  
   180  			// parse ip cidr ranges into set
   181  			subnetPrefix, err := netip.ParsePrefix(subnet.ipRange)
   182  			if err != nil {
   183  				return nil, fmt.Errorf("failed to parse ip and prefix from subnet range '%s', err: %v", subnet.ipRange, err)
   184  			}
   185  			existingIPs.AddPrefix(subnetPrefix)
   186  			for _, aliasRange := range sn.SecondaryIpRanges {
   187  				aliasPrefix, err := netip.ParsePrefix(aliasRange.IpCidrRange)
   188  				if err != nil {
   189  					return nil, fmt.Errorf("failed to parse ip and prefix from secondary range '%s', err: %v", aliasRange.IpCidrRange, err)
   190  				}
   191  				existingIPs.AddPrefix(aliasPrefix)
   192  			}
   193  
   194  			store.subnets[region][subnet.Name] = subnet
   195  		}
   196  	}
   197  	existingSet, err := existingIPs.IPSet()
   198  	if err != nil {
   199  		return nil, fmt.Errorf("failed to build IPSet, err: %v", err)
   200  	}
   201  
   202  	var availableIPs netipx.IPSetBuilder
   203  	// start with all private ips
   204  	availableIPs.AddPrefix(netip.MustParsePrefix(allIPs))
   205  	// remove in use ranges
   206  	availableIPs.RemoveSet(existingSet)
   207  	// remove ips reserved by gcp
   208  	availableIPs.RemovePrefix(netip.MustParsePrefix(gcpIPs))
   209  	// build available set from what remains
   210  	availableSet, err := availableIPs.IPSet()
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	store.ipset = availableSet
   215  
   216  	return store, nil
   217  }
   218  
   219  // initState fetches the current state of networks from gcp and builds
   220  // a local representation for further processing
   221  func (r *IPRanger) InitState(project string) error {
   222  	if r.stores == nil {
   223  		r.stores = make(map[string]*rangeStore)
   224  	}
   225  	subnets, err := r.subnetSvc.List(project)
   226  	if err != nil {
   227  		return fmt.Errorf("failed to list subnets. err: %v", err)
   228  	}
   229  
   230  	store, err := buildRangeStore(subnets)
   231  	if err != nil {
   232  		return err
   233  	}
   234  	r.stores[project] = store
   235  
   236  	return nil
   237  }
   238  
   239  func fromRegionURI(uri string) string {
   240  	split := strings.Split(uri, "/")
   241  	return split[len(split)-1]
   242  }
   243  
   244  func toRegionURI(project, region string) string {
   245  	return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s", project, region)
   246  }
   247  

View as plain text