diff --git a/cmd/spinifex/cmd/service.go b/cmd/spinifex/cmd/service.go index 10724e8f9..c3e1f0167 100644 --- a/cmd/spinifex/cmd/service.go +++ b/cmd/spinifex/cmd/service.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/mulgadc/spinifex/spinifex/config" + "github.com/mulgadc/spinifex/spinifex/network/external" "github.com/mulgadc/spinifex/spinifex/service" "github.com/mulgadc/spinifex/spinifex/services/awsgw" "github.com/mulgadc/spinifex/spinifex/services/nats" @@ -730,9 +731,9 @@ var vpcdStartCmd = &cobra.Command{ nodeConfig := clusterConfig.Nodes[clusterConfig.Node] // Map cluster-wide external pools to vpcd config - var extPools []vpcd.ExternalPoolConfig + var extPools []external.ExternalPoolConfig for _, p := range clusterConfig.Network.ExternalPools { - extPools = append(extPools, vpcd.ExternalPoolConfig{ + extPools = append(extPools, external.ExternalPoolConfig{ Name: p.Name, Source: p.Source, BindBridge: p.BindBridge, diff --git a/spinifex/daemon/daemon.go b/spinifex/daemon/daemon.go index e677efe82..bc75e94d3 100644 --- a/spinifex/daemon/daemon.go +++ b/spinifex/daemon/daemon.go @@ -55,6 +55,7 @@ import ( handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2" handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam" "github.com/mulgadc/spinifex/spinifex/instancetypes" + "github.com/mulgadc/spinifex/spinifex/network/external" "github.com/mulgadc/spinifex/spinifex/network/external/dhcp" "github.com/mulgadc/spinifex/spinifex/network/host" "github.com/mulgadc/spinifex/spinifex/objectstore" @@ -1307,10 +1308,10 @@ func (d *Daemon) startCluster() error { if jsErr != nil { slog.Warn("Failed to get JetStream for external IPAM", "err", jsErr) } else { - var pools []handlers_ec2_vpc.ExternalPoolConfig + var pools []external.ExternalPoolConfig anyDHCP := false for _, p := range d.clusterConfig.Network.ExternalPools { - pools = append(pools, handlers_ec2_vpc.ExternalPoolConfig{ + pools = append(pools, external.ExternalPoolConfig{ Name: p.Name, Source: p.Source, BindBridge: p.BindBridge, diff --git a/spinifex/daemon/daemon_handlers_test.go b/spinifex/daemon/daemon_handlers_test.go index 7f94f280a..5d7940220 100644 --- a/spinifex/daemon/daemon_handlers_test.go +++ b/spinifex/daemon/daemon_handlers_test.go @@ -29,6 +29,7 @@ import ( handlers_ec2_tags "github.com/mulgadc/spinifex/spinifex/handlers/ec2/tags" handlers_ec2_volume "github.com/mulgadc/spinifex/spinifex/handlers/ec2/volume" handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc" + "github.com/mulgadc/spinifex/spinifex/network/external" "github.com/mulgadc/spinifex/spinifex/objectstore" "github.com/mulgadc/spinifex/spinifex/qmp" "github.com/mulgadc/spinifex/spinifex/testutil" @@ -3535,7 +3536,7 @@ func TestDelegateHandlers_EIP(t *testing.T) { js, err := nc.JetStream() require.NoError(t, err) - ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{ + ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{ {Name: "test-pool", RangeStart: "192.168.100.2", RangeEnd: "192.168.100.254", Gateway: "192.168.100.1", PrefixLen: 24}, }) require.NoError(t, err) diff --git a/spinifex/daemon/daemon_system_instance_test.go b/spinifex/daemon/daemon_system_instance_test.go index 70f34fca8..02704818d 100644 --- a/spinifex/daemon/daemon_system_instance_test.go +++ b/spinifex/daemon/daemon_system_instance_test.go @@ -14,6 +14,7 @@ import ( handlers_ec2_eip "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eip" handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc" "github.com/mulgadc/spinifex/spinifex/handlers/elbv2" + "github.com/mulgadc/spinifex/spinifex/network/external" "github.com/mulgadc/spinifex/spinifex/tags" "github.com/mulgadc/spinifex/spinifex/testutil" "github.com/mulgadc/spinifex/spinifex/vm" @@ -496,7 +497,7 @@ func TestLaunchSystemInstance_NATFailureRollsBackPublicIP(t *testing.T) { js, err := jsNC.JetStream() require.NoError(t, err) - ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{ + ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{ {Name: "wan-test", RangeStart: "203.0.113.10", RangeEnd: "203.0.113.20", Gateway: "203.0.113.1", PrefixLen: 24}, }) require.NoError(t, err) @@ -618,7 +619,7 @@ func TestReleaseSystemInstanceEIP_ReleasesEipServiceAllocation(t *testing.T) { js, err := jsNC.JetStream() require.NoError(t, err) - ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{ + ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{ {Name: "wan-test", RangeStart: "203.0.113.10", RangeEnd: "203.0.113.20", Gateway: "203.0.113.1", PrefixLen: 24}, }) require.NoError(t, err) diff --git a/spinifex/handlers/ec2/eip/service_impl_test.go b/spinifex/handlers/ec2/eip/service_impl_test.go index 3dc95c99f..8d2a2cd24 100644 --- a/spinifex/handlers/ec2/eip/service_impl_test.go +++ b/spinifex/handlers/ec2/eip/service_impl_test.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" handlers_ec2_vpc "github.com/mulgadc/spinifex/spinifex/handlers/ec2/vpc" + "github.com/mulgadc/spinifex/spinifex/network/external" "github.com/mulgadc/spinifex/spinifex/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,8 +16,8 @@ import ( const testAccountID = "123456789012" -func testPool() handlers_ec2_vpc.ExternalPoolConfig { - return handlers_ec2_vpc.ExternalPoolConfig{ +func testPool() external.ExternalPoolConfig { + return external.ExternalPoolConfig{ Name: "test-pool", RangeStart: "198.51.100.10", RangeEnd: "198.51.100.20", @@ -30,7 +31,7 @@ func setupTestEIP(t *testing.T) (*EIPServiceImpl, *handlers_ec2_vpc.ExternalIPAM _, nc, js := testutil.StartTestJetStream(t) pool := testPool() - ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []handlers_ec2_vpc.ExternalPoolConfig{pool}) + ipam, err := handlers_ec2_vpc.NewExternalIPAM(js, []external.ExternalPoolConfig{pool}) require.NoError(t, err) svc, err := NewEIPServiceImpl(nc, ipam, nil) diff --git a/spinifex/handlers/ec2/vpc/external_ipam.go b/spinifex/handlers/ec2/vpc/external_ipam.go index 10c1380a4..485884a72 100644 --- a/spinifex/handlers/ec2/vpc/external_ipam.go +++ b/spinifex/handlers/ec2/vpc/external_ipam.go @@ -1,6 +1,7 @@ package handlers_ec2_vpc import ( + "cmp" "context" "errors" "fmt" @@ -25,31 +26,11 @@ const ( KVBucketExternalIPAMVersion = external.KVBucketStaticPoolVersion ) -// ExternalPoolConfig is the admin-defined IP pool from spinifex.toml. -// Duplicated from network/external.ExternalPoolConfig pending consolidation. -type ExternalPoolConfig struct { - Name string - Source string // "static" (default) or "dhcp" - BindBridge string // Linux bridge for DHCP DORA (source=dhcp only) - RangeStart string - RangeEnd string - Gateway string - GatewayIP string - PrefixLen int - Region string - AZ string - // GwLrpRangeStart/End reserves a sub-range of the LAN for OVN gateway - // LRP IPs in centralized NAT mode. IPAM must skip these addresses or - // the per-VM EIP allocator and vpcd will fight over them. - GwLrpRangeStart string - GwLrpRangeEnd string -} - // ExternalIPAM is the AWS-facing entry point for external IP allocation, // dispatching to StaticPoolAllocator or dhcp.DHCPPoolAllocator per pool name. type ExternalIPAM struct { kv nats.KeyValue - pools []ExternalPoolConfig + pools []external.ExternalPoolConfig static *external.StaticPoolAllocator perPool map[string]external.Allocator // dhcp overrides; static pools fall through to `static` } @@ -57,7 +38,7 @@ type ExternalIPAM struct { // NewExternalIPAM creates a new ExternalIPAM. Static pools wire through // external.StaticPoolAllocator; DHCP-sourced pools wait for EnableDHCP // to install the per-pool dhcp.DHCPPoolAllocator. -func NewExternalIPAM(js nats.JetStreamContext, pools []ExternalPoolConfig) (*ExternalIPAM, error) { +func NewExternalIPAM(js nats.JetStreamContext, pools []external.ExternalPoolConfig) (*ExternalIPAM, error) { staticPools := filterStatic(pools) var ( alloc *external.StaticPoolAllocator @@ -65,7 +46,7 @@ func NewExternalIPAM(js nats.JetStreamContext, pools []ExternalPoolConfig) (*Ext ) if len(staticPools) > 0 { var err error - alloc, err = external.NewStaticPoolAllocator(js, toExternalPools(staticPools)) + alloc, err = external.NewStaticPoolAllocator(js, staticPools) if err != nil { return nil, err } @@ -75,8 +56,8 @@ func NewExternalIPAM(js nats.JetStreamContext, pools []ExternalPoolConfig) (*Ext } // NewExternalIPAMWithKV creates an ExternalIPAM with an existing KV bucket (for testing). -func NewExternalIPAMWithKV(kv nats.KeyValue, pools []ExternalPoolConfig) *ExternalIPAM { - alloc := external.NewStaticPoolAllocatorWithKV(kv, toExternalPools(filterStatic(pools))) +func NewExternalIPAMWithKV(kv nats.KeyValue, pools []external.ExternalPoolConfig) *ExternalIPAM { + alloc := external.NewStaticPoolAllocatorWithKV(kv, filterStatic(pools)) return &ExternalIPAM{kv: kv, pools: pools, static: alloc, perPool: map[string]external.Allocator{}} } @@ -91,7 +72,7 @@ func (m *ExternalIPAM) EnableDHCP(client *dhcp.NATSClient) error { if p.Source != external.SourceDHCP { continue } - m.perPool[p.Name] = dhcp.NewDHCPPoolAllocator(client, toExternalPools([]ExternalPoolConfig{p})[0]) + m.perPool[p.Name] = dhcp.NewDHCPPoolAllocator(client, p) } return nil } @@ -165,7 +146,7 @@ func (m *ExternalIPAM) allocatorFor(poolName string) (external.Allocator, error) // findPool returns the best pool for the given region/AZ using the same // fallback order as topology.go: AZ-scoped → region-scoped → unscoped. -func (m *ExternalIPAM) findPool(region, az string) *ExternalPoolConfig { +func (m *ExternalIPAM) findPool(region, az string) *external.ExternalPoolConfig { for i := range m.pools { p := &m.pools[i] if p.AZ != "" && p.AZ == az && p.Region == region { @@ -187,34 +168,9 @@ func (m *ExternalIPAM) findPool(region, az string) *ExternalPoolConfig { return nil } -// toExternalPools converts the handlers-side ExternalPoolConfig into the -// network/external mirror. The duplicate type goes away in the deferred -// L5 cleanup. -func toExternalPools(pools []ExternalPoolConfig) []external.ExternalPoolConfig { - out := make([]external.ExternalPoolConfig, len(pools)) - for i, p := range pools { - out[i] = external.ExternalPoolConfig{ - Name: p.Name, - Source: p.Source, - BindBridge: p.BindBridge, - RangeStart: p.RangeStart, - RangeEnd: p.RangeEnd, - Gateway: p.Gateway, - GatewayIP: p.GatewayIP, - PrefixLen: p.PrefixLen, - DNSServers: nil, - Region: p.Region, - AZ: p.AZ, - GwLrpRangeStart: p.GwLrpRangeStart, - GwLrpRangeEnd: p.GwLrpRangeEnd, - } - } - return out -} - // filterStatic returns only the static pools (Source unset or "static"). -func filterStatic(pools []ExternalPoolConfig) []ExternalPoolConfig { - out := make([]ExternalPoolConfig, 0, len(pools)) +func filterStatic(pools []external.ExternalPoolConfig) []external.ExternalPoolConfig { + out := make([]external.ExternalPoolConfig, 0, len(pools)) for _, p := range pools { if p.Source == "" || p.Source == external.SourceStatic { out = append(out, p) @@ -224,7 +180,7 @@ func filterStatic(pools []ExternalPoolConfig) []ExternalPoolConfig { } // ValidatePoolConfig checks that a pool config is valid. -func ValidatePoolConfig(pool ExternalPoolConfig) error { +func ValidatePoolConfig(pool external.ExternalPoolConfig) error { if pool.Name == "" { return fmt.Errorf("pool name is required") } @@ -271,7 +227,5 @@ func ValidatePoolConfig(pool ExternalPoolConfig) error { } func compareIPs(a, b net.IP) int { - ai := ipToInt(a.To4()) - bi := ipToInt(b.To4()) - return ai.Cmp(bi) + return cmp.Compare(ipToInt(a), ipToInt(b)) } diff --git a/spinifex/handlers/ec2/vpc/external_ipam_test.go b/spinifex/handlers/ec2/vpc/external_ipam_test.go index fbda29a13..5a2adff86 100644 --- a/spinifex/handlers/ec2/vpc/external_ipam_test.go +++ b/spinifex/handlers/ec2/vpc/external_ipam_test.go @@ -4,12 +4,13 @@ import ( "sync" "testing" + "github.com/mulgadc/spinifex/spinifex/network/external" "github.com/mulgadc/spinifex/spinifex/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func setupTestExternalIPAM(t *testing.T, pools []ExternalPoolConfig) *ExternalIPAM { +func setupTestExternalIPAM(t *testing.T, pools []external.ExternalPoolConfig) *ExternalIPAM { t.Helper() _, _, js := testutil.StartTestJetStream(t) @@ -18,8 +19,8 @@ func setupTestExternalIPAM(t *testing.T, pools []ExternalPoolConfig) *ExternalIP return ipam } -func testPool() ExternalPoolConfig { - return ExternalPoolConfig{ +func testPool() external.ExternalPoolConfig { + return external.ExternalPoolConfig{ Name: "wan", RangeStart: "192.168.1.150", RangeEnd: "192.168.1.160", @@ -30,7 +31,7 @@ func testPool() ExternalPoolConfig { func TestExternalIPAM_AllocateSequential(t *testing.T) { pool := testPool() - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) // .150 is reserved for gateway, so first allocable is .151 ip1, poolName, err := ipam.AllocateIP("", "", PurposeENIPublic, "", "eni-1", "i-1") @@ -49,7 +50,7 @@ func TestExternalIPAM_AllocateSequential(t *testing.T) { func TestExternalIPAM_GatewayIPReserved(t *testing.T) { pool := testPool() - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) record, err := ipam.GetPoolRecord("wan") require.NoError(t, err) @@ -67,7 +68,7 @@ func TestExternalIPAM_GatewayIPReserved(t *testing.T) { func TestExternalIPAM_ExplicitGatewayIP(t *testing.T) { pool := testPool() pool.GatewayIP = "192.168.1.155" - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) record, err := ipam.GetPoolRecord("wan") require.NoError(t, err) @@ -86,7 +87,7 @@ func TestExternalIPAM_ExplicitGatewayIP(t *testing.T) { func TestExternalIPAM_Release(t *testing.T) { pool := testPool() - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) ip1, _, err := ipam.AllocateIP("", "", PurposeENIPublic, "", "eni-1", "i-1") require.NoError(t, err) @@ -108,14 +109,14 @@ func TestExternalIPAM_Release(t *testing.T) { } func TestExternalIPAM_Exhaustion(t *testing.T) { - pool := ExternalPoolConfig{ + pool := external.ExternalPoolConfig{ Name: "tiny", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.3", Gateway: "10.0.0.254", PrefixLen: 24, } - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) // .1 reserved for gateway, .2 and .3 allocable ip1, _, err := ipam.AllocateIP("", "", PurposeENIPublic, "", "eni-1", "i-1") @@ -133,7 +134,7 @@ func TestExternalIPAM_Exhaustion(t *testing.T) { func TestExternalIPAM_CASConflict(t *testing.T) { pool := testPool() - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) // Concurrent allocations should all succeed (CAS retry handles conflicts) var wg sync.WaitGroup @@ -165,7 +166,7 @@ func TestExternalIPAM_CASConflict(t *testing.T) { } func TestExternalIPAM_MultiPool(t *testing.T) { - pools := []ExternalPoolConfig{ + pools := []external.ExternalPoolConfig{ { Name: "us-east", RangeStart: "203.0.113.2", @@ -201,7 +202,7 @@ func TestExternalIPAM_MultiPool(t *testing.T) { } func TestExternalIPAM_PoolFallback(t *testing.T) { - pools := []ExternalPoolConfig{ + pools := []external.ExternalPoolConfig{ { Name: "az-pool", RangeStart: "10.0.0.1", @@ -249,7 +250,7 @@ func TestExternalIPAM_PoolFallback(t *testing.T) { } func TestExternalIPAM_SpecificPool(t *testing.T) { - pools := []ExternalPoolConfig{ + pools := []external.ExternalPoolConfig{ { Name: "pool-a", RangeStart: "10.0.0.1", @@ -276,47 +277,47 @@ func TestExternalIPAM_SpecificPool(t *testing.T) { func TestExternalIPAM_RangeValidation(t *testing.T) { tests := []struct { name string - pool ExternalPoolConfig + pool external.ExternalPoolConfig wantErr string }{ { name: "empty name", - pool: ExternalPoolConfig{RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"}, + pool: external.ExternalPoolConfig{RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"}, wantErr: "pool name is required", }, { name: "bad range_start", - pool: ExternalPoolConfig{Name: "bad", RangeStart: "not-an-ip", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"}, + pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "not-an-ip", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"}, wantErr: "invalid range_start", }, { name: "bad range_end", - pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "not-an-ip", Gateway: "10.0.0.254"}, + pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "not-an-ip", Gateway: "10.0.0.254"}, wantErr: "invalid range_end", }, { name: "start > end", - pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.10", RangeEnd: "10.0.0.1", Gateway: "10.0.0.254"}, + pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.10", RangeEnd: "10.0.0.1", Gateway: "10.0.0.254"}, wantErr: "greater than range_end", }, { name: "missing gateway", - pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10"}, + pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10"}, wantErr: "gateway is required", }, { name: "bad gateway", - pool: ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "nope"}, + pool: external.ExternalPoolConfig{Name: "bad", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "nope"}, wantErr: "invalid gateway IP", }, { name: "bad gateway_ip", - pool: ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254", GatewayIP: "nope"}, + pool: external.ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254", GatewayIP: "nope"}, wantErr: "invalid gateway_ip", }, { name: "valid", - pool: ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"}, + pool: external.ExternalPoolConfig{Name: "ok", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", Gateway: "10.0.0.254"}, }, } @@ -338,7 +339,7 @@ func TestExternalIPAM_InitFromConfig(t *testing.T) { _, _, js := testutil.StartTestJetStream(t) // First init - ipam1, err := NewExternalIPAM(js, []ExternalPoolConfig{pool}) + ipam1, err := NewExternalIPAM(js, []external.ExternalPoolConfig{pool}) require.NoError(t, err) // Allocate an IP @@ -347,7 +348,7 @@ func TestExternalIPAM_InitFromConfig(t *testing.T) { assert.Equal(t, "192.168.1.151", ip) // Second init (simulating restart) — should not lose allocation - ipam2, err := NewExternalIPAM(js, []ExternalPoolConfig{pool}) + ipam2, err := NewExternalIPAM(js, []external.ExternalPoolConfig{pool}) require.NoError(t, err) record, err := ipam2.GetPoolRecord("wan") @@ -358,14 +359,14 @@ func TestExternalIPAM_InitFromConfig(t *testing.T) { func TestExternalIPAM_ReleaseNotAllocated(t *testing.T) { pool := testPool() - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) err := ipam.ReleaseIP("wan", "192.168.1.200", "") assert.ErrorContains(t, err, "not allocated") } func TestExternalIPAM_NoPoolAvailable(t *testing.T) { - pool := ExternalPoolConfig{ + pool := external.ExternalPoolConfig{ Name: "us-only", RangeStart: "10.0.0.1", RangeEnd: "10.0.0.10", @@ -374,7 +375,7 @@ func TestExternalIPAM_NoPoolAvailable(t *testing.T) { Region: "us-east-1", AZ: "us-east-1a", } - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) // No pool matches eu-west _, _, err := ipam.AllocateIP("eu-west-1", "eu-west-1a", PurposeENIPublic, "", "eni-1", "i-1") @@ -383,19 +384,19 @@ func TestExternalIPAM_NoPoolAvailable(t *testing.T) { func TestExternalIPAM_FindPoolByName_NotFound(t *testing.T) { pool := testPool() - ipam := setupTestExternalIPAM(t, []ExternalPoolConfig{pool}) + ipam := setupTestExternalIPAM(t, []external.ExternalPoolConfig{pool}) // AllocateFromPool with a non-existent pool name: the pool "nonexistent" // has no KV record, so getRecord fails. kv := ipam.kv - ipam2 := NewExternalIPAMWithKV(kv, []ExternalPoolConfig{pool}) + ipam2 := NewExternalIPAMWithKV(kv, []external.ExternalPoolConfig{pool}) _, err := ipam2.AllocateFromPool("nonexistent", PurposeENIPublic, "", "eni-1", "i-1") assert.Error(t, err) } func TestValidatePoolConfig(t *testing.T) { - base := func() ExternalPoolConfig { - return ExternalPoolConfig{ + base := func() external.ExternalPoolConfig { + return external.ExternalPoolConfig{ Name: "wan", Gateway: "192.168.1.1", RangeStart: "192.168.1.100", @@ -405,37 +406,37 @@ func TestValidatePoolConfig(t *testing.T) { } cases := []struct { name string - mutate func(*ExternalPoolConfig) + mutate func(*external.ExternalPoolConfig) wantErr string }{ - {name: "ok", mutate: func(p *ExternalPoolConfig) {}}, - {name: "no name", mutate: func(p *ExternalPoolConfig) { p.Name = "" }, wantErr: "pool name is required"}, - {name: "no gateway", mutate: func(p *ExternalPoolConfig) { p.Gateway = "" }, wantErr: "gateway is required"}, - {name: "bad gateway", mutate: func(p *ExternalPoolConfig) { p.Gateway = "x" }, wantErr: "invalid gateway IP"}, - {name: "bad gateway_ip", mutate: func(p *ExternalPoolConfig) { p.GatewayIP = "x" }, wantErr: "invalid gateway_ip"}, - {name: "bad range_start", mutate: func(p *ExternalPoolConfig) { p.RangeStart = "x" }, wantErr: "invalid range_start"}, - {name: "bad range_end", mutate: func(p *ExternalPoolConfig) { p.RangeEnd = "x" }, wantErr: "invalid range_end"}, - {name: "range reversed", mutate: func(p *ExternalPoolConfig) { + {name: "ok", mutate: func(p *external.ExternalPoolConfig) {}}, + {name: "no name", mutate: func(p *external.ExternalPoolConfig) { p.Name = "" }, wantErr: "pool name is required"}, + {name: "no gateway", mutate: func(p *external.ExternalPoolConfig) { p.Gateway = "" }, wantErr: "gateway is required"}, + {name: "bad gateway", mutate: func(p *external.ExternalPoolConfig) { p.Gateway = "x" }, wantErr: "invalid gateway IP"}, + {name: "bad gateway_ip", mutate: func(p *external.ExternalPoolConfig) { p.GatewayIP = "x" }, wantErr: "invalid gateway_ip"}, + {name: "bad range_start", mutate: func(p *external.ExternalPoolConfig) { p.RangeStart = "x" }, wantErr: "invalid range_start"}, + {name: "bad range_end", mutate: func(p *external.ExternalPoolConfig) { p.RangeEnd = "x" }, wantErr: "invalid range_end"}, + {name: "range reversed", mutate: func(p *external.ExternalPoolConfig) { p.RangeStart = "192.168.1.200" p.RangeEnd = "192.168.1.100" }, wantErr: "greater than range_end"}, - {name: "bad gw_lrp_start", mutate: func(p *ExternalPoolConfig) { + {name: "bad gw_lrp_start", mutate: func(p *external.ExternalPoolConfig) { p.GwLrpRangeStart = "x" p.GwLrpRangeEnd = "192.168.1.20" }, wantErr: "invalid gw_lrp_range_start"}, - {name: "bad gw_lrp_end", mutate: func(p *ExternalPoolConfig) { + {name: "bad gw_lrp_end", mutate: func(p *external.ExternalPoolConfig) { p.GwLrpRangeStart = "192.168.1.20" p.GwLrpRangeEnd = "x" }, wantErr: "invalid gw_lrp_range_end"}, - {name: "gw_lrp reversed", mutate: func(p *ExternalPoolConfig) { + {name: "gw_lrp reversed", mutate: func(p *external.ExternalPoolConfig) { p.GwLrpRangeStart = "192.168.1.30" p.GwLrpRangeEnd = "192.168.1.20" }, wantErr: "greater than gw_lrp_range_end"}, - {name: "gw_lrp overlaps range", mutate: func(p *ExternalPoolConfig) { + {name: "gw_lrp overlaps range", mutate: func(p *external.ExternalPoolConfig) { p.GwLrpRangeStart = "192.168.1.105" p.GwLrpRangeEnd = "192.168.1.108" }, wantErr: "overlaps range"}, - {name: "gw_lrp valid below range", mutate: func(p *ExternalPoolConfig) { + {name: "gw_lrp valid below range", mutate: func(p *external.ExternalPoolConfig) { p.GwLrpRangeStart = "192.168.1.20" p.GwLrpRangeEnd = "192.168.1.29" }}, diff --git a/spinifex/handlers/ec2/vpc/ipam.go b/spinifex/handlers/ec2/vpc/ipam.go index 3b20378f7..f38d93a18 100644 --- a/spinifex/handlers/ec2/vpc/ipam.go +++ b/spinifex/handlers/ec2/vpc/ipam.go @@ -1,11 +1,11 @@ package handlers_ec2_vpc import ( + "encoding/binary" "encoding/json" "errors" "fmt" "log/slog" - "math/big" "net" "github.com/mulgadc/spinifex/spinifex/migrate" @@ -188,16 +188,14 @@ func (m *IPAM) nextAvailableIP(record *IPAMRecord) (string, error) { } ones, bits := ipNet.Mask.Size() - hostBits := bits - ones // always 0-32 for IPv4 CIDRs, safe for uint conversion - totalIPs := new(big.Int).Lsh(big.NewInt(1), uint(hostBits)) //#nosec G115 - hostBits is 0-32 for IPv4 + hostBits := bits - ones // 0-32 for IPv4 CIDRs + totalIPs := uint64(1) << hostBits // fits uint64 (max 2^32) // Start at offset 4 (.0=network, .1=gateway, .2=DNS, .3=reserved) networkIP := ipToInt(ipNet.IP) - for offset := int64(4); offset < totalIPs.Int64()-1; offset++ { // -1 for broadcast - candidateInt := new(big.Int).Add(networkIP, big.NewInt(offset)) - candidate := intToIP(candidateInt).String() - + for offset := uint64(4); offset < totalIPs-1; offset++ { // -1 for broadcast + candidate := intToIP(networkIP + uint32(offset)).String() if !allocated[candidate] { return candidate, nil } @@ -206,20 +204,18 @@ func (m *IPAM) nextAvailableIP(record *IPAMRecord) (string, error) { return "", fmt.Errorf("subnet %s exhausted, no IPs available", record.CidrBlock) } -// ipToInt converts a net.IP to a big.Int. -func ipToInt(ip net.IP) *big.Int { +// ipToInt converts an IPv4 net.IP to its uint32 representation. +func ipToInt(ip net.IP) uint32 { ip = ip.To4() if ip == nil { - return big.NewInt(0) + return 0 } - return new(big.Int).SetBytes(ip) + return binary.BigEndian.Uint32(ip) } -// intToIP converts a big.Int back to a net.IP (IPv4). -func intToIP(n *big.Int) net.IP { - b := n.Bytes() - // Pad to 4 bytes for IPv4 - ip := make(net.IP, 4) - copy(ip[4-len(b):], b) +// intToIP converts a uint32 back to an IPv4 net.IP. +func intToIP(n uint32) net.IP { + ip := make(net.IP, net.IPv4len) + binary.BigEndian.PutUint32(ip, n) return ip } diff --git a/spinifex/handlers/ec2/vpc/ipam_test.go b/spinifex/handlers/ec2/vpc/ipam_test.go index ac7473d4c..047ebe46c 100644 --- a/spinifex/handlers/ec2/vpc/ipam_test.go +++ b/spinifex/handlers/ec2/vpc/ipam_test.go @@ -192,8 +192,7 @@ func TestIPAM_InvalidCIDR(t *testing.T) { } func TestIPAM_IpToIntNil(t *testing.T) { - result := ipToInt(nil) - assert.Equal(t, int64(0), result.Int64()) + assert.Equal(t, uint32(0), ipToInt(nil)) } func itoa(i int) string { diff --git a/spinifex/vpcd/vpcd.go b/spinifex/vpcd/vpcd.go index 963491ec8..ef1ab1225 100644 --- a/spinifex/vpcd/vpcd.go +++ b/spinifex/vpcd/vpcd.go @@ -103,7 +103,7 @@ type Config struct { // ExternalMode is "pool" or "" (disabled). ExternalMode string // ExternalPools holds the cluster-wide external IP pool configs. - ExternalPools []ExternalPoolConfig + ExternalPools []external.ExternalPoolConfig // Bootstrap holds the default VPC config from spinifex.toml for first-boot reconciliation. Bootstrap *BootstrapVPC // ExternalInterface is the WAN NIC name (e.g., "enp0s3"). Used by @@ -119,23 +119,6 @@ type Config struct { AZ string } -// ExternalPoolConfig mirrors config.ExternalPool for vpcd's internal use. -type ExternalPoolConfig struct { - Name string - Source string // "static" (default) or "dhcp" - BindBridge string // Linux bridge for DHCP DORA (source=dhcp only) - RangeStart string - RangeEnd string - Gateway string - GatewayIP string - PrefixLen int - DNSServers []string - Region string - AZ string - GwLrpRangeStart string // Sub-range for OVN gateway LRP IPs in centralized NAT mode. - GwLrpRangeEnd string -} - // Service implements the Spinifex service interface for vpcd. type Service struct { Config *Config @@ -450,7 +433,7 @@ func launchService(cfg *Config) error { var igwPool *external.ExternalPoolConfig if cfg.ExternalMode != "" && len(cfg.ExternalPools) > 0 { - p := externalPoolConfigToShared(cfg.ExternalPools[0]) + p := cfg.ExternalPools[0] igwPool = &p } @@ -608,7 +591,7 @@ func launchService(cfg *Config) error { // pickDNSServer returns the OVN dhcp_options dns_server from the first unscoped pool with DNS servers. // Empty falls back to topology.NewLiveManager's default. -func pickDNSServer(pools []ExternalPoolConfig) string { +func pickDNSServer(pools []external.ExternalPoolConfig) string { for _, p := range pools { if p.Region == "" && p.AZ == "" && len(p.DNSServers) > 0 { return "{" + strings.Join(p.DNSServers, ", ") + "}" @@ -665,25 +648,6 @@ func pickGatewayAllocator(pool *external.ExternalPoolConfig, ovnClient ovn.Clien return external.NewStaticRangeAllocator(ovnClient) } -// externalPoolConfigToShared translates vpcd's ExternalPoolConfig into the network/external shared type. -func externalPoolConfigToShared(p ExternalPoolConfig) external.ExternalPoolConfig { - return external.ExternalPoolConfig{ - Name: p.Name, - Source: p.Source, - BindBridge: p.BindBridge, - RangeStart: p.RangeStart, - RangeEnd: p.RangeEnd, - Gateway: p.Gateway, - GatewayIP: p.GatewayIP, - PrefixLen: p.PrefixLen, - DNSServers: p.DNSServers, - Region: p.Region, - AZ: p.AZ, - GwLrpRangeStart: p.GwLrpRangeStart, - GwLrpRangeEnd: p.GwLrpRangeEnd, - } -} - // resolveBridgeConfig picks bridge mode (auto-detecting when unset) and always uses "br-wan" as the WAN bridge. func resolveBridgeConfig(cfgBridgeMode, externalIface string) (string, string) { bridgeMode := cfgBridgeMode