@@ -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 )
6059TF_VERSION : Optional [version .Version ] = None
6160TF_PROVIDER_CONFIG = """
6261provider "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+
386384def 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+
442515def 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