Skip to content

Add custom update support for VPCEndpoint resource to handle ModifyVpcEndpoint calls #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 17, 2025
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
103 changes: 103 additions & 0 deletions pkg/resource/vpc_endpoint/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"

"github.com/aws-controllers-k8s/ec2-controller/pkg/tags"
"github.com/aws/aws-sdk-go-v2/aws"
svcsdk "github.com/aws/aws-sdk-go-v2/service/ec2"
svcsdktypes "github.com/aws/aws-sdk-go-v2/service/ec2/types"
)
Expand Down Expand Up @@ -81,6 +82,67 @@ func (rm *resourceManager) customUpdateVPCEndpoint(
}
}

if !delta.DifferentExcept("Spec.Tags") {
return desired, nil
}

// Handle modifications that require ModifyVpcEndpoint API call
// avoid making the ModifyVpcEndpoint API call if only tags are modified
input := &svcsdk.ModifyVpcEndpointInput{
VpcEndpointId: latest.ko.Status.VPCEndpointID,
}

if delta.DifferentAt("Spec.SubnetIDs") {
toAdd, toRemove := calculateSubnetDifferences(
aws.ToStringSlice(desired.ko.Spec.SubnetIDs),
aws.ToStringSlice(latest.ko.Spec.SubnetIDs))
input.AddSubnetIds = toAdd
input.RemoveSubnetIds = toRemove
}

if delta.DifferentAt("Spec.RouteTableIDs") {
toAdd, toRemove := calculateSubnetDifferences(
aws.ToStringSlice(desired.ko.Spec.RouteTableIDs),
aws.ToStringSlice(latest.ko.Spec.RouteTableIDs))
input.AddRouteTableIds = toAdd
input.RemoveRouteTableIds = toRemove
}

if delta.DifferentAt("Spec.PolicyDocument") {
input.PolicyDocument = desired.ko.Spec.PolicyDocument
}

if delta.DifferentAt("Spec.PrivateDNSEnabled") {
input.PrivateDnsEnabled = desired.ko.Spec.PrivateDNSEnabled
}

if delta.DifferentAt("Spec.SecurityGroupIDs") {
toAdd, toRemove := calculateSubnetDifferences(
aws.ToStringSlice(desired.ko.Spec.SecurityGroupIDs),
aws.ToStringSlice(latest.ko.Spec.SecurityGroupIDs))
input.AddSecurityGroupIds = toAdd
input.RemoveSecurityGroupIds = toRemove
}

if delta.DifferentAt("Spec.DNSOptions") && desired.ko.Spec.DNSOptions != nil {
if desired.ko.Spec.DNSOptions != nil {
input.DnsOptions = &svcsdktypes.DnsOptionsSpecification{
DnsRecordIpType: svcsdktypes.DnsRecordIpType(*desired.ko.Spec.DNSOptions.DNSRecordIPType),
}
}
}

if delta.DifferentAt("Spec.IPAddressType") {
if desired.ko.Spec.IPAddressType != nil {
input.IpAddressType = svcsdktypes.IpAddressType(*desired.ko.Spec.IPAddressType)
}
}

_, err = rm.sdkapi.ModifyVpcEndpoint(ctx, input)
rm.metrics.RecordAPICall("UPDATE", "ModifyVpcEndpoint", err)
if err != nil {
return nil, err
}
return updated, nil
}

Expand All @@ -107,3 +169,44 @@ func updateTagSpecificationsInCreateRequest(r *resource,
input.TagSpecifications = []svcsdktypes.TagSpecification{desiredTagSpecs}
}
}

// calculateSubnetDifferences returns two slices:
// 1. Elements in desired that are not in latest (to add)
// 2. Elements in latest that are not in desired (to remove)
func calculateSubnetDifferences(desired, latest []string) ([]string, []string) {
if desired == nil {
desired = []string{}
}
if latest == nil {
latest = []string{}
}

desiredMap := make(map[string]bool)
latestMap := make(map[string]bool)

for _, id := range desired {
desiredMap[id] = true
}
for _, id := range latest {
latestMap[id] = true
}

var toAdd []string
var toRemove []string

// Find elements to add (in desired but not in latest)
for id := range desiredMap {
if !latestMap[id] {
toAdd = append(toAdd, id)
}
}

// Find elements to remove (in latest but not in desired)
for id := range latestMap {
if !desiredMap[id] {
toRemove = append(toRemove, id)
}
}

return toAdd, toRemove
}
2 changes: 1 addition & 1 deletion test/e2e/resources/vpc_endpoint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ spec:
vpcID: $VPC_ID
tags:
- key: $TAG_KEY
value: $TAG_VALUE
value: $TAG_VALUE
13 changes: 13 additions & 0 deletions test/e2e/resources/vpc_endpoint_modify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: ec2.services.k8s.aws/v1alpha1
kind: VPCEndpoint
metadata:
name: $VPC_ENDPOINT_NAME
spec:
serviceName: $SERVICE_NAME
vpcID: $VPC_ID
vpcEndpointType: Interface
subnetIDs:
- $SUBNET_ID
tags:
- key: $TAG_KEY
value: $TAG_VALUE
6 changes: 5 additions & 1 deletion test/e2e/service_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ def service_bootstrap() -> Resources:
logging.getLogger().setLevel(logging.INFO)

resources = BootstrapResources(
SharedTestVPC=VPC(name_prefix="e2e-test-vpc", num_public_subnet=1, num_private_subnet=0),
SharedTestVPC=VPC(
name_prefix="e2e-test-vpc",
num_public_subnet=2,
num_private_subnet=0
),
FlowLogsBucket=Bucket(
"ack-ec2-controller-flow-log-tests",
),
Expand Down
102 changes: 100 additions & 2 deletions test/e2e/tests/test_vpc_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
ENDPOINT_SERVICE_NAME = "com.amazonaws.%s.s3" % REGION

CREATE_WAIT_AFTER_SECONDS = 10
DELETE_WAIT_AFTER_SECONDS = 10
DELETE_WAIT_AFTER_SECONDS = 180
MODIFY_WAIT_AFTER_SECONDS = 5

@pytest.fixture
Expand Down Expand Up @@ -82,6 +82,57 @@ def simple_vpc_endpoint(request):
assert deleted
except:
pass

@pytest.fixture
def modify_vpc_endpoint(request):
test_resource_values = REPLACEMENT_VALUES.copy()
resource_name = random_suffix_name("vpc-endpoint-test", 24)
test_vpc = get_bootstrap_resources().SharedTestVPC
vpc_id = test_vpc.vpc_id

test_resource_values["VPC_ENDPOINT_NAME"] = resource_name
test_resource_values["SERVICE_NAME"] = ENDPOINT_SERVICE_NAME
test_resource_values["VPC_ID"] = vpc_id
test_resource_values["SUBNET_ID"] = test_vpc.public_subnets.subnet_ids[0]


marker = request.node.get_closest_marker("resource_data")
if marker is not None:
data = marker.args[0]
if 'tag_key' in data:
test_resource_values["TAG_KEY"] = data["tag_key"]
if 'tag_value' in data:
test_resource_values["TAG_VALUE"] = data["tag_value"]

# Load VPC Endpoint CR
resource_data = load_ec2_resource(
"vpc_endpoint_modify",
additional_replacements=test_resource_values,
)
logging.debug(resource_data)

# Create k8s resource
ref = k8s.CustomResourceReference(
CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL,
resource_name, namespace="default",
)
k8s.create_custom_resource(ref, resource_data)
time.sleep(CREATE_WAIT_AFTER_SECONDS)

cr = k8s.wait_resource_consumed_by_controller(ref)
assert cr is not None
assert k8s.get_resource_exists(ref)

yield (ref, cr)

# Try to delete, if doesn't already exist
try:
_, deleted = k8s.delete_custom_resource(ref, 3, 10)
assert deleted
except:
pass


@service_marker
@pytest.mark.canary
class TestVpcEndpoint:
Expand Down Expand Up @@ -272,4 +323,51 @@ def test_terminal_condition_invalid_service(self):

expected_msg = "InvalidServiceName: The Vpc Endpoint Service 'InvalidService' does not exist"
terminal_condition = k8s.get_resource_condition(ref, "ACK.Terminal")
assert expected_msg in terminal_condition['message']
assert expected_msg in terminal_condition['message']

def test_update_subnets(self, ec2_client, modify_vpc_endpoint):
(ref, cr) = modify_vpc_endpoint
resource_id = cr["status"]["vpcEndpointID"]

time.sleep(CREATE_WAIT_AFTER_SECONDS)

# Check VPC Endpoint exists in AWS
ec2_validator = EC2Validator(ec2_client)
ec2_validator.assert_vpc_endpoint(resource_id)

# Get initial state
vpc_endpoint = ec2_validator.get_vpc_endpoint(resource_id)
initial_subnets = vpc_endpoint.get("SubnetIds", [])

# Get an additional subnet from the test VPC
test_vpc = get_bootstrap_resources().SharedTestVPC
available_subnets = test_vpc.public_subnets.subnet_ids
new_subnet = next(subnet for subnet in available_subnets if subnet not in initial_subnets)

# Update subnets
updated_subnets = initial_subnets + [new_subnet]

# Patch the VPC Endpoint
updates = {
"spec": {"subnetIDs": updated_subnets}
}

k8s.patch_custom_resource(ref, updates)
time.sleep(MODIFY_WAIT_AFTER_SECONDS)

# Check resource synced successfully
assert k8s.wait_on_condition(
ref, "ACK.ResourceSynced", "True", wait_periods=5)

# Verify the update in AWS
vpc_endpoint = ec2_validator.get_vpc_endpoint(resource_id)
assert set(vpc_endpoint["SubnetIds"]) == set(updated_subnets)

# Delete k8s resource
_, deleted = k8s.delete_custom_resource(ref)
assert deleted is True

time.sleep(DELETE_WAIT_AFTER_SECONDS)

# Check VPC Endpoint no longer exists in AWS
ec2_validator.assert_vpc_endpoint(resource_id, exists=False)