@@ -13,8 +13,12 @@ import os
1313import sys
1414import glob
1515import subprocess
16+ import json
17+ import textwrap
1618
19+ from packaging import version
1720from urllib .parse import urlparse
21+ from typing import Optional
1822
1923PARENT_FOLDER = os .path .realpath (os .path .join (os .path .dirname (__file__ ), ".." ))
2024if os .path .isdir (os .path .join (PARENT_FOLDER , ".venv" )):
@@ -34,14 +38,15 @@ TF_CMD = os.environ.get("TF_CMD") or "terraform"
3438LS_PROVIDERS_FILE = os .environ .get ("LS_PROVIDERS_FILE" ) or "localstack_providers_override.tf"
3539LOCALSTACK_HOSTNAME = urlparse (AWS_ENDPOINT_URL ).hostname or os .environ .get ("LOCALSTACK_HOSTNAME" ) or "localhost"
3640EDGE_PORT = int (urlparse (AWS_ENDPOINT_URL ).port or os .environ .get ("EDGE_PORT" ) or 4566 )
41+ TF_VERSION : Optional [version .Version ] = None
3742TF_PROVIDER_CONFIG = """
3843provider "aws" {
3944 access_key = "<access_key>"
4045 secret_key = "test"
4146 skip_credentials_validation = true
4247 skip_metadata_api_check = true
4348 <configs>
44- endpoints {
49+ endpoints {
4550<endpoints>
4651 }
4752}
@@ -56,10 +61,7 @@ terraform {
5661
5762 access_key = "test"
5863 secret_key = "test"
59- endpoint = "<s3_endpoint>"
60- iam_endpoint = "<iam_endpoint>"
61- sts_endpoint = "<sts_endpoint>"
62- dynamodb_endpoint = "<dynamodb_endpoint>"
64+ <endpoints>
6365 skip_credentials_validation = true
6466 skip_metadata_api_check = true
6567 }
@@ -126,7 +128,7 @@ def create_provider_config_file(provider_aliases=None):
126128 "<access_key>" ,
127129 get_access_key (provider ) if CUSTOMIZE_ACCESS_KEY else DEFAULT_ACCESS_KEY
128130 )
129- endpoints = "\n " .join ([f'{ s } = "{ get_service_endpoint (s )} "' for s in services ])
131+ endpoints = "\n " .join ([f' { s } = "{ get_service_endpoint (s )} "' for s in services ])
130132 provider_config = provider_config .replace ("<endpoints>" , endpoints )
131133 additional_configs = []
132134 if use_s3_path_style ():
@@ -139,7 +141,7 @@ def create_provider_config_file(provider_aliases=None):
139141 region = provider .get ("region" ) or get_region ()
140142 if isinstance (region , list ):
141143 region = region [0 ]
142- additional_configs += [f' region = "{ region } "' ]
144+ additional_configs += [f'region = "{ region } "' ]
143145 provider_config = provider_config .replace ("<configs>" , "\n " .join (additional_configs ))
144146 provider_configs .append (provider_config )
145147
@@ -203,17 +205,38 @@ def generate_s3_backend_config() -> str:
203205 "key" : "terraform.tfstate" ,
204206 "dynamodb_table" : "tf-test-state" ,
205207 "region" : get_region (),
206- "s3_endpoint" : get_service_endpoint ("s3" ),
207- "iam_endpoint" : get_service_endpoint ("iam" ),
208- "sts_endpoint" : get_service_endpoint ("sts" ),
209- "dynamodb_endpoint" : get_service_endpoint ("dynamodb" ),
208+ "endpoints" : {
209+ "s3" : get_service_endpoint ("s3" ),
210+ "iam" : get_service_endpoint ("iam" ),
211+ "sso" : get_service_endpoint ("sso" ),
212+ "sts" : get_service_endpoint ("sts" ),
213+ "dynamodb" : get_service_endpoint ("dynamodb" ),
214+ },
210215 }
211216 configs .update (backend_config )
212217 get_or_create_bucket (configs ["bucket" ])
213218 get_or_create_ddb_table (configs ["dynamodb_table" ], region = configs ["region" ])
214219 result = TF_S3_BACKEND_CONFIG
215220 for key , value in configs .items ():
216- value = str (value ).lower () if isinstance (value , bool ) else str (value )
221+ if isinstance (value , bool ):
222+ value = str (value ).lower ()
223+ elif isinstance (value , dict ):
224+ is_tf_legacy = not (TF_VERSION .major > 1 or (TF_VERSION .major == 1 and TF_VERSION .minor > 5 ))
225+ if key == "endpoints" and is_tf_legacy :
226+ value = textwrap .indent (
227+ text = textwrap .dedent (f"""\
228+ endpoint = "{ value ["s3" ]} "
229+ iam_endpoint = "{ value ["iam" ]} "
230+ sts_endpoint = "{ value ["sts" ]} "
231+ dynamodb_endpoint = "{ value ["dynamodb" ]} "
232+ """ ),
233+ prefix = " " * 4 )
234+ else :
235+ value = textwrap .indent (
236+ text = f"{ key } = {{\n " + "\n " .join ([f' { k } = "{ v } "' for k , v in value .items ()]) + "\n }" ,
237+ prefix = " " * 4 )
238+ else :
239+ value = str (value )
217240 result = result .replace (f"<{ key } >" , value )
218241 return result
219242
@@ -347,6 +370,12 @@ def parse_tf_files() -> dict:
347370 return result
348371
349372
373+ def get_tf_version (env ):
374+ global TF_VERSION
375+ output = subprocess .run ([f"{ TF_CMD } " , "version" , "-json" ], env = env , check = True , capture_output = True ).stdout .decode ("utf-8" )
376+ TF_VERSION = version .parse (json .loads (output )["terraform_version" ])
377+
378+
350379def run_tf_exec (cmd , env ):
351380 """Run terraform using os.exec - can be useful as it does not require any I/O
352381 handling for stdin/out/err. Does *not* allow us to perform any cleanup logic."""
@@ -395,6 +424,14 @@ def main():
395424 env = dict (os .environ )
396425 cmd = [TF_CMD ] + sys .argv [1 :]
397426
427+ try :
428+ get_tf_version (env )
429+ if not TF_VERSION :
430+ raise ValueError
431+ except (FileNotFoundError , ValueError ) as e :
432+ print (f"Unable to determine version. See error message for details: { e } " )
433+ exit (1 )
434+
398435 # create TF provider config file
399436 providers = determine_provider_aliases ()
400437 config_file = create_provider_config_file (providers )
0 commit comments