mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-21 11:46:37 +00:00
195 lines
9.1 KiB
Markdown
195 lines
9.1 KiB
Markdown
# 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:
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
```
|