From 5b2ca9373bfaf171979219f89a466dee79bf9ce9 Mon Sep 17 00:00:00 2001
From: Julian Sommer <115516918+juliansommer@users.noreply.github.com>
Date: Wed, 30 Sep 2026 10:58:45 +1000
Subject: [PATCH] fix: add and drop network response fields to match AWS, as
far as the SDK's shapes allow
---
spinifex/daemon/daemon.go | 6 +++
spinifex/gateway/ec2.go | 11 +++-
spinifex/gateway/ec2_emptyresult_test.go | 45 ++++++++++++++++
.../handlers/ec2/routetable/service_impl.go | 3 +-
.../ec2/routetable/service_impl_test.go | 4 +-
spinifex/handlers/ec2/vpc/default_vpc.go | 1 -
spinifex/handlers/ec2/vpc/default_vpc_test.go | 14 +++++
spinifex/handlers/ec2/vpc/eni.go | 51 ++++++++++++++++---
spinifex/handlers/ec2/vpc/eni_test.go | 40 +++++++++++++++
spinifex/handlers/ec2/vpc/security_group.go | 22 +++++---
.../handlers/ec2/vpc/security_group_test.go | 18 +++++--
spinifex/handlers/ec2/vpc/service_impl.go | 19 +++++--
12 files changed, 207 insertions(+), 27 deletions(-)
diff --git a/spinifex/daemon/daemon.go b/spinifex/daemon/daemon.go
index 369f1e1e8..16e2d0ed4 100644
--- a/spinifex/daemon/daemon.go
+++ b/spinifex/daemon/daemon.go
@@ -1806,6 +1806,12 @@ func (d *Daemon) startCluster() error {
// describe-tags agrees with each resource's own describe from birth and
// stops answering for it once it is deleted.
d.vpcService.SetCentralTagStore(d.tagsService)
+ // Name interfaces as their instances are named, so the two agree.
+ region, internalDomain := d.config.Region, handlers_dns.ResolveInternalDomain(d.config)
+ d.vpcService.SetPrivateDNSNamer(func(privateIP string) string {
+ _, private := handlers_dns.EC2DNSNames(region, "", internalDomain, "", privateIP)
+ return private
+ })
d.routeTableService, err = initServiceWithRetry("RouteTable service", func() (*handlers_ec2_routetable.RouteTableServiceImpl, error) {
return handlers_ec2_routetable.NewRouteTableServiceImplWithNATS(d.ctx, d.config, d.natsConn)
diff --git a/spinifex/gateway/ec2.go b/spinifex/gateway/ec2.go
index d57334a34..3cb16f668 100644
--- a/spinifex/gateway/ec2.go
+++ b/spinifex/gateway/ec2.go
@@ -109,8 +109,15 @@ func ec2Handler[In any](handler func(ctx context.Context, input *In, gw *Gateway
// ec2ListsAsSet names the list fields AWS omits when empty instead of
// rendering them. Each is rendered as its handler set it, so nil omits it.
var ec2ListsAsSet = map[reflect.Type][]string{
- reflect.TypeFor[ec2.CreateKeyPairOutput](): {"Tags"},
- reflect.TypeFor[ec2.Image](): {"Tags", "ProductCodes"},
+ reflect.TypeFor[ec2.CreateKeyPairOutput](): {"Tags"},
+ reflect.TypeFor[ec2.Image](): {"Tags", "ProductCodes"},
+ reflect.TypeFor[ec2.CreateSecurityGroupOutput](): {"Tags"},
+ reflect.TypeFor[ec2.SecurityGroup](): {"Tags"},
+ reflect.TypeFor[ec2.SecurityGroupRule](): {"Tags"},
+ reflect.TypeFor[ec2.RevokeSecurityGroupIngressOutput](): {"UnknownIpPermissions"},
+ reflect.TypeFor[ec2.RevokeSecurityGroupEgressOutput](): {"UnknownIpPermissions"},
+ reflect.TypeFor[ec2.NetworkInterface](): {"Ipv4Prefixes", "Ipv6Prefixes"},
+ reflect.TypeFor[ec2.Vpc](): {"Ipv6CidrBlockAssociationSet"},
}
// marshalEC2Response renders an EC2 handler's output into the action's XML
diff --git a/spinifex/gateway/ec2_emptyresult_test.go b/spinifex/gateway/ec2_emptyresult_test.go
index d5ffef11b..02e9a77c0 100644
--- a/spinifex/gateway/ec2_emptyresult_test.go
+++ b/spinifex/gateway/ec2_emptyresult_test.go
@@ -136,3 +136,48 @@ func TestEC2_ListsAWSOmitsWhenEmpty(t *testing.T) {
})
assert.Contains(t, tagged, "- kv
")
}
+
+// The network lists AWS omits when empty: an untagged security group's and
+// rule's tagSet, Revoke*'s unknownIpPermissionSet, an interface's prefix
+// lists, and a described VPC's IPv6 association set.
+func TestEC2_NetworkListsAWSOmitsWhenEmpty(t *testing.T) {
+ sg := render[ec2.CreateSecurityGroupInput](t, "CreateSecurityGroup", ec2.CreateSecurityGroupOutput{GroupId: aws.String("sg-1")})
+ assert.NotContains(t, sg, "tagSet")
+
+ groups := render[ec2.DescribeSecurityGroupsInput](t, "DescribeSecurityGroups", ec2.DescribeSecurityGroupsOutput{
+ SecurityGroups: []*ec2.SecurityGroup{{GroupId: aws.String("sg-1")}},
+ })
+ assert.NotContains(t, groups, "tagSet")
+ assert.Contains(t, groups, "")
+
+ auth := render[ec2.AuthorizeSecurityGroupIngressInput](t, "AuthorizeSecurityGroupIngress", ec2.AuthorizeSecurityGroupIngressOutput{
+ SecurityGroupRules: []*ec2.SecurityGroupRule{{SecurityGroupRuleId: aws.String("sgr-1")}},
+ })
+ assert.NotContains(t, auth, "tagSet")
+
+ rules := render[ec2.DescribeSecurityGroupRulesInput](t, "DescribeSecurityGroupRules", ec2.DescribeSecurityGroupRulesOutput{
+ SecurityGroupRules: []*ec2.SecurityGroupRule{{SecurityGroupRuleId: aws.String("sgr-1"), Tags: []*ec2.Tag{}}},
+ })
+ assert.Contains(t, rules, "")
+
+ revoke := render[ec2.RevokeSecurityGroupIngressInput](t, "RevokeSecurityGroupIngress", ec2.RevokeSecurityGroupIngressOutput{Return: aws.Bool(true)})
+ assert.NotContains(t, revoke, "unknownIpPermissionSet")
+ revokeEgress := render[ec2.RevokeSecurityGroupEgressInput](t, "RevokeSecurityGroupEgress", ec2.RevokeSecurityGroupEgressOutput{Return: aws.Bool(true)})
+ assert.NotContains(t, revokeEgress, "unknownIpPermissionSet")
+
+ enis := render[ec2.DescribeNetworkInterfacesInput](t, "DescribeNetworkInterfaces", ec2.DescribeNetworkInterfacesOutput{
+ NetworkInterfaces: []*ec2.NetworkInterface{{NetworkInterfaceId: aws.String("eni-1")}},
+ })
+ assert.NotContains(t, enis, "ipv4PrefixSet")
+ assert.NotContains(t, enis, "ipv6PrefixSet")
+ assert.Contains(t, enis, "")
+
+ vpcs := render[ec2.DescribeVpcsInput](t, "DescribeVpcs", ec2.DescribeVpcsOutput{Vpcs: []*ec2.Vpc{{VpcId: aws.String("vpc-1")}}})
+ assert.NotContains(t, vpcs, "ipv6CidrBlockAssociationSet")
+
+ created := render[ec2.CreateVpcInput](t, "CreateVpc", ec2.CreateVpcOutput{Vpc: &ec2.Vpc{
+ VpcId: aws.String("vpc-1"),
+ Ipv6CidrBlockAssociationSet: []*ec2.VpcIpv6CidrBlockAssociation{},
+ }})
+ assert.Contains(t, created, "")
+}
diff --git a/spinifex/handlers/ec2/routetable/service_impl.go b/spinifex/handlers/ec2/routetable/service_impl.go
index f3e8ec39b..e52f37367 100644
--- a/spinifex/handlers/ec2/routetable/service_impl.go
+++ b/spinifex/handlers/ec2/routetable/service_impl.go
@@ -551,7 +551,8 @@ func (s *RouteTableServiceImpl) CreateRouteTable(ctx context.Context, input *ec2
}
return &ec2.CreateRouteTableOutput{
- RouteTable: recordToEC2(record),
+ RouteTable: recordToEC2(record),
+ ClientToken: input.ClientToken,
}, nil
}
diff --git a/spinifex/handlers/ec2/routetable/service_impl_test.go b/spinifex/handlers/ec2/routetable/service_impl_test.go
index 1a33646e0..b6af1c132 100644
--- a/spinifex/handlers/ec2/routetable/service_impl_test.go
+++ b/spinifex/handlers/ec2/routetable/service_impl_test.go
@@ -103,9 +103,11 @@ func TestCreateRouteTable(t *testing.T) {
t.Parallel()
svc := setupTestService(t)
out, err := svc.CreateRouteTable(t.Context(), &ec2.CreateRouteTableInput{
- VpcId: aws.String("vpc-test1"),
+ VpcId: aws.String("vpc-test1"),
+ ClientToken: aws.String("token-1"),
}, testAccountID)
require.NoError(t, err)
+ assert.Equal(t, "token-1", aws.StringValue(out.ClientToken), "AWS echoes the request's ClientToken")
rtb := out.RouteTable
assert.NotEmpty(t, *rtb.RouteTableId)
diff --git a/spinifex/handlers/ec2/vpc/default_vpc.go b/spinifex/handlers/ec2/vpc/default_vpc.go
index a5eaf060c..95a47b70c 100644
--- a/spinifex/handlers/ec2/vpc/default_vpc.go
+++ b/spinifex/handlers/ec2/vpc/default_vpc.go
@@ -270,7 +270,6 @@ func (s *VPCServiceImpl) buildDefaultVPC(ctx context.Context, accountID string,
AZ: s.localAZ(),
EnableDnsSupport: true, // AWS default
EnableDnsHostnames: true, // AWS default for default VPC
- Tags: map[string]string{"Name": "default"},
CreatedAt: time.Now(),
})
if err != nil {
diff --git a/spinifex/handlers/ec2/vpc/default_vpc_test.go b/spinifex/handlers/ec2/vpc/default_vpc_test.go
index c698f4d0d..b3b4f30d4 100644
--- a/spinifex/handlers/ec2/vpc/default_vpc_test.go
+++ b/spinifex/handlers/ec2/vpc/default_vpc_test.go
@@ -270,3 +270,17 @@ func TestEnsureDefaultVPC_UsesBootstrapIDs(t *testing.T) {
assert.Equal(t, ids.IgwId, info.InternetGatewayId)
}
}
+
+// AWS leaves the default VPC untagged.
+func TestEnsureDefaultVPC_LeavesVPCUntagged(t *testing.T) {
+ t.Parallel()
+ svcs, _, _ := newNodeServices(t, 1)
+
+ info, err := svcs[0].EnsureDefaultVPC("000000000001", handlers_ec2_vpc.BootstrapIDs{})
+ require.NoError(t, err)
+
+ out, err := svcs[0].DescribeVpcs(t.Context(), &ec2.DescribeVpcsInput{VpcIds: []*string{aws.String(info.VpcId)}}, "000000000001")
+ require.NoError(t, err)
+ require.Len(t, out.Vpcs, 1)
+ assert.Empty(t, out.Vpcs[0].Tags)
+}
diff --git a/spinifex/handlers/ec2/vpc/eni.go b/spinifex/handlers/ec2/vpc/eni.go
index e45a5a164..768908c31 100644
--- a/spinifex/handlers/ec2/vpc/eni.go
+++ b/spinifex/handlers/ec2/vpc/eni.go
@@ -216,8 +216,10 @@ func (s *VPCServiceImpl) CreateNetworkInterface(ctx context.Context, input *ec2.
return nil, err
}
+ groupNames := make(map[string]string)
+ s.fillSGNames(ctx, accountID, record.SecurityGroupIds, groupNames)
return &ec2.CreateNetworkInterfaceOutput{
- NetworkInterface: s.eniRecordToEC2(&record, accountID),
+ NetworkInterface: s.eniRecordToEC2(&record, accountID, groupNames),
}, nil
}
@@ -542,6 +544,7 @@ func (s *VPCServiceImpl) DescribeNetworkInterfaces(ctx context.Context, input *e
}
enis := make([]*ec2.NetworkInterface, 0)
+ groupNames := make(map[string]string)
eniIDs := make(map[string]bool)
for _, id := range input.NetworkInterfaceIds {
@@ -586,7 +589,8 @@ func (s *VPCServiceImpl) DescribeNetworkInterfaces(ctx context.Context, input *e
continue
}
- enis = append(enis, s.eniRecordToEC2(&record, accountID))
+ s.fillSGNames(ctx, accountID, record.SecurityGroupIds, groupNames)
+ enis = append(enis, s.eniRecordToEC2(&record, accountID, groupNames))
}
// If specific ENI IDs were requested but not found, return error
@@ -1008,8 +1012,32 @@ func (s *VPCServiceImpl) updateENIPublicIP(ctx context.Context, accountID, eniId
return nil
}
-// eniRecordToEC2 converts an ENI record to an EC2 NetworkInterface.
-func (s *VPCServiceImpl) eniRecordToEC2(record *ENIRecord, accountID string) *ec2.NetworkInterface {
+// fillSGNames records in names the name of each group in ids it does not
+// already hold. A group that cannot be read is recorded as "", so it is listed
+// without a name and not looked up again.
+func (s *VPCServiceImpl) fillSGNames(ctx context.Context, accountID string, ids []string, names map[string]string) {
+ for _, id := range ids {
+ if _, ok := names[id]; ok {
+ continue
+ }
+ names[id] = ""
+ entry, err := s.sgKV.Get(ctx, utils.AccountKey(accountID, id))
+ if err != nil {
+ slog.WarnContext(ctx, "fillSGNames: SG read failed", "groupId", id, "err", err)
+ continue
+ }
+ var rec SecurityGroupRecord
+ if err := json.Unmarshal(entry.Value(), &rec); err != nil {
+ slog.WarnContext(ctx, "fillSGNames: SG unmarshal failed", "groupId", id, "err", err)
+ continue
+ }
+ names[id] = rec.GroupName
+ }
+}
+
+// eniRecordToEC2 converts an ENI record to an EC2 NetworkInterface, naming
+// each security group from groupNames.
+func (s *VPCServiceImpl) eniRecordToEC2(record *ENIRecord, accountID string, groupNames map[string]string) *ec2.NetworkInterface {
// ENIs with spinifex:managed-by tag are system-managed (e.g. by ELBv2)
requesterManaged := record.Tags["spinifex:managed-by"] != ""
@@ -1038,13 +1066,22 @@ func (s *VPCServiceImpl) eniRecordToEC2(record *ENIRecord, accountID string) *ec
if len(record.SecurityGroupIds) > 0 {
groups := make([]*ec2.GroupIdentifier, 0, len(record.SecurityGroupIds))
for _, sgId := range record.SecurityGroupIds {
- groups = append(groups, &ec2.GroupIdentifier{
- GroupId: aws.String(sgId),
- })
+ group := &ec2.GroupIdentifier{GroupId: aws.String(sgId)}
+ if name := groupNames[sgId]; name != "" {
+ group.GroupName = aws.String(name)
+ }
+ groups = append(groups, group)
}
eni.Groups = groups
}
+ if s.privateDNSName != nil && record.PrivateIpAddress != "" {
+ if name := s.privateDNSName(record.PrivateIpAddress); name != "" {
+ eni.PrivateDnsName = aws.String(name)
+ eni.PrivateIpAddresses[0].PrivateDnsName = aws.String(name)
+ }
+ }
+
if record.PublicIpAddress != "" {
eni.Association = &ec2.NetworkInterfaceAssociation{
PublicIp: aws.String(record.PublicIpAddress),
diff --git a/spinifex/handlers/ec2/vpc/eni_test.go b/spinifex/handlers/ec2/vpc/eni_test.go
index 752874caf..0d5d7da6d 100644
--- a/spinifex/handlers/ec2/vpc/eni_test.go
+++ b/spinifex/handlers/ec2/vpc/eni_test.go
@@ -50,6 +50,46 @@ func TestCreateNetworkInterface(t *testing.T) {
assert.NotEmpty(t, *eni.MacAddress)
}
+func TestNetworkInterface_NamesGroupsAndPrivateAddress(t *testing.T) {
+ t.Parallel()
+ svc := setupTestVPCService(t)
+ vpcId := createTestVPC(t, svc, "10.0.0.0/16")
+ subnetId := createTestSubnet(t, svc, vpcId, "10.0.1.0/24")
+ sgId := createTestSG(t, svc, vpcId, "web")
+
+ // Without a namer an interface carries no private name.
+ unnamed := createTestENI(t, svc, subnetId)
+ desc, err := svc.DescribeNetworkInterfaces(context.Background(), &ec2.DescribeNetworkInterfacesInput{
+ NetworkInterfaceIds: []*string{aws.String(unnamed)},
+ }, testAccountID)
+ require.NoError(t, err)
+ require.Len(t, desc.NetworkInterfaces, 1)
+ assert.Nil(t, desc.NetworkInterfaces[0].PrivateDnsName)
+
+ svc.SetPrivateDNSNamer(func(ip string) string { return "name-of-" + ip })
+ out, err := svc.CreateNetworkInterface(context.Background(), &ec2.CreateNetworkInterfaceInput{
+ SubnetId: aws.String(subnetId),
+ Groups: []*string{aws.String(sgId)},
+ }, testAccountID)
+ require.NoError(t, err)
+
+ desc, err = svc.DescribeNetworkInterfaces(context.Background(), &ec2.DescribeNetworkInterfacesInput{
+ NetworkInterfaceIds: []*string{out.NetworkInterface.NetworkInterfaceId},
+ }, testAccountID)
+ require.NoError(t, err)
+ require.Len(t, desc.NetworkInterfaces, 1)
+
+ for name, eni := range map[string]*ec2.NetworkInterface{"create": out.NetworkInterface, "describe": desc.NetworkInterfaces[0]} {
+ want := "name-of-" + aws.StringValue(eni.PrivateIpAddress)
+ assert.Equal(t, want, aws.StringValue(eni.PrivateDnsName), name)
+ require.Len(t, eni.PrivateIpAddresses, 1, name)
+ assert.Equal(t, want, aws.StringValue(eni.PrivateIpAddresses[0].PrivateDnsName), name)
+ require.Len(t, eni.Groups, 1, name)
+ assert.Equal(t, sgId, aws.StringValue(eni.Groups[0].GroupId), name)
+ assert.Equal(t, "web", aws.StringValue(eni.Groups[0].GroupName), name)
+ }
+}
+
func TestCreateNetworkInterface_SequentialIPs(t *testing.T) {
t.Parallel()
svc := setupTestVPCService(t)
diff --git a/spinifex/handlers/ec2/vpc/security_group.go b/spinifex/handlers/ec2/vpc/security_group.go
index 5926516f8..389f4d4e8 100644
--- a/spinifex/handlers/ec2/vpc/security_group.go
+++ b/spinifex/handlers/ec2/vpc/security_group.go
@@ -286,6 +286,7 @@ func (s *VPCServiceImpl) CreateSecurityGroup(ctx context.Context, input *ec2.Cre
return &ec2.CreateSecurityGroupOutput{
GroupId: aws.String(groupId),
+ Tags: utils.MapToEC2Tags(record.Tags),
}, nil
}
@@ -864,6 +865,13 @@ func (s *VPCServiceImpl) DescribeSecurityGroupRules(ctx context.Context, input *
}
rules, nextToken := paging.EC2Page(rules, func(r *ec2.SecurityGroupRule) string { return *r.SecurityGroupRuleId }, pageReq)
+ // AWS lists an untagged rule with empty Tags here, but omits them from
+ // the Authorize* responses that share the rule shape.
+ for _, r := range rules {
+ if r.Tags == nil {
+ r.Tags = []*ec2.Tag{}
+ }
+ }
slog.InfoContext(ctx, "DescribeSecurityGroupRules completed", "count", len(rules), "accountID", accountID)
@@ -982,8 +990,8 @@ func applySGRuleTags(rules []SGRule, specs []*ec2.TagSpecification) []SGRule {
}
// sgRuleToSecurityGroupRule flattens a stored SGRule into the AWS API shape.
-// accountID supplies GroupOwnerId and ReferencedGroupInfo.UserId; VpcId is
-// derived from the parent record (same-VPC references are enforced on write).
+// accountID supplies GroupOwnerId and ReferencedGroupInfo.UserId. AWS names
+// no VpcId for a same-VPC reference, the only kind Spinifex allows.
func sgRuleToSecurityGroupRule(record *SecurityGroupRecord, rule SGRule, isEgress bool, accountID string) *ec2.SecurityGroupRule {
// An all-protocol rule has no ports to report. AWS answers -1 for both;
// reporting the stored zeroes offers a caller two ports it never sent.
@@ -1014,7 +1022,6 @@ func sgRuleToSecurityGroupRule(record *SecurityGroupRecord, rule SGRule, isEgres
out.ReferencedGroupInfo = &ec2.ReferencedSecurityGroup{
GroupId: aws.String(rule.SourceSG),
UserId: aws.String(accountID),
- VpcId: aws.String(record.VpcId),
}
}
return out
@@ -1841,8 +1848,8 @@ func (s *VPCServiceImpl) sgRecordToEC2(record *SecurityGroupRecord, accountID st
Description: aws.String(record.Description),
VpcId: aws.String(record.VpcId),
OwnerId: aws.String(accountID),
- IpPermissions: sgRulesToIpPermissions(record.IngressRules),
- IpPermissionsEgress: sgRulesToIpPermissions(record.EgressRules),
+ IpPermissions: sgRulesToIpPermissions(record.IngressRules, accountID),
+ IpPermissionsEgress: sgRulesToIpPermissions(record.EgressRules, accountID),
}
sg.Tags = utils.MapToEC2Tags(record.Tags)
@@ -1954,7 +1961,8 @@ func ipPermissionsToSGRules(perms []*ec2.IpPermission, mode sgParseMode) ([]SGRu
}
// sgRulesToIpPermissions converts SGRule slice to AWS IpPermission slice.
-func sgRulesToIpPermissions(rules []SGRule) []*ec2.IpPermission {
+// accountID owns every referenced group, since references stay in one VPC.
+func sgRulesToIpPermissions(rules []SGRule, accountID string) []*ec2.IpPermission {
// Group rules by protocol+port range
type permKey struct {
IpProtocol string
@@ -1992,7 +2000,7 @@ func sgRulesToIpPermissions(rules []SGRule) []*ec2.IpPermission {
perm.Ipv6Ranges = append(perm.Ipv6Ranges, ipRange)
}
if rule.SourceSG != "" {
- pair := &ec2.UserIdGroupPair{GroupId: aws.String(rule.SourceSG)}
+ pair := &ec2.UserIdGroupPair{GroupId: aws.String(rule.SourceSG), UserId: aws.String(accountID)}
if rule.Description != "" {
pair.Description = aws.String(rule.Description)
}
diff --git a/spinifex/handlers/ec2/vpc/security_group_test.go b/spinifex/handlers/ec2/vpc/security_group_test.go
index 8e05ad60b..7f6b81bd6 100644
--- a/spinifex/handlers/ec2/vpc/security_group_test.go
+++ b/spinifex/handlers/ec2/vpc/security_group_test.go
@@ -44,6 +44,7 @@ func TestCreateSecurityGroup_Success(t *testing.T) {
}, testAccountID)
require.NoError(t, err)
assert.NotEmpty(t, *out.GroupId)
+ assert.Nil(t, out.Tags, "AWS omits Tags from an untagged CreateSecurityGroup")
}
func TestCreateSecurityGroup_MissingGroupName(t *testing.T) {
@@ -930,7 +931,7 @@ func TestSGRulesToIpPermissions_IPv6RoundTrip(t *testing.T) {
perms := sgRulesToIpPermissions([]SGRule{
{IpProtocol: "-1", CidrIp: "0.0.0.0/0"},
{IpProtocol: "-1", CidrIpv6: "::/0"},
- })
+ }, testAccountID)
require.Len(t, perms, 1)
require.Len(t, perms[0].IpRanges, 1)
require.Len(t, perms[0].Ipv6Ranges, 1)
@@ -945,7 +946,7 @@ func TestSGRulesToIpPermissions_Conversion(t *testing.T) {
{IpProtocol: "-1", SourceSG: "sg-other"},
}
- perms := sgRulesToIpPermissions(rules)
+ perms := sgRulesToIpPermissions(rules, testAccountID)
require.Len(t, perms, 2)
// Order is non-deterministic (map-based), find each by protocol
@@ -968,6 +969,7 @@ func TestSGRulesToIpPermissions_Conversion(t *testing.T) {
assert.Nil(t, allPerm.ToPort)
assert.Len(t, allPerm.UserIdGroupPairs, 1)
assert.Equal(t, "sg-other", *allPerm.UserIdGroupPairs[0].GroupId)
+ assert.Equal(t, testAccountID, aws.StringValue(allPerm.UserIdGroupPairs[0].UserId))
}
func TestSGRuleKey(t *testing.T) {
@@ -1456,6 +1458,9 @@ func TestDescribeSecurityGroups_FilterByTag(t *testing.T) {
},
}, testAccountID)
require.NoError(t, err)
+ require.Len(t, out.Tags, 1, "CreateSecurityGroup echoes the requested tags")
+ assert.Equal(t, "Env", aws.StringValue(out.Tags[0].Key))
+ assert.Equal(t, "prod", aws.StringValue(out.Tags[0].Value))
createTestSG(t, svc, vpcID, "untagged-sg")
@@ -2309,7 +2314,7 @@ func TestDescribeSecurityGroupRules_ReferencedGroupInfo(t *testing.T) {
dstSG := createTestSG(t, svc, vpcID, "dest")
proto := "tcp"
- _, err := svc.AuthorizeSecurityGroupIngress(context.Background(), &ec2.AuthorizeSecurityGroupIngressInput{
+ authOut, err := svc.AuthorizeSecurityGroupIngress(context.Background(), &ec2.AuthorizeSecurityGroupIngressInput{
GroupId: aws.String(dstSG),
IpPermissions: []*ec2.IpPermission{{
IpProtocol: &proto,
@@ -2319,6 +2324,8 @@ func TestDescribeSecurityGroupRules_ReferencedGroupInfo(t *testing.T) {
}},
}, testAccountID)
require.NoError(t, err)
+ require.Len(t, authOut.SecurityGroupRules, 1)
+ assert.Nil(t, authOut.SecurityGroupRules[0].Tags, "Authorize* omits Tags on an untagged rule")
out, err := svc.DescribeSecurityGroupRules(context.Background(), &ec2.DescribeSecurityGroupRulesInput{
Filters: []*ec2.Filter{{Name: aws.String("group-id"), Values: []*string{aws.String(dstSG)}}},
@@ -2330,11 +2337,12 @@ func TestDescribeSecurityGroupRules_ReferencedGroupInfo(t *testing.T) {
if r.ReferencedGroupInfo != nil {
require.NotNil(t, r.ReferencedGroupInfo.GroupId)
require.NotNil(t, r.ReferencedGroupInfo.UserId)
- require.NotNil(t, r.ReferencedGroupInfo.VpcId)
assert.Equal(t, srcSG, *r.ReferencedGroupInfo.GroupId)
assert.Equal(t, testAccountID, *r.ReferencedGroupInfo.UserId)
- assert.Equal(t, vpcID, *r.ReferencedGroupInfo.VpcId)
+ assert.Nil(t, r.ReferencedGroupInfo.VpcId, "AWS names no VpcId for a same-VPC reference")
assert.Nil(t, r.CidrIpv4)
+ assert.NotNil(t, r.Tags, "DescribeSecurityGroupRules lists an untagged rule with empty Tags")
+ assert.Empty(t, r.Tags)
found = true
}
}
diff --git a/spinifex/handlers/ec2/vpc/service_impl.go b/spinifex/handlers/ec2/vpc/service_impl.go
index 0d9f82dd9..0225c6000 100644
--- a/spinifex/handlers/ec2/vpc/service_impl.go
+++ b/spinifex/handlers/ec2/vpc/service_impl.go
@@ -98,6 +98,10 @@ type VPCServiceImpl struct {
// A nil value leaves both unchanged.
centralTags CentralTagStore
+ // Optional: injected after construction to name interfaces' private
+ // addresses. Nil leaves them unnamed.
+ privateDNSName func(privateIP string) string
+
// disableDefaultPublicIP seeds default subnets with MapPublicIpOnLaunch=false
// (non-pool external modes have no public IPs to assign). Zero value keeps
// the AWS-faithful default of true.
@@ -117,6 +121,13 @@ func (s *VPCServiceImpl) SetExternalIPAM(ipam *ExternalIPAM, eipKV jetstream.Key
s.eipKV = eipKV
}
+// SetPrivateDNSNamer injects how a private address is named, so an interface
+// reports the same PrivateDnsName as the instance using it. Nil, or a namer
+// that answers "", leaves interfaces unnamed.
+func (s *VPCServiceImpl) SetPrivateDNSNamer(namer func(privateIP string) string) {
+ s.privateDNSName = namer
+}
+
// SetCentralTagStore injects the central tag store so create paths project
// their record tags into it and delete paths clear them. Nil leaves both
// unchanged.
@@ -328,9 +339,11 @@ func (s *VPCServiceImpl) CreateVpc(ctx context.Context, input *ec2.CreateVpcInpu
return nil, errors.New(awserrors.ErrorServerInternal)
}
- return &ec2.CreateVpcOutput{
- Vpc: s.vpcRecordToEC2(&record, accountID),
- }, nil
+ // AWS renders the empty IPv6 association list on CreateVpc only;
+ // DescribeVpcs omits it.
+ vpc := s.vpcRecordToEC2(&record, accountID)
+ vpc.Ipv6CidrBlockAssociationSet = []*ec2.VpcIpv6CidrBlockAssociation{}
+ return &ec2.CreateVpcOutput{Vpc: vpc}, nil
}
// requireVPCExists returns InvalidVpcID.NotFound if the VPC doesn't exist for