From b2223f6f9c33f434bedf01761957073fb25f9883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20N=C3=A9meth?= <43646571+lakkeger@users.noreply.github.com> Date: Wed, 4 Oct 2023 14:10:04 +0000 Subject: [PATCH 1/3] Add support to multi-account environments --- .gitignore | 2 ++ README.md | 3 +++ bin/tflocal | 23 ++++++++++++++++++++++- setup.cfg | 2 +- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8da177b..dbf0bff 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ dist/ *.pyc __pycache__/ + +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 6e98829..c6b8fbc 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ The following environment variables can be configured: * `USE_EXEC`: whether to use `os.exec` instead of `subprocess.Popen` (try using this in case of I/O issues) * `_ENDPOINT`: setting a custom service endpoint, e.g., `COGNITO_IDP_ENDPOINT=http://example.com` * `AWS_DEFAULT_REGION`: the AWS region to use (default: `us-east-1`, or determined from local credentials if `boto3` is installed) +* `CUSTOMISE_ACCESS_KEY`: enables to override the static AWS Access Key ID +* `AWS_ACCESS_KEY_ID`: the AWS Access Key ID to use for multi account setups, (default: `test`, or determined from local credentials if `CUSTOMISE_ACCESS_KEY=1`, `boto3` is installed and credentials are set, if either of the `AWS_PROFILE` or `AWS_DEFAULT_PROFILE` variables set, it uses the respective profile, default is `default`) ## Usage @@ -39,6 +41,7 @@ please refer to the man pages of `terraform --help`. ## Change Log +* v0.14: Add support to multi-account environments * v0.13: Fix S3 automatic `use_s3_path_style` detection when setting S3_HOSTNAME or LOCALSTACK_HOSTNAME * v0.12: Fix local endpoint overrides for Terraform AWS provider 5.9.0; fix parsing of alias and region defined as value lists * v0.11: Minor fix to handle boolean values in S3 backend configs diff --git a/bin/tflocal b/bin/tflocal index 696d060..b13c3f8 100755 --- a/bin/tflocal +++ b/bin/tflocal @@ -24,6 +24,8 @@ from localstack_client import config # noqa: E402 import hcl2 # noqa: E402 DEFAULT_REGION = "us-east-1" +DEFAULT_ACCESS_KEY = "test" +CUSTOMISE_ACCESS_KEY = os.environ.get("CUSTOMISE_ACCESS_KEY") == '1' LOCALHOST_HOSTNAME = "localhost.localstack.cloud" S3_HOSTNAME = os.environ.get("S3_HOSTNAME") or f"s3.{LOCALHOST_HOSTNAME}" USE_EXEC = str(os.environ.get("USE_EXEC")).strip().lower() in ["1", "true"] @@ -33,7 +35,7 @@ LOCALSTACK_HOSTNAME = os.environ.get("LOCALSTACK_HOSTNAME") or "localhost" EDGE_PORT = int(os.environ.get("EDGE_PORT") or 4566) TF_PROVIDER_CONFIG = """ provider "aws" { - access_key = "test" + access_key = "" secret_key = "test" skip_credentials_validation = true skip_metadata_api_check = true @@ -118,6 +120,10 @@ def create_provider_config_file(provider_aliases=None): for provider in provider_aliases: endpoints = "\n".join([f'{s} = "{get_service_endpoint(s)}"' for s in services]) provider_config = TF_PROVIDER_CONFIG.replace("", endpoints) + provider_config = provider_config.replace( + "", + get_access_key() if CUSTOMISE_ACCESS_KEY else DEFAULT_ACCESS_KEY + ) additional_configs = [] if use_s3_path_style(): additional_configs += [" s3_use_path_style = true"] @@ -242,6 +248,21 @@ def get_region() -> str: # fall back to default region return region or DEFAULT_REGION +def get_access_key() -> str: + profile = str(os.environ.get("AWS_PROFILE") or os.environ.get("AWS_DEFAULT_PROFILE") or "default").strip() + access_key = str(os.environ.get("AWS_ACCESS_KEY_ID") or "").strip() + if access_key: + return access_key + try: + # If boto3 is installed, try to get the access_key from local credentials. + # Note that boto3 is currently not included in the dependencies, to + # keep the library lightweight. + import boto3 + access_key = boto3.session.Session(profile_name=profile).get_credentials().access_key + except Exception: + pass + # fall back to default region + return access_key or DEFAULT_ACCESS_KEY def get_service_endpoint(service: str) -> str: """Get the service endpoint URL for the given service name""" diff --git a/setup.cfg b/setup.cfg index 3c63bef..f4916e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = terraform-local -version = 0.13 +version = 0.14 url = https://github.com/localstack/terraform-local author = LocalStack Team author_email = info@localstack.cloud From 3af55e85d1ceb1067b849fe63e5853a47fb737ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20N=C3=A9meth?= <43646571+lakkeger@users.noreply.github.com> Date: Fri, 6 Oct 2023 07:49:49 +0000 Subject: [PATCH 2/3] Add support for provider access_key option --- bin/tflocal | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/bin/tflocal b/bin/tflocal index b13c3f8..26e2caa 100755 --- a/bin/tflocal +++ b/bin/tflocal @@ -122,7 +122,7 @@ def create_provider_config_file(provider_aliases=None): provider_config = TF_PROVIDER_CONFIG.replace("", endpoints) provider_config = provider_config.replace( "", - get_access_key() if CUSTOMISE_ACCESS_KEY else DEFAULT_ACCESS_KEY + get_access_key(provider) if CUSTOMISE_ACCESS_KEY else DEFAULT_ACCESS_KEY ) additional_configs = [] if use_s3_path_style(): @@ -248,21 +248,28 @@ def get_region() -> str: # fall back to default region return region or DEFAULT_REGION -def get_access_key() -> str: - profile = str(os.environ.get("AWS_PROFILE") or os.environ.get("AWS_DEFAULT_PROFILE") or "default").strip() - access_key = str(os.environ.get("AWS_ACCESS_KEY_ID") or "").strip() - if access_key: - return access_key + +def get_access_key(provider=None) -> str: + access_key = str(os.environ.get("AWS_ACCESS_KEY_ID") or provider.get("access_key", "")).strip() + if access_key and access_key != DEFAULT_ACCESS_KEY: + # Change live access key to mocked one + return deactivate_access_key(access_key) try: # If boto3 is installed, try to get the access_key from local credentials. # Note that boto3 is currently not included in the dependencies, to # keep the library lightweight. import boto3 + profile = str(os.environ.get("AWS_PROFILE") or os.environ.get("AWS_DEFAULT_PROFILE", "default")).strip() access_key = boto3.session.Session(profile_name=profile).get_credentials().access_key except Exception: pass # fall back to default region - return access_key or DEFAULT_ACCESS_KEY + return deactivate_access_key(access_key or DEFAULT_ACCESS_KEY) + + +def deactivate_access_key(access_key) -> str: + return "L" + access_key[1:] if access_key[0] == "A" else access_key + def get_service_endpoint(service: str) -> str: """Get the service endpoint URL for the given service name""" From c1006ecb661488a478679382f24cf7fbb912f2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20N=C3=A9meth?= <43646571+lakkeger@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:01:52 +0000 Subject: [PATCH 3/3] Add parsing for additional options --- bin/tflocal | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/bin/tflocal b/bin/tflocal index 26e2caa..addb2a0 100755 --- a/bin/tflocal +++ b/bin/tflocal @@ -136,6 +136,7 @@ def create_provider_config_file(provider_aliases=None): if isinstance(region, list): region = region[0] additional_configs += [f' region = "{region}"'] + additional_configs += [generate_additional_configs(filter_configs(provider))] provider_config = provider_config.replace("", "\n".join(additional_configs)) provider_configs.append(provider_config) @@ -214,6 +215,42 @@ def generate_s3_backend_config() -> str: return result +def filter_configs(configs: dict) -> dict: + return dict(filter( + lambda item: item[0] not in ("access_key", "secret_key", "alias", "region"), + configs.items() + )) + + +def generate_additional_configs(configs, indent=1) -> str: + result = "" + for argument, value in configs.items(): + if isinstance(value, str) and not check_tf_string(value): + value = f'"{value}"' + + if isinstance(value, list): + try: + if isinstance(value[0], dict): + # Unwrapping dicts as hlc2 for config blocks creating a list + # with a single dict element + value = value[0] + else: + value = repr(value).replace("'", '"') + except IndexError: + value = "[]" + + if isinstance(value, dict): + result = "\n".join([ + result, + f"{indent * ' '}{argument} {{", + f"{generate_additional_configs(value,indent + 1)}", + f"{indent * ' '}}}", + ]) + else: + result = "\n".join([result, f"{indent * ' '}{argument} = {value}"]) + return result + + # --- # AWS CLIENT UTILS # --- @@ -361,6 +398,15 @@ def run_tf_subprocess(cmd, env): PROCESS.communicate() sys.exit(PROCESS.returncode) +def check_tf_string (expr: str) -> bool: + tf_prefixes = ("var.", "data.", "local.", "module.", "output.") + if expr in ("true","false"): + return True + if expr.startswith(tf_prefixes): + return True + if "(" in expr: + return True + return False # --- # UTIL FUNCTIONS