feat: DescribeAddressesAttribute (#103)

This commit is contained in:
Julian Sommer
2026-03-31 10:14:27 +10:00
committed by GitHub
parent 0d2e91da37
commit a7925d65f2
10 changed files with 159 additions and 1 deletions
+1
View File
@@ -492,6 +492,7 @@ func (d *Daemon) subscribeAll() error {
natsSub{"ec2.AssociateAddress", d.handleEC2AssociateAddress, "spinifex-workers"},
natsSub{"ec2.DisassociateAddress", d.handleEC2DisassociateAddress, "spinifex-workers"},
natsSub{"ec2.DescribeAddresses", d.handleEC2DescribeAddresses, "spinifex-workers"},
natsSub{"ec2.DescribeAddressesAttribute", d.handleEC2DescribeAddressesAttribute, "spinifex-workers"},
)
}
+4
View File
@@ -21,3 +21,7 @@ func (d *Daemon) handleEC2DisassociateAddress(msg *nats.Msg) {
func (d *Daemon) handleEC2DescribeAddresses(msg *nats.Msg) {
handleNATSRequest(msg, d.eipService.DescribeAddresses)
}
func (d *Daemon) handleEC2DescribeAddressesAttribute(msg *nats.Msg) {
handleNATSRequest(msg, d.eipService.DescribeAddressesAttribute)
}
+3
View File
@@ -318,6 +318,9 @@ var ec2Actions = map[string]EC2Handler{
"DescribeAddresses": ec2Handler(func(input *ec2.DescribeAddressesInput, gw *GatewayConfig, accountID string) (any, error) {
return gateway_ec2_eip.DescribeAddresses(input, gw.NATSConn, accountID)
}),
"DescribeAddressesAttribute": ec2Handler(func(input *ec2.DescribeAddressesAttributeInput, gw *GatewayConfig, accountID string) (any, error) {
return gateway_ec2_eip.DescribeAddressesAttribute(input, gw.NATSConn, accountID)
}),
"CreateNatGateway": ec2Handler(func(input *ec2.CreateNatGatewayInput, gw *GatewayConfig, accountID string) (any, error) {
return gateway_ec2_natgw.CreateNatGateway(input, gw.NATSConn, accountID)
}),
@@ -0,0 +1,27 @@
package gateway_ec2_eip
import (
"errors"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/spinifex/spinifex/awserrors"
handlers_ec2_eip "github.com/mulgadc/spinifex/spinifex/handlers/ec2/eip"
"github.com/nats-io/nats.go"
)
// DescribeAddressesAttribute handles the EC2 DescribeAddressesAttribute API call.
func DescribeAddressesAttribute(input *ec2.DescribeAddressesAttributeInput, natsConn *nats.Conn, accountID string) (ec2.DescribeAddressesAttributeOutput, error) {
var output ec2.DescribeAddressesAttributeOutput
if input == nil {
return output, errors.New(awserrors.ErrorInvalidParameterValue)
}
svc := handlers_ec2_eip.NewNATSEIPService(natsConn)
result, err := svc.DescribeAddressesAttribute(input, accountID)
if err != nil {
return output, err
}
return *result, nil
}
+12
View File
@@ -57,6 +57,18 @@ func TestDescribeAddresses_NilNATS(t *testing.T) {
assert.Error(t, err)
}
// DescribeAddressesAttribute tests
func TestDescribeAddressesAttribute_NilInput(t *testing.T) {
_, err := DescribeAddressesAttribute(nil, nil, testAccountID)
assert.EqualError(t, err, awserrors.ErrorInvalidParameterValue)
}
func TestDescribeAddressesAttribute_NilNATS(t *testing.T) {
_, err := DescribeAddressesAttribute(&ec2.DescribeAddressesAttributeInput{}, nil, testAccountID)
assert.Error(t, err)
}
// DisassociateAddress tests
func TestDisassociateAddress_NilInput(t *testing.T) {
+1 -1
View File
@@ -1029,7 +1029,7 @@ func TestEC2ActionMapCompleteness(t *testing.T) {
"AuthorizeSecurityGroupIngress", "AuthorizeSecurityGroupEgress",
"RevokeSecurityGroupIngress", "RevokeSecurityGroupEgress",
"DescribeInstanceCreditSpecifications",
"AllocateAddress", "ReleaseAddress", "AssociateAddress", "DisassociateAddress", "DescribeAddresses",
"AllocateAddress", "ReleaseAddress", "AssociateAddress", "DisassociateAddress", "DescribeAddresses", "DescribeAddressesAttribute",
"CreateRouteTable", "DeleteRouteTable", "DescribeRouteTables",
"CreateRoute", "DeleteRoute", "ReplaceRoute",
"AssociateRouteTable", "DisassociateRouteTable", "ReplaceRouteTableAssociation",
+1
View File
@@ -9,4 +9,5 @@ type EIPService interface {
AssociateAddress(input *ec2.AssociateAddressInput, accountID string) (*ec2.AssociateAddressOutput, error)
DisassociateAddress(input *ec2.DisassociateAddressInput, accountID string) (*ec2.DisassociateAddressOutput, error)
DescribeAddresses(input *ec2.DescribeAddressesInput, accountID string) (*ec2.DescribeAddressesOutput, error)
DescribeAddressesAttribute(input *ec2.DescribeAddressesAttributeInput, accountID string) (*ec2.DescribeAddressesAttributeOutput, error)
}
+66
View File
@@ -368,6 +368,72 @@ func (s *EIPServiceImpl) DescribeAddresses(input *ec2.DescribeAddressesInput, ac
}, nil
}
// DescribeAddressesAttribute returns per-EIP attributes (e.g. domain-name).
// Spinifex doesn't support reverse-DNS PTR records, so PtrRecord is left nil.
// Unlike DescribeAddresses, this returns an empty list (not an error) when
// requested AllocationIds are not found. This matches real AWS behavior.
func (s *EIPServiceImpl) DescribeAddressesAttribute(input *ec2.DescribeAddressesAttributeInput, accountID string) (*ec2.DescribeAddressesAttributeOutput, error) {
var addresses []*ec2.AddressAttribute
if len(input.AllocationIds) > 0 {
// Direct lookups — O(n) on requested IDs rather than scanning all EIPs.
for _, id := range input.AllocationIds {
if id == nil {
continue
}
key := utils.AccountKey(accountID, *id)
entry, err := s.eipKV.Get(key)
if err != nil {
continue // not found, skip
}
var record EIPRecord
if err := json.Unmarshal(entry.Value(), &record); err != nil {
slog.Warn("Failed to unmarshal EIP record", "key", key, "error", err)
continue
}
addresses = append(addresses, &ec2.AddressAttribute{
AllocationId: aws.String(record.AllocationId),
PublicIp: aws.String(record.PublicIp),
})
}
} else {
// No filter — scan all EIPs for this account.
prefix := accountID + "."
keys, err := s.eipKV.Keys()
if err != nil && !errors.Is(err, nats.ErrNoKeysFound) {
return nil, errors.New(awserrors.ErrorServerInternal)
}
for _, k := range keys {
if k == utils.VersionKey {
continue
}
if !strings.HasPrefix(k, prefix) {
continue
}
entry, err := s.eipKV.Get(k)
if err != nil {
slog.Warn("Failed to get EIP record", "key", k, "error", err)
continue
}
var record EIPRecord
if err := json.Unmarshal(entry.Value(), &record); err != nil {
slog.Warn("Failed to unmarshal EIP record", "key", k, "error", err)
continue
}
addresses = append(addresses, &ec2.AddressAttribute{
AllocationId: aws.String(record.AllocationId),
PublicIp: aws.String(record.PublicIp),
})
}
}
slog.Info("DescribeAddressesAttribute completed", "count", len(addresses), "accountID", accountID)
return &ec2.DescribeAddressesAttributeOutput{
Addresses: addresses,
}, nil
}
// lookupENI retrieves an ENI record by its ID using the VPC service.
func (s *EIPServiceImpl) lookupENI(accountID, eniID string) (*handlers_ec2_vpc.ENIRecord, error) {
output, err := s.vpcService.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{
@@ -293,6 +293,46 @@ func TestEIP_FindByAssociationID_NotFound(t *testing.T) {
assert.Contains(t, err.Error(), "InvalidAssociationID.NotFound")
}
func TestEIP_DescribeAddressesAttribute(t *testing.T) {
svc, _ := setupTestEIP(t)
// Allocate multiple EIPs
out1, err := svc.AllocateAddress(&ec2.AllocateAddressInput{}, testAccountID)
require.NoError(t, err)
out2, err := svc.AllocateAddress(&ec2.AllocateAddressInput{}, testAccountID)
require.NoError(t, err)
// Describe all — should return both
desc, err := svc.DescribeAddressesAttribute(&ec2.DescribeAddressesAttributeInput{}, testAccountID)
require.NoError(t, err)
assert.Len(t, desc.Addresses, 2)
// Each entry should have AllocationId and PublicIp populated
for _, addr := range desc.Addresses {
assert.NotNil(t, addr.AllocationId)
assert.NotNil(t, addr.PublicIp)
assert.Nil(t, addr.PtrRecord) // no reverse-DNS support
}
// Filter by specific allocation ID
desc2, err := svc.DescribeAddressesAttribute(&ec2.DescribeAddressesAttributeInput{
AllocationIds: []*string{out1.AllocationId},
}, testAccountID)
require.NoError(t, err)
assert.Len(t, desc2.Addresses, 1)
assert.Equal(t, *out1.AllocationId, *desc2.Addresses[0].AllocationId)
assert.Equal(t, *out1.PublicIp, *desc2.Addresses[0].PublicIp)
// Filter by unknown allocation ID — returns empty, not error
desc3, err := svc.DescribeAddressesAttribute(&ec2.DescribeAddressesAttributeInput{
AllocationIds: []*string{aws.String("eipalloc-nonexistent")},
}, testAccountID)
require.NoError(t, err)
assert.Empty(t, desc3.Addresses)
_ = out2
}
func TestEIP_DescribeAddresses(t *testing.T) {
svc, _ := setupTestEIP(t)
@@ -37,3 +37,7 @@ func (s *NATSEIPService) DisassociateAddress(input *ec2.DisassociateAddressInput
func (s *NATSEIPService) DescribeAddresses(input *ec2.DescribeAddressesInput, accountID string) (*ec2.DescribeAddressesOutput, error) {
return utils.NATSRequest[ec2.DescribeAddressesOutput](s.natsConn, "ec2.DescribeAddresses", input, 30*time.Second, accountID)
}
func (s *NATSEIPService) DescribeAddressesAttribute(input *ec2.DescribeAddressesAttributeInput, accountID string) (*ec2.DescribeAddressesAttributeOutput, error) {
return utils.NATSRequest[ec2.DescribeAddressesAttributeOutput](s.natsConn, "ec2.DescribeAddressesAttribute", input, 30*time.Second, accountID)
}