fix: add and drop network response fields to match AWS, as far as the SDK's shapes allow

This commit is contained in:
Julian Sommer
2026-09-30 10:58:45 +10:00
parent c4b284b8c9
commit 5b2ca9373b
12 changed files with 207 additions and 27 deletions
+6
View File
@@ -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)
+9 -2
View File
@@ -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
+45
View File
@@ -136,3 +136,48 @@ func TestEC2_ListsAWSOmitsWhenEmpty(t *testing.T) {
})
assert.Contains(t, tagged, "<tagSet><item><key>k</key><value>v</value></item></tagSet>")
}
// 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, "<ipPermissions></ipPermissions>")
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, "<tagSet></tagSet>")
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, "<groupSet></groupSet>")
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, "<ipv6CidrBlockAssociationSet></ipv6CidrBlockAssociationSet>")
}
@@ -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
}
@@ -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)
-1
View File
@@ -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 {
@@ -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)
}
+44 -7
View File
@@ -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),
+40
View File
@@ -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)
+15 -7
View File
@@ -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)
}
@@ -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
}
}
+16 -3
View File
@@ -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