Files
spinifex/docs/development/feature/eigw.md
T
2026-02-11 11:00:53 +11:00

9.1 KiB

Egress-Only Internet Gateway (EIGW)

Overview

Egress-Only Internet Gateways provide outbound-only IPv6 connectivity for instances in a VPC. In Hive, EIGWs are lightweight metadata objects persisted in NATS JetStream KV. The implementation covers the three standard EC2 API operations: Create, Delete, and Describe.

Key Design Decisions

1. NATS JetStream KV for persistence

EIGW records are stored in a dedicated KV bucket (hive-eigw) with 10-revision history. This avoids introducing a database dependency -- the same NATS infrastructure used for messaging also handles persistence.

The getOrCreateKVBucket helper tries CreateKeyValue first, then falls back to KeyValue (open existing). This makes the service idempotent across restarts.

Trade-off: KV is not suited for complex queries (e.g. filtering by tag values). Describe currently iterates all keys and filters in-memory, which is fine for typical EIGW counts but wouldn't scale to thousands of records.

2. Constructor fails loudly, daemon falls back gracefully

NewEgressOnlyIGWServiceImplWithNATS returns an error if the KV bucket can't be created or opened. This ensures the service never operates with a nil KV handle, eliminating an entire class of nil-pointer bugs.

The daemon handles this with a graceful fallback:

if eigwSvc, err := handlers_ec2_eigw.NewEgressOnlyIGWServiceImplWithNATS(d.config, d.natsConn); err != nil {
    slog.Warn("Failed to initialize EIGW service, falling back to in-memory", "error", err)
    d.eigwService = handlers_ec2_eigw.NewEgressOnlyIGWServiceImpl(d.config)
} else {
    d.eigwService = eigwSvc
}

Trade-off: The in-memory fallback (NewEgressOnlyIGWServiceImpl) has no KV backing, so EIGW state won't survive daemon restarts. This is acceptable for degraded mode -- the alternative is refusing to start the daemon entirely.

3. crypto/rand for ID generation

EIGW IDs (eigw- + 16 hex chars) are generated using crypto/rand + encoding/hex. This matches the pattern used by instance and volume ID generation elsewhere in the codebase.

func generateEgressOnlyIGWID() string {
    b := make([]byte, 8)
    if _, err := rand.Read(b); err != nil {
        panic("crypto/rand failed: " + err.Error())
    }
    return "eigw-" + hex.EncodeToString(b)
}

The panic on crypto/rand failure is intentional -- if the OS entropy source is broken, the system has larger problems.

4. Error strings are awserrors constants

All errors returned from service_impl.go use errors.New(awserrors.ErrorXxx) with exact constant values. This is critical because the error string travels through a round-trip:

service_impl: errors.New(awserrors.ErrorMissingParameter)
  -> daemon handleNATSRequest: utils.GenerateErrorPayload(err.Error())
    -> JSON over NATS: {"Code": "MissingParameter"}
      -> gateway NATSRequest[Out]: ValidateErrorPayload extracts Code
        -> gateway ErrorHandler: awserrors.ErrorLookup[err.Error()] -> HTTP status + XML

If the error string doesn't match a key in ErrorLookup, the gateway returns a generic 500. Using fmt.Errorf with extra context would break this chain.

5. Centralized nil NATS check in the gateway

Rather than checking natsConn == nil in each gateway function, a single check in EC2_Request covers all NATS-dependent actions. An allowlist (ec2LocalActions) exempts actions that work without NATS:

var ec2LocalActions = map[string]bool{
    "DescribeRegions":           true,
    "DescribeAvailabilityZones": true,
    "DescribeAccountAttributes": true,
}

If gw.NATSConn is nil and the action isn't in the allowlist, the gateway returns ErrorServerInternal immediately.

6. Tag filtering by resource type

Create accepts TagSpecifications but only applies tags where ResourceType equals "egress-only-internet-gateway". Tags with other resource types (e.g. "instance") are silently ignored, matching AWS behavior.

Request Flow

AWS SDK (create-egress-only-internet-gateway --vpc-id vpc-xxx)
  -> Gateway (port 9999): ec2Handler parses AWS query params into SDK struct
    -> Validate input (nil check, required fields)
    -> NATS ec2.CreateEgressOnlyInternetGateway: routed to daemon via "hive-workers" queue group
      -> Daemon handleEC2CreateEgressOnlyInternetGateway
        -> handleNATSRequest: unmarshal -> EgressOnlyIGWServiceImpl.Create -> marshal -> respond
          1. Validate VpcId is present
          2. Generate eigw-{16hex} ID via crypto/rand
          3. Build EgressOnlyIGWRecord with state "attached"
          4. Process matching tag specifications
          5. JSON marshal and store in NATS KV (hive-eigw bucket)
        <- NATS response: ec2.CreateEgressOnlyInternetGatewayOutput JSON
    <- Gateway: wrap in XML (CreateEgressOnlyInternetGatewayResponse)
  <- AWS SDK: response

NATS Topics

Topic Direction Purpose
ec2.CreateEgressOnlyInternetGateway Gateway -> Daemon Create new EIGW attached to a VPC
ec2.DeleteEgressOnlyInternetGateway Gateway -> Daemon Delete EIGW by ID
ec2.DescribeEgressOnlyInternetGateways Gateway -> Daemon List/filter EIGWs

All topics use the hive-workers queue group for load balancing across daemon instances.

Storage Layout

NATS JetStream KV bucket: hive-eigw (history: 10)

Key: eigw-a1b2c3d4e5f6g7h8
Value: {
  "egress_only_internet_gateway_id": "eigw-a1b2c3d4e5f6g7h8",
  "vpc_id": "vpc-abc123",
  "state": "attached",
  "tags": {"Name": "my-eigw", "Env": "prod"},
  "created_at": "2026-02-10T12:00:00Z"
}

Files Changed

File Change
hive/handlers/ec2/eigw/service.go EgressOnlyIGWService interface (3 methods)
hive/handlers/ec2/eigw/service_impl.go Direct implementation with NATS JetStream KV persistence
hive/handlers/ec2/eigw/service_impl_test.go 12 test cases against embedded NATS server
hive/handlers/ec2/eigw/service_nats.go NATS proxy (gateway -> daemon via utils.NATSRequest)
hive/gateway/ec2/eigw/CreateEgressOnlyInternetGateway.go Gateway validation + handler for Create
hive/gateway/ec2/eigw/DeleteEgressOnlyInternetGateway.go Gateway validation + handler for Delete
hive/gateway/ec2/eigw/DescribeEgressOnlyInternetGateways.go Gateway validation + handler for Describe
hive/gateway/ec2/eigw/eigw_test.go Gateway validation tests (9 cases)
hive/gateway/ec2.go 3 entries in ec2Actions map; centralized nil NATS check
hive/daemon/daemon.go eigwService field, 3 NATS subscriptions, service initialization with fallback
hive/daemon/daemon_handlers.go 3 handler functions delegating to handleNATSRequest
hive/awserrors/awserrors.go ErrorInvalidEgressOnlyInternetGatewayId.Malformed and .NotFound constants + lookup entries

Test Coverage

Service Implementation Tests (service_impl_test.go)

Test What it validates
TestCreateEgressOnlyInternetGateway Basic create: ID format, VPC attachment, state
TestCreateEgressOnlyInternetGateway_MissingVpcId Rejects nil VpcId with MissingParameter
TestCreateEgressOnlyInternetGateway_EmptyVpcId Rejects empty string VpcId
TestCreateEgressOnlyInternetGateway_WithTags Tags applied and persisted through describe round-trip
TestCreateEgressOnlyInternetGateway_TagsWrongResourceType Tags with non-matching resource type are ignored
TestDeleteEgressOnlyInternetGateway Delete succeeds, describe confirms removal
TestDeleteEgressOnlyInternetGateway_NotFound Returns NotFound for nonexistent ID
TestDeleteEgressOnlyInternetGateway_MissingID Rejects nil ID with MissingParameter
TestDeleteEgressOnlyInternetGateway_EmptyID Rejects empty string ID
TestDescribeEgressOnlyInternetGateways_All Lists all EIGWs when no filter specified
TestDescribeEgressOnlyInternetGateways_ByID Filters by specific EIGW ID
TestDescribeEgressOnlyInternetGateways_Empty Returns empty list for empty bucket

All tests run against an embedded NATS server with JetStream enabled.

Gateway Validation Tests (eigw_test.go)

Test What it validates
ValidateCreateEgressOnlyInternetGatewayInput nil input, missing VpcId, empty VpcId, valid input
ValidateDeleteEgressOnlyInternetGatewayInput nil input, missing ID, empty ID, valid input
ValidateDescribeEgressOnlyInternetGatewaysInput nil input, empty input, with filter IDs

AWS CLI Usage

# Create
aws ec2 create-egress-only-internet-gateway \
  --vpc-id vpc-abc123 \
  --tag-specifications 'ResourceType=egress-only-internet-gateway,Tags=[{Key=Name,Value=my-eigw}]' \
  --endpoint-url https://localhost:9999

# Describe all
aws ec2 describe-egress-only-internet-gateways \
  --endpoint-url https://localhost:9999

# Describe by ID
aws ec2 describe-egress-only-internet-gateways \
  --egress-only-internet-gateway-ids eigw-a1b2c3d4e5f6g7h8 \
  --endpoint-url https://localhost:9999

# Delete
aws ec2 delete-egress-only-internet-gateway \
  --egress-only-internet-gateway-id eigw-a1b2c3d4e5f6g7h8 \
  --endpoint-url https://localhost:9999