Skip to content

Commit f7d6dff

Browse files
hubbcapsJessie Mossbentsku
authored
Add support for remote state blocks (#79)
Co-authored-by: Jessie Moss <jessie.moss@dutchbros.com> Co-authored-by: Benjamin Simon <benjh.simon@gmail.com>
1 parent 9ceb16e commit f7d6dff

File tree

5 files changed

+267
-49
lines changed

5 files changed

+267
-49
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.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
8081
* v0.22.0: Fix S3 backend forcing DynamoDB State Lock to be enabled by default
8182
* v0.21.0: Add ability to drop an override file in additional locations
8283
* v0.20.1: Fix list config rendering

bin/tflocal

Lines changed: 104 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import textwrap
1818

1919
from packaging import version
2020
from urllib.parse import urlparse
21-
from typing import Iterable, Optional
21+
from typing import Iterable, Optional, Dict, Tuple
2222

2323
PARENT_FOLDER = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
2424
if os.path.isdir(os.path.join(PARENT_FOLDER, ".venv")):
@@ -75,6 +75,14 @@ terraform {
7575
}
7676
}
7777
"""
78+
TF_REMOTE_STATE_CONFIG = """
79+
data "terraform_remote_state" "<name>" {
80+
backend = "s3"
81+
<workspace-placeholder>
82+
config = {<configs>
83+
}
84+
}
85+
"""
7886
PROCESS = None
7987
# some services have aliases which are mutually exclusive to each other
8088
# see https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/custom-service-endpoints#available-endpoint-customizations
@@ -215,6 +223,9 @@ def create_provider_config_file(provider_file_path: str, provider_aliases=None)
215223
# create s3 backend config
216224
tf_config += generate_s3_backend_config()
217225

226+
# create remote state config
227+
tf_config += generate_remote_state_config()
228+
218229
# write temporary config file
219230
write_provider_config_file(provider_file_path, tf_config)
220231

@@ -261,31 +272,90 @@ def determine_provider_aliases() -> list:
261272

262273
def generate_s3_backend_config() -> str:
263274
"""Generate an S3 `backend {..}` block with local endpoints, if configured"""
264-
is_tf_legacy = TF_VERSION < version.Version("1.6")
265-
backend_config = None
275+
s3_backend_config = {}
266276
tf_files = parse_tf_files()
267277
for filename, obj in tf_files.items():
268278
if LS_PROVIDERS_FILE == filename:
269279
continue
270280
tf_configs = ensure_list(obj.get("terraform", []))
271281
for tf_config in tf_configs:
272-
tmp_backend_config = ensure_list(tf_config.get("backend"))
273-
if tmp_backend_config[0]:
274-
backend_config = tmp_backend_config[0]
275-
break
276-
backend_config = backend_config and backend_config.get("s3")
277-
if not backend_config:
282+
if tf_config.get("backend"):
283+
backend_config = ensure_list(tf_config.get("backend"))[0]
284+
if backend_config.get("s3"):
285+
s3_backend_config = backend_config["s3"]
286+
break
287+
288+
if not s3_backend_config:
278289
return ""
279290

291+
config_values, config_string = _generate_s3_backend_config(s3_backend_config)
292+
if not DRY_RUN:
293+
get_or_create_bucket(config_values["bucket"])
294+
if "dynamodb_table" in config_values:
295+
get_or_create_ddb_table(
296+
config_values["dynamodb_table"],
297+
region=config_values["region"],
298+
)
299+
300+
result = TF_S3_BACKEND_CONFIG.replace("<configs>", config_string)
301+
return result
302+
303+
304+
def generate_remote_state_config() -> str:
305+
"""
306+
Generate configuration for terraform_remote_state data sources to use LocalStack endpoints.
307+
Similar to generate_s3_backend_config but for terraform_remote_state blocks.
308+
"""
309+
310+
tf_files = parse_tf_files()
311+
result = ""
312+
for filename, obj in tf_files.items():
313+
if LS_PROVIDERS_FILE == filename:
314+
continue
315+
data_blocks = ensure_list(obj.get("data", []))
316+
for data_block in data_blocks:
317+
terraform_remote_state = data_block.get("terraform_remote_state")
318+
if not terraform_remote_state:
319+
continue
320+
for data_name, data_config in terraform_remote_state.items():
321+
if data_config.get("backend") != "s3":
322+
continue
323+
# Create override for S3 remote state
324+
backend_config = data_config.get("config", {})
325+
if not backend_config:
326+
continue
327+
workspace = data_config.get("workspace", "")
328+
if workspace:
329+
if workspace[0] == "$":
330+
workspace = workspace.lstrip('${').rstrip('}')
331+
else:
332+
workspace = f'"{workspace}"'
333+
workspace = f"workspace = {workspace}"
334+
335+
_, config_str = _generate_s3_backend_config(backend_config)
336+
337+
# Create the final config
338+
remote_state_config = TF_REMOTE_STATE_CONFIG.replace(
339+
"<name>", data_name
340+
) \
341+
.replace("<configs>", config_str) \
342+
.replace("<workspace-placeholder>", workspace)
343+
result += remote_state_config
344+
345+
return result
346+
347+
348+
def _generate_s3_backend_config(backend_config: Dict) -> Tuple[Dict, str]:
349+
is_tf_legacy = TF_VERSION < version.Version("1.6")
280350
legacy_endpoint_mappings = {
281351
"endpoint": "s3",
282352
"iam_endpoint": "iam",
283353
"sts_endpoint": "sts",
284354
"dynamodb_endpoint": "dynamodb",
285355
}
286356

287-
configs = {
288-
# note: default values, updated by `backend_config` further below...
357+
# Set up default config
358+
default_config = {
289359
"bucket": "tf-test-state",
290360
"key": "terraform.tfstate",
291361
"region": get_region(),
@@ -300,6 +370,7 @@ def generate_s3_backend_config() -> str:
300370
"dynamodb": get_service_endpoint("dynamodb"),
301371
},
302372
}
373+
303374
# Merge in legacy endpoint configs if not existing already
304375
if is_tf_legacy and backend_config.get("endpoints"):
305376
print(
@@ -308,56 +379,53 @@ def generate_s3_backend_config() -> str:
308379
exit(1)
309380
for legacy_endpoint, endpoint in legacy_endpoint_mappings.items():
310381
if (
311-
legacy_endpoint in backend_config
312-
and backend_config.get("endpoints")
313-
and endpoint in backend_config["endpoints"]
382+
legacy_endpoint in backend_config
383+
and backend_config.get("endpoints")
384+
and endpoint in backend_config["endpoints"]
314385
):
315386
del backend_config[legacy_endpoint]
316387
continue
317388
if legacy_endpoint in backend_config and (
318-
not backend_config.get("endpoints")
319-
or endpoint not in backend_config["endpoints"]
389+
not backend_config.get("endpoints")
390+
or endpoint not in backend_config["endpoints"]
320391
):
321392
if not backend_config.get("endpoints"):
322393
backend_config["endpoints"] = {}
323394
backend_config["endpoints"].update(
324395
{endpoint: backend_config[legacy_endpoint]}
325396
)
326397
del backend_config[legacy_endpoint]
398+
327399
# Add any missing default endpoints
328400
if backend_config.get("endpoints"):
329401
backend_config["endpoints"] = {
330402
k: backend_config["endpoints"].get(k) or v
331-
for k, v in configs["endpoints"].items()
403+
for k, v in default_config["endpoints"].items()
332404
}
405+
333406
backend_config["access_key"] = (
334407
get_access_key(backend_config) if CUSTOMIZE_ACCESS_KEY else DEFAULT_ACCESS_KEY
335408
)
336-
configs.update(backend_config)
337-
if not DRY_RUN:
338-
get_or_create_bucket(configs["bucket"])
339-
if "dynamodb_table" in configs:
340-
get_or_create_ddb_table(configs["dynamodb_table"], region=configs["region"])
341-
result = TF_S3_BACKEND_CONFIG
342-
config_options = ""
343-
for key, value in sorted(configs.items()):
409+
410+
# Update with user-provided configs
411+
default_config.update(backend_config)
412+
# Generate config string
413+
config_string = ""
414+
for key, value in sorted(default_config.items()):
344415
if isinstance(value, bool):
345416
value = str(value).lower()
346417
elif isinstance(value, dict):
347418
if key == "endpoints" and is_tf_legacy:
348419
for legacy_endpoint, endpoint in legacy_endpoint_mappings.items():
349-
config_options += (
350-
f'\n {legacy_endpoint} = "{configs[key][endpoint]}"'
351-
)
420+
config_string += f'\n {legacy_endpoint} = "{default_config[key][endpoint]}"'
352421
continue
353422
else:
423+
joined_values = "\n".join([f' {k} = "{v}"' for k, v in value.items()])
354424
value = textwrap.indent(
355-
text=f"{key} = {{\n"
356-
+ "\n".join([f' {k} = "{v}"' for k, v in value.items()])
357-
+ "\n}",
425+
text=f"{key} = {{\n{joined_values}\n}}",
358426
prefix=" " * 4,
359427
)
360-
config_options += f"\n{value}"
428+
config_string += f"\n{value}"
361429
continue
362430
elif isinstance(value, list):
363431
# TODO this will break if it's a list of dicts or other complex object
@@ -366,13 +434,13 @@ def generate_s3_backend_config() -> str:
366434
value = f"[{', '.join(as_string)}]"
367435
else:
368436
value = f'"{str(value)}"'
369-
config_options += f"\n {key} = {value}"
370-
result = result.replace("<configs>", config_options)
371-
return result
437+
config_string += f"\n {key} = {value}"
438+
439+
return default_config, config_string
372440

373441

374442
def check_override_file(providers_file: str) -> None:
375-
"""Checks override file existance"""
443+
"""Checks override file existence"""
376444
if os.path.exists(providers_file):
377445
msg = f"Providers override file {providers_file} already exists"
378446
err_msg = msg + " - please delete it first, exiting..."

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.22.0
3+
version = 0.23.0
44
url = https://github.com/localstack/terraform-local
55
author = LocalStack Team
66
author_email = info@localstack.cloud

tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
@pytest.fixture(scope="session", autouse=True)
66
def start_localstack():
77
subprocess.check_output(["localstack", "start", "-d"])
8+
subprocess.check_output(["localstack", "wait"])
89

910
yield
1011

0 commit comments

Comments
 (0)