Skip to content

Commit d69ae04

Browse files
author
Jessie Moss
committed
Bugfixes for remote_state blocks
1 parent fbb5dc6 commit d69ae04

File tree

1 file changed

+98
-28
lines changed

1 file changed

+98
-28
lines changed

bin/tflocal

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ LOCALSTACK_HOSTNAME = (
5555
or os.environ.get("LOCALSTACK_HOSTNAME")
5656
or "localhost"
5757
)
58-
EDGE_PORT = int(
59-
urlparse(AWS_ENDPOINT_URL).port or os.environ.get("EDGE_PORT") or 4566)
58+
EDGE_PORT = int(urlparse(AWS_ENDPOINT_URL).port or os.environ.get("EDGE_PORT") or 4566)
6059
TF_VERSION: Optional[version.Version] = None
6160
TF_PROVIDER_CONFIG = """
6261
provider "aws" {
@@ -194,8 +193,7 @@ def create_provider_config_file(provider_file_path: str, provider_aliases=None)
194193
for provider in provider_aliases:
195194
provider_config = TF_PROVIDER_CONFIG.replace(
196195
"<access_key>",
197-
get_access_key(
198-
provider) if CUSTOMIZE_ACCESS_KEY else DEFAULT_ACCESS_KEY,
196+
get_access_key(provider) if CUSTOMIZE_ACCESS_KEY else DEFAULT_ACCESS_KEY,
199197
)
200198
endpoints = "\n".join(
201199
[f' {s} = "{get_service_endpoint(s)}"' for s in services]
@@ -343,8 +341,7 @@ def generate_s3_backend_config() -> str:
343341
for k, v in configs["endpoints"].items()
344342
}
345343
backend_config["access_key"] = (
346-
get_access_key(
347-
backend_config) if CUSTOMIZE_ACCESS_KEY else DEFAULT_ACCESS_KEY
344+
get_access_key(backend_config) if CUSTOMIZE_ACCESS_KEY else DEFAULT_ACCESS_KEY
348345
)
349346
configs.update(backend_config)
350347
if not DRY_RUN:
@@ -383,62 +380,138 @@ def generate_s3_backend_config() -> str:
383380
result = result.replace("<configs>", config_options)
384381
return result
385382

383+
386384
def generate_remote_state_config() -> str:
387385
"""
388386
Generate configuration for terraform_remote_state data sources to use LocalStack endpoints.
389387
Similar to generate_s3_backend_config but for terraform_remote_state blocks.
390388
"""
389+
390+
is_tf_legacy = TF_VERSION < version.Version("1.6")
391391
tf_files = parse_tf_files()
392-
if not tf_files:
393-
return ""
392+
393+
legacy_endpoint_mappings = {
394+
"endpoint": "s3",
395+
"iam_endpoint": "iam",
396+
"sts_endpoint": "sts",
397+
"dynamodb_endpoint": "dynamodb",
398+
}
394399

395400
result = ""
396401
for filename, obj in tf_files.items():
397402
if LS_PROVIDERS_FILE == filename:
398403
continue
399-
400404
data_blocks = ensure_list(obj.get("data", []))
401405
for data_block in data_blocks:
402406
terraform_remote_state = data_block.get("terraform_remote_state")
403407
if not terraform_remote_state:
404408
continue
405-
406409
for data_name, data_config in terraform_remote_state.items():
407410
if data_config.get("backend") != "s3":
408411
continue
409-
410412
# Create override for S3 remote state
411413
config_attrs = data_config.get("config", {})
412414
if not config_attrs:
413415
continue
414-
416+
# Merge in legacy endpoint configs if not existing already
417+
if is_tf_legacy and config_attrs.get("endpoints"):
418+
print(
419+
"Warning: Unsupported backend option(s) detected (`endpoints`). Please make sure you always use the corresponding options to your Terraform version."
420+
)
421+
exit(1)
422+
for legacy_endpoint, endpoint in legacy_endpoint_mappings.items():
423+
if (
424+
legacy_endpoint in config_attrs
425+
and config_attrs.get("endpoints")
426+
and endpoint in config_attrs["endpoints"]
427+
):
428+
del config_attrs[legacy_endpoint]
429+
continue
430+
if legacy_endpoint in config_attrs and (
431+
not config_attrs.get("endpoints")
432+
or endpoint not in config_attrs["endpoints"]
433+
):
434+
if not config_attrs.get("endpoints"):
435+
config_attrs["endpoints"] = {}
436+
config_attrs["endpoints"].update(
437+
{endpoint: config_attrs[legacy_endpoint]}
438+
)
439+
del config_attrs[legacy_endpoint]
440+
415441
# Set up default configs
416442
configs = {
417443
"bucket": config_attrs.get("bucket", "tf-test-state"),
418444
"key": config_attrs.get("key", "terraform.tfstate"),
419445
"region": config_attrs.get("region", get_region()),
420-
"endpoint": get_service_endpoint("s3"),
446+
"endpoints": {
447+
"s3": get_service_endpoint("s3"),
448+
"iam": get_service_endpoint("iam"),
449+
"sso": get_service_endpoint("sso"),
450+
"sts": get_service_endpoint("sts"),
451+
},
421452
}
422-
453+
454+
# Add any missing default endpoints
455+
if config_attrs.get("endpoints"):
456+
config_attrs["endpoints"] = {
457+
k: config_attrs["endpoints"].get(k) or v
458+
for k, v in configs["endpoints"].items()
459+
}
460+
423461
# Update with user-provided configs
424462
configs.update(config_attrs)
425-
463+
426464
# Generate config string
427465
config_options = ""
466+
# for key, value in sorted(configs.items()):
467+
# if isinstance(value, bool):
468+
# value = str(value).lower()
469+
# elif isinstance(value, (str, int, float)):
470+
# value = f'"{value}"'
471+
# config_options += f"\n {key} = {value}"
428472
for key, value in sorted(configs.items()):
429473
if isinstance(value, bool):
430474
value = str(value).lower()
431-
elif isinstance(value, (str, int, float)):
432-
value = f'"{value}"'
433-
config_options += f'\n {key} = {value}'
434-
475+
elif isinstance(value, dict):
476+
if key == "endpoints" and is_tf_legacy:
477+
for (
478+
legacy_endpoint,
479+
endpoint,
480+
) in legacy_endpoint_mappings.items():
481+
config_options += f'\n {legacy_endpoint} = "{configs[key][endpoint]}"'
482+
continue
483+
else:
484+
value = textwrap.indent(
485+
text=f"{key} = {{\n"
486+
+ "\n".join(
487+
[f' {k} = "{v}"' for k, v in value.items()]
488+
)
489+
+ "\n}",
490+
prefix=" " * 4,
491+
)
492+
config_options += f"\n{value}"
493+
continue
494+
elif isinstance(value, list):
495+
# TODO this will break if it's a list of dicts or other complex object
496+
# this serialization logic should probably be moved to a separate recursive function
497+
as_string = [f'"{item}"' for item in value]
498+
value = f"[{', '.join(as_string)}]"
499+
else:
500+
value = f'"{str(value)}"'
501+
config_options += f"\n {key} = {value}"
502+
435503
# Create the final config
436-
remote_state_config = TF_REMOTE_STATE_CONFIG.replace("<name>", data_name)
437-
remote_state_config = remote_state_config.replace("<configs>", config_options)
504+
remote_state_config = TF_REMOTE_STATE_CONFIG.replace(
505+
"<name>", data_name
506+
)
507+
remote_state_config = remote_state_config.replace(
508+
"<configs>", config_options
509+
)
438510
result += remote_state_config
439-
511+
440512
return result
441513

514+
442515
def check_override_file(providers_file: str) -> None:
443516
"""Checks override file existance"""
444517
if os.path.exists(providers_file):
@@ -559,8 +632,7 @@ def get_or_create_bucket(bucket_name: str):
559632
region = s3_client.meta.region_name
560633
kwargs = {}
561634
if region != "us-east-1":
562-
kwargs = {"CreateBucketConfiguration": {
563-
"LocationConstraint": region}}
635+
kwargs = {"CreateBucketConfiguration": {"LocationConstraint": region}}
564636
return s3_client.create_bucket(Bucket=bucket_name, **kwargs)
565637

566638

@@ -574,8 +646,7 @@ def get_or_create_ddb_table(table_name: str, region: str = None):
574646
TableName=table_name,
575647
BillingMode="PAY_PER_REQUEST",
576648
KeySchema=[{"AttributeName": "LockID", "KeyType": "HASH"}],
577-
AttributeDefinitions=[
578-
{"AttributeName": "LockID", "AttributeType": "S"}],
649+
AttributeDefinitions=[{"AttributeName": "LockID", "AttributeType": "S"}],
579650
)
580651

581652

@@ -685,8 +756,7 @@ def main():
685756
if not TF_VERSION:
686757
raise ValueError
687758
except (FileNotFoundError, ValueError) as e:
688-
print(
689-
f"Unable to determine version. See error message for details: {e}")
759+
print(f"Unable to determine version. See error message for details: {e}")
690760
exit(1)
691761

692762
config_override_files = []

0 commit comments

Comments
 (0)