diff --git a/spinifex/handlers/ochrevector/hostport.go b/spinifex/handlers/ochrevector/hostport.go index 268545a8e..4ebee7b2d 100644 --- a/spinifex/handlers/ochrevector/hostport.go +++ b/spinifex/handlers/ochrevector/hostport.go @@ -6,6 +6,7 @@ import ( "fmt" "log/slog" "net/netip" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" @@ -135,6 +136,11 @@ func ensureDaemonENI(ctx context.Context, vpcSvc vpcProvisioner, subnetID, nodeI // (tests/e2e/harness/rds.go) duplicates it the same way. const applianceInstanceTagKey = "spinifex:rds-db-instance" +// endpointENIDescriptionPrefix mirrors the description handlers/rds gives the +// customer (endpoint) ENI in resolveCustomerENI. The management NIC shares the +// rds-db-instance tag, so description is what tells the reachable ENI apart. +const endpointENIDescriptionPrefix = "RDS endpoint ENI for " + // resolveApplianceTarget finds the appliance's own customer ENI by the tag // handlers/rds stamps on it at launch, returning the real private IP to dial // and the subnet it lives in -- never the vanity endpoint hostname, which is @@ -152,13 +158,22 @@ func resolveApplianceTarget(ctx context.Context, deps HostPortDeps, identifier s if err != nil { return "", "", "", fmt.Errorf("ochrevector: describe appliance ENI for %s: %w", identifier, err) } + // Both the endpoint ENI and the management NIC carry the rds-db-instance + // tag, but only the endpoint ENI (in the customer VPC) is reachable from the + // daemon. Prefer it by description; fall back to any usable tagged NIC. var ni *ec2.NetworkInterface if out != nil { for _, cand := range out.NetworkInterfaces { - if aws.StringValue(cand.PrivateIpAddress) != "" && aws.StringValue(cand.SubnetId) != "" { + if aws.StringValue(cand.PrivateIpAddress) == "" || aws.StringValue(cand.SubnetId) == "" { + continue + } + if strings.HasPrefix(aws.StringValue(cand.Description), endpointENIDescriptionPrefix) { ni = cand break } + if ni == nil { + ni = cand + } } } if ni == nil { diff --git a/spinifex/handlers/ochrevector/hostport_test.go b/spinifex/handlers/ochrevector/hostport_test.go index 54d17d6dd..1db498ae1 100644 --- a/spinifex/handlers/ochrevector/hostport_test.go +++ b/spinifex/handlers/ochrevector/hostport_test.go @@ -98,6 +98,26 @@ func (f *fakeVPC) putApplianceENI(eniID, identifier, subnetID, ip string) { } } +// putTaggedENIWithDescription seeds an appliance-tagged ENI that also carries +// a description, so a test can present both the endpoint ENI and the +// management NIC that share the rds-db-instance tag at launch. +func (f *fakeVPC) putTaggedENIWithDescription(eniID, identifier, subnetID, ip, description string) { + f.mu.Lock() + defer f.mu.Unlock() + if f.records == nil { + f.records = map[string]*ec2.NetworkInterface{} + } + f.records[eniID] = &ec2.NetworkInterface{ + NetworkInterfaceId: aws.String(eniID), + SubnetId: aws.String(subnetID), + PrivateIpAddress: aws.String(ip), + Description: aws.String(description), + TagSet: []*ec2.Tag{ + {Key: aws.String(applianceInstanceTagKey), Value: aws.String(identifier)}, + }, + } +} + // tagValue returns the value of key in tags, or "" if absent. func tagValue(tags []*ec2.Tag, key string) string { for _, t := range tags { @@ -404,6 +424,31 @@ func TestResolveApplianceTarget_FindsTheTaggedApplianceENI(t *testing.T) { assert.Equal(t, "10.244.1.0/24", cidr) } +// The endpoint ENI and the management NIC share the rds-db-instance tag, but +// only the endpoint ENI (described "RDS endpoint ENI for ...") is reachable +// from the daemon. The resolver must pick it by description regardless of the +// order DescribeNetworkInterfaces returns them -- map iteration is unordered, +// so a repeat loop exercises both orderings. +func TestResolveApplianceTarget_PrefersEndpointENIOverManagementNIC(t *testing.T) { + const mgmtSubnet = "subnet-mgmt-0001" + const mgmtIP = "10.251.185.4" + for range 16 { + h := newHostPortHarness() + h.putSubnet(testApplianceSubnet, "10.244.1.0/24") + h.putSubnet(mgmtSubnet, "10.251.185.0/24") + h.vpc.putTaggedENIWithDescription("eni-mgmt", testApplianceIdentifier, mgmtSubnet, mgmtIP, + "RDS management NIC for "+testApplianceIdentifier) + h.vpc.putTaggedENIWithDescription("eni-endpoint", testApplianceIdentifier, testApplianceSubnet, testApplianceEndpoint, + endpointENIDescriptionPrefix+testApplianceIdentifier) + + dialIP, subnetID, cidr, err := resolveApplianceTarget(t.Context(), h.deps(), testApplianceIdentifier) + require.NoError(t, err) + require.Equal(t, testApplianceEndpoint, dialIP, "must dial the endpoint ENI, never the management NIC") + require.Equal(t, testApplianceSubnet, subnetID) + require.Equal(t, "10.244.1.0/24", cidr) + } +} + // An ENI tagged for a different identifier must never match. func TestResolveApplianceTarget_IgnoresOtherIdentifiersTags(t *testing.T) { h := newHostPortHarness()