Skip to content
Merged
Changes from 11 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
135 changes: 135 additions & 0 deletions bin/tflocal
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ terraform {
}
}
"""
TF_REMOTE_STATE_CONFIG = """
data "terraform_remote_state" "<name>" {
backend = "s3"
config = {<configs>
}
}
"""
PROCESS = None
# some services have aliases which are mutually exclusive to each other
# see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/custom-service-endpoints#available-endpoint-customizations
Expand Down Expand Up @@ -215,6 +222,9 @@ def create_provider_config_file(provider_file_path: str, provider_aliases=None)
# create s3 backend config
tf_config += generate_s3_backend_config()

# create remote state config
tf_config += generate_remote_state_config()

# write temporary config file
write_provider_config_file(provider_file_path, tf_config)

Expand Down Expand Up @@ -371,6 +381,131 @@ def generate_s3_backend_config() -> str:
return result


def generate_remote_state_config() -> str:
"""
Generate configuration for terraform_remote_state data sources to use LocalStack endpoints.
Similar to generate_s3_backend_config but for terraform_remote_state blocks.
"""

is_tf_legacy = TF_VERSION < version.Version("1.6")
tf_files = parse_tf_files()

legacy_endpoint_mappings = {
"endpoint": "s3",
"iam_endpoint": "iam",
"sts_endpoint": "sts",
"dynamodb_endpoint": "dynamodb",
}

result = ""
for filename, obj in tf_files.items():
if LS_PROVIDERS_FILE == filename:
continue
data_blocks = ensure_list(obj.get("data", []))
for data_block in data_blocks:
terraform_remote_state = data_block.get("terraform_remote_state")
if not terraform_remote_state:
continue
for data_name, data_config in terraform_remote_state.items():
if data_config.get("backend") != "s3":
continue
# Create override for S3 remote state
config_attrs = data_config.get("config", {})
if not config_attrs:
continue
# Merge in legacy endpoint configs if not existing already
if is_tf_legacy and config_attrs.get("endpoints"):
print(
"Warning: Unsupported backend option(s) detected (`endpoints`). Please make sure you always use the corresponding options to your Terraform version."
)
exit(1)
for legacy_endpoint, endpoint in legacy_endpoint_mappings.items():
if (
legacy_endpoint in config_attrs
and config_attrs.get("endpoints")
and endpoint in config_attrs["endpoints"]
):
del config_attrs[legacy_endpoint]
continue
if legacy_endpoint in config_attrs and (
not config_attrs.get("endpoints")
or endpoint not in config_attrs["endpoints"]
):
if not config_attrs.get("endpoints"):
config_attrs["endpoints"] = {}
config_attrs["endpoints"].update(
{endpoint: config_attrs[legacy_endpoint]}
)
del config_attrs[legacy_endpoint]

# Set up default configs
configs = {
"bucket": config_attrs.get("bucket", "tf-test-state"),
"key": config_attrs.get("key", "terraform.tfstate"),
"region": config_attrs.get("region", get_region()),
"endpoints": {
"s3": get_service_endpoint("s3"),
"iam": get_service_endpoint("iam"),
"sso": get_service_endpoint("sso"),
"sts": get_service_endpoint("sts"),
},
}

# Add any missing default endpoints
if config_attrs.get("endpoints"):
config_attrs["endpoints"] = {
k: config_attrs["endpoints"].get(k) or v
for k, v in configs["endpoints"].items()
}

# Update with user-provided configs
configs.update(config_attrs)

# Generate config string
config_options = ""
for key, value in sorted(configs.items()):
if isinstance(value, bool):
value = str(value).lower()
elif isinstance(value, dict):
if key == "endpoints" and is_tf_legacy:
for (
legacy_endpoint,
endpoint,
) in legacy_endpoint_mappings.items():
config_options += f'\n {legacy_endpoint} = "{configs[key][endpoint]}"'
continue
else:
value = textwrap.indent(
text=f"{key} = {{\n"
+ "\n".join(
[f' {k} = "{v}"' for k, v in value.items()]
)
+ "\n}",
prefix=" " * 4,
)
config_options += f"\n{value}"
continue
elif isinstance(value, list):
# TODO this will break if it's a list of dicts or other complex object
# this serialization logic should probably be moved to a separate recursive function
as_string = [f'"{item}"' for item in value]
value = f"[{', '.join(as_string)}]"
else:
value = f'"{str(value)}"'
config_options += f"\n {key} = {value}"

# Create the final config
remote_state_config = TF_REMOTE_STATE_CONFIG.replace(
"<name>", data_name
)
remote_state_config = remote_state_config.replace(
"<configs>", config_options
)
result += remote_state_config

return result


def check_override_file(providers_file: str) -> None:
"""Checks override file existance"""
if os.path.exists(providers_file):
Expand Down