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
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,11 @@ jobs:
python3 - <<'EOF'
import json, sys
from pathlib import Path
# The instance-based cases (01-04, 06) fail on a fresh emulator
# because no AMI is seeded (see PR #43); add them once that lands.
REQUIRED = {"00-simple-vpc", "05-vpc-with-subnet"}
REQUIRED = {
"00-simple-vpc", "01-hello-world", "02-one-server",
"03-one-webserver", "04-one-webserver-with-vars",
"05-vpc-with-subnet", "06-instance-in-vpc",
}
runs = sorted(p for p in Path('tests/tf/runs').iterdir() if p.is_dir())
summary = runs[-1] / 'summary.json' if runs else None
if not summary or not summary.exists():
Expand Down
50 changes: 48 additions & 2 deletions emulators/aws-ec2/emulator_core/services/ami.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,47 @@ class Ami_Backend:
def __init__(self):
self.state = EC2State.get()
self.resources = self.state.amis # alias to shared store
self._ensure_default_images()

def _ensure_default_images(self) -> None:
if self.resources:
return

amazon_linux_2 = Ami(
image_id="ami-0f9fc25dd2506cf6d",
owner_id="137112412989",
image_owner_id="137112412989",
image_owner_alias="amazon",
name="amzn2-ami-hvm-2.0.20240109.0-x86_64-gp2",
description="Amazon Linux 2 AMI (HVM), SSD Volume Type",
state="available",
image_state="available",
image_type="machine",
architecture="x86_64",
creation_date="2024-01-09T00:00:00.000Z",
is_public=True,
image_location="amazon/amzn2-ami-hvm-2.0.20240109.0-x86_64-gp2",
platform_details="Linux/UNIX",
root_device_name="/dev/xvda",
root_device_type="ebs",
virtualization_type="hvm",
ena_support=True,
sriov_net_support="simple",
hypervisor="xen",
block_device_mappings=[
{
"deviceName": "/dev/xvda",
"ebs": {
"deleteOnTermination": True,
"encrypted": False,
"snapshotId": "snap-0f9fc25dd2506cf6d",
"volumeSize": 8,
"volumeType": "gp2",
},
}
],
)
self.resources[amazon_linux_2.image_id] = amazon_linux_2

def _utc_now(self) -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
Expand Down Expand Up @@ -635,7 +676,13 @@ def DescribeImages(self, params: Dict[str, Any]):

owners = params.get("Owner.N", []) or []
if owners:
resources = [image for image in resources if image.owner_id in owners or image.image_owner_id in owners]
resources = [
image
for image in resources
if image.owner_id in owners
or image.image_owner_id in owners
or image.image_owner_alias in owners
]

include_disabled = str2bool(params.get("IncludeDisabled"))
include_deprecated = str2bool(params.get("IncludeDeprecated"))
Expand Down Expand Up @@ -3111,4 +3158,3 @@ def serialize(action: str, data: Dict[str, Any], request_id: str) -> str:
if action not in serializers:
raise ValueError(f"Unknown action: {action}")
return serializers[action](data, request_id)

34 changes: 21 additions & 13 deletions emulators/aws-ec2/emulator_core/services/dhcpoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def CreateDhcpOptions(self, params: Dict[str, Any]):
"Missing required parameter: DhcpConfiguration.N",
)

dhcp_options_id = self._generate_id("dhcp")
dhcp_options_id = self._generate_id("dopt")
tag_set: List[Dict[str, Any]] = []
for spec in params.get("TagSpecification.N", []) or []:
tag_set.extend(spec.get("Tags", []) or [])
Expand All @@ -132,7 +132,7 @@ def CreateDhcpOptions(self, params: Dict[str, Any]):
self.resources[dhcp_options_id] = resource

return {
'dhcpOptions': [resource.to_dict()],
'dhcpOptions': resource.to_dict(),
}

def DeleteDhcpOptions(self, params: Dict[str, Any]):
Expand Down Expand Up @@ -211,8 +211,23 @@ def parse_associate_dhcp_options_request(md: Dict[str, Any]) -> Dict[str, Any]:

@staticmethod
def parse_create_dhcp_options_request(md: Dict[str, Any]) -> Dict[str, Any]:
def parse_dhcp_configurations() -> List[Dict[str, Any]]:
configurations: List[Dict[str, Any]] = []
index = 1
while True:
key = get_scalar(md, f"DhcpConfiguration.{index}.Key")
if key is None:
break
values = get_indexed_list(md, f"DhcpConfiguration.{index}.Value")
configurations.append({
"key": key,
"valueSet": [{"value": value} for value in values],
})
index += 1
return configurations

return {
"DhcpConfiguration.N": get_indexed_list(md, "DhcpConfiguration"),
"DhcpConfiguration.N": parse_dhcp_configurations(),
"DryRun": str2bool(get_scalar(md, "DryRun")),
"TagSpecification.N": parse_tags(md, "TagSpecification"),
}
Expand Down Expand Up @@ -345,15 +360,9 @@ def serialize_create_dhcp_options_response(data: Dict[str, Any], request_id: str
if _dhcpOptions_key:
param_data = data[_dhcpOptions_key]
indent_str = " " * 1
if param_data:
xml_parts.append(f'{indent_str}<dhcpOptionsSet>')
for item in param_data:
xml_parts.append(f'{indent_str} <item>')
xml_parts.extend(dhcpoptions_ResponseSerializer._serialize_nested_fields(item, 2))
xml_parts.append(f'{indent_str} </item>')
xml_parts.append(f'{indent_str}</dhcpOptionsSet>')
else:
xml_parts.append(f'{indent_str}<dhcpOptionsSet/>')
xml_parts.append(f'{indent_str}<dhcpOptions>')
xml_parts.extend(dhcpoptions_ResponseSerializer._serialize_nested_fields(param_data, 2))
xml_parts.append(f'{indent_str}</dhcpOptions>')
xml_parts.append(f'</CreateDhcpOptionsResponse>')
return "\n".join(xml_parts)

Expand Down Expand Up @@ -428,4 +437,3 @@ def serialize(action: str, data: Dict[str, Any], request_id: str) -> str:
if action not in serializers:
raise ValueError(f"Unknown action: {action}")
return serializers[action](data, request_id)

3 changes: 1 addition & 2 deletions emulators/aws-ec2/emulator_core/services/internetgateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def DetachInternetGateway(self, params: Dict[str, Any]):
)

resource.vpc_id = None
resource.attachment_set = [{"state": "detached", "vpcId": vpc_id}]
resource.attachment_set = []

if hasattr(vpc, "internet_gateway_ids") and internet_gateway_id in vpc.internet_gateway_ids:
vpc.internet_gateway_ids.remove(internet_gateway_id)
Expand Down Expand Up @@ -778,4 +778,3 @@ def serialize(action: str, data: Dict[str, Any], request_id: str) -> str:
if action not in serializers:
raise ValueError(f"Unknown action: {action}")
return serializers[action](data, request_id)

27 changes: 26 additions & 1 deletion emulators/aws-ec2/emulator_core/services/vpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,32 @@ def DescribeVpcs(self, params: Dict[str, Any]):
else:
resources = list(self.resources.values())

resources = apply_filters(resources, params.get("Filter.N", []))
filters = params.get("Filter.N", []) or []
special_filter_names = {
"cidr-block-association.association-id",
"ipv6-cidr-block-association.association-id",
}
for resource_filter in filters:
name = resource_filter.get("Name", "")
values = set(resource_filter.get("Values", []) or [])
if name not in special_filter_names or not values:
continue
association_attr = (
"ipv6_cidr_block_association_set"
if name.startswith("ipv6-")
else "cidr_block_association_set"
)
resources = [
resource
for resource in resources
if any(
str(association.get("associationId", "")) in values
for association in getattr(resource, association_attr, []) or []
if isinstance(association, dict)
)
]
normal_filters = [f for f in filters if f.get("Name", "") not in special_filter_names]
resources = apply_filters(resources, normal_filters)
vpc_set = [resource.to_dict() for resource in resources[:max_results]]

return {
Expand Down
5 changes: 2 additions & 3 deletions emulators/aws-ec2/emulator_core/services/vpcpeering.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Dict, List, Any, Optional
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from dataclasses import dataclass, field, asdict
from enum import Enum
import uuid
Expand Down Expand Up @@ -189,7 +189,7 @@ def _build_vpc_info(vpc_obj, owner_override: Optional[str], region_override: Opt

resource = VpcPeering(
accepter_vpc_info=accepter_info,
expiration_time="",
expiration_time=(datetime.now(timezone.utc) + timedelta(days=7)).isoformat().replace("+00:00", "Z"),
requester_vpc_info=requester_info,
status=status,
tag_set=tag_set,
Expand Down Expand Up @@ -661,4 +661,3 @@ def serialize(action: str, data: Dict[str, Any], request_id: str) -> str:
if action not in serializers:
raise ValueError(f"Unknown action: {action}")
return serializers[action](data, request_id)

14 changes: 11 additions & 3 deletions emulators/aws-ec2/emulator_core/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Dict, Any, List, Union, Optional
from werkzeug.datastructures import MultiDict
import html
import fnmatch

# ==================== TYPE ALIASES ====================
# These help clarify what types functions return
Expand Down Expand Up @@ -376,6 +377,13 @@ def serialize_error_response(error_data: ErrorResponse, request_id: str) -> str:
</Response>"""


def _matches_filter_value(actual: str, expected_values: List[str]) -> bool:
for expected in expected_values:
if fnmatch.fnmatchcase(actual, str(expected)):
return True
return False


def apply_filters(resources: List[Any], filters: List[Filter]) -> List[Any]:
"""
Apply AWS-style filters to a list of resource objects or dicts.
Expand Down Expand Up @@ -425,7 +433,7 @@ def apply_filters(resources: List[Any], filters: List[Filter]) -> List[Any]:
if isinstance(tag, dict) and tag.get("Key") == tag_key:
tag_value = tag.get("Value", "")
break
if tag_value is None or tag_value not in values:
if tag_value is None or not _matches_filter_value(str(tag_value), values):
match = False
break
continue
Expand All @@ -452,14 +460,14 @@ def apply_filters(resources: List[Any], filters: List[Filter]) -> List[Any]:
elif isinstance(obj, list):
# List field: pass if any element matches any value
list_strs = [str(item) for item in obj]
if not any(v in list_strs for v in values):
if not any(_matches_filter_value(item, values) for item in list_strs):
match = False
break
continue
else:
val_str = str(obj)

if val_str not in values:
if not _matches_filter_value(val_str, values):
match = False
break

Expand Down
2 changes: 1 addition & 1 deletion emulators/aws-ec2/tests/tf/01-hello-world/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ provider "aws" {

# Create an EC2 instance
resource "aws_instance" "example" {
ami = "ami-13095cba0d3649579"
ami = "ami-0f9fc25dd2506cf6d"
instance_type = "t2.micro"
}
2 changes: 1 addition & 1 deletion emulators/aws-ec2/tests/tf/02-one-server/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ provider "aws" {

# Create an EC2 instance
resource "aws_instance" "example" {
ami = "ami-13095cba0d3649579"
ami = "ami-0f9fc25dd2506cf6d"
instance_type = "t2.micro"

tags = {
Expand Down
2 changes: 1 addition & 1 deletion emulators/aws-ec2/tests/tf/03-one-webserver/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ resource "aws_security_group" "instance" {

# Create an EC2 instance
resource "aws_instance" "example" {
ami = "ami-13095cba0d3649579"
ami = "ami-0f9fc25dd2506cf6d"
instance_type = "t2.micro"
vpc_security_group_ids = ["${aws_security_group.instance.id}"]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ resource "aws_security_group" "instance" {

# Create an EC2 instance
resource "aws_instance" "example" {
ami = "ami-13095cba0d3649579"
ami = "ami-0f9fc25dd2506cf6d"
instance_type = "t2.micro"
vpc_security_group_ids = ["${aws_security_group.instance.id}"]

Expand Down
2 changes: 1 addition & 1 deletion emulators/aws-ec2/tests/tf/06-instance-in-vpc/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ resource "aws_security_group" "instance" {

# EC2 instance in VPC
resource "aws_instance" "example" {
ami = "ami-13095cba0d3649579"
ami = "ami-0f9fc25dd2506cf6d"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.main.id}"
vpc_security_group_ids = ["${aws_security_group.instance.id}"]
Expand Down
Loading