qontract-reconcile 0.10.2.dev345__py3-none-any.whl → 0.10.2.dev408__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/METADATA +11 -10
  2. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/RECORD +126 -120
  3. reconcile/aus/base.py +17 -14
  4. reconcile/automated_actions/config/integration.py +12 -0
  5. reconcile/aws_account_manager/integration.py +2 -2
  6. reconcile/aws_ami_cleanup/integration.py +6 -7
  7. reconcile/aws_ami_share.py +69 -62
  8. reconcile/aws_cloudwatch_log_retention/integration.py +155 -126
  9. reconcile/aws_ecr_image_pull_secrets.py +2 -2
  10. reconcile/aws_iam_keys.py +1 -0
  11. reconcile/aws_saml_idp/integration.py +7 -1
  12. reconcile/aws_saml_roles/integration.py +9 -3
  13. reconcile/change_owners/change_owners.py +1 -1
  14. reconcile/change_owners/diff.py +2 -4
  15. reconcile/checkpoint.py +11 -3
  16. reconcile/cli.py +33 -8
  17. reconcile/dashdotdb_dora.py +4 -11
  18. reconcile/database_access_manager.py +118 -111
  19. reconcile/endpoints_discovery/integration.py +4 -1
  20. reconcile/endpoints_discovery/merge_request_manager.py +9 -11
  21. reconcile/external_resources/factories.py +5 -12
  22. reconcile/external_resources/integration.py +1 -1
  23. reconcile/external_resources/manager.py +5 -3
  24. reconcile/external_resources/meta.py +0 -1
  25. reconcile/external_resources/model.py +10 -10
  26. reconcile/external_resources/reconciler.py +5 -2
  27. reconcile/external_resources/secrets_sync.py +4 -6
  28. reconcile/external_resources/state.py +5 -4
  29. reconcile/gabi_authorized_users.py +8 -5
  30. reconcile/gitlab_housekeeping.py +13 -15
  31. reconcile/gitlab_mr_sqs_consumer.py +2 -2
  32. reconcile/gitlab_owners.py +15 -11
  33. reconcile/gql_definitions/automated_actions/instance.py +41 -2
  34. reconcile/gql_definitions/aws_ami_cleanup/aws_accounts.py +10 -0
  35. reconcile/gql_definitions/aws_cloudwatch_log_retention/aws_accounts.py +22 -61
  36. reconcile/gql_definitions/aws_saml_idp/aws_accounts.py +10 -0
  37. reconcile/gql_definitions/aws_saml_roles/aws_accounts.py +10 -0
  38. reconcile/gql_definitions/common/aws_vpc_requests.py +10 -0
  39. reconcile/gql_definitions/common/clusters.py +2 -0
  40. reconcile/gql_definitions/external_resources/external_resources_namespaces.py +84 -1
  41. reconcile/gql_definitions/external_resources/external_resources_settings.py +2 -0
  42. reconcile/gql_definitions/fragments/aws_account_common.py +2 -0
  43. reconcile/gql_definitions/fragments/aws_organization.py +33 -0
  44. reconcile/gql_definitions/fragments/aws_vpc_request.py +2 -0
  45. reconcile/gql_definitions/introspection.json +3474 -1986
  46. reconcile/gql_definitions/jira_permissions_validator/jira_boards_for_permissions_validator.py +4 -0
  47. reconcile/gql_definitions/terraform_init/aws_accounts.py +14 -0
  48. reconcile/gql_definitions/terraform_resources/terraform_resources_namespaces.py +33 -1
  49. reconcile/gql_definitions/terraform_tgw_attachments/aws_accounts.py +10 -0
  50. reconcile/jenkins_worker_fleets.py +1 -0
  51. reconcile/jira_permissions_validator.py +236 -121
  52. reconcile/ocm/types.py +6 -0
  53. reconcile/openshift_base.py +47 -1
  54. reconcile/openshift_cluster_bots.py +2 -1
  55. reconcile/openshift_resources_base.py +6 -2
  56. reconcile/openshift_saas_deploy.py +2 -2
  57. reconcile/openshift_saas_deploy_trigger_cleaner.py +3 -5
  58. reconcile/openshift_upgrade_watcher.py +3 -3
  59. reconcile/queries.py +131 -0
  60. reconcile/saas_auto_promotions_manager/subscriber.py +4 -3
  61. reconcile/slack_usergroups.py +4 -3
  62. reconcile/sql_query.py +1 -0
  63. reconcile/statuspage/integrations/maintenances.py +4 -3
  64. reconcile/statuspage/status.py +5 -8
  65. reconcile/templates/rosa-classic-cluster-creation.sh.j2 +4 -0
  66. reconcile/templates/rosa-hcp-cluster-creation.sh.j2 +3 -0
  67. reconcile/templating/renderer.py +2 -1
  68. reconcile/terraform_aws_route53.py +7 -1
  69. reconcile/terraform_init/integration.py +185 -21
  70. reconcile/terraform_resources.py +11 -1
  71. reconcile/terraform_tgw_attachments.py +7 -1
  72. reconcile/terraform_users.py +7 -0
  73. reconcile/terraform_vpc_peerings.py +14 -3
  74. reconcile/terraform_vpc_resources/integration.py +7 -0
  75. reconcile/typed_queries/aws_account_tags.py +41 -0
  76. reconcile/typed_queries/saas_files.py +2 -2
  77. reconcile/utils/aggregated_list.py +4 -3
  78. reconcile/utils/aws_api.py +51 -20
  79. reconcile/utils/aws_api_typed/api.py +38 -9
  80. reconcile/utils/aws_api_typed/cloudformation.py +149 -0
  81. reconcile/utils/aws_api_typed/logs.py +73 -0
  82. reconcile/utils/datetime_util.py +67 -0
  83. reconcile/utils/differ.py +2 -3
  84. reconcile/utils/early_exit_cache.py +3 -2
  85. reconcile/utils/expiration.py +7 -3
  86. reconcile/utils/external_resource_spec.py +24 -1
  87. reconcile/utils/filtering.py +1 -1
  88. reconcile/utils/helm.py +2 -1
  89. reconcile/utils/helpers.py +1 -1
  90. reconcile/utils/jinja2/utils.py +4 -96
  91. reconcile/utils/jira_client.py +82 -63
  92. reconcile/utils/jjb_client.py +9 -12
  93. reconcile/utils/jobcontroller/controller.py +1 -1
  94. reconcile/utils/jobcontroller/models.py +17 -1
  95. reconcile/utils/json.py +32 -0
  96. reconcile/utils/merge_request_manager/merge_request_manager.py +3 -3
  97. reconcile/utils/merge_request_manager/parser.py +2 -2
  98. reconcile/utils/mr/app_interface_reporter.py +2 -2
  99. reconcile/utils/mr/base.py +2 -2
  100. reconcile/utils/mr/notificator.py +2 -2
  101. reconcile/utils/mr/update_access_report_base.py +3 -4
  102. reconcile/utils/oc.py +113 -95
  103. reconcile/utils/oc_filters.py +3 -3
  104. reconcile/utils/ocm/products.py +6 -0
  105. reconcile/utils/ocm/search_filters.py +3 -6
  106. reconcile/utils/ocm/service_log.py +3 -5
  107. reconcile/utils/openshift_resource.py +10 -5
  108. reconcile/utils/output.py +3 -2
  109. reconcile/utils/pagerduty_api.py +5 -5
  110. reconcile/utils/runtime/integration.py +1 -2
  111. reconcile/utils/runtime/runner.py +2 -2
  112. reconcile/utils/saasherder/models.py +2 -1
  113. reconcile/utils/saasherder/saasherder.py +9 -7
  114. reconcile/utils/slack_api.py +24 -2
  115. reconcile/utils/sloth.py +171 -2
  116. reconcile/utils/sqs_gateway.py +2 -1
  117. reconcile/utils/state.py +2 -1
  118. reconcile/utils/terraform_client.py +4 -3
  119. reconcile/utils/terrascript_aws_client.py +165 -111
  120. reconcile/utils/vault.py +1 -1
  121. reconcile/vault_replication.py +107 -42
  122. tools/app_interface_reporter.py +4 -4
  123. tools/cli_commands/systems_and_tools.py +5 -1
  124. tools/qontract_cli.py +25 -13
  125. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/WHEEL +0 -0
  126. {qontract_reconcile-0.10.2.dev345.dist-info → qontract_reconcile-0.10.2.dev408.dist-info}/entry_points.txt +0 -0
@@ -22,7 +22,6 @@ from typing import (
22
22
  TYPE_CHECKING,
23
23
  Any,
24
24
  Self,
25
- TypeAlias,
26
25
  cast,
27
26
  )
28
27
 
@@ -152,6 +151,7 @@ from reconcile.github_org import get_default_config
152
151
  from reconcile.gql_definitions.terraform_resources.terraform_resources_namespaces import (
153
152
  NamespaceTerraformResourceLifecycleV1,
154
153
  )
154
+ from reconcile.typed_queries.aws_account_tags import get_aws_account_tags
155
155
  from reconcile.utils import gql
156
156
  from reconcile.utils.aws_api import (
157
157
  AmiTag,
@@ -178,7 +178,11 @@ from reconcile.utils.external_resources import (
178
178
  from reconcile.utils.git import is_file_in_git_repo
179
179
  from reconcile.utils.gitlab_api import GitLabApi
180
180
  from reconcile.utils.jenkins_api import JenkinsApi
181
- from reconcile.utils.jinja2.utils import process_extracurlyjinja2_template
181
+ from reconcile.utils.jinja2.utils import (
182
+ process_extracurlyjinja2_template,
183
+ process_jinja2_template,
184
+ )
185
+ from reconcile.utils.json import json_dumps
182
186
  from reconcile.utils.password_validator import (
183
187
  PasswordPolicy,
184
188
  PasswordValidator,
@@ -202,7 +206,7 @@ if TYPE_CHECKING:
202
206
  from reconcile.utils.ocm import OCMMap
203
207
 
204
208
 
205
- TFResource: TypeAlias = type[
209
+ type TFResource = type[
206
210
  Resource | Data | Module | Provider | Variable | Output | Locals | Terraform
207
211
  ]
208
212
 
@@ -267,6 +271,7 @@ VARIABLE_KEYS = [
267
271
  "extra_tags",
268
272
  "lifecycle",
269
273
  "max_session_duration",
274
+ "secret_format",
270
275
  ]
271
276
 
272
277
  EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
@@ -286,12 +291,6 @@ SUPPORTED_ALB_LISTENER_RULE_CONDITION_TYPE_MAPPING = {
286
291
  "source-ip": "source_ip",
287
292
  }
288
293
 
289
- DEFAULT_TAGS = {
290
- "tags": {
291
- "app": "app-sre-infra",
292
- },
293
- }
294
-
295
294
  AWS_ELB_ACCOUNT_IDS = {
296
295
  "us-east-1": "127311923021",
297
296
  "us-east-2": "033677994240",
@@ -475,6 +474,7 @@ class TerrascriptClient:
475
474
  integration_prefix: str,
476
475
  thread_pool_size: int,
477
476
  accounts: Iterable[MutableMapping[str, Any]],
477
+ default_tags: Mapping[str, str] | None,
478
478
  settings: Mapping[str, Any] | None = None,
479
479
  prefetch_resources_by_schemas: Iterable[str] | None = None,
480
480
  secret_reader: SecretReaderBase | None = None,
@@ -488,6 +488,7 @@ class TerrascriptClient:
488
488
  else:
489
489
  self.secret_reader = SecretReader(settings=settings)
490
490
  self.configs: dict[str, dict] = {}
491
+ self.default_tags = default_tags or {"app": "app-sre-infra"}
491
492
  self.populate_configs(filtered_accounts)
492
493
  self.versions: dict[str, str] = {
493
494
  a["name"]: a["providerVersion"] for a in filtered_accounts
@@ -508,7 +509,7 @@ class TerrascriptClient:
508
509
  region=region,
509
510
  alias=region,
510
511
  skip_region_validation=True,
511
- default_tags=DEFAULT_TAGS,
512
+ default_tags={"tags": config["tags"]},
512
513
  )
513
514
 
514
515
  # Add default region, which will be in resourcesDefaultRegion
@@ -517,7 +518,7 @@ class TerrascriptClient:
517
518
  secret_key=config["aws_secret_access_key"],
518
519
  region=config["resourcesDefaultRegion"],
519
520
  skip_region_validation=True,
520
- default_tags=DEFAULT_TAGS,
521
+ default_tags={"tags": config["tags"]},
521
522
  )
522
523
 
523
524
  ts += Terraform(
@@ -800,6 +801,9 @@ class TerrascriptClient:
800
801
  config["supportedDeploymentRegions"] = account["supportedDeploymentRegions"]
801
802
  config["resourcesDefaultRegion"] = account["resourcesDefaultRegion"]
802
803
  config["terraformState"] = account["terraformState"]
804
+ config["tags"] = dict(self.default_tags) | get_aws_account_tags(
805
+ account.get("organization", None)
806
+ )
803
807
  self.configs[account_name] = config
804
808
 
805
809
  def _get_partition(self, account: str) -> str:
@@ -1069,25 +1073,15 @@ class TerrascriptClient:
1069
1073
  config = self.configs[account_name]
1070
1074
  existing_provider_aliases = {p.get("alias") for p in ts["provider"]["aws"]}
1071
1075
  if alias not in existing_provider_aliases:
1072
- if assume_role:
1073
- ts += provider.aws(
1074
- access_key=config["aws_access_key_id"],
1075
- secret_key=config["aws_secret_access_key"],
1076
- region=region,
1077
- alias=alias,
1078
- assume_role={"role_arn": assume_role},
1079
- skip_region_validation=True,
1080
- default_tags=DEFAULT_TAGS,
1081
- )
1082
- else:
1083
- ts += provider.aws(
1084
- access_key=config["aws_access_key_id"],
1085
- secret_key=config["aws_secret_access_key"],
1086
- region=region,
1087
- alias=alias,
1088
- skip_region_validation=True,
1089
- default_tags=DEFAULT_TAGS,
1090
- )
1076
+ ts += provider.aws(
1077
+ access_key=config["aws_access_key_id"],
1078
+ secret_key=config["aws_secret_access_key"],
1079
+ region=region,
1080
+ alias=alias,
1081
+ skip_region_validation=True,
1082
+ default_tags={"tags": config["tags"]},
1083
+ **{"assume_role": {"role_arn": assume_role}} if assume_role else {},
1084
+ )
1091
1085
 
1092
1086
  def populate_route53(
1093
1087
  self, desired_state: Iterable[Mapping[str, Any]], default_ttl: int = 300
@@ -1947,7 +1941,7 @@ class TerrascriptClient:
1947
1941
  em_identifier = f"{identifier}-enhanced-monitoring"
1948
1942
  em_values = {
1949
1943
  "name": em_identifier,
1950
- "assume_role_policy": json.dumps(assume_role_policy, sort_keys=True),
1944
+ "assume_role_policy": json_dumps(assume_role_policy),
1951
1945
  }
1952
1946
  role_tf_resource = aws_iam_role(em_identifier, **em_values)
1953
1947
  tf_resources.append(role_tf_resource)
@@ -2216,6 +2210,43 @@ class TerrascriptClient:
2216
2210
  letters_and_digits = string.ascii_letters + string.digits
2217
2211
  return "".join(random.choice(letters_and_digits) for i in range(string_length))
2218
2212
 
2213
+ @staticmethod
2214
+ def _build_tf_resource_s3_lifecycle_rules(
2215
+ versioning: bool,
2216
+ common_values: Mapping[str, Any],
2217
+ ) -> list[dict]:
2218
+ lifecycle_rules = common_values.get("lifecycle_rules") or []
2219
+ if versioning and not any(
2220
+ "noncurrent_version_expiration" in lr for lr in lifecycle_rules
2221
+ ):
2222
+ # Add a default noncurrent object expiration rule
2223
+ # if one isn't already set
2224
+ rule = {
2225
+ "id": "expire_noncurrent_versions",
2226
+ "enabled": True,
2227
+ "noncurrent_version_expiration": {"days": 30},
2228
+ "expiration": {"expired_object_delete_marker": True},
2229
+ "abort_incomplete_multipart_upload_days": 3,
2230
+ }
2231
+ lifecycle_rules.append(rule)
2232
+
2233
+ if storage_class := common_values.get("storage_class"):
2234
+ sc = storage_class.upper()
2235
+ days = "1"
2236
+ if sc.endswith("_IA"):
2237
+ # Infrequent Access storage class has minimum 30 days
2238
+ # before transition
2239
+ days = "30"
2240
+ rule = {
2241
+ "id": sc + "_storage_class",
2242
+ "enabled": True,
2243
+ "transition": {"days": days, "storage_class": sc},
2244
+ "noncurrent_version_transition": {"days": days, "storage_class": sc},
2245
+ }
2246
+ lifecycle_rules.append(rule)
2247
+
2248
+ return lifecycle_rules
2249
+
2219
2250
  def populate_tf_resource_s3(self, spec: ExternalResourceSpec) -> aws_s3_bucket:
2220
2251
  account = spec.provisioner_name
2221
2252
  identifier = spec.identifier
@@ -2255,47 +2286,11 @@ class TerrascriptClient:
2255
2286
  request_payer = common_values.get("request_payer")
2256
2287
  if request_payer:
2257
2288
  values["request_payer"] = request_payer
2258
- lifecycle_rules = common_values.get("lifecycle_rules")
2259
- if lifecycle_rules:
2260
- # common_values['lifecycle_rules'] is a list of lifecycle_rules
2289
+ if lifecycle_rules := self._build_tf_resource_s3_lifecycle_rules(
2290
+ versioning=versioning,
2291
+ common_values=common_values,
2292
+ ):
2261
2293
  values["lifecycle_rule"] = lifecycle_rules
2262
- if versioning:
2263
- lrs = values.get("lifecycle_rule", [])
2264
- expiration_rule = False
2265
- for lr in lrs:
2266
- if "noncurrent_version_expiration" in lr:
2267
- expiration_rule = True
2268
- break
2269
- if not expiration_rule:
2270
- # Add a default noncurrent object expiration rule if
2271
- # if one isn't already set
2272
- rule = {
2273
- "id": "expire_noncurrent_versions",
2274
- "enabled": "true",
2275
- "noncurrent_version_expiration": {"days": 30},
2276
- }
2277
- if len(lrs) > 0:
2278
- lrs.append(rule)
2279
- else:
2280
- lrs = rule
2281
- sc = common_values.get("storage_class")
2282
- if sc:
2283
- sc = sc.upper()
2284
- days = "1"
2285
- if sc.endswith("_IA"):
2286
- # Infrequent Access storage class has minimum 30 days
2287
- # before transition
2288
- days = "30"
2289
- rule = {
2290
- "id": sc + "_storage_class",
2291
- "enabled": "true",
2292
- "transition": {"days": days, "storage_class": sc},
2293
- "noncurrent_version_transition": {"days": days, "storage_class": sc},
2294
- }
2295
- if values.get("lifecycle_rule"):
2296
- values["lifecycle_rule"].append(rule)
2297
- else:
2298
- values["lifecycle_rule"] = rule
2299
2294
  cors_rules = common_values.get("cors_rules")
2300
2295
  if cors_rules:
2301
2296
  # common_values['cors_rules'] is a list of cors_rules
@@ -2345,7 +2340,7 @@ class TerrascriptClient:
2345
2340
  }
2346
2341
  ],
2347
2342
  }
2348
- rc_values["assume_role_policy"] = json.dumps(role, sort_keys=True)
2343
+ rc_values["assume_role_policy"] = json_dumps(role)
2349
2344
  role_resource = aws_iam_role(id, **rc_values)
2350
2345
  tf_resources.append(role_resource)
2351
2346
 
@@ -2383,7 +2378,7 @@ class TerrascriptClient:
2383
2378
  },
2384
2379
  ],
2385
2380
  }
2386
- rc_values["policy"] = json.dumps(policy, sort_keys=True)
2381
+ rc_values["policy"] = json_dumps(policy)
2387
2382
  policy_resource = aws_iam_policy(id, **rc_values)
2388
2383
  tf_resources.append(policy_resource)
2389
2384
 
@@ -2598,7 +2593,7 @@ class TerrascriptClient:
2598
2593
  },
2599
2594
  ],
2600
2595
  }
2601
- values["policy"] = json.dumps(policy, sort_keys=True)
2596
+ values["policy"] = json_dumps(policy)
2602
2597
  values["depends_on"] = self.get_dependencies([user_tf_resource])
2603
2598
 
2604
2599
  tf_aws_iam_policy = aws_iam_policy(identifier, **values)
@@ -2877,7 +2872,7 @@ class TerrascriptClient:
2877
2872
  values: dict[str, Any] = {
2878
2873
  "name": identifier,
2879
2874
  "tags": common_values["tags"],
2880
- "assume_role_policy": json.dumps(assume_role_policy),
2875
+ "assume_role_policy": json_dumps(assume_role_policy),
2881
2876
  }
2882
2877
 
2883
2878
  inline_policy = common_values.get("inline_policy")
@@ -2931,7 +2926,7 @@ class TerrascriptClient:
2931
2926
  self, account: str, name: str, policy: Mapping[str, Any]
2932
2927
  ) -> None:
2933
2928
  tf_aws_iam_policy = aws_iam_policy(
2934
- f"{account}-{name}", name=name, policy=json.dumps(policy)
2929
+ f"{account}-{name}", name=name, policy=json_dumps(policy)
2935
2930
  )
2936
2931
  self.add_resource(account, tf_aws_iam_policy)
2937
2932
 
@@ -2973,7 +2968,7 @@ class TerrascriptClient:
2973
2968
  role_tf_resource = aws_iam_role(
2974
2969
  f"{account}-{name}",
2975
2970
  name=name,
2976
- assume_role_policy=json.dumps(assume_role_policy),
2971
+ assume_role_policy=json_dumps(assume_role_policy),
2977
2972
  managed_policy_arns=managed_policy_arns,
2978
2973
  max_session_duration=max_session_duration_hours * 3600,
2979
2974
  )
@@ -3017,7 +3012,7 @@ class TerrascriptClient:
3017
3012
  all_queues.append(queue_name)
3018
3013
  sqs_policy = values.pop("sqs_policy", None)
3019
3014
  if sqs_policy is not None:
3020
- values["policy"] = json.dumps(sqs_policy, sort_keys=True)
3015
+ values["policy"] = json_dumps(sqs_policy)
3021
3016
  dl_queue = values.pop("dl_queue", None)
3022
3017
  if dl_queue is not None:
3023
3018
  max_receive_count = int(values.pop("max_receive_count", 10))
@@ -3031,9 +3026,7 @@ class TerrascriptClient:
3031
3026
  "deadLetterTargetArn": "${" + dl_data.arn + "}",
3032
3027
  "maxReceiveCount": max_receive_count,
3033
3028
  }
3034
- values["redrive_policy"] = json.dumps(
3035
- redrive_policy, sort_keys=True
3036
- )
3029
+ values["redrive_policy"] = json_dumps(redrive_policy)
3037
3030
  kms_master_key_id = values.pop("kms_master_key_id", None)
3038
3031
  if kms_master_key_id is not None:
3039
3032
  if kms_master_key_id.startswith("arn:"):
@@ -3106,7 +3099,7 @@ class TerrascriptClient:
3106
3099
  "Resource": list(kms_keys),
3107
3100
  }
3108
3101
  policy["Statement"].append(kms_statement)
3109
- values["policy"] = json.dumps(policy, sort_keys=True)
3102
+ values["policy"] = json_dumps(policy)
3110
3103
  policy_tf_resource = aws_iam_policy(policy_identifier, **values)
3111
3104
  tf_resources.append(policy_tf_resource)
3112
3105
 
@@ -3256,7 +3249,7 @@ class TerrascriptClient:
3256
3249
  }
3257
3250
  ],
3258
3251
  }
3259
- values["policy"] = json.dumps(policy, sort_keys=True)
3252
+ values["policy"] = json_dumps(policy)
3260
3253
  values["depends_on"] = self.get_dependencies([user_tf_resource])
3261
3254
 
3262
3255
  tf_aws_iam_policy = aws_iam_policy(identifier, **values)
@@ -3366,7 +3359,7 @@ class TerrascriptClient:
3366
3359
  },
3367
3360
  ],
3368
3361
  }
3369
- values_policy["policy"] = json.dumps(policy, sort_keys=True)
3362
+ values_policy["policy"] = json_dumps(policy)
3370
3363
  values_policy["depends_on"] = self.get_dependencies([user_tf_resource])
3371
3364
 
3372
3365
  tf_aws_iam_policy = aws_iam_policy(identifier, **values_policy)
@@ -3416,7 +3409,7 @@ class TerrascriptClient:
3416
3409
  }
3417
3410
  ],
3418
3411
  }
3419
- values_policy["policy"] = json.dumps(policy, sort_keys=True)
3412
+ values_policy["policy"] = json_dumps(policy)
3420
3413
  values_policy["depends_on"] = self.get_dependencies([bucket_tf_resource])
3421
3414
  region = common_values.get("region") or self.default_regions.get(account)
3422
3415
  assert region # make mypy happy
@@ -3562,7 +3555,7 @@ class TerrascriptClient:
3562
3555
  }
3563
3556
  ],
3564
3557
  }
3565
- sqs_values["policy"] = json.dumps(sqs_policy, sort_keys=True)
3558
+ sqs_values["policy"] = json_dumps(sqs_policy)
3566
3559
 
3567
3560
  kms_encryption = common_values.get("kms_encryption", False)
3568
3561
  if kms_encryption:
@@ -3598,7 +3591,7 @@ class TerrascriptClient:
3598
3591
  },
3599
3592
  ],
3600
3593
  }
3601
- kms_values["policy"] = json.dumps(kms_policy, sort_keys=True)
3594
+ kms_values["policy"] = json_dumps(kms_policy)
3602
3595
  if provider:
3603
3596
  kms_values["provider"] = provider
3604
3597
 
@@ -3696,7 +3689,7 @@ class TerrascriptClient:
3696
3689
  "Resource": [sqs_values["kms_master_key_id"]],
3697
3690
  }
3698
3691
  policy["Statement"].append(kms_statement)
3699
- values_policy["policy"] = json.dumps(policy, sort_keys=True)
3692
+ values_policy["policy"] = json_dumps(policy)
3700
3693
  policy_tf_resource = aws_iam_policy(sqs_identifier, **values_policy)
3701
3694
  tf_resources.append(policy_tf_resource)
3702
3695
 
@@ -3767,7 +3760,7 @@ class TerrascriptClient:
3767
3760
  role_identifier = f"{identifier}-lambda-execution-role"
3768
3761
  role_values = {
3769
3762
  "name": role_identifier,
3770
- "assume_role_policy": json.dumps(assume_role_policy, sort_keys=True),
3763
+ "assume_role_policy": json_dumps(assume_role_policy),
3771
3764
  }
3772
3765
 
3773
3766
  role_tf_resource = aws_iam_role(role_identifier, **role_values)
@@ -3799,7 +3792,7 @@ class TerrascriptClient:
3799
3792
 
3800
3793
  policy_values = {
3801
3794
  "role": "${" + role_tf_resource.id + "}",
3802
- "policy": json.dumps(policy, sort_keys=True),
3795
+ "policy": json_dumps(policy),
3803
3796
  }
3804
3797
  policy_tf_resource = aws_iam_role_policy(policy_identifier, **policy_values)
3805
3798
  tf_resources.append(policy_tf_resource)
@@ -3927,7 +3920,7 @@ class TerrascriptClient:
3927
3920
  }
3928
3921
  values = {
3929
3922
  "name": identifier,
3930
- "policy": json.dumps(policy, sort_keys=True),
3923
+ "policy": json_dumps(policy),
3931
3924
  "depends_on": self.get_dependencies([user_tf_resource]),
3932
3925
  }
3933
3926
 
@@ -4048,7 +4041,7 @@ class TerrascriptClient:
4048
4041
  role_identifier = f"{identifier}-lambda-execution-role"
4049
4042
  role_values = {
4050
4043
  "name": role_identifier,
4051
- "assume_role_policy": json.dumps(assume_role_policy, sort_keys=True),
4044
+ "assume_role_policy": json_dumps(assume_role_policy),
4052
4045
  "tags": tags,
4053
4046
  }
4054
4047
 
@@ -4085,7 +4078,7 @@ class TerrascriptClient:
4085
4078
  policy_tf_resource = aws_iam_policy(
4086
4079
  policy_identifier,
4087
4080
  name=policy_identifier,
4088
- policy=json.dumps(policy, sort_keys=True),
4081
+ policy=json_dumps(policy),
4089
4082
  tags=tags,
4090
4083
  )
4091
4084
  tf_resources.append(policy_tf_resource)
@@ -4300,7 +4293,7 @@ class TerrascriptClient:
4300
4293
  # iam user policy
4301
4294
  values_policy: dict[str, Any] = {
4302
4295
  "name": identifier,
4303
- "policy": json.dumps(policy, sort_keys=True),
4296
+ "policy": json_dumps(policy),
4304
4297
  "depends_on": self.get_dependencies([user_tf_resource]),
4305
4298
  }
4306
4299
 
@@ -4418,10 +4411,7 @@ class TerrascriptClient:
4418
4411
 
4419
4412
  :return: key is AWS account name and value is terraform configuration
4420
4413
  """
4421
- return {
4422
- name: json.dumps(ts, indent=2, sort_keys=True)
4423
- for name, ts in self.tss.items()
4424
- }
4414
+ return {name: json_dumps(ts, indent=2) for name, ts in self.tss.items()}
4425
4415
 
4426
4416
  def init_values(
4427
4417
  self, spec: ExternalResourceSpec, init_tags: bool = True
@@ -4533,7 +4523,7 @@ class TerrascriptClient:
4533
4523
  output_name = output_format.format(
4534
4524
  spec.output_prefix, self.integration_prefix, "annotations"
4535
4525
  )
4536
- anno_json = json.dumps(spec.annotations()).encode("utf-8")
4526
+ anno_json = json_dumps(spec.annotations()).encode("utf-8")
4537
4527
  output_value = base64.b64encode(anno_json).decode()
4538
4528
  tf_resources.append(Output(output_name, value=output_value))
4539
4529
 
@@ -4673,7 +4663,7 @@ class TerrascriptClient:
4673
4663
  }
4674
4664
  log_groups_policy_values = {
4675
4665
  "policy_name": "es-log-publishing-permissions",
4676
- "policy_document": json.dumps(log_groups_policy, sort_keys=True),
4666
+ "policy_document": json_dumps(log_groups_policy),
4677
4667
  }
4678
4668
  resource_policy = aws_cloudwatch_log_resource_policy(
4679
4669
  "es_log_publishing_resource_policy",
@@ -5005,7 +4995,7 @@ class TerrascriptClient:
5005
4995
  }
5006
4996
  ],
5007
4997
  }
5008
- es_values["access_policies"] = json.dumps(access_policies, sort_keys=True)
4998
+ es_values["access_policies"] = json_dumps(access_policies)
5009
4999
 
5010
5000
  region = values.get("region") or self.default_regions.get(account)
5011
5001
  assert region # make mypy happy
@@ -5061,7 +5051,7 @@ class TerrascriptClient:
5061
5051
 
5062
5052
  version_values = {
5063
5053
  "secret_id": "${" + aws_secret_resource.id + "}",
5064
- "secret_string": json.dumps(master_user, sort_keys=True),
5054
+ "secret_string": json_dumps(master_user),
5065
5055
  }
5066
5056
  if provider:
5067
5057
  version_values["provider"] = provider
@@ -5088,7 +5078,7 @@ class TerrascriptClient:
5088
5078
  iam_policy_resource = aws_iam_policy(
5089
5079
  secret_identifier,
5090
5080
  name=f"{identifier}-secretsmanager-policy",
5091
- policy=json.dumps(policy, sort_keys=True),
5081
+ policy=json_dumps(policy),
5092
5082
  tags=tags,
5093
5083
  )
5094
5084
  tf_resources.append(iam_policy_resource)
@@ -5500,7 +5490,7 @@ class TerrascriptClient:
5500
5490
  lb_access_logs_s3_bucket_policy_values = {
5501
5491
  "provider": provider,
5502
5492
  "bucket": f"${{{lb_access_logs_s3_bucket_tf_resource.id}}}",
5503
- "policy": json.dumps(policy, sort_keys=True),
5493
+ "policy": json_dumps(policy),
5504
5494
  }
5505
5495
  lb_access_logs_s3_bucket_policy_tf_resource = aws_s3_bucket_policy(
5506
5496
  policy_identifier, **lb_access_logs_s3_bucket_policy_values
@@ -5813,9 +5803,13 @@ class TerrascriptClient:
5813
5803
  assert secret # make mypy happy
5814
5804
  secret_data = self.secret_reader.read_all(secret)
5815
5805
 
5806
+ secret_format = common_values.get("secret_format")
5807
+ if secret_format is not None:
5808
+ secret_data = self._apply_secret_format(str(secret_format), secret_data)
5809
+
5816
5810
  version_values: dict[str, Any] = {
5817
5811
  "secret_id": "${" + aws_secret_resource.id + "}",
5818
- "secret_string": json.dumps(secret_data, sort_keys=True),
5812
+ "secret_string": json_dumps(secret_data),
5819
5813
  }
5820
5814
 
5821
5815
  if self._multiregion_account(account):
@@ -5836,6 +5830,66 @@ class TerrascriptClient:
5836
5830
 
5837
5831
  self.add_resources(account, tf_resources)
5838
5832
 
5833
+ @staticmethod
5834
+ def _unflatten_dotted_keys_dict(flat_dict: dict[str, str]) -> dict[str, Any]:
5835
+ """Convert a flat dictionary with dotted keys to a nested dictionary.
5836
+
5837
+ Example:
5838
+ {"db.host": "localhost", "db.port": "5432"} ->
5839
+ {"db": {"host": "localhost", "port": "5432"}}
5840
+
5841
+ Raises:
5842
+ ValueError: If there are conflicting keys (e.g., "a.b" and "a.b.c")
5843
+ """
5844
+ result: dict[str, Any] = {}
5845
+ for key, value in flat_dict.items():
5846
+ parts = key.split(".")
5847
+ current = result
5848
+ for i, part in enumerate(parts[:-1]):
5849
+ if part not in current:
5850
+ current[part] = {}
5851
+ elif not isinstance(current[part], dict):
5852
+ # Conflict: trying to traverse through a non-dict value
5853
+ conflicting_path = ".".join(parts[: i + 1])
5854
+ raise ValueError(
5855
+ f"Conflicting keys detected: '{conflicting_path}' is both a "
5856
+ f"value and a nested path in key '{key}'"
5857
+ )
5858
+ current = current[part]
5859
+
5860
+ # Check if we're trying to set a value where a dict already exists
5861
+ if parts[-1] in current and isinstance(current[parts[-1]], dict):
5862
+ raise ValueError(
5863
+ f"Conflicting keys detected: '{key}' conflicts with nested keys"
5864
+ )
5865
+
5866
+ current[parts[-1]] = value
5867
+
5868
+ return result
5869
+
5870
+ @staticmethod
5871
+ def _apply_secret_format(
5872
+ secret_format: str, secret_data: dict[str, str]
5873
+ ) -> dict[str, str]:
5874
+ # Convert flat dict with dotted keys to nested dict for Jinja2
5875
+ nested_secret_data = TerrascriptClient._unflatten_dotted_keys_dict(secret_data)
5876
+ rendered_data = process_jinja2_template(secret_format, nested_secret_data)
5877
+
5878
+ parsed_data = json.loads(rendered_data)
5879
+
5880
+ if not isinstance(parsed_data, dict):
5881
+ raise ValueError("secret_format must be a dictionary")
5882
+
5883
+ # validate secret is a dict[str, str]
5884
+ for k, v in parsed_data.items():
5885
+ if not isinstance(k, str):
5886
+ raise ValueError(f"key '{k}' is not a string")
5887
+
5888
+ if not isinstance(v, str):
5889
+ raise ValueError(f"dictionary value '{v}' under '{k}' is not a string")
5890
+
5891
+ return parsed_data
5892
+
5839
5893
  def get_commit_sha(self, repo_info: Mapping) -> str:
5840
5894
  url = repo_info["url"]
5841
5895
  ref = repo_info["ref"]
@@ -6135,7 +6189,7 @@ class TerrascriptClient:
6135
6189
  lambda_iam_role_resource = aws_iam_role(
6136
6190
  "lambda_role",
6137
6191
  name=f"ocm-{identifier}-cognito-lambda-role",
6138
- assume_role_policy=json.dumps(lambda_role_policy),
6192
+ assume_role_policy=json_dumps(lambda_role_policy),
6139
6193
  managed_policy_arns=[lambda_managed_policy_arn],
6140
6194
  force_detach_policies=False,
6141
6195
  max_session_duration=3600,
@@ -6810,7 +6864,7 @@ class TerrascriptClient:
6810
6864
  )
6811
6865
  tf_resources.append(api_gateway_stage_resource)
6812
6866
 
6813
- rest_api_policy = json.dumps({
6867
+ rest_api_policy = json_dumps({
6814
6868
  "Version": "2012-10-17",
6815
6869
  "Statement": [
6816
6870
  {
@@ -6914,7 +6968,7 @@ class TerrascriptClient:
6914
6968
  },
6915
6969
  ],
6916
6970
  }
6917
- cloudwatch_assume_role_policy = json.dumps(policy, sort_keys=True)
6971
+ cloudwatch_assume_role_policy = json_dumps(policy)
6918
6972
 
6919
6973
  cloudwatch_iam_role_resource = aws_iam_role(
6920
6974
  "cloudwatch_assume_role",
@@ -6942,7 +6996,7 @@ class TerrascriptClient:
6942
6996
  ],
6943
6997
  }
6944
6998
 
6945
- cloudwatch_iam_policy_document = json.dumps(policy, sort_keys=True)
6999
+ cloudwatch_iam_policy_document = json_dumps(policy)
6946
7000
 
6947
7001
  cloudwatch_iam_policy_resource = aws_iam_policy(
6948
7002
  "cloudwatch",
@@ -7187,7 +7241,7 @@ class TerrascriptClient:
7187
7241
 
7188
7242
  version_values = {
7189
7243
  "secret_id": "${" + secret_resource.arn + "}",
7190
- "secret_string": json.dumps(secret, sort_keys=True),
7244
+ "secret_string": json_dumps(secret),
7191
7245
  }
7192
7246
  version_resource = aws_secretsmanager_secret_version(
7193
7247
  secret_identifier, **version_values
@@ -7196,7 +7250,7 @@ class TerrascriptClient:
7196
7250
 
7197
7251
  secret_policy_values = {
7198
7252
  "secret_arn": "${" + secret_resource.arn + "}",
7199
- "policy": json.dumps({
7253
+ "policy": json_dumps({
7200
7254
  "Version": "2012-10-17",
7201
7255
  "Statement": [
7202
7256
  {
reconcile/utils/vault.py CHANGED
@@ -207,7 +207,7 @@ class VaultClient:
207
207
  a v2 KV engine)
208
208
  """
209
209
  secret_path = secret["path"]
210
- secret_version = secret.get("version")
210
+ secret_version = secret.get("version", SECRET_VERSION_LATEST)
211
211
 
212
212
  kv_version = self._get_mount_version_by_secret_path(secret_path)
213
213