Skip to content

Commit

Permalink
Return existing reservation if podRef matches
Browse files Browse the repository at this point in the history
  • Loading branch information
xagent003 authored and Jayanth Reddy committed Feb 1, 2024
1 parent 2999b72 commit 23a153d
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 22 deletions.
24 changes: 14 additions & 10 deletions pkg/allocate/allocate.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,9 @@ func IterateForAssignment(ipnet net.IPNet, rangeStart net.IP, rangeEnd net.IP, r
logging.Debugf("IterateForAssignment input >> range_start: %v | range_end: %v | ipnet: %v | first IP: %v | last IP: %v",
rangeStart, rangeEnd, ipnet, firstIP, lastIP)

// Build reserved map.
reserved := make(map[string]bool)
for _, r := range reserveList {
reserved[r.IP.String()] = true
reserved := make(map[string]string)
for _, r := range reservelist {
reserved[r.IP.String()] = r.PodRef
}

// Build excluded list, "192.168.2.229/30", "192.168.1.229/30".
Expand All @@ -115,12 +114,17 @@ func IterateForAssignment(ipnet net.IPNet, rangeStart net.IP, rangeEnd net.IP, r
excluded = append(excluded, subnet)
}

// Iterate over every IP address in the range, accounting for reserved IPs and exclude ranges. Make sure that ip is
// within ipnet, and make sure that ip is smaller than lastIP.
for ip := firstIP; ipnet.Contains(ip) && iphelpers.CompareIPs(ip, lastIP) <= 0; ip = iphelpers.IncIP(ip) {
// If already reserved, skip it.
if reserved[ip.String()] {
continue
// Iterate every IP address in the range
var assignedip net.IP
performedassignment := false
endip := IPAddOffset(lastip, uint64(1))
for i := firstip; !i.Equal(endip); i = IPAddOffset(i, uint64(1)) {
// if already reserved, skip it
if reserved[i.String()] != "" {
if reserved[i.String()] != podRef {
continue
}
logging.Debugf("Found existing reservation %v with matching podRef %s", i.String(), podRef)
}
// If this IP is within the range of one of the excluded subnets, jump to the exluded subnet's broadcast address
// and skip.
Expand Down
247 changes: 247 additions & 0 deletions pkg/storage/etcd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package storage

import (
"context"
"fmt"
"net"
"strings"
"time"

"github.com/coreos/etcd/clientv3"

Check failure on line 10 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / e2e test

cannot query module due to -mod=vendor

Check failure on line 10 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, arm64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 10 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, amd64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 10 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / test (1.20.x, ubuntu-latest)

cannot query module due to -mod=vendor
"github.com/coreos/etcd/clientv3/concurrency"

Check failure on line 11 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / e2e test

cannot query module due to -mod=vendor

Check failure on line 11 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, arm64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 11 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, amd64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 11 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / test (1.20.x, ubuntu-latest)

cannot query module due to -mod=vendor
"github.com/coreos/etcd/pkg/transport"

Check failure on line 12 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / e2e test

cannot query module due to -mod=vendor

Check failure on line 12 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, arm64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 12 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, amd64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 12 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / test (1.20.x, ubuntu-latest)

cannot query module due to -mod=vendor
"github.com/dougbtv/whereabouts/pkg/allocate"

Check failure on line 13 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / e2e test

cannot query module due to -mod=vendor

Check failure on line 13 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, arm64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 13 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, amd64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 13 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / test (1.20.x, ubuntu-latest)

cannot query module due to -mod=vendor
"github.com/dougbtv/whereabouts/pkg/logging"

Check failure on line 14 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / e2e test

cannot query module due to -mod=vendor

Check failure on line 14 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, arm64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 14 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, amd64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 14 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / test (1.20.x, ubuntu-latest)

cannot query module due to -mod=vendor
"github.com/dougbtv/whereabouts/pkg/types"

Check failure on line 15 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / e2e test

cannot query module due to -mod=vendor

Check failure on line 15 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, arm64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 15 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / build (1.20.x, amd64, ubuntu-latest)

cannot query module due to -mod=vendor

Check failure on line 15 in pkg/storage/etcd.go

View workflow job for this annotation

GitHub Actions / test (1.20.x, ubuntu-latest)

cannot query module due to -mod=vendor
)

const whereaboutsPrefix = "/whereabouts"

var (
// DialTimeout defines how long we dial etcd
DialTimeout = 2 * time.Second
)

// NewETCDIPAM returns a new IPAM Client configured to an etcd backend
func NewETCDIPAM(ipamConf types.IPAMConfig) (*ETCDIPAM, error) {
cfg := clientv3.Config{
DialTimeout: DialTimeout,
Endpoints: []string{ipamConf.EtcdHost},
Username: ipamConf.EtcdUsername,
Password: ipamConf.EtcdPassword,
}
if cert, key := ipamConf.EtcdCertFile, ipamConf.EtcdKeyFile; cert != "" && key != "" {
tlsInfo := transport.TLSInfo{
CertFile: cert,
KeyFile: key,
TrustedCAFile: ipamConf.EtcdCACertFile,
}
tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
return nil, err
}
cfg.TLS = tlsConfig
}
client, err := clientv3.New(cfg)
if err != nil {
return nil, err
}

session, err := concurrency.NewSession(client)
if err != nil {
return nil, err
}
mutex := concurrency.NewMutex(session, fmt.Sprintf("%s/%s", whereaboutsPrefix, ipamConf.Range))

// acquire our lock
if err := mutex.Lock(context.Background()); err != nil {
return nil, err
}

return &ETCDIPAM{client, clientv3.NewKV(client), mutex, session}, nil
}

// ETCDIPAM manages ip blocks in an etcd backend
type ETCDIPAM struct {
client *clientv3.Client
kv clientv3.KV

mutex *concurrency.Mutex
session *concurrency.Session
}

// Status tests connectivity to the etcd backend
func (i *ETCDIPAM) Status(ctx context.Context) error {
_, err := i.kv.Get(ctx, "anykey")
return err
}

// EtcdOverlappingRangeStore represents a set of cluster wide resources
type EtcdOverlappingRangeStore struct {
client *clientv3.Client
}

// GetOverlappingRangeStore returns an OverlappingRangeStore interface
func (i *ETCDIPAM) GetOverlappingRangeStore() (OverlappingRangeStore, error) {
return &EtcdOverlappingRangeStore{i.client}, nil
}

// IsAllocatedInOverlappingRange checks to see if the IP is allocated across the whole cluster (and not just the current range)
func (i *EtcdOverlappingRangeStore) IsAllocatedInOverlappingRange(ctx context.Context, ip net.IP, podRef string) (bool, error) {
logging.Debugf("ETCD IsAllocatedInOverlappingRange is NOT IMPLEMENTED!!!! TODO")
return false, nil
}

// UpdateOverlappingRangeAllocation updates our clusterwide allocation for overlapping ranges.
func (i *EtcdOverlappingRangeStore) UpdateOverlappingRangeAllocation(ctx context.Context, mode int, ip net.IP, containerID string, podRef string) error {
logging.Debugf("ETCD UpdateOverlappingRangeWide is NOT IMPLEMENTED!!!! TODO")
return nil
}

// Close shuts down the clients etcd connections
func (i *ETCDIPAM) Close() error {
defer i.client.Close()
defer i.session.Close()
return i.mutex.Unlock(context.Background())
}

// GetIPPool returns a storage.IPPool for the given range
func (i *ETCDIPAM) GetIPPool(ctx context.Context, ipRange string) (IPPool, error) {
reservelist, err := i.getRange(ctx, ipRange)
if err != nil {
return nil, err
}
return &ETCDIPPool{i.kv, ipRange, reservelist}, nil
}

// GetRange gets the reserved list of IPs for a range
func (i *ETCDIPAM) getRange(ctx context.Context, iprange string) ([]types.IPReservation, error) {
reservelist, err := i.kv.Get(ctx, fmt.Sprintf("%s/%s", whereaboutsPrefix, iprange))
if err != nil {
return nil, err
}
if len(reservelist.Kvs) == 0 {
return nil, nil
}

reservations := strings.Split(string(reservelist.Kvs[0].Value), "\n")
list := make([]types.IPReservation, len(reservations))
for i, raw := range reservations {
split := strings.SplitN(raw, " ", 2)
if len(split) != 2 {
return nil, nil
}
list[i] = types.IPReservation{
IP: net.ParseIP(split[0]),
ContainerID: split[1],
}
}

return list, nil
}

// ETCDIPPool represents a range and its parsed set of allocations
type ETCDIPPool struct {
kv clientv3.KV
ipRange string
allocations []types.IPReservation
}

// Allocations returns the initially retrieved set of allocations for this pool
func (p *ETCDIPPool) Allocations() []types.IPReservation {
return p.allocations
}

// Update sets the pool allocated IP list to the given IP reservations
func (p *ETCDIPPool) Update(ctx context.Context, reservations []types.IPReservation) error {
var raw []string
for _, r := range reservations {
raw = append(raw, fmt.Sprintf("%s %s", r.IP.String(), r.ContainerID))
}
_, err := p.kv.Put(ctx, fmt.Sprintf("%s/%s", whereaboutsPrefix, p.ipRange), strings.Join(raw, "\n"))
return err
}

// IPManagement manages ip allocation and deallocation from a storage perspective
func IPManagementEtcd(mode int, ipamConf types.IPAMConfig, containerID string, podRef string) (net.IPNet, error) {

logging.Debugf("IPManagement -- mode: %v / host: %v / containerID: %v / podRef: %v", mode, ipamConf.EtcdHost, containerID, podRef)

var newip net.IPNet
// Skip invalid modes
switch mode {
case types.Allocate, types.Deallocate:
default:
return newip, fmt.Errorf("Got an unknown mode passed to IPManagement: %v", mode)
}

var ipam Store
var pool IPPool
var err error
if ipamConf.Datastore != types.DatastoreETCD {
return net.IPNet{}, logging.Errorf("wrong 'datastore' value in IPAM config: %s", ipamConf.Datastore)
}
ipam, err = NewETCDIPAM(ipamConf)
if err != nil {
return newip, logging.Errorf("IPAM %s client initialization error: %v", ipamConf.Datastore, err)
}
defer ipam.Close()

ctx, cancel := context.WithTimeout(context.Background(), RequestTimeout)
defer cancel()

// Check our connectivity first
if err := ipam.Status(ctx); err != nil {
logging.Errorf("IPAM connectivity error: %v", err)
return newip, err
}

// handle the ip add/del until successful
RETRYLOOP:
for j := 0; j < DatastoreRetries; j++ {
select {
case <-ctx.Done():
return newip, nil
default:
// retry the IPAM loop if the context has not been cancelled
}

pool, err = ipam.GetIPPool(ctx, ipamConf.Range)
if err != nil {
logging.Errorf("IPAM error reading pool allocations (attempt: %d): %v", j, err)
if e, ok := err.(Temporary); ok && e.Temporary() {
continue
}
return newip, err
}

reservelist := pool.Allocations()
var updatedreservelist []types.IPReservation
switch mode {
case types.Allocate:
newip, updatedreservelist, err = allocate.AssignIP(ipamConf, reservelist, containerID, podRef)
if err != nil {
logging.Errorf("Error assigning IP: %v", err)
return newip, err
}
case types.Deallocate:
updatedreservelist, _, err = allocate.DeallocateIP(reservelist, containerID)
if err != nil {
logging.Errorf("Error deallocating IP: %v", err)
return newip, err
}
}

err = pool.Update(ctx, updatedreservelist)
if err != nil {
logging.Errorf("IPAM error updating pool (attempt: %d): %v", j, err)
if e, ok := err.(Temporary); ok && e.Temporary() {
continue
}
break RETRYLOOP
}
break RETRYLOOP
}

return newip, err
}
30 changes: 21 additions & 9 deletions pkg/storage/kubernetes/ipam.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,8 @@ func (i *KubernetesIPAM) GetOverlappingRangeStore() (storage.OverlappingRangeSto
return &KubernetesOverlappingRangeStore{i.client, i.containerID, i.namespace}, nil
}

// IsAllocatedInOverlappingRange checks for IP addresses to see if they're allocated cluster wide, for overlapping
// ranges.
func (c *KubernetesOverlappingRangeStore) IsAllocatedInOverlappingRange(ctx context.Context, ip net.IP,
networkName string) (bool, error) {
normalizedIP := normalizeIP(ip, networkName)
// IsAllocatedInOverlappingRange checks for IP addresses to see if they're allocated cluster wide, for overlapping ranges
func (c *KubernetesOverlappingRangeStore) IsAllocatedInOverlappingRange(ctx context.Context, ip net.IP, podRef string) (bool, error) {

logging.Debugf("OverlappingRangewide allocation check; normalized IP: %q, IP: %q, networkName: %q",
normalizedIP, ip, networkName)
Expand All @@ -216,8 +213,12 @@ func (c *KubernetesOverlappingRangeStore) IsAllocatedInOverlappingRange(ctx cont
return false, fmt.Errorf("k8s get OverlappingRangeIPReservation error: %s", err)
}

logging.Debugf("Normalized IP is reserved; normalized IP: %q, IP: %q, networkName: %q",
normalizedIP, ip, networkName)
if clusteripres.Spec.PodRef == podRef {
logging.Debugf("IP %v matches existing podRef %s", ip, podRef)
return false, nil
}

logging.Debugf("IP %v is reserved cluster wide.", ip)
return true, nil
}

Expand All @@ -242,8 +243,10 @@ func (c *KubernetesOverlappingRangeStore) UpdateOverlappingRangeAllocation(ctx c
PodRef: podRef,
}

_, err = c.client.WhereaboutsV1alpha1().OverlappingRangeIPReservations(c.namespace).Create(
ctx, clusteripres, metav1.CreateOptions{})
if err := c.client.Create(ctx, clusteripres); errors.IsAlreadyExists(err) {
logging.Debugf("clusteripres already exists, updating with %v", clusteripres.Spec)
err = c.client.Update(ctx, clusteripres)
}

case whereaboutstypes.Deallocate:
verb = "deallocate"
Expand Down Expand Up @@ -500,6 +503,15 @@ func IPManagementKubernetesUpdate(ctx context.Context, mode int, ipam *Kubernete
logging.Errorf("IPAM error getting OverlappingRangeStore: %v", err)
return newips, err
}
// Now check if this is allocated overlappingrange wide
// When it's allocated overlappingrange wide, we add it to a local reserved list
// And we try again.
if ipamConf.OverlappingRanges {
isallocated, err := overlappingrangestore.IsAllocatedInOverlappingRange(ctx, newip.IP, podRef)
if err != nil {
logging.Errorf("Error checking overlappingrange allocation: %v", err)
return newip, err
}

pool, err = ipam.GetIPPool(requestCtx, PoolIdentifier{IpRange: ipRange.Range, NetworkName: ipamConf.NetworkName})
if err != nil {
Expand Down
5 changes: 2 additions & 3 deletions pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ type Store interface {

// OverlappingRangeStore is an interface for wrapping overlappingrange storage options
type OverlappingRangeStore interface {
IsAllocatedInOverlappingRange(ctx context.Context, ip net.IP, networkName string) (bool, error)
UpdateOverlappingRangeAllocation(ctx context.Context, mode int, ip net.IP, containerID string, podRef,
networkName string) error
IsAllocatedInOverlappingRange(ctx context.Context, ip net.IP, podRef string) (bool, error)
UpdateOverlappingRangeAllocation(ctx context.Context, mode int, ip net.IP, containerID string, podRef string) error
}

type Temporary interface {
Expand Down

0 comments on commit 23a153d

Please sign in to comment.