Skip to content

Commit bb2c3ca

Browse files
committed
custom update support modifyVPCEngpoint
1 parent 9bc5f3c commit bb2c3ca

File tree

5 files changed

+222
-4
lines changed

5 files changed

+222
-4
lines changed

pkg/resource/vpc_endpoint/hooks.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
2222

2323
"github.com/aws-controllers-k8s/ec2-controller/pkg/tags"
24+
"github.com/aws/aws-sdk-go-v2/aws"
2425
svcsdk "github.com/aws/aws-sdk-go-v2/service/ec2"
2526
svcsdktypes "github.com/aws/aws-sdk-go-v2/service/ec2/types"
2627
)
@@ -81,6 +82,67 @@ func (rm *resourceManager) customUpdateVPCEndpoint(
8182
}
8283
}
8384

85+
if !delta.DifferentExcept("Spec.Tags") {
86+
return desired, nil
87+
}
88+
89+
// Handle modifications that require ModifyVpcEndpoint API call
90+
// avoid making the ModifyVpcEndpoint API call if only tags are modified
91+
input := &svcsdk.ModifyVpcEndpointInput{
92+
VpcEndpointId: latest.ko.Status.VPCEndpointID,
93+
}
94+
95+
if delta.DifferentAt("Spec.SubnetIDs") {
96+
toAdd, toRemove := calculateSubnetDifferences(
97+
aws.ToStringSlice(desired.ko.Spec.SubnetIDs),
98+
aws.ToStringSlice(latest.ko.Spec.SubnetIDs))
99+
input.AddSubnetIds = toAdd
100+
input.RemoveSubnetIds = toRemove
101+
}
102+
103+
if delta.DifferentAt("Spec.RouteTableIDs") {
104+
toAdd, toRemove := calculateSubnetDifferences(
105+
aws.ToStringSlice(desired.ko.Spec.RouteTableIDs),
106+
aws.ToStringSlice(latest.ko.Spec.RouteTableIDs))
107+
input.AddRouteTableIds = toAdd
108+
input.RemoveRouteTableIds = toRemove
109+
}
110+
111+
if delta.DifferentAt("Spec.PolicyDocument") {
112+
input.PolicyDocument = desired.ko.Spec.PolicyDocument
113+
}
114+
115+
if delta.DifferentAt("Spec.PrivateDNSEnabled") {
116+
input.PrivateDnsEnabled = desired.ko.Spec.PrivateDNSEnabled
117+
}
118+
119+
if delta.DifferentAt("Spec.SecurityGroupIDs") {
120+
toAdd, toRemove := calculateSubnetDifferences(
121+
aws.ToStringSlice(desired.ko.Spec.SecurityGroupIDs),
122+
aws.ToStringSlice(latest.ko.Spec.SecurityGroupIDs))
123+
input.AddSecurityGroupIds = toAdd
124+
input.RemoveSecurityGroupIds = toRemove
125+
}
126+
127+
if delta.DifferentAt("Spec.DNSOptions") && desired.ko.Spec.DNSOptions != nil {
128+
if desired.ko.Spec.DNSOptions != nil {
129+
input.DnsOptions = &svcsdktypes.DnsOptionsSpecification{
130+
DnsRecordIpType: svcsdktypes.DnsRecordIpType(*desired.ko.Spec.DNSOptions.DNSRecordIPType),
131+
}
132+
}
133+
}
134+
135+
if delta.DifferentAt("Spec.IPAddressType") {
136+
if desired.ko.Spec.IPAddressType != nil {
137+
input.IpAddressType = svcsdktypes.IpAddressType(*desired.ko.Spec.IPAddressType)
138+
}
139+
}
140+
141+
_, err = rm.sdkapi.ModifyVpcEndpoint(ctx, input)
142+
rm.metrics.RecordAPICall("UPDATE", "ModifyVpcEndpoint", err)
143+
if err != nil {
144+
return nil, err
145+
}
84146
return updated, nil
85147
}
86148

@@ -107,3 +169,44 @@ func updateTagSpecificationsInCreateRequest(r *resource,
107169
input.TagSpecifications = []svcsdktypes.TagSpecification{desiredTagSpecs}
108170
}
109171
}
172+
173+
// calculateSubnetDifferences returns two slices:
174+
// 1. Elements in desired that are not in latest (to add)
175+
// 2. Elements in latest that are not in desired (to remove)
176+
func calculateSubnetDifferences(desired, latest []string) ([]string, []string) {
177+
if desired == nil {
178+
desired = []string{}
179+
}
180+
if latest == nil {
181+
latest = []string{}
182+
}
183+
184+
desiredMap := make(map[string]bool)
185+
latestMap := make(map[string]bool)
186+
187+
for _, id := range desired {
188+
desiredMap[id] = true
189+
}
190+
for _, id := range latest {
191+
latestMap[id] = true
192+
}
193+
194+
var toAdd []string
195+
var toRemove []string
196+
197+
// Find elements to add (in desired but not in latest)
198+
for id := range desiredMap {
199+
if !latestMap[id] {
200+
toAdd = append(toAdd, id)
201+
}
202+
}
203+
204+
// Find elements to remove (in latest but not in desired)
205+
for id := range latestMap {
206+
if !desiredMap[id] {
207+
toRemove = append(toRemove, id)
208+
}
209+
}
210+
211+
return toAdd, toRemove
212+
}

test/e2e/resources/vpc_endpoint.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ spec:
77
vpcID: $VPC_ID
88
tags:
99
- key: $TAG_KEY
10-
value: $TAG_VALUE
10+
value: $TAG_VALUE
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
apiVersion: ec2.services.k8s.aws/v1alpha1
2+
kind: VPCEndpoint
3+
metadata:
4+
name: $VPC_ENDPOINT_NAME
5+
spec:
6+
serviceName: $SERVICE_NAME
7+
vpcID: $VPC_ID
8+
vpcEndpointType: Interface
9+
subnetIDs:
10+
- $SUBNET_ID
11+
tags:
12+
- key: $TAG_KEY
13+
value: $TAG_VALUE

test/e2e/service_bootstrap.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ def service_bootstrap() -> Resources:
2626
logging.getLogger().setLevel(logging.INFO)
2727

2828
resources = BootstrapResources(
29-
SharedTestVPC=VPC(name_prefix="e2e-test-vpc", num_public_subnet=1, num_private_subnet=0),
29+
SharedTestVPC=VPC(
30+
name_prefix="e2e-test-vpc",
31+
num_public_subnet=2,
32+
num_private_subnet=0
33+
),
3034
FlowLogsBucket=Bucket(
3135
"ack-ec2-controller-flow-log-tests",
3236
),

test/e2e/tests/test_vpc_endpoint.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
ENDPOINT_SERVICE_NAME = "com.amazonaws.%s.s3" % REGION
3434

3535
CREATE_WAIT_AFTER_SECONDS = 10
36-
DELETE_WAIT_AFTER_SECONDS = 10
36+
DELETE_WAIT_AFTER_SECONDS = 180
3737
MODIFY_WAIT_AFTER_SECONDS = 5
3838

3939
@pytest.fixture
@@ -82,6 +82,57 @@ def simple_vpc_endpoint(request):
8282
assert deleted
8383
except:
8484
pass
85+
86+
@pytest.fixture
87+
def modify_vpc_endpoint(request):
88+
test_resource_values = REPLACEMENT_VALUES.copy()
89+
resource_name = random_suffix_name("vpc-endpoint-test", 24)
90+
test_vpc = get_bootstrap_resources().SharedTestVPC
91+
vpc_id = test_vpc.vpc_id
92+
93+
test_resource_values["VPC_ENDPOINT_NAME"] = resource_name
94+
test_resource_values["SERVICE_NAME"] = ENDPOINT_SERVICE_NAME
95+
test_resource_values["VPC_ID"] = vpc_id
96+
test_resource_values["SUBNET_ID"] = test_vpc.public_subnets.subnet_ids[0]
97+
98+
99+
marker = request.node.get_closest_marker("resource_data")
100+
if marker is not None:
101+
data = marker.args[0]
102+
if 'tag_key' in data:
103+
test_resource_values["TAG_KEY"] = data["tag_key"]
104+
if 'tag_value' in data:
105+
test_resource_values["TAG_VALUE"] = data["tag_value"]
106+
107+
# Load VPC Endpoint CR
108+
resource_data = load_ec2_resource(
109+
"vpc_endpoint_modify",
110+
additional_replacements=test_resource_values,
111+
)
112+
logging.debug(resource_data)
113+
114+
# Create k8s resource
115+
ref = k8s.CustomResourceReference(
116+
CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL,
117+
resource_name, namespace="default",
118+
)
119+
k8s.create_custom_resource(ref, resource_data)
120+
time.sleep(CREATE_WAIT_AFTER_SECONDS)
121+
122+
cr = k8s.wait_resource_consumed_by_controller(ref)
123+
assert cr is not None
124+
assert k8s.get_resource_exists(ref)
125+
126+
yield (ref, cr)
127+
128+
# Try to delete, if doesn't already exist
129+
try:
130+
_, deleted = k8s.delete_custom_resource(ref, 3, 10)
131+
assert deleted
132+
except:
133+
pass
134+
135+
85136
@service_marker
86137
@pytest.mark.canary
87138
class TestVpcEndpoint:
@@ -272,4 +323,51 @@ def test_terminal_condition_invalid_service(self):
272323

273324
expected_msg = "InvalidServiceName: The Vpc Endpoint Service 'InvalidService' does not exist"
274325
terminal_condition = k8s.get_resource_condition(ref, "ACK.Terminal")
275-
assert expected_msg in terminal_condition['message']
326+
assert expected_msg in terminal_condition['message']
327+
328+
def test_update_subnets(self, ec2_client, modify_vpc_endpoint):
329+
(ref, cr) = modify_vpc_endpoint
330+
resource_id = cr["status"]["vpcEndpointID"]
331+
332+
time.sleep(CREATE_WAIT_AFTER_SECONDS)
333+
334+
# Check VPC Endpoint exists in AWS
335+
ec2_validator = EC2Validator(ec2_client)
336+
ec2_validator.assert_vpc_endpoint(resource_id)
337+
338+
# Get initial state
339+
vpc_endpoint = ec2_validator.get_vpc_endpoint(resource_id)
340+
initial_subnets = vpc_endpoint.get("SubnetIds", [])
341+
342+
# Get an additional subnet from the test VPC
343+
test_vpc = get_bootstrap_resources().SharedTestVPC
344+
available_subnets = test_vpc.public_subnets.subnet_ids
345+
new_subnet = next(subnet for subnet in available_subnets if subnet not in initial_subnets)
346+
347+
# Update subnets
348+
updated_subnets = initial_subnets + [new_subnet]
349+
350+
# Patch the VPC Endpoint
351+
updates = {
352+
"spec": {"subnetIDs": updated_subnets}
353+
}
354+
355+
k8s.patch_custom_resource(ref, updates)
356+
time.sleep(MODIFY_WAIT_AFTER_SECONDS)
357+
358+
# Check resource synced successfully
359+
assert k8s.wait_on_condition(
360+
ref, "ACK.ResourceSynced", "True", wait_periods=5)
361+
362+
# Verify the update in AWS
363+
vpc_endpoint = ec2_validator.get_vpc_endpoint(resource_id)
364+
assert set(vpc_endpoint["SubnetIds"]) == set(updated_subnets)
365+
366+
# Delete k8s resource
367+
_, deleted = k8s.delete_custom_resource(ref)
368+
assert deleted is True
369+
370+
time.sleep(DELETE_WAIT_AFTER_SECONDS)
371+
372+
# Check VPC Endpoint no longer exists in AWS
373+
ec2_validator.assert_vpc_endpoint(resource_id, exists=False)

0 commit comments

Comments
 (0)