Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/coverage/aws/vpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,15 @@ PrefixLists is an OPTIONAL AWS capability (type-asserted).
| `GetManagedPrefixListEntries` | |
| `ModifyManagedPrefixList` | |

### ServicePrefixLists

ServicePrefixLists is an OPTIONAL AWS capability (type-asserted). The lists

| Operation | Description |
| --- | --- |
| `DescribeAWSManagedPrefixLists` | DescribeAWSManagedPrefixLists returns the same lists in the managed prefix |
| `DescribePrefixLists` | DescribePrefixLists returns the service lists for region, narrowed to ids |

### SubnetAttributes

SubnetAttributes is an OPTIONAL capability, discovered by type assertion.
Expand Down Expand Up @@ -410,6 +419,14 @@ VPCEndpointServices is an OPTIONAL AWS capability (type-asserted).
| `DescribeVPCEndpointServicePermissions` | |
| `ModifyVPCEndpointServicePermissions` | |

### VPCEndpointSetModifier

VPCEndpointSetModifier is an OPTIONAL AWS capability (type-asserted). It

| Operation | Description |
| --- | --- |
| `ModifyVPCEndpointSets` | |

### VPNConnections

VPNConnections is an OPTIONAL AWS capability (type-asserted).
Expand Down
23 changes: 23 additions & 0 deletions docs/coverage/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -11796,6 +11796,20 @@
}
]
},
{
"name": "ServicePrefixLists",
"doc": "ServicePrefixLists is an OPTIONAL AWS capability (type-asserted). The lists",
"operations": [
{
"name": "DescribeAWSManagedPrefixLists",
"doc": "DescribeAWSManagedPrefixLists returns the same lists in the managed prefix"
},
{
"name": "DescribePrefixLists",
"doc": "DescribePrefixLists returns the service lists for region, narrowed to ids"
}
]
},
{
"name": "SubnetAttributes",
"doc": "SubnetAttributes is an OPTIONAL capability, discovered by type assertion.",
Expand Down Expand Up @@ -11970,6 +11984,15 @@
}
]
},
{
"name": "VPCEndpointSetModifier",
"doc": "VPCEndpointSetModifier is an OPTIONAL AWS capability (type-asserted). It",
"operations": [
{
"name": "ModifyVPCEndpointSets"
}
]
},
{
"name": "VPNConnections",
"doc": "VPNConnections is an OPTIONAL AWS capability (type-asserted).",
Expand Down
44 changes: 36 additions & 8 deletions providers/aws/vpc/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
// each specified subnet. Gateway-type endpoints hold no interfaces.
const vpcEndpointTypeInterface = "Interface"

// vpcEndpointTypeGateway is the endpoint type that routes to the service
// through a prefix-list route in each of its route tables.
const vpcEndpointTypeGateway = "Gateway"

// endpointENIDescription is the description stamped on the ENIs an Interface
// endpoint occupies, so DeleteVpcEndpoint can release exactly this endpoint's set.
func endpointENIDescription(endpointID string) string {
Expand Down Expand Up @@ -64,11 +68,20 @@ func (m *Mock) CreateVPCEndpoint(
State: "available",
SubnetIDs: copyStringSlice(cfg.SubnetIDs),
SecurityGroupIDs: copyStringSlice(cfg.SecurityGroupIDs),
RouteTableIDs: copyStringSlice(cfg.RouteTableIDs),
Tags: copyTags(cfg.Tags),
CreatedAt: m.opts.Clock.Now().Format(timeFormat),
}

// The route-table check, the store write and the route sync run under one
// lock hold, so a concurrent Delete or a second endpoint for the same
// service cannot slip in between.
m.mu.Lock()
defer m.mu.Unlock()

if err := m.setEndpointRouteTables(ep, cfg.RouteTableIDs); err != nil {
return nil, err
}

// An Interface endpoint provisions one requester-managed ENI per subnet, which
// consumes a subnet IP and (like a NAT gateway's ENI) blocks a premature subnet
// or VPC delete. Gateway endpoints hold none.
Expand All @@ -80,16 +93,22 @@ func (m *Mock) CreateVPCEndpoint(
}

m.endpoints.Set(id, ep)
m.syncEndpointRoutes(ep)

return copyEndpoint(ep), nil
}

// DeleteVPCEndpoint deletes the VPC endpoint with the given ID, releasing any
// backing ENIs an Interface endpoint provisioned.
// backing ENIs an Interface endpoint provisioned and the prefix-list routes a
// Gateway endpoint added.
func (m *Mock) DeleteVPCEndpoint(
_ context.Context, id string,
) error {
if !m.endpoints.Has(id) {
m.mu.Lock()
defer m.mu.Unlock()

ep, ok := m.endpoints.Get(id)
if !ok {
return errors.Newf(
errors.NotFound,
"vpc endpoint %q not found", id,
Expand All @@ -99,6 +118,10 @@ func (m *Mock) DeleteVPCEndpoint(
m.endpoints.Delete(id)
m.releaseManagedENIs(endpointENIDescription(id))

gone := *ep
gone.RouteTableIDs = nil
m.syncEndpointRoutes(&gone)

return nil
}

Expand Down Expand Up @@ -126,7 +149,8 @@ func (m *Mock) DescribeVPCEndpoints(
), nil
}

// ModifyVPCEndpoint updates a VPC endpoint configuration.
// ModifyVPCEndpoint replaces an endpoint's id sets and tags. A nil set leaves
// that set unchanged. The AWS wire layer uses ModifyVPCEndpointSets instead.
//
//nolint:gocritic // hugeParam: interface method signature cannot be changed.
func (m *Mock) ModifyVPCEndpoint(
Expand All @@ -145,6 +169,14 @@ func (m *Mock) ModifyVPCEndpoint(
)
}

if cfg.RouteTableIDs != nil {
if err := m.setEndpointRouteTables(ep, cfg.RouteTableIDs); err != nil {
return nil, err
}

m.syncEndpointRoutes(ep)
}

if len(cfg.SubnetIDs) > 0 {
ep.SubnetIDs = copyStringSlice(cfg.SubnetIDs)
}
Expand All @@ -153,10 +185,6 @@ func (m *Mock) ModifyVPCEndpoint(
ep.SecurityGroupIDs = copyStringSlice(cfg.SecurityGroupIDs)
}

if len(cfg.RouteTableIDs) > 0 {
ep.RouteTableIDs = copyStringSlice(cfg.RouteTableIDs)
}

if len(cfg.Tags) > 0 {
ep.Tags = copyTags(cfg.Tags)
}
Expand Down
153 changes: 153 additions & 0 deletions providers/aws/vpc/endpoint_sets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package vpc

import (
"context"
"sort"

"github.com/stackshy/cloudemu/v2/errors"
"github.com/stackshy/cloudemu/v2/services/networking/driver"
)

// ModifyVPCEndpointSets applies the Add*/Remove* part of ModifyVpcEndpoint to
// the endpoint's current route tables, subnets and security groups. The read
// and the write happen under one lock hold, so parallel modifies of the same
// endpoint (Terraform creates route table associations concurrently) all land.
func (m *Mock) ModifyVPCEndpointSets(
_ context.Context, id string, change *driver.VPCEndpointSetChange,
) (*driver.VPCEndpoint, error) {
m.mu.Lock()
defer m.mu.Unlock()

ep, ok := m.endpoints.Get(id)
if !ok {
return nil, errors.Newf(errors.NotFound, "vpc endpoint %q not found", id)
}

rts := applyIDDelta(ep.RouteTableIDs, change.AddRouteTableIDs, change.RemoveRouteTableIDs)
if err := m.setEndpointRouteTables(ep, rts); err != nil {
return nil, err
}

m.syncEndpointRoutes(ep)

subnets := applyIDDelta(ep.SubnetIDs, change.AddSubnetIDs, change.RemoveSubnetIDs)
if ep.EndpointType == vpcEndpointTypeInterface {
m.syncEndpointENIs(ep, subnets)
}

ep.SubnetIDs = subnets
ep.SecurityGroupIDs = applyIDDelta(ep.SecurityGroupIDs, change.AddSecurityGroupIDs, change.RemoveSecurityGroupIDs)

return copyEndpoint(ep), nil
}

// applyIDDelta returns cur without the removed ids and with the added ones
// appended, deduplicated. An id named in both lists is dropped.
func applyIDDelta(cur, add, remove []string) []string {
drop := make(map[string]bool, len(remove))
for _, id := range remove {
drop[id] = true
}

seen := map[string]bool{}
out := make([]string, 0, len(cur)+len(add))

for _, list := range [][]string{cur, add} {
for _, id := range list {
if drop[id] || seen[id] {
continue
}

seen[id] = true

out = append(out, id)
}
}

return out
}

// setEndpointRouteTables sets ep's route tables to ids (deduplicated). A
// Gateway endpoint cannot take a table that already routes the same service
// through another endpoint: EC2 allows one endpoint route per service per
// route table and answers RouteAlreadyExists. The caller holds m.mu and syncs
// the routes afterwards.
func (m *Mock) setEndpointRouteTables(ep *driver.VPCEndpoint, ids []string) error {
ids = applyIDDelta(nil, ids, nil)

if plID := endpointPrefixList(ep); plID != "" {
for _, rtID := range ids {
if m.routeTableHasOtherEndpointRoute(rtID, plID, ep.ID) {
return errors.Newf(errors.AlreadyExists,
"route table %s already has a route with destination-prefix-list-id %s", rtID, plID)
}
}
}

ep.RouteTableIDs = ids

return nil
}

// routeTableHasOtherEndpointRoute reports whether rtID routes plID to an
// endpoint other than endpointID. The caller holds m.mu.
func (m *Mock) routeTableHasOtherEndpointRoute(rtID, plID, endpointID string) bool {
rt, ok := m.routeTables.Get(rtID)
if !ok {
return false
}

for _, r := range rt.Routes {
if r.DestinationPrefixListID == plID && r.TargetID != endpointID {
return true
}
}

return false
}

// syncEndpointENIs gives an Interface endpoint exactly one ENI in each of
// subnets, releasing the ENIs of subnets it left. The caller holds m.mu.
func (m *Mock) syncEndpointENIs(ep *driver.VPCEndpoint, subnets []string) {
want := make(map[string]bool, len(subnets))
for _, s := range subnets {
want[s] = true
}

desc := endpointENIDescription(ep.ID)
have := map[string]bool{}

var ids []string

for id, eni := range m.enis.All() {
if eni.Description != desc {
continue
}

if !want[eni.SubnetID] || have[eni.SubnetID] {
m.enis.Delete(id)
continue
}

have[eni.SubnetID] = true

ids = append(ids, id)
}

for _, s := range subnets {
if !have[s] {
ids = append(ids, m.attachManagedENI(ep.VPCID, s, desc).ID)
}
}

sort.Strings(ids)
ep.NetworkInterfaceIDs = ids
}

// dropRouteTableFromEndpoints removes a deleted route table from every
// endpoint that listed it. The caller holds m.mu.
func (m *Mock) dropRouteTableFromEndpoints(rtID string) {
for _, ep := range m.endpoints.All() {
ep.RouteTableIDs = applyIDDelta(ep.RouteTableIDs, nil, []string{rtID})
}
}
Loading
Loading