diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aa3cb5f..d3c6e04 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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():
diff --git a/emulators/aws-ec2/emulator_core/services/ami.py b/emulators/aws-ec2/emulator_core/services/ami.py
index 2a68f58..cde59c0 100644
--- a/emulators/aws-ec2/emulator_core/services/ami.py
+++ b/emulators/aws-ec2/emulator_core/services/ami.py
@@ -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")
@@ -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"))
@@ -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)
-
diff --git a/emulators/aws-ec2/emulator_core/services/dhcpoptions.py b/emulators/aws-ec2/emulator_core/services/dhcpoptions.py
index 6552d88..0ebd021 100644
--- a/emulators/aws-ec2/emulator_core/services/dhcpoptions.py
+++ b/emulators/aws-ec2/emulator_core/services/dhcpoptions.py
@@ -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 [])
@@ -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]):
@@ -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"),
}
@@ -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}')
- for item in param_data:
- xml_parts.append(f'{indent_str} - ')
- xml_parts.extend(dhcpoptions_ResponseSerializer._serialize_nested_fields(item, 2))
- xml_parts.append(f'{indent_str}
')
- xml_parts.append(f'{indent_str}')
- else:
- xml_parts.append(f'{indent_str}')
+ xml_parts.append(f'{indent_str}')
+ xml_parts.extend(dhcpoptions_ResponseSerializer._serialize_nested_fields(param_data, 2))
+ xml_parts.append(f'{indent_str}')
xml_parts.append(f'')
return "\n".join(xml_parts)
@@ -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)
-
diff --git a/emulators/aws-ec2/emulator_core/services/internetgateway.py b/emulators/aws-ec2/emulator_core/services/internetgateway.py
index 4aeb81a..51f1ca0 100644
--- a/emulators/aws-ec2/emulator_core/services/internetgateway.py
+++ b/emulators/aws-ec2/emulator_core/services/internetgateway.py
@@ -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)
@@ -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)
-
diff --git a/emulators/aws-ec2/emulator_core/services/vpc.py b/emulators/aws-ec2/emulator_core/services/vpc.py
index 0bb656c..6aeea9c 100644
--- a/emulators/aws-ec2/emulator_core/services/vpc.py
+++ b/emulators/aws-ec2/emulator_core/services/vpc.py
@@ -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 {
diff --git a/emulators/aws-ec2/emulator_core/services/vpcpeering.py b/emulators/aws-ec2/emulator_core/services/vpcpeering.py
index d6ecc6d..c008360 100644
--- a/emulators/aws-ec2/emulator_core/services/vpcpeering.py
+++ b/emulators/aws-ec2/emulator_core/services/vpcpeering.py
@@ -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
@@ -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,
@@ -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)
-
diff --git a/emulators/aws-ec2/emulator_core/utils.py b/emulators/aws-ec2/emulator_core/utils.py
index 1f4b901..b850803 100644
--- a/emulators/aws-ec2/emulator_core/utils.py
+++ b/emulators/aws-ec2/emulator_core/utils.py
@@ -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
@@ -376,6 +377,13 @@ def serialize_error_response(error_data: ErrorResponse, request_id: str) -> str:
"""
+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.
@@ -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
@@ -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
diff --git a/emulators/aws-ec2/tests/tf/01-hello-world/main.tf b/emulators/aws-ec2/tests/tf/01-hello-world/main.tf
index c788f67..da8f41e 100644
--- a/emulators/aws-ec2/tests/tf/01-hello-world/main.tf
+++ b/emulators/aws-ec2/tests/tf/01-hello-world/main.tf
@@ -5,6 +5,6 @@ provider "aws" {
# Create an EC2 instance
resource "aws_instance" "example" {
- ami = "ami-13095cba0d3649579"
+ ami = "ami-0f9fc25dd2506cf6d"
instance_type = "t2.micro"
}
\ No newline at end of file
diff --git a/emulators/aws-ec2/tests/tf/02-one-server/main.tf b/emulators/aws-ec2/tests/tf/02-one-server/main.tf
index 5458f44..b6a3b16 100644
--- a/emulators/aws-ec2/tests/tf/02-one-server/main.tf
+++ b/emulators/aws-ec2/tests/tf/02-one-server/main.tf
@@ -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 = {
diff --git a/emulators/aws-ec2/tests/tf/03-one-webserver/main.tf b/emulators/aws-ec2/tests/tf/03-one-webserver/main.tf
index f47088e..725ae45 100644
--- a/emulators/aws-ec2/tests/tf/03-one-webserver/main.tf
+++ b/emulators/aws-ec2/tests/tf/03-one-webserver/main.tf
@@ -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}"]
diff --git a/emulators/aws-ec2/tests/tf/04-one-webserver-with-vars/main.tf b/emulators/aws-ec2/tests/tf/04-one-webserver-with-vars/main.tf
index bb631ab..74ca21e 100644
--- a/emulators/aws-ec2/tests/tf/04-one-webserver-with-vars/main.tf
+++ b/emulators/aws-ec2/tests/tf/04-one-webserver-with-vars/main.tf
@@ -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}"]
diff --git a/emulators/aws-ec2/tests/tf/06-instance-in-vpc/main.tf b/emulators/aws-ec2/tests/tf/06-instance-in-vpc/main.tf
index 3f7156e..6f48d85 100644
--- a/emulators/aws-ec2/tests/tf/06-instance-in-vpc/main.tf
+++ b/emulators/aws-ec2/tests/tf/06-instance-in-vpc/main.tf
@@ -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}"]