Skip to content

Commit e299148

Browse files
authored
Fix versioned service endpoints (#81)
1 parent f7d6dff commit e299148

File tree

4 files changed

+107
-4
lines changed

4 files changed

+107
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ ADDITIONAL_TF_OVERRIDE_LOCATIONS=/path/to/module1,path/to/module2 tflocal plan
7777

7878
## Change Log
7979

80+
* v0.23.1: Fix endpoint overrides for Terraform AWS provider >= 6.0.0-beta2
8081
* v0.23.0: Add support for `terraform_remote_state` with `s3` backend to read the state stored in local S3 backend; fix S3 backend config detection with multiple Terraform blocks
8182
* v0.22.0: Fix S3 backend forcing DynamoDB State Lock to be enabled by default
8283
* v0.21.0: Add ability to drop an override file in additional locations

bin/tflocal

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ LOCALSTACK_HOSTNAME = (
5656
or "localhost"
5757
)
5858
EDGE_PORT = int(urlparse(AWS_ENDPOINT_URL).port or os.environ.get("EDGE_PORT") or 4566)
59+
AWS_PROVIDER_NAME_SUFFIX = "/hashicorp/aws"
60+
AWS_PROVIDER_VERSION: Optional[version.Version] = None
5961
TF_VERSION: Optional[version.Version] = None
6062
TF_PROVIDER_CONFIG = """
6163
provider "aws" {
@@ -128,6 +130,15 @@ SERVICE_ALIASES = [
128130
]
129131
# service names to be excluded (not yet available in TF)
130132
SERVICE_EXCLUSIONS = ["meteringmarketplace"]
133+
134+
# we can exclude some service endpoints based on the AWS provider version
135+
# those limits are exclusive, meaning 6.0.0b2 is the first version to fail with those endpoints, so only a lower version
136+
# will have that setting
137+
VERSIONED_SERVICE_EXCLUSIONS = {
138+
"iotanalytics": {"min": version.Version("0"), "max": version.Version("6.0.0b2")},
139+
"iotevents": {"min": version.Version("0"), "max": version.Version("6.0.0b2")},
140+
}
141+
131142
# maps services to be replaced with alternative names
132143
# skip services which do not have equivalent endpoint overrides
133144
# see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/custom-service-endpoints
@@ -173,7 +184,8 @@ def create_provider_config_file(provider_file_path: str, provider_aliases=None)
173184

174185
# create list of service names
175186
services = list(config.get_service_ports())
176-
services = [srvc for srvc in services if srvc not in SERVICE_EXCLUSIONS]
187+
services = [srvc for srvc in services if srvc not in SERVICE_EXCLUSIONS and is_service_endpoint_supported(srvc)]
188+
177189
services = [s.replace("-", "") for s in services]
178190
for old, new in SERVICE_REPLACEMENTS.items():
179191
try:
@@ -606,6 +618,33 @@ def get_tf_version(env):
606618
TF_VERSION = version.parse(json.loads(output)["terraform_version"])
607619

608620

621+
def get_provider_version_from_lock_file() -> Optional[version.Version]:
622+
global AWS_PROVIDER_VERSION
623+
lock_file = os.path.join(get_default_provider_folder_path(), ".terraform.lock.hcl")
624+
625+
if not os.path.exists(lock_file):
626+
return
627+
628+
provider_version = None
629+
with open(lock_file, "r") as fp:
630+
result = hcl2.load(fp)
631+
for provider in result.get("provider", []):
632+
for provider_name, provider_config in provider.items():
633+
if provider_name.endswith(AWS_PROVIDER_NAME_SUFFIX):
634+
provider_version = provider_config.get("version")
635+
636+
if provider_version:
637+
AWS_PROVIDER_VERSION = version.parse(provider_version)
638+
639+
640+
def is_service_endpoint_supported(service_name: str) -> bool:
641+
if service_name not in VERSIONED_SERVICE_EXCLUSIONS or not AWS_PROVIDER_VERSION:
642+
return True
643+
644+
supported_versions = VERSIONED_SERVICE_EXCLUSIONS[service_name]
645+
return supported_versions["min"] < AWS_PROVIDER_VERSION < supported_versions["max"]
646+
647+
609648
def run_tf_exec(cmd, env):
610649
"""Run terraform using os.exec - can be useful as it does not require any I/O
611650
handling for stdin/out/err. Does *not* allow us to perform any cleanup logic."""
@@ -686,6 +725,9 @@ def main():
686725
print(f"Unable to determine version. See error message for details: {e}")
687726
exit(1)
688727

728+
if len(sys.argv) > 1 and sys.argv[1] != "init":
729+
get_provider_version_from_lock_file()
730+
689731
config_override_files = []
690732

691733
for folder_path in get_folder_paths_that_require_an_override_file():

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = terraform-local
3-
version = 0.23.0
3+
version = 0.23.1
44
url = https://github.com/localstack/terraform-local
55
author = LocalStack Team
66
author_email = info@localstack.cloud

tests/test_apply.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,60 @@ def test_s3_remote_data_source_with_workspace(monkeypatch):
390390
assert result["data"][1]["terraform_remote_state"]["build_infra"]["workspace"] == "build"
391391

392392

393+
@pytest.mark.parametrize("provider_version", ["5.99.1", "6.0.0-beta2"])
394+
def test_versioned_endpoints(monkeypatch, provider_version):
395+
sns_topic_name = f"test-topic-{short_uid()}"
396+
config = """
397+
terraform {
398+
required_providers {
399+
aws = {
400+
source = "hashicorp/aws"
401+
version = "= %s"
402+
}
403+
}
404+
}
405+
406+
provider "aws" {
407+
region = "us-east-1"
408+
access_key = "test"
409+
secret_key = "test"
410+
skip_credentials_validation = true
411+
skip_metadata_api_check = true
412+
skip_requesting_account_id = true
413+
endpoints {
414+
sns = "http://localhost:4566"
415+
}
416+
}
417+
418+
resource "aws_sns_topic" "example" {
419+
name = "%s"
420+
}
421+
""" % (provider_version, sns_topic_name)
422+
423+
with tempfile.TemporaryDirectory(delete=True) as temp_dir:
424+
with open(os.path.join(temp_dir, "test.tf"), "w") as f:
425+
f.write(config)
426+
427+
# we need the `terraform init` command to create a lock file, so it cannot be a `DRY_RUN`
428+
run([TFLOCAL_BIN, "init"], cwd=temp_dir, env=dict(os.environ))
429+
monkeypatch.setenv("DRY_RUN", "1")
430+
run([TFLOCAL_BIN, "apply", "-auto-approve"], cwd=temp_dir, env=dict(os.environ))
431+
432+
override_file = os.path.join(temp_dir, "localstack_providers_override.tf")
433+
assert check_override_file_exists(override_file)
434+
435+
with open(override_file, "r") as fp:
436+
result = hcl2.load(fp)
437+
endpoints = result["provider"][0]["aws"]["endpoints"][0]
438+
if provider_version == "5.99.1":
439+
assert "iotanalytics" in endpoints
440+
assert "iotevents" in endpoints
441+
else:
442+
# we add this assertion to be sure, but Terraform wouldn't deploy with them
443+
assert "iotanalytics" not in endpoints
444+
assert "iotevents" not in endpoints
445+
446+
393447
def test_dry_run(monkeypatch):
394448
monkeypatch.setenv("DRY_RUN", "1")
395449
state_bucket = "tf-state-dry-run"
@@ -638,8 +692,14 @@ def get_version():
638692
return version.parse(json.loads(output)["terraform_version"])
639693

640694

641-
def deploy_tf_script(script: str, cleanup: bool = True, env_vars: Dict[str, str] = None, user_input: str = None):
642-
# TODO the delete keyword was added in python 3.12, and the README and setup.cfg claims compatibility with earlier python versions
695+
def deploy_tf_script(
696+
script: str,
697+
cleanup: bool = True,
698+
env_vars: Dict[str, str] = None,
699+
user_input: str = None,
700+
):
701+
# TODO the delete keyword was added in python 3.12, and the README and setup.cfg claims compatibility
702+
# with earlier python versions
643703
with tempfile.TemporaryDirectory(delete=cleanup) as temp_dir:
644704
with open(os.path.join(temp_dir, "test.tf"), "w") as f:
645705
f.write(script)

0 commit comments

Comments
 (0)