atlas-init 0.6.0__py3-none-any.whl → 0.8.0__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 (65) hide show
  1. atlas_init/__init__.py +1 -1
  2. atlas_init/atlas_init.yaml +1 -0
  3. atlas_init/cli_args.py +19 -1
  4. atlas_init/cli_tf/ci_tests.py +116 -24
  5. atlas_init/cli_tf/example_update.py +20 -8
  6. atlas_init/cli_tf/go_test_run.py +14 -2
  7. atlas_init/cli_tf/go_test_summary.py +334 -82
  8. atlas_init/cli_tf/go_test_tf_error.py +20 -12
  9. atlas_init/cli_tf/hcl/modifier.py +22 -8
  10. atlas_init/cli_tf/hcl/modifier2.py +120 -0
  11. atlas_init/cli_tf/openapi.py +10 -6
  12. atlas_init/html_out/__init__.py +0 -0
  13. atlas_init/html_out/md_export.py +143 -0
  14. atlas_init/sdk_ext/__init__.py +0 -0
  15. atlas_init/sdk_ext/go.py +102 -0
  16. atlas_init/sdk_ext/typer_app.py +18 -0
  17. atlas_init/settings/env_vars.py +25 -3
  18. atlas_init/settings/env_vars_generated.py +2 -0
  19. atlas_init/tf/.terraform.lock.hcl +33 -33
  20. atlas_init/tf/modules/aws_s3/provider.tf +1 -1
  21. atlas_init/tf/modules/aws_vpc/provider.tf +1 -1
  22. atlas_init/tf/modules/cloud_provider/provider.tf +1 -1
  23. atlas_init/tf/modules/cluster/provider.tf +1 -1
  24. atlas_init/tf/modules/encryption_at_rest/provider.tf +1 -1
  25. atlas_init/tf/modules/federated_vars/federated_vars.tf +1 -2
  26. atlas_init/tf/modules/federated_vars/provider.tf +1 -1
  27. atlas_init/tf/modules/project_extra/provider.tf +1 -1
  28. atlas_init/tf/modules/stream_instance/provider.tf +1 -1
  29. atlas_init/tf/modules/vpc_peering/provider.tf +1 -1
  30. atlas_init/tf/modules/vpc_privatelink/versions.tf +1 -1
  31. atlas_init/tf/providers.tf +1 -1
  32. atlas_init/tf_ext/__init__.py +0 -0
  33. atlas_init/tf_ext/__main__.py +3 -0
  34. atlas_init/tf_ext/api_call.py +325 -0
  35. atlas_init/tf_ext/args.py +32 -0
  36. atlas_init/tf_ext/constants.py +3 -0
  37. atlas_init/tf_ext/gen_examples.py +141 -0
  38. atlas_init/tf_ext/gen_module_readme.py +131 -0
  39. atlas_init/tf_ext/gen_resource_main.py +195 -0
  40. atlas_init/tf_ext/gen_resource_output.py +71 -0
  41. atlas_init/tf_ext/gen_resource_variables.py +159 -0
  42. atlas_init/tf_ext/gen_versions.py +10 -0
  43. atlas_init/tf_ext/models.py +106 -0
  44. atlas_init/tf_ext/models_module.py +454 -0
  45. atlas_init/tf_ext/newres.py +90 -0
  46. atlas_init/tf_ext/paths.py +126 -0
  47. atlas_init/tf_ext/plan_diffs.py +140 -0
  48. atlas_init/tf_ext/provider_schema.py +199 -0
  49. atlas_init/tf_ext/py_gen.py +294 -0
  50. atlas_init/tf_ext/schema_to_dataclass.py +522 -0
  51. atlas_init/tf_ext/settings.py +188 -0
  52. atlas_init/tf_ext/tf_dep.py +324 -0
  53. atlas_init/tf_ext/tf_desc_gen.py +53 -0
  54. atlas_init/tf_ext/tf_desc_update.py +0 -0
  55. atlas_init/tf_ext/tf_mod_gen.py +263 -0
  56. atlas_init/tf_ext/tf_mod_gen_provider.py +124 -0
  57. atlas_init/tf_ext/tf_modules.py +395 -0
  58. atlas_init/tf_ext/tf_vars.py +158 -0
  59. atlas_init/tf_ext/typer_app.py +28 -0
  60. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/METADATA +5 -3
  61. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/RECORD +64 -31
  62. atlas_init-0.8.0.dist-info/entry_points.txt +5 -0
  63. atlas_init-0.6.0.dist-info/entry_points.txt +0 -2
  64. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/WHEEL +0 -0
  65. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,25 +2,25 @@
2
2
  # Manual edits may be lost in future updates.
3
3
 
4
4
  provider "registry.terraform.io/hashicorp/aws" {
5
- version = "5.98.0"
5
+ version = "5.100.0"
6
6
  constraints = "~> 5.0"
7
7
  hashes = [
8
- "h1:neMFK/kP1KT6cTGID+Tkkt8L7PsN9XqwrPDGXVw3WVY=",
9
- "zh:23377bd90204b6203b904f48f53edcae3294eb072d8fc18a4531c0cde531a3a1",
10
- "zh:2e55a6ea14cc43b08cf82d43063e96c5c2f58ee953c2628523d0ee918fe3b609",
11
- "zh:4885a817c16fdaaeddc5031edc9594c1f300db0e5b23be7cd76a473e7dcc7b4f",
12
- "zh:6ca7177ad4e5c9d93dee4be1ac0792b37107df04657fddfe0c976f36abdd18b5",
13
- "zh:78bf8eb0a67bae5dede09666676c7a38c9fb8d1b80a90ba06cf36ae268257d6f",
14
- "zh:874b5a99457a3f88e2915df8773120846b63d820868a8f43082193f3dc84adcb",
15
- "zh:95e1e4cf587cde4537ac9dfee9e94270652c812ab31fce3a431778c053abf354",
8
+ "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=",
9
+ "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644",
10
+ "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2",
11
+ "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274",
12
+ "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b",
13
+ "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862",
14
+ "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342",
16
15
  "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
17
- "zh:a75145b58b241d64570803e6565c72467cd664633df32678755b51871f553e50",
18
- "zh:aa31b13d0b0e8432940d6892a48b6268721fa54a02ed62ee42745186ee32f58d",
19
- "zh:ae4565770f76672ce8e96528cbb66afdade1f91383123c079c7fdeafcb3d2877",
20
- "zh:b99f042c45bf6aa69dd73f3f6d9cbe0b495b30442c526e0b3810089c059ba724",
21
- "zh:bbb38e86d926ef101cefafe8fe090c57f2b1356eac9fc5ec81af310c50375897",
22
- "zh:d03c89988ba4a0bd3cfc8659f951183ae7027aa8018a7ca1e53a300944af59cb",
23
- "zh:d179ef28843fe663fc63169291a211898199009f0d3f63f0a6f65349e77727ec",
16
+ "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93",
17
+ "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2",
18
+ "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e",
19
+ "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421",
20
+ "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4",
21
+ "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9",
22
+ "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9",
23
+ "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70",
24
24
  ]
25
25
  }
26
26
 
@@ -103,23 +103,23 @@ provider "registry.terraform.io/hashicorp/random" {
103
103
  }
104
104
 
105
105
  provider "registry.terraform.io/mongodb/mongodbatlas" {
106
- version = "1.33.0"
107
- constraints = "1.33.0"
106
+ version = "1.37.0"
107
+ constraints = ">= 1.33.0"
108
108
  hashes = [
109
- "h1:z8zdSte741iw5ij4wkY8XBAXftY7gEsFjYRMjyk1EX0=",
110
- "zh:04e26c9e1dfd11c114c5dbd4e4375bb3a329fbaa2f39f92d01f6bba9d76923ba",
111
- "zh:1d175f30a1a2505578c63d7ef76c7c1c846d086ab0e6a1bcebf32d3bfe10a2f6",
112
- "zh:1fd08f988587efa41121dbe0cae7298fa8aa739c0038aa2443f8deb53e7367ad",
113
- "zh:4c6c337f7a53882e8b5431ba13276bfd1f423becfd4dbbcb0f68443532354455",
114
- "zh:4f7028a474d00012280a6069d74584e6eeda2f85be2300040012210f91daa97a",
115
- "zh:6ad4d292b60350dae24eb1a6721a7b35f34129ad42a00125ffb7ab6b4565eb15",
116
- "zh:6c8f5a14edd77433f559e8440c2b6ee7333c13e5256aa76a17eb585046bef0c1",
117
- "zh:8e18a10107beeb9d677e96cc0b25b6f48be9dfbedc62da7bdde7b03b9bfc34b7",
118
- "zh:9130c4a1c06ba4587235c479eae182f86b4baadd82a90bd3c69ad54cf2a8c37d",
119
- "zh:a1464ff45db030c6b94f9e80e5890061baf4cb3a8595d473d422e2778fc6a5fb",
120
- "zh:a20f97e43befeb8a0737cfc2f1a3ea3e483d5928e0baf7dc841bad67d21ac90b",
121
- "zh:d3e440568d5f3d2197555ba614d08950cb7fbdf605f3ec2f4b6fc9edff563668",
122
- "zh:de11d29abae0ceefd11b6d21575c3335b586d9b27fd5a8ebc8c40a6ce67c01a9",
123
- "zh:fea06f8ccd2aec629ca7f24b4cbbd8cba832044d7ee0ee395b3c6e8eab4ed903",
109
+ "h1:Bn3O8yBFQ25GeP7bqvBEdyLFp/ZZGtxAZI9c1KU/Wek=",
110
+ "zh:0bb3d85fa4680d804b25341838ae4a5740f59c95b677e818aaeb74957093477c",
111
+ "zh:14ec370dcdf9c8a92311179089d972a2476c8d17d15ea3cd06c1e772782dcdf6",
112
+ "zh:16f42470f48c6bf727968cf0ed0fe64dfbdaf49dee626707367073c92006c354",
113
+ "zh:363bd381dbfed2f3c9a8da2aece2242d8bf2e0a94586422624352c000bcd59d7",
114
+ "zh:6762334cebaf7a72afdf72dde4edaf3263339e271cbb3946bbaae27f67551b6b",
115
+ "zh:730006225c2bb49f717610b3f8e1a7413d137b2721de541a254af7856869a203",
116
+ "zh:9aa5e9bd25788918e6ab7764f37f63d8afe6717c259e17475eed4660b8bed409",
117
+ "zh:ae0b2ad8660bed54eb1e91da863548bb368701181874d44a989b387a90bc7022",
118
+ "zh:b2fccfc349dc720239cd348b615790ce44ca7e3ff9f3f89fb30d4a1c7fe58793",
119
+ "zh:b879a1e1e2cf6585d596da131b150dcf66dd1e937576a161b3afac8bd3ab8e8a",
120
+ "zh:d23223ac3808f05023d531748e1c718ee0c2277fce8baa0268576220f6cb83a2",
121
+ "zh:dc6e6776807113723329d730b3cbc4c9d29d0c1c985414d290a63698d8ec06f8",
122
+ "zh:dff2961bf4b8f8a9d187bfd7d03839a2c2e7c1def13e83bddf82f0cbd794425f",
123
+ "zh:f5f3fc3b0b80f76c3b535ef33c76064347e96e086b7574f73e1f3931e1ab0b4d",
124
124
  ]
125
125
  }
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  }
8
8
 
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  aws = {
8
8
  source = "hashicorp/aws"
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  aws = {
8
8
  source = "hashicorp/aws"
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  }
8
8
 
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  }
8
8
 
@@ -37,7 +37,6 @@ output "env_vars" {
37
37
  MONGODB_ATLAS_FEDERATED_GROUP_ID = var.project_id
38
38
  MONGODB_ATLAS_FEDERATED_IDP_ID = data.mongodbatlas_federated_settings_org_config.current.identity_provider_id # 20 character legacy needed for PATCH on org
39
39
  # MONGODB_ATLAS_FEDERATED_IDP_ID = data.mongodbatlas_federated_settings_org_config.current.okta_idp_id # used for org PATCH
40
- MONGODB_ATLAS_FEDERATED_SETTINGS_ASSOCIATED_DOMAIN = data.mongodbatlas_federated_settings_org_config.current.domain_allow_list[0]
40
+ MONGODB_ATLAS_FEDERATED_SETTINGS_ASSOCIATED_DOMAIN = try(data.mongodbatlas_federated_settings_org_config.current.domain_allow_list[0], "no-domain-set-by-atlas-init.com")
41
41
  }
42
-
43
42
  }
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  }
8
8
 
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  }
8
8
  }
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  }
8
8
  }
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  }
8
8
 
@@ -6,7 +6,7 @@ terraform {
6
6
  }
7
7
  mongodbatlas = {
8
8
  source = "mongodb/mongodbatlas"
9
- version = "1.33"
9
+ version = ">=1.33"
10
10
  }
11
11
  }
12
12
 
@@ -2,7 +2,7 @@ terraform {
2
2
  required_providers {
3
3
  mongodbatlas = {
4
4
  source = "mongodb/mongodbatlas"
5
- version = "1.33"
5
+ version = ">=1.33"
6
6
  }
7
7
  aws = {
8
8
  source = "hashicorp/aws"
File without changes
@@ -0,0 +1,3 @@
1
+ from atlas_init.tf_ext.typer_app import typer_main
2
+
3
+ typer_main()
@@ -0,0 +1,325 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from collections import defaultdict
5
+ from concurrent.futures import Future, as_completed
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ import requests
10
+ import typer
11
+ from ask_shell import new_task, print_to_live, run_pool
12
+ from model_lib import dump, parse_model
13
+ from pydantic import BaseModel, Field, model_validator
14
+ from requests.auth import HTTPDigestAuth
15
+ from rich.markdown import Markdown
16
+ from zero_3rdparty.file_utils import ensure_parents_write_text
17
+ from zero_3rdparty.str_utils import ensure_prefix, ensure_suffix, instance_repr
18
+
19
+ from atlas_init.cli_tf.mock_tf_log import resolve_admin_api_path
20
+ from atlas_init.cli_tf.openapi import OpenapiSchema
21
+ from atlas_init.settings.env_vars import init_settings
22
+ from atlas_init.settings.env_vars_generated import AtlasSettingsWithProject
23
+ from atlas_init.settings.env_vars_modules import (
24
+ TFModuleCluster,
25
+ TFModuleFederated_Vars,
26
+ TFModuleProject_Extra,
27
+ TFModuleStream_Instance,
28
+ )
29
+ from atlas_init.settings.path import load_dotenv
30
+ from atlas_init.tf_ext.settings import TfExtSettings
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ ALLOWED_MISSING_VARS: set[str] = {
35
+ "alertConfigId",
36
+ "alertId",
37
+ "clientId",
38
+ "cloudProvider",
39
+ "invoiceId",
40
+ # "name",
41
+ "pipelineName",
42
+ "processId",
43
+ "username",
44
+ }
45
+ ALLOWED_ERROR_CODES: set[str] = {
46
+ "CANNOT_USE_CLUSTER_IN_SERVERLESS_INSTANCE_API",
47
+ "VALIDATION_ERROR",
48
+ "UNEXPECTED_ERROR",
49
+ "CANNOT_USE_NON_FLEX_CLUSTER_IN_FLEX_API",
50
+ "CHECKPOINTS_ONLY_ON_CONTINOUS_BACKUP",
51
+ "INCORRECT_BACKUP_API_ENDPOINT",
52
+ }
53
+
54
+
55
+ # export ATLAS_INIT_TEST_SUITES=clusterm10,s3,federated,project,stream_connection
56
+ def resolve_path_variables() -> dict[str, str]:
57
+ settings = init_settings()
58
+ env_vars_full = load_dotenv(settings.env_vars_vs_code)
59
+ atlas_settings = AtlasSettingsWithProject(**env_vars_full)
60
+ cluster_settings = TFModuleCluster(**env_vars_full)
61
+ project_settings = TFModuleProject_Extra(**env_vars_full)
62
+ stream_settings = TFModuleStream_Instance(**env_vars_full)
63
+ federated_settings = TFModuleFederated_Vars(**env_vars_full)
64
+ return {
65
+ "orgId": atlas_settings.MONGODB_ATLAS_ORG_ID,
66
+ "cloudProvider": "AWS",
67
+ "federationSettingsId": federated_settings.MONGODB_ATLAS_FEDERATION_SETTINGS_ID,
68
+ "clusterName": cluster_settings.MONGODB_ATLAS_CLUSTER_NAME,
69
+ "name": cluster_settings.MONGODB_ATLAS_CLUSTER_NAME,
70
+ "groupId": atlas_settings.MONGODB_ATLAS_PROJECT_ID,
71
+ "teamId": project_settings.MONGODB_ATLAS_TEAM_ID,
72
+ "tenantName": stream_settings.MONGODB_ATLAS_STREAM_INSTANCE_NAME,
73
+ "apiUserId": atlas_settings.MONGODB_ATLAS_PROJECT_OWNER_ID,
74
+ "username": atlas_settings.MONGODB_ATLAS_USER_EMAIL,
75
+ }
76
+
77
+
78
+ class ApiCall(BaseModel):
79
+ operation_id: str
80
+ path: str
81
+ accept_header: str = "application/vnd.atlas.2023-01-01+json"
82
+ query_args: dict[str, str] = Field(default_factory=dict)
83
+
84
+ def __str__(self):
85
+ return instance_repr(self, ["operation_id", "path"])
86
+
87
+ def path_with_variables(self, path_variables: dict[str, str]):
88
+ return self.path.format(**path_variables)
89
+
90
+ @model_validator(mode="after")
91
+ def check_path_variables(self):
92
+ self.accept_header = ensure_prefix(self.accept_header, "application/vnd.atlas.")
93
+ self.accept_header = ensure_suffix(self.accept_header, "+json")
94
+ return self
95
+
96
+
97
+ class UnresolvedPathsError(Exception):
98
+ def __init__(self, missing_var_paths: dict[str, list[str]]) -> None:
99
+ self.missing_var_paths = missing_var_paths
100
+ missing_vars_formatted = "\n".join(f"{var}: {paths}" for var, paths in missing_var_paths.items())
101
+ super().__init__(f"Failed to resolve path variables:\nMissing vars: {missing_vars_formatted}")
102
+
103
+
104
+ class ApiCalls(BaseModel):
105
+ calls: list[ApiCall] = Field(default_factory=list)
106
+ ignored_calls: list[ApiCall] = Field(default_factory=list)
107
+ path_variables: dict[str, str] = Field(default_factory=resolve_path_variables)
108
+ skip_validation: bool = False
109
+
110
+ @model_validator(mode="after")
111
+ def check_path_variables(self):
112
+ if self.skip_validation:
113
+ return self
114
+ missing_vars_paths: dict[str, list[str]] = defaultdict(list)
115
+ ok_calls = []
116
+ for call in self.calls:
117
+ try:
118
+ call.path_with_variables(self.path_variables)
119
+ ok_calls.append(call)
120
+ except KeyError as e:
121
+ missing_vars_paths[str(e).strip("'")].append(f"{call.operation_id} {call.path}")
122
+ self.ignored_calls.append(call)
123
+ continue
124
+ for allowed_missing in sorted(ALLOWED_MISSING_VARS):
125
+ if allowed_missing in missing_vars_paths:
126
+ logger.info(f"Allowed missing variable {allowed_missing}: {missing_vars_paths[allowed_missing]}")
127
+ del missing_vars_paths[allowed_missing]
128
+ if missing_vars_paths:
129
+ raise UnresolvedPathsError(missing_var_paths=missing_vars_paths)
130
+ self.calls = ok_calls
131
+ return self
132
+
133
+ def dump_to_dict(self) -> dict:
134
+ return {
135
+ "calls": [call.model_dump(exclude_defaults=True, exclude_unset=True) for call in self.calls],
136
+ }
137
+
138
+
139
+ @lru_cache
140
+ def _public_private_key() -> tuple[str, str]:
141
+ public_key = os.environ.get("MONGODB_ATLAS_PUBLIC_KEY")
142
+ private_key = os.environ.get("MONGODB_ATLAS_PRIVATE_KEY")
143
+ if not public_key or not private_key:
144
+ raise ValueError("MONGODB_ATLAS_PUBLIC_KEY and MONGODB_ATLAS_PRIVATE_KEY must be set in environment variables.")
145
+ return public_key, private_key
146
+
147
+
148
+ class APICallError(Exception):
149
+ def __init__(self, api_call: ApiCall, json_response: dict, error: requests.exceptions.HTTPError):
150
+ self.api_call = api_call
151
+ self.json_response = json_response
152
+ super().__init__(f"Failed to make API call {api_call}:\njson={json_response}\n{error}")
153
+
154
+ @property
155
+ def error_code(self) -> str:
156
+ return self.json_response.get("errorCode", "")
157
+
158
+
159
+ def call_api(api_call: ApiCall, path_variables: dict[str, str]) -> dict:
160
+ resolved_path = api_call.path_with_variables(path_variables)
161
+ response = requests.get(
162
+ f"https://cloud-dev.mongodb.com/{resolved_path.lstrip('/')}",
163
+ params=api_call.query_args,
164
+ headers={"Accept": api_call.accept_header, "Content-Type": "application/json"},
165
+ auth=HTTPDigestAuth(*_public_private_key()),
166
+ timeout=30,
167
+ )
168
+ try:
169
+ response_json = response.json()
170
+ except requests.exceptions.JSONDecodeError as e:
171
+ logger.error(f"Failed to parse_json {api_call}: {e}")
172
+ response_json = {}
173
+ try:
174
+ response.raise_for_status()
175
+ except requests.exceptions.HTTPError as e:
176
+ raise APICallError(api_call, response_json, e) from e
177
+ return response_json
178
+
179
+
180
+ class NoSelfLinkError(Exception):
181
+ def __init__(self, json_response: dict) -> None:
182
+ self.json_response = json_response
183
+ super().__init__("No self link found in response")
184
+
185
+
186
+ def parse_href_response(json_response: dict) -> str:
187
+ for ref in json_response.get("links", []):
188
+ if ref.get("rel") == "self":
189
+ return ref.get("href")
190
+ raise NoSelfLinkError(json_response)
191
+
192
+
193
+ def api_config(
194
+ config_path_str: str = typer.Option("", "-p", "--path", help="Path to the API config file"),
195
+ query_args_str: str = typer.Option(
196
+ '{"pageNum": "0", "itemsPerPage": "0"}', "-q", "--query-args", help="Query arguments for the API call"
197
+ ),
198
+ verbose: bool = typer.Option(False, "-v", "--verbose", help="Verbose output"),
199
+ ):
200
+ query_args: dict[str, str] = json.loads(query_args_str)
201
+ if config_path_str == "":
202
+ with new_task("Find API Calls that use pagination"):
203
+ config_path = dump_config_path(query_args)
204
+ else:
205
+ config_path = Path(config_path_str)
206
+ assert config_path.exists(), f"Config file {config_path} does not exist."
207
+ model = parse_model(config_path, t=ApiCalls)
208
+ total_calls = len(model.calls)
209
+ assert _public_private_key(), "Public and private keys must be set in environment variables."
210
+ path_variables = model.path_variables
211
+ op_id_path_self_qstring: dict[tuple[str, str], str] = {}
212
+ with run_pool(
213
+ task_name="make API calls", max_concurrent_submits=10, threads_used_per_submit=1, total=total_calls
214
+ ) as pool:
215
+ futures: dict[Future, ApiCall] = {
216
+ pool.submit(call_api, api_call, path_variables): api_call for api_call in model.calls
217
+ }
218
+ for future in as_completed(futures):
219
+ api_call = futures[future]
220
+ try:
221
+ result = future.result()
222
+ except APICallError as e:
223
+ if e.error_code in ALLOWED_ERROR_CODES:
224
+ logger.info(f"Allowed error code {e.error_code} in response for {api_call}")
225
+ model.ignored_calls.append(api_call)
226
+ continue
227
+ raise
228
+ except Exception as e:
229
+ logger.error(e)
230
+ continue
231
+ try:
232
+ href = parse_href_response(result)
233
+ op_id_path_self_qstring[(api_call.operation_id, api_call.path)] = href.split("?")[-1]
234
+ except NoSelfLinkError as e:
235
+ logger.error(f"{api_call} did not have a self link in the response:\n{e.json_response}")
236
+ continue
237
+ logger.info(f"API call {api_call} completed successfully with self ref:\n{href}")
238
+ if verbose:
239
+ logger.info(f"Response for {api_call.query_args} was:\n{dump(result, 'pretty_json')}")
240
+ query_args_str = "&".join(f"{key}={value}" for key, value in query_args.items())
241
+ md_report: list[str] = [
242
+ f"# Pagination Report for query_args='{query_args_str}'",
243
+ "",
244
+ "## Checked endpoints",
245
+ "",
246
+ "Operation ID | Path | SelfQueryString",
247
+ "--- | --- | ---",
248
+ *[
249
+ f"{operation_id} | {path} | {self_query_string}"
250
+ for (operation_id, path), self_query_string in op_id_path_self_qstring.items()
251
+ ],
252
+ "",
253
+ "## Ignored endpoints (not checked)",
254
+ "",
255
+ "Operation ID | Path",
256
+ "--- | ---",
257
+ *[f"{call.operation_id} | {call.path}" for call in model.ignored_calls],
258
+ ]
259
+ md_content = "\n".join(md_report)
260
+ md = Markdown(md_content)
261
+ print_to_live(md)
262
+ output_path = TfExtSettings.from_env().pagination_output_path(query_args_str)
263
+ ensure_parents_write_text(output_path, md_content)
264
+ logger.info(f"Pagination report saved to {output_path}")
265
+ return md
266
+
267
+
268
+ def api(
269
+ path: str = typer.Option("-p", "--path", help="Path to the API endpoint"),
270
+ query_string: str = typer.Option("", "-q", "--query-string", help="Query string for the API call"),
271
+ ):
272
+ assert path, "Path must be provided."
273
+ accept_header = "application/vnd.atlas.2023-01-01+json"
274
+ url = f"https://cloud-dev.mongodb.com/{path.lstrip('/')}?{query_string}"
275
+ logger.info(f"Calling {url}")
276
+ try:
277
+ r = requests.get(
278
+ url,
279
+ headers={"Accept": accept_header, "Content-Type": "application/json"},
280
+ auth=HTTPDigestAuth(*_public_private_key()),
281
+ timeout=30,
282
+ )
283
+ print(r.text)
284
+ r.raise_for_status()
285
+ except requests.exceptions.HTTPError as e:
286
+ print(e)
287
+ print(e.response)
288
+
289
+
290
+ def dump_config_path(query_args: dict[str, str]) -> Path:
291
+ settings = TfExtSettings.from_env()
292
+ latest_api_spec = resolve_admin_api_path()
293
+ model = parse_model(latest_api_spec, t=OpenapiSchema)
294
+ paginated_paths: list[ApiCall] = []
295
+ path_versions = list(model.path_method_api_versions())
296
+
297
+ for (path, method, code), versions in path_versions:
298
+ if method != "get" or code != "200":
299
+ continue
300
+ assert len(versions) == 1, f"{path} {method} {code} has multiple versions: {versions}"
301
+ get_method = model.get_method(path)
302
+ if not get_method:
303
+ continue
304
+ parameters = get_method.get("parameters", [])
305
+ for param in parameters:
306
+ if param_ref := param.get("$ref"):
307
+ if param_ref.endswith("itemsPerPage"):
308
+ version = versions[0].strftime("%Y-%m-%d")
309
+ paginated_paths.append(
310
+ ApiCall(
311
+ path=path,
312
+ query_args=query_args,
313
+ accept_header=f"application/vnd.atlas.{version}+json",
314
+ operation_id=get_method["operationId"],
315
+ )
316
+ )
317
+ config_path = settings.api_calls_path
318
+ calls = ApiCalls(
319
+ calls=paginated_paths,
320
+ skip_validation=True,
321
+ )
322
+ calls_yaml = dump(calls.dump_to_dict(), "yaml")
323
+ logger.info(f"Dumped {len(paginated_paths)} API calls to {config_path}")
324
+ ensure_parents_write_text(config_path, calls_yaml)
325
+ return config_path
@@ -0,0 +1,32 @@
1
+ import typer
2
+
3
+
4
+ def default_skippped_directories() -> list[str]:
5
+ return [
6
+ "prometheus-and-teams", # Provider registry.terraform.io/hashicorp/template v2.2.0 does not have a package available for your current platform, darwin_arm64.
7
+ ]
8
+
9
+
10
+ REPO_PATH_ATLAS_ARG = typer.Argument(..., help="Path to the mongodbatlas-terraform-provider repository")
11
+ SKIP_EXAMPLES_DIRS_OPTION = typer.Option(
12
+ ...,
13
+ "--skip-examples",
14
+ help="Skip example directories with these names",
15
+ default_factory=default_skippped_directories,
16
+ show_default=True,
17
+ )
18
+ TF_CLI_CONFIG_FILE_ENV_NAME = "TF_CLI_CONFIG_FILE"
19
+ TF_CLI_CONFIG_FILE_ARG = typer.Option(
20
+ "",
21
+ "-tf-cli",
22
+ "--tf-cli-config-file",
23
+ envvar=TF_CLI_CONFIG_FILE_ENV_NAME,
24
+ help="Terraform CLI config file",
25
+ )
26
+ ENV_NAME_REPO_PATH_ATLAS_PROVIDER = "REPO_PATH_ATLAS_PROVIDER"
27
+ TF_REPO_PATH_ATLAS = typer.Option(
28
+ "",
29
+ "--tf-repo-path-atlas",
30
+ help="Path to the mongodbatlas-terraform-provider repository",
31
+ envvar=ENV_NAME_REPO_PATH_ATLAS_PROVIDER,
32
+ )
@@ -0,0 +1,3 @@
1
+ DEFAULT_EXTERNAL_SUBSTRINGS = ["aws", "azure", "google", "gcp"]
2
+ DEFAULT_INTERNAL_SUBSTRINGS = ["atlas", "mongo", "aws_region", "gcp_region", "azure_region", "cidr"]
3
+ ATLAS_PROVIDER_NAME = "mongodbatlas"