mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 10:52:04 +00:00
fix: dial the ochre appliance customer ENI, not the management NIC (#851)
Both the endpoint ENI and the management NIC carry the rds-db-instance tag, so a first-match pick non-deterministically dialed the unreachable management NIC. Select the endpoint ENI by its RDS description, falling back to any usable tagged NIC.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user