atlas-init 0.9.0__tar.gz → 0.10.1__tar.gz

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 (158) hide show
  1. {atlas_init-0.9.0 → atlas_init-0.10.1}/PKG-INFO +2 -1
  2. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/__init__.py +1 -1
  3. atlas_init-0.10.1/atlas_init/cli_root/aws_clean.py +108 -0
  4. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/hcl/modifier2.py +51 -1
  5. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/paths.py +18 -1
  6. atlas_init-0.10.1/atlas_init/tf_ext/run_tf.py +20 -0
  7. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/settings.py +8 -0
  8. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_mod_gen.py +4 -19
  9. atlas_init-0.10.1/atlas_init/tf_ext/tf_ws.py +272 -0
  10. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/typer_app.py +2 -1
  11. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/typer_app.py +2 -1
  12. {atlas_init-0.9.0 → atlas_init-0.10.1}/pyproject.toml +1 -0
  13. {atlas_init-0.9.0 → atlas_init-0.10.1}/.gitignore +0 -0
  14. {atlas_init-0.9.0 → atlas_init-0.10.1}/LICENSE +0 -0
  15. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/__main__.py +0 -0
  16. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/atlas_init.yaml +0 -0
  17. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli.py +0 -0
  18. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_args.py +0 -0
  19. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_cfn/__init__.py +0 -0
  20. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_cfn/app.py +0 -0
  21. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_cfn/aws.py +0 -0
  22. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_cfn/cfn_parameter_finder.py +0 -0
  23. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_cfn/contract.py +0 -0
  24. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_cfn/example.py +0 -0
  25. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_cfn/files.py +0 -0
  26. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_helper/__init__.py +0 -0
  27. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_helper/go.py +0 -0
  28. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_helper/run.py +0 -0
  29. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_helper/run_manager.py +0 -0
  30. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_helper/sdk.py +0 -0
  31. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_helper/sdk_auto_changes.py +0 -0
  32. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_helper/tf_runner.py +0 -0
  33. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_root/__init__.py +0 -0
  34. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_root/go_test.py +0 -0
  35. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_root/mms_released.py +0 -0
  36. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_root/trigger.py +0 -0
  37. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/__init__.py +0 -0
  38. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/app.py +0 -0
  39. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/changelog.py +0 -0
  40. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/ci_tests.py +0 -0
  41. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/codegen/__init__.py +0 -0
  42. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/codegen/models.py +0 -0
  43. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/codegen/openapi_minimal.py +0 -0
  44. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/debug_logs.py +0 -0
  45. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/debug_logs_test_data.py +0 -0
  46. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/debug_logs_test_data_package_config.py +0 -0
  47. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/example_update.py +0 -0
  48. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/github_logs.py +0 -0
  49. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/go_test_run.py +0 -0
  50. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/go_test_summary.py +0 -0
  51. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/go_test_tf_error.py +0 -0
  52. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/hcl/__init__.py +0 -0
  53. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/hcl/cli.py +0 -0
  54. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/hcl/cluster_mig.py +0 -0
  55. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/hcl/modifier.py +0 -0
  56. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/hcl/parser.py +0 -0
  57. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/log_clean.py +0 -0
  58. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/mock_tf_log.py +0 -0
  59. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/openapi.py +0 -0
  60. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema.py +0 -0
  61. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_go_parser.py +0 -0
  62. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_inspection.py +0 -0
  63. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_table.py +0 -0
  64. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_table_models.py +0 -0
  65. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_v2.py +0 -0
  66. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_v2_sdk.py +0 -0
  67. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_v3.py +0 -0
  68. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_v3_sdk.py +0 -0
  69. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_v3_sdk_base.py +0 -0
  70. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cli_tf/schema_v3_sdk_create.py +0 -0
  71. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cloud/__init__.py +0 -0
  72. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/cloud/aws.py +0 -0
  73. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/crud/__init__.py +0 -0
  74. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/crud/mongo_client.py +0 -0
  75. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/crud/mongo_dao.py +0 -0
  76. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/crud/mongo_utils.py +0 -0
  77. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/html_out/__init__.py +0 -0
  78. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/html_out/md_export.py +0 -0
  79. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/humps.py +0 -0
  80. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/repos/__init__.py +0 -0
  81. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/repos/cfn.py +0 -0
  82. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/repos/go_sdk.py +0 -0
  83. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/repos/path.py +0 -0
  84. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/sdk_ext/__init__.py +0 -0
  85. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/sdk_ext/go.py +0 -0
  86. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/sdk_ext/typer_app.py +0 -0
  87. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/__init__.py +0 -0
  88. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/config.py +0 -0
  89. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/env_vars.py +0 -0
  90. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/env_vars_generated.py +0 -0
  91. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/env_vars_modules.py +0 -0
  92. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/interactive.py +0 -0
  93. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/interactive2.py +0 -0
  94. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/path.py +0 -0
  95. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/settings/rich_utils.py +0 -0
  96. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/terraform.yaml +0 -0
  97. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/.terraform.lock.hcl +0 -0
  98. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/always.tf +0 -0
  99. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/main.tf +0 -0
  100. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/aws_kms/aws_kms.tf +0 -0
  101. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/aws_kms/provider.tf +0 -0
  102. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/aws_s3/aws_s3.tf +0 -0
  103. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/aws_s3/provider.tf +0 -0
  104. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/aws_vars/aws_vars.tf +0 -0
  105. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/aws_vpc/aws_vpc.tf +0 -0
  106. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/aws_vpc/provider.tf +0 -0
  107. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cfn/assume_role_services.yaml +0 -0
  108. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cfn/cfn.tf +0 -0
  109. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cfn/kms.tf +0 -0
  110. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cfn/provider.tf +0 -0
  111. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cfn/resource_actions.yaml +0 -0
  112. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cfn/variables.tf +0 -0
  113. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cloud_provider/cloud_provider.tf +0 -0
  114. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cloud_provider/provider.tf +0 -0
  115. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cluster/cluster.tf +0 -0
  116. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/cluster/provider.tf +0 -0
  117. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/encryption_at_rest/main.tf +0 -0
  118. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/encryption_at_rest/provider.tf +0 -0
  119. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/federated_vars/federated_vars.tf +0 -0
  120. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/federated_vars/provider.tf +0 -0
  121. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/project_extra/project_extra.tf +0 -0
  122. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/project_extra/provider.tf +0 -0
  123. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/stream_instance/provider.tf +0 -0
  124. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/stream_instance/stream_instance.tf +0 -0
  125. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/vpc_peering/provider.tf +0 -0
  126. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/vpc_peering/vpc_peering.tf +0 -0
  127. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/vpc_privatelink/atlas-privatelink.tf +0 -0
  128. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/vpc_privatelink/variables.tf +0 -0
  129. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/modules/vpc_privatelink/versions.tf +0 -0
  130. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/outputs.tf +0 -0
  131. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/providers.tf +0 -0
  132. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf/variables.tf +0 -0
  133. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/__init__.py +0 -0
  134. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/__main__.py +0 -0
  135. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/api_call.py +0 -0
  136. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/args.py +0 -0
  137. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/constants.py +0 -0
  138. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/gen_examples.py +0 -0
  139. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/gen_readme.py +0 -0
  140. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/gen_resource_main.py +0 -0
  141. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/gen_resource_output.py +0 -0
  142. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/gen_resource_variables.py +0 -0
  143. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/gen_versions.py +0 -0
  144. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/models.py +0 -0
  145. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/models_module.py +0 -0
  146. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/newres.py +0 -0
  147. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/plan_diffs.py +0 -0
  148. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/provider_schema.py +0 -0
  149. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/py_gen.py +0 -0
  150. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/schema_to_dataclass.py +0 -0
  151. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_dep.py +0 -0
  152. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_desc_gen.py +0 -0
  153. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_desc_update.py +0 -0
  154. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_example_readme.py +0 -0
  155. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_mod_gen_provider.py +0 -0
  156. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_modules.py +0 -0
  157. {atlas_init-0.9.0 → atlas_init-0.10.1}/atlas_init/tf_ext/tf_vars.py +0 -0
  158. {atlas_init-0.9.0 → atlas_init-0.10.1}/readme.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atlas-init
3
- Version: 0.9.0
3
+ Version: 0.10.1
4
4
  Project-URL: Documentation, https://github.com/EspenAlbert/atlas-init#readme
5
5
  Project-URL: Issues, https://github.com/EspenAlbert/atlas-init/issues
6
6
  Project-URL: Source, https://github.com/EspenAlbert/atlas-init
@@ -20,6 +20,7 @@ Requires-Dist: inflection==0.5.1
20
20
  Requires-Dist: model-lib
21
21
  Requires-Dist: motor==3.7.1
22
22
  Requires-Dist: mypy-boto3-cloudformation==1.37.22
23
+ Requires-Dist: mypy-boto3-iam>=1.40.0
23
24
  Requires-Dist: orjson==3.10.13
24
25
  Requires-Dist: pydantic-settings==2.7.1
25
26
  Requires-Dist: pydot==4.0.1
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- VERSION = "0.9.0"
3
+ VERSION = "0.10.1"
4
4
 
5
5
 
6
6
  def running_in_repo() -> bool:
@@ -0,0 +1,108 @@
1
+ import logging
2
+ from datetime import datetime, timedelta
3
+ from pathlib import Path
4
+
5
+ import humanize
6
+ import typer
7
+ from ask_shell import run_and_wait
8
+ from boto3.session import Session
9
+ from model_lib import Entity
10
+ from mypy_boto3_iam import IAMClient
11
+ from mypy_boto3_iam.type_defs import RoleTypeDef
12
+ from zero_3rdparty.datetime_utils import utc_now
13
+
14
+ from atlas_init.cloud.aws import PascalAlias
15
+ from atlas_init.settings.env_vars import init_settings
16
+ from atlas_init.typer_app import app_command
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class LastUsed(Entity):
22
+ model_config = PascalAlias
23
+ last_used_date: datetime
24
+
25
+
26
+ class IAMRole(Entity):
27
+ model_config = PascalAlias
28
+ arn: str
29
+ role_name: str
30
+ create_date: datetime
31
+ role_last_used: LastUsed | None = None
32
+
33
+
34
+ @app_command()
35
+ def aws_clean(
36
+ skip_iam_roles: bool = typer.Option(False, help="skip iam roles"),
37
+ iam_role_prefix_name: str = typer.Option(
38
+ "mongodb-atlas-test-acc-tf-",
39
+ help="prefix name of iam roles to clean",
40
+ ),
41
+ ):
42
+ init_settings()
43
+ if skip_iam_roles:
44
+ return
45
+ client: IAMClient = Session().client("iam") # pyright: ignore[reportAssignmentType]
46
+ all_roles: list[RoleTypeDef] = []
47
+ aws_account_id = run_and_wait("aws sts get-caller-identity --query Account --output text").stdout_one_line
48
+
49
+ roles_response = client.list_roles()
50
+
51
+ all_roles.extend(roles_response["Roles"])
52
+ marker = roles_response.get("Marker", "")
53
+ while marker:
54
+ roles_response = client.list_roles(Marker=marker)
55
+ all_roles.extend(roles_response["Roles"])
56
+ marker = roles_response.get("Marker", "")
57
+ total_roles = len(all_roles)
58
+ logger.info(f"found {total_roles} roles")
59
+ roles_parsed: list[IAMRole] = []
60
+ delete_if_created_before = utc_now() - timedelta(days=5)
61
+ delete_count = 0
62
+ role_names: list[str] = []
63
+ for role in all_roles:
64
+ parsed = IAMRole.model_validate(role)
65
+ roles_parsed.append(parsed)
66
+ role_name = parsed.role_name
67
+ role_names.append(role_name)
68
+ if not role_name.startswith(iam_role_prefix_name):
69
+ continue
70
+ # want to delete 'mongodb-atlas-test-acc-tf-1345851232260229574'
71
+ # want to keep 'mongodb-atlas-test-acc-tf-7973337217371171538-git-ear'?
72
+ if role_name[-1].isdigit() and parsed.create_date < delete_if_created_before:
73
+ logger.info(f"role: {parsed.arn} will be deleted")
74
+ delete_count += 1
75
+ delete_role(client, role_name)
76
+ else:
77
+ logger.info(f"skipping role: {parsed.arn}, created: {humanize.naturaltime(parsed.create_date)}")
78
+ logger.info(f"deleted {delete_count}/{total_roles} roles")
79
+ out_path = Path(f"aws_roles_{aws_account_id}.txt")
80
+ out_path.write_text("\n".join(sorted(role_names)))
81
+
82
+
83
+ def delete_role(client: IAMClient, role_name: str):
84
+ try:
85
+ attached_policies = client.list_attached_role_policies(RoleName=role_name)
86
+ for policy in attached_policies["AttachedPolicies"]:
87
+ policy_arn = policy.get("PolicyArn")
88
+ if policy_arn:
89
+ logger.info(f"detaching managed policy: {policy_arn} from role: {role_name}")
90
+ client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
91
+ except Exception as e:
92
+ logger.warning(f"failed to detach managed policies from role {role_name}: {e}")
93
+
94
+ # Second, delete all inline policies
95
+ try:
96
+ inline_policies = client.list_role_policies(RoleName=role_name)
97
+ for policy_name in inline_policies["PolicyNames"]:
98
+ logger.info(f"deleting inline policy: {policy_name} from role: {role_name}")
99
+ client.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
100
+ except Exception as e:
101
+ logger.warning(f"failed to delete inline policies from role {role_name}: {e}")
102
+
103
+ # Finally, delete the role
104
+ try:
105
+ client.delete_role(RoleName=role_name)
106
+ logger.info(f"deleted role: {role_name}")
107
+ except Exception as e:
108
+ logger.error(f"failed to delete role {role_name}: {e}")
@@ -2,10 +2,12 @@ from collections import defaultdict
2
2
  import logging
3
3
  from contextlib import suppress
4
4
  from pathlib import Path
5
- from typing import NamedTuple
5
+ from typing import Any, NamedTuple
6
6
  from lark import Token, Transformer, Tree, UnexpectedToken, v_args
7
7
  from hcl2.transformer import Attribute, DictTransformer
8
8
  from hcl2.api import reverse_transform, writes, parses
9
+ from model_lib import Entity
10
+ from pydantic import field_validator
9
11
  import rich
10
12
 
11
13
  logger = logging.getLogger(__name__)
@@ -58,6 +60,54 @@ def attribute_transfomer(attr_name: str, obj_key: str, new_value: str) -> tuple[
58
60
  return AttributeTransformer(with_meta=True), changes
59
61
 
60
62
 
63
+ _unset = object()
64
+
65
+
66
+ class TFVar(Entity):
67
+ name: str
68
+ description: str | None = ""
69
+ default: Any = _unset
70
+ type: str = ""
71
+ sensitive: bool = False
72
+
73
+ @field_validator("default", mode="before")
74
+ def unpack_token(cls, v: Any) -> Any:
75
+ if isinstance(v, Token):
76
+ return v.value.strip('"')
77
+ return v
78
+
79
+
80
+ def variable_reader_typed(tree: Tree) -> dict[str, TFVar]:
81
+ variables: dict[str, TFVar] = {}
82
+
83
+ class TFVarReader(DictTransformer):
84
+ def __init__(self, with_meta: bool = False, *, name: str):
85
+ super().__init__(with_meta)
86
+ self.kwargs: dict[str, Any] = {
87
+ "name": name,
88
+ }
89
+
90
+ def attribute(self, args: list) -> Attribute:
91
+ if len(args) == 3:
92
+ name, _, value = args
93
+ self.kwargs[name] = value
94
+ return super().attribute(args)
95
+
96
+ class BlockReader(Transformer):
97
+ @v_args(tree=True)
98
+ def block(self, block_tree: Tree) -> Tree:
99
+ current_block_name = _identifier_name(block_tree)
100
+ if current_block_name == "variable":
101
+ variable_name = token_name(block_tree.children[1])
102
+ reader = TFVarReader(name=variable_name)
103
+ reader.transform(block_tree)
104
+ variables[variable_name] = TFVar(**reader.kwargs)
105
+ return block_tree
106
+
107
+ BlockReader().transform(tree)
108
+ return variables
109
+
110
+
61
111
  def variable_reader(tree: Tree) -> dict[str, str | None]:
62
112
  """
63
113
  Reads the variable names from a parsed HCL2 tree.
@@ -9,7 +9,14 @@ from model_lib import Entity
9
9
  from pydantic import Field, RootModel
10
10
  from zero_3rdparty.file_utils import iter_paths
11
11
 
12
- from atlas_init.cli_tf.hcl.modifier2 import resource_types_vars_usage, safe_parse, variable_reader, variable_usages
12
+ from atlas_init.cli_tf.hcl.modifier2 import (
13
+ TFVar,
14
+ resource_types_vars_usage,
15
+ safe_parse,
16
+ variable_reader,
17
+ variable_reader_typed,
18
+ variable_usages,
19
+ )
13
20
  from atlas_init.tf_ext.constants import ATLAS_PROVIDER_NAME, DEFAULT_EXTERNAL_SUBSTRINGS, DEFAULT_INTERNAL_SUBSTRINGS
14
21
 
15
22
  logger = logging.getLogger(__name__)
@@ -32,6 +39,16 @@ def get_example_directories(repo_path: Path, skip_names: list[str]):
32
39
  return example_dirs
33
40
 
34
41
 
42
+ def find_variables_typed(variables_tf: Path) -> dict[str, TFVar]:
43
+ if not variables_tf.exists():
44
+ return {}
45
+ tree = safe_parse(variables_tf)
46
+ if not tree:
47
+ logger.warning(f"Failed to parse {variables_tf}")
48
+ return {}
49
+ return variable_reader_typed(tree)
50
+
51
+
35
52
  def find_variables(variables_tf: Path) -> dict[str, str | None]:
36
53
  if not variables_tf.exists():
37
54
  return {}
@@ -0,0 +1,20 @@
1
+ from ask_shell import new_task, run_and_wait
2
+ from pathlib import Path
3
+
4
+
5
+ def validate_tf_workspace(
6
+ tf_workdir: Path, *, tf_cli_config_file: Path | None = None, env_extra: dict[str, str] | None = None
7
+ ):
8
+ terraform_commands = [
9
+ "terraform init",
10
+ "terraform fmt .",
11
+ "terraform validate .",
12
+ ]
13
+ env_extra = env_extra or {}
14
+ if tf_cli_config_file:
15
+ env_extra["TF_CLI_CONFIG_FILE"] = str(tf_cli_config_file)
16
+ with new_task("Terraform Module Validate Checks", total=len(terraform_commands)) as task:
17
+ for command in terraform_commands:
18
+ attempts = 3 if command == "terraform init" else 1 # terraform init can fail due to network issues
19
+ run_and_wait(command, cwd=tf_workdir, env=env_extra, attempts=attempts)
20
+ task.update(advance=1)
@@ -175,6 +175,14 @@ class TfExtSettings(StaticSettings):
175
175
  def provider_cache_dir(self, provider_name: str) -> Path:
176
176
  return self.cache_root / "provider_cache" / provider_name
177
177
 
178
+ @property
179
+ def variable_plan_resolvers_file_path(self) -> Path:
180
+ return self.static_root / "variable_plan_resolvers.yaml"
181
+
182
+ @property
183
+ def variable_plan_resolvers_dumped_file_path(self) -> Path:
184
+ return self.static_root / "variable_plan_resolvers_dumped.yaml"
185
+
178
186
 
179
187
  def init_tf_ext_settings(*, allow_empty_out_path: bool = False) -> TfExtSettings:
180
188
  settings = TfExtSettings.from_env()
@@ -32,6 +32,7 @@ from atlas_init.tf_ext.plan_diffs import (
32
32
  read_variables_path,
33
33
  )
34
34
  from atlas_init.tf_ext.provider_schema import AtlasSchemaInfo, ResourceSchema, parse_atlas_schema
35
+ from atlas_init.tf_ext.run_tf import validate_tf_workspace
35
36
  from atlas_init.tf_ext.schema_to_dataclass import convert_and_format
36
37
  from atlas_init.tf_ext.settings import TfExtSettings
37
38
 
@@ -131,29 +132,13 @@ def generate_resource_module(config: ModuleGenConfig, resource_type: str, atlas_
131
132
  def finalize_and_validate_module(config: ModuleGenConfig) -> Path:
132
133
  dump_versions_tf(config.module_out_path, skip_python=config.skip_python)
133
134
  logger.info(f"Module dumped to {config.module_out_path}, running checks")
134
- validate_module(config.module_out_path, tf_cli_config_file=config.settings.tf_cli_config_file)
135
+ validate_tf_workspace(config.module_out_path, tf_cli_config_file=config.settings.tf_cli_config_file)
135
136
  return config.module_out_path
136
137
 
137
138
 
138
139
  OUT_BINARY_PATH = "tfplan.binary"
139
140
 
140
141
 
141
- def validate_module(tf_workdir: Path, *, tf_cli_config_file: Path | None = None):
142
- terraform_commands = [
143
- "terraform init",
144
- "terraform fmt .",
145
- "terraform validate .",
146
- ]
147
- env_extra = {}
148
- if tf_cli_config_file:
149
- env_extra["TF_CLI_CONFIG_FILE"] = str(tf_cli_config_file)
150
- with new_task("Terraform Module Validate Checks", total=len(terraform_commands)) as task:
151
- for command in terraform_commands:
152
- attempts = 3 if command == "terraform init" else 1 # terraform init can fail due to network issues
153
- run_and_wait(command, cwd=tf_workdir, env=env_extra, attempts=attempts)
154
- task.update(advance=1)
155
-
156
-
157
142
  def module_examples_and_readme(config: ModuleGenConfig, *, example_var_file: Path | None = None) -> Path:
158
143
  path = config.module_out_path
159
144
  if (examples_test := config.examples_test_path) and examples_test.exists():
@@ -165,7 +150,7 @@ def module_examples_and_readme(config: ModuleGenConfig, *, example_var_file: Pat
165
150
  if examples_generated:
166
151
  with run_pool("Validating examples", total=len(examples_generated), exit_wait_timeout=60) as pool:
167
152
  for example_path in examples_generated:
168
- pool.submit(validate_module, example_path)
153
+ pool.submit(validate_tf_workspace, example_path)
169
154
 
170
155
  attribute_descriptions = parse_attribute_descriptions(config.settings)
171
156
  settings = config.settings
@@ -225,7 +210,7 @@ def example_plan_checks(config: ModuleGenConfig, timeout_all_seconds: int = 60)
225
210
  with TemporaryDirectory() as temp_dir:
226
211
  stored_plan = Path(temp_dir) / "plan.json"
227
212
  tf_dir = config.example_path(check.example_name)
228
- validate_module(tf_dir)
213
+ validate_tf_workspace(tf_dir)
229
214
  var_arg = f" -var-file={variables_path}" if variables_path else ""
230
215
  run_and_wait(f"terraform plan -out={OUT_BINARY_PATH}{var_arg}", cwd=tf_dir)
231
216
  run_and_wait(f"terraform show -json {OUT_BINARY_PATH} > {stored_plan}", cwd=tf_dir)
@@ -0,0 +1,272 @@
1
+ from datetime import datetime
2
+ from enum import StrEnum
3
+ import fnmatch
4
+ import logging
5
+ from collections import defaultdict
6
+ from pathlib import Path
7
+ from typing import Any, Literal, Self
8
+
9
+ import typer
10
+ import humanize
11
+ from ask_shell import run_and_wait, run_pool
12
+ from model_lib import Entity, dump, parse_model
13
+ from pydantic import ConfigDict, Field
14
+ from zero_3rdparty.file_utils import ensure_parents_write_text, iter_paths_and_relative
15
+ import stringcase
16
+
17
+ from atlas_init.cli_tf.hcl.modifier2 import TFVar
18
+ from atlas_init.settings.env_vars import init_settings
19
+ from atlas_init.settings.env_vars_generated import AtlasSettingsWithProject, AWSSettings
20
+ from atlas_init.tf_ext.paths import find_variables_typed
21
+ from atlas_init.tf_ext.settings import TfExtSettings, init_tf_ext_settings
22
+ from atlas_init.tf_ext.tf_mod_gen import validate_tf_workspace
23
+
24
+ logger = logging.getLogger(__name__)
25
+ LOCKFILE_NAME = ".terraform.tfstate.lock.info"
26
+ PascalAlias = ConfigDict(alias_generator=stringcase.pascalcase, populate_by_name=True)
27
+
28
+
29
+ class Lockfile(Entity):
30
+ model_config = PascalAlias
31
+ created: datetime
32
+ path: str
33
+ operation: str
34
+
35
+ def __str__(self) -> str:
36
+ return (
37
+ f"lockfile for state {self.path} created={humanize.naturaltime(self.created)}, operation={self.operation})"
38
+ )
39
+
40
+
41
+ class ResolvedEnvVar(Entity):
42
+ var_matches: list[str] = Field(default_factory=list)
43
+ name: str
44
+ value: str
45
+ sensitive: bool = False
46
+ type: Literal["env"] = "env"
47
+
48
+ def can_resolve(self, variable: TFVar) -> bool:
49
+ return any(fnmatch.fnmatch(variable.name, var) for var in self.var_matches)
50
+
51
+
52
+ class ResolvedStringVar(Entity):
53
+ var_matches: list[str] = Field(default_factory=list)
54
+ value: str = ""
55
+ sensitive: bool = False
56
+ type: Literal["string"] = "string"
57
+
58
+ def can_resolve(self, variable: TFVar) -> bool:
59
+ if variable.type and variable.type != self.type:
60
+ return False
61
+ return any(fnmatch.fnmatch(variable.name, var) for var in self.var_matches)
62
+
63
+
64
+ ResolverVar = ResolvedStringVar | ResolvedEnvVar
65
+
66
+
67
+ def as_tfvars_env(resolver_vars: dict[str, ResolverVar]) -> tuple[dict[str, Any], dict[str, Any]]:
68
+ env_vars = {}
69
+ tf_vars = {}
70
+ for var_name, var in resolver_vars.items():
71
+ match var:
72
+ case ResolvedEnvVar(name=name, value=value):
73
+ env_vars[name] = value
74
+ case ResolvedStringVar(value=value):
75
+ tf_vars[var_name] = value
76
+ return tf_vars, env_vars
77
+
78
+
79
+ class _MissingResolverVarsError(Exception):
80
+ def __init__(self, missing_vars: list[str], path: Path, rel_path: str):
81
+ self.missing_vars = missing_vars
82
+ self.path = path
83
+ self.rel_path = rel_path
84
+ super().__init__(f"Missing variables: {missing_vars} for path: {path} with rel_path: {rel_path}")
85
+
86
+ def __str__(self) -> str:
87
+ return f"Missing variables: {self.missing_vars} for path: {self.path} with rel_path: {self.rel_path}"
88
+
89
+
90
+ class VariablesPlanResolver(Entity):
91
+ paths: dict[str, list[ResolverVar]]
92
+
93
+ def merge(self, other: Self) -> Self:
94
+ merged = defaultdict(list)
95
+ for path, vars in self.paths.items():
96
+ merged[path].extend(vars)
97
+ for path, vars in other.paths.items():
98
+ merged[path].extend(vars)
99
+ return type(self)(paths=merged)
100
+
101
+ def variable_path_matches(self, path: Path, rel_path: str) -> list[ResolverVar]:
102
+ resolved = []
103
+ for path_pattern, vars in self.paths.items():
104
+ if fnmatch.fnmatch(rel_path, path_pattern):
105
+ resolved.extend(vars)
106
+ return resolved
107
+
108
+ def resolve_vars(self, path: Path, rel_path: str) -> dict[str, ResolverVar]:
109
+ variables = find_variables_typed(path / "variables.tf")
110
+ resolved_vars: dict[str, ResolverVar] = {}
111
+ for var in variables.values():
112
+ for resolver_var in self.variable_path_matches(path, rel_path):
113
+ if resolver_var.can_resolve(var):
114
+ resolved_vars[var.name] = resolver_var
115
+ if missing_vars := set(variables.keys()) - set(resolved_vars.keys()):
116
+ raise _MissingResolverVarsError(sorted(missing_vars), path, rel_path)
117
+ return resolved_vars
118
+
119
+
120
+ def update_dumped_vars(path: Path) -> VariablesPlanResolver:
121
+ assert init_settings(AWSSettings, AtlasSettingsWithProject), "Settings must be initialized"
122
+ project_settings = AtlasSettingsWithProject.from_env()
123
+ dumped_vars = VariablesPlanResolver(
124
+ paths={
125
+ "*": [
126
+ ResolvedStringVar(
127
+ var_matches=["project*"],
128
+ value=project_settings.MONGODB_ATLAS_PROJECT_ID,
129
+ sensitive=False,
130
+ ),
131
+ ResolvedStringVar(
132
+ var_matches=["org*"],
133
+ value=project_settings.MONGODB_ATLAS_ORG_ID,
134
+ sensitive=False,
135
+ ),
136
+ ResolvedEnvVar(
137
+ var_matches=["atlas_private_key"],
138
+ sensitive=True,
139
+ value=project_settings.MONGODB_ATLAS_PRIVATE_KEY,
140
+ name="MONGODB_ATLAS_PRIVATE_KEY",
141
+ ),
142
+ ResolvedEnvVar(
143
+ var_matches=["atlas_public_key"],
144
+ sensitive=True,
145
+ value=project_settings.MONGODB_ATLAS_PUBLIC_KEY,
146
+ name="MONGODB_ATLAS_PUBLIC_KEY",
147
+ ),
148
+ ResolvedEnvVar(
149
+ var_matches=["atlas_base_url"],
150
+ sensitive=False,
151
+ value=project_settings.MONGODB_ATLAS_BASE_URL,
152
+ name="MONGODB_ATLAS_BASE_URL",
153
+ ),
154
+ ]
155
+ }
156
+ )
157
+ yaml = dump(dumped_vars, "yaml")
158
+ ensure_parents_write_text(path, yaml)
159
+ return dumped_vars
160
+
161
+
162
+ _ignored_workspace_dirs = [
163
+ ".terraform",
164
+ ]
165
+
166
+
167
+ class TFWorkspaceRunConfig(Entity):
168
+ path: Path
169
+ rel_path: str
170
+ resolved_vars: dict[str, Any]
171
+ resolved_env_vars: dict[str, Any]
172
+
173
+ def tf_data_dir(self, settings: TfExtSettings) -> Path:
174
+ repo_out = settings.repo_out
175
+ assert self.path.is_relative_to(repo_out.base), f"path {self.path} is not relative to {repo_out.base}"
176
+ relative_repo_path = str(self.path.relative_to(repo_out.base))
177
+ return settings.static_root / "tf-ws-check" / relative_repo_path / ".terraform"
178
+
179
+ def tf_vars_path_json(self, settings: TfExtSettings) -> Path:
180
+ return self.tf_data_dir(settings) / "vars.auto.tfvars.json"
181
+
182
+
183
+ class TFWsCommands(StrEnum):
184
+ VALIDATE = "validate"
185
+ PLAN = "plan"
186
+ APPLY = "apply"
187
+ DESTROY = "destroy"
188
+
189
+
190
+ def tf_ws(
191
+ command: TFWsCommands = typer.Argument("plan", help="The command to run in the workspace"),
192
+ root_path: Path = typer.Option(
193
+ ...,
194
+ "-p",
195
+ "--root-path",
196
+ help="Path to the root directory, will recurse and look for **/main.tf",
197
+ default_factory=Path.cwd,
198
+ ),
199
+ ):
200
+ settings = init_tf_ext_settings()
201
+ variable_resolvers = update_dumped_vars(settings.variable_plan_resolvers_dumped_file_path)
202
+ manual_path = settings.variable_plan_resolvers_file_path
203
+ if manual_path.exists():
204
+ manual_resolvers = parse_model(manual_path, t=VariablesPlanResolver)
205
+ variable_resolvers = variable_resolvers.merge(manual_resolvers)
206
+
207
+ def include_path(rel_path: str) -> bool:
208
+ return all(
209
+ f"/{ignored_dir}/" not in rel_path and not rel_path.startswith(f"{ignored_dir}/")
210
+ for ignored_dir in _ignored_workspace_dirs
211
+ )
212
+
213
+ paths = sorted(
214
+ (path.parent, rel_path)
215
+ for path, rel_path in iter_paths_and_relative(root_path, "main.tf", only_files=True)
216
+ if include_path(rel_path)
217
+ )
218
+ run_configs = []
219
+ missing_vars_errors = []
220
+ for path, rel_path in paths:
221
+ try:
222
+ resolver_vars = variable_resolvers.resolve_vars(path, rel_path)
223
+ resolved_vars, resolved_env_vars = as_tfvars_env(resolver_vars)
224
+ run_configs.append(
225
+ TFWorkspaceRunConfig(
226
+ path=path, rel_path=rel_path, resolved_vars=resolved_vars, resolved_env_vars=resolved_env_vars
227
+ )
228
+ )
229
+ except _MissingResolverVarsError as e:
230
+ missing_vars_errors.append(e)
231
+ continue
232
+ if missing_vars_errors:
233
+ missing_vars_formatted = "\n".join(str(e) for e in missing_vars_errors)
234
+ logger.warning(f"Missing variables:\n{missing_vars_formatted}")
235
+
236
+ run_count = len(run_configs)
237
+ assert run_count > 0, f"No run configs found from {root_path}"
238
+
239
+ def run_cmd(run_config: TFWorkspaceRunConfig):
240
+ tf_vars_str = dump(run_config.resolved_vars, "pretty_json")
241
+ tf_vars_path = run_config.tf_vars_path_json(settings)
242
+ ensure_parents_write_text(tf_vars_path, tf_vars_str)
243
+ env_extra = run_config.resolved_env_vars | {"TF_DATA_DIR": str(run_config.tf_data_dir(settings))}
244
+
245
+ lockfile_path = run_config.path / LOCKFILE_NAME
246
+ if lockfile_path.exists():
247
+ lockfile = parse_model(lockfile_path, t=Lockfile, format="json")
248
+ logger.warning(f"Lockfile exists for {run_config.path}, skipping: {lockfile}")
249
+ return
250
+
251
+ validate_tf_workspace(run_config.path, tf_cli_config_file=settings.tf_cli_config_file, env_extra=env_extra)
252
+ if command == TFWsCommands.VALIDATE:
253
+ return
254
+ command_extra = ""
255
+ if command in {TFWsCommands.APPLY, TFWsCommands.DESTROY}:
256
+ command_extra = " -auto-approve"
257
+
258
+ run_and_wait(
259
+ f"terraform {command} -var-file={tf_vars_path}{command_extra}",
260
+ cwd=run_config.path,
261
+ env=env_extra,
262
+ user_input=run_count == 1,
263
+ )
264
+
265
+ with run_pool(f"{command} in TF Workspaces", total=run_count, max_concurrent_submits=9) as pool:
266
+ futures = {pool.submit(run_cmd, run_config): run_config for run_config in run_configs}
267
+ for future, run_config in futures.items():
268
+ try:
269
+ future.result()
270
+ except Exception as e:
271
+ logger.error(f"Error running {command} for {run_config.path}: {e}")
272
+ continue
@@ -1,7 +1,7 @@
1
1
  from ask_shell import configure_logging
2
2
  from typer import Typer
3
3
 
4
- from atlas_init.tf_ext import api_call, settings, tf_desc_gen, tf_example_readme, tf_mod_gen_provider
4
+ from atlas_init.tf_ext import api_call, settings, tf_desc_gen, tf_example_readme, tf_mod_gen_provider, tf_ws
5
5
 
6
6
 
7
7
  def typer_main():
@@ -21,6 +21,7 @@ def typer_main():
21
21
  app.command(name="mod-gen-provider")(tf_mod_gen_provider.tf_mod_gen_provider_resource_modules)
22
22
  app.command(name="check-env-vars")(settings.init_tf_ext_settings)
23
23
  app.command(name="example-readme")(tf_example_readme.tf_example_readme)
24
+ app.command(name="ws")(tf_ws.tf_ws)
24
25
  configure_logging(app)
25
26
  app()
26
27
 
@@ -58,11 +58,12 @@ app_command = partial(
58
58
 
59
59
 
60
60
  def extra_root_commands():
61
- from atlas_init.cli_root import go_test, trigger, mms_released
61
+ from atlas_init.cli_root import go_test, trigger, mms_released, aws_clean
62
62
 
63
63
  assert trigger
64
64
  assert go_test
65
65
  assert mms_released
66
+ assert aws_clean
66
67
 
67
68
 
68
69
  @app.callback(invoke_without_command=True)
@@ -38,6 +38,7 @@ dependencies = [
38
38
  "ask-shell>=0.0.5",
39
39
  "model-lib",
40
40
  "zero-3rdparty==1.0.0b5",
41
+ "mypy-boto3-iam>=1.40.0",
41
42
  ]
42
43
 
43
44
  # [tool.uv.sources]
File without changes
File without changes
File without changes