mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
feat: DescribeAddressesAttribute (#103)
This commit is contained in:
@@ -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"},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user