atlas-init 0.4.3__py3-none-any.whl → 0.4.5__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.
- atlas_init/__init__.py +2 -3
- atlas_init/cli.py +2 -1
- atlas_init/cli_cfn/app.py +4 -7
- atlas_init/cli_cfn/cfn_parameter_finder.py +3 -30
- atlas_init/cli_cfn/contract.py +9 -10
- atlas_init/cli_cfn/example.py +8 -8
- atlas_init/cli_helper/go.py +18 -14
- atlas_init/cli_helper/tf_runner.py +14 -11
- atlas_init/cli_root/trigger.py +24 -11
- atlas_init/cli_tf/app.py +1 -1
- atlas_init/cli_tf/github_logs.py +4 -16
- atlas_init/cli_tf/hcl/modifier.py +115 -14
- atlas_init/cli_tf/mock_tf_log.py +3 -2
- atlas_init/cli_tf/schema_v3.py +2 -2
- atlas_init/cli_tf/schema_v3_sdk_base.py +1 -1
- atlas_init/settings/env_vars.py +130 -166
- atlas_init/settings/env_vars_generated.py +40 -9
- atlas_init/settings/env_vars_modules.py +71 -0
- atlas_init/settings/path.py +3 -9
- atlas_init/typer_app.py +3 -3
- {atlas_init-0.4.3.dist-info → atlas_init-0.4.5.dist-info}/METADATA +2 -2
- {atlas_init-0.4.3.dist-info → atlas_init-0.4.5.dist-info}/RECORD +25 -29
- atlas_init/cli_tf/example_update_test/test_update_example.tf +0 -23
- atlas_init/cli_tf/example_update_test.py +0 -96
- atlas_init/cli_tf/hcl/modifier_test/test_process_variables_output_.tf +0 -25
- atlas_init/cli_tf/hcl/modifier_test/test_process_variables_variable_.tf +0 -24
- atlas_init/cli_tf/hcl/modifier_test.py +0 -95
- {atlas_init-0.4.3.dist-info → atlas_init-0.4.5.dist-info}/WHEEL +0 -0
- {atlas_init-0.4.3.dist-info → atlas_init-0.4.5.dist-info}/entry_points.txt +0 -0
- {atlas_init-0.4.3.dist-info → atlas_init-0.4.5.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,7 @@ import logging
|
|
2
2
|
from collections import defaultdict
|
3
3
|
from copy import deepcopy
|
4
4
|
from pathlib import Path
|
5
|
+
from typing import Callable
|
5
6
|
|
6
7
|
import hcl2
|
7
8
|
from lark import Token, Tree, UnexpectedToken
|
@@ -51,6 +52,8 @@ def update_description(tree: Tree, new_descriptions: dict[str, str], existing_na
|
|
51
52
|
def token_name(token: Token | Tree) -> str:
|
52
53
|
if isinstance(token, Token):
|
53
54
|
return token.value.strip('"')
|
55
|
+
if isinstance(token, Tree) and token.data == "identifier":
|
56
|
+
return token.children[0].value.strip('"') # type: ignore
|
54
57
|
err_msg = f"unexpected token type {type(token)} for token name"
|
55
58
|
raise ValueError(err_msg)
|
56
59
|
|
@@ -102,31 +105,46 @@ def create_description_attribute(description_value: str) -> Tree:
|
|
102
105
|
return Tree(Token("RULE", "attribute"), children)
|
103
106
|
|
104
107
|
|
105
|
-
def
|
108
|
+
def process_generic(
|
106
109
|
node: Tree,
|
107
|
-
|
108
|
-
|
110
|
+
tree_match: Callable[[Tree], bool],
|
111
|
+
tree_call: Callable[[Tree], Tree],
|
109
112
|
depth=0,
|
110
|
-
|
111
|
-
block_type: str,
|
112
|
-
) -> Tree:
|
113
|
+
):
|
113
114
|
new_children = []
|
114
115
|
logger.debug(f"[{depth}] (tree)\t|", " " * depth, node.data)
|
115
116
|
for child in node.children:
|
116
117
|
if isinstance(child, Tree):
|
117
|
-
if
|
118
|
-
child =
|
119
|
-
|
120
|
-
)
|
121
|
-
new_children.append(
|
122
|
-
process_descriptions(child, name_updates, existing_names, depth + 1, block_type=block_type)
|
123
|
-
)
|
118
|
+
if tree_match(child):
|
119
|
+
child = tree_call(child)
|
120
|
+
new_children.append(process_generic(child, tree_match, tree_call, depth + 1))
|
124
121
|
else:
|
125
122
|
new_children.append(process_token(child, depth + 1))
|
126
|
-
|
127
123
|
return Tree(node.data, new_children)
|
128
124
|
|
129
125
|
|
126
|
+
def process_descriptions(
|
127
|
+
node: Tree,
|
128
|
+
name_updates: dict[str, str],
|
129
|
+
existing_names: dict[str, list[str]],
|
130
|
+
depth=0,
|
131
|
+
*,
|
132
|
+
block_type: str,
|
133
|
+
) -> Tree:
|
134
|
+
def tree_match(tree: Tree) -> bool:
|
135
|
+
return is_block_type(tree, block_type)
|
136
|
+
|
137
|
+
def tree_call(tree: Tree) -> Tree:
|
138
|
+
return update_description(tree, name_updates, existing_names)
|
139
|
+
|
140
|
+
return process_generic(
|
141
|
+
node,
|
142
|
+
tree_match,
|
143
|
+
tree_call,
|
144
|
+
depth=depth,
|
145
|
+
)
|
146
|
+
|
147
|
+
|
130
148
|
def update_descriptions(tf_path: Path, new_names: dict[str, str], block_type: str) -> tuple[str, dict[str, list[str]]]:
|
131
149
|
try:
|
132
150
|
tree = hcl2.parses(tf_path.read_text()) # type: ignore
|
@@ -142,3 +160,86 @@ def update_descriptions(tf_path: Path, new_names: dict[str, str], block_type: st
|
|
142
160
|
)
|
143
161
|
new_tf = hcl2.writes(new_tree) # type: ignore
|
144
162
|
return new_tf, existing_descriptions
|
163
|
+
|
164
|
+
|
165
|
+
def _block_name_body(tree: Tree) -> tuple[str, Tree]:
|
166
|
+
try:
|
167
|
+
_, name_token, body = tree.children
|
168
|
+
name = token_name(name_token)
|
169
|
+
except (IndexError, AttributeError) as e:
|
170
|
+
raise ValueError("unexpected block structure") from e
|
171
|
+
return name, body
|
172
|
+
|
173
|
+
|
174
|
+
def _read_attribute(tree_body: Tree, attribute_name: str) -> Tree | None:
|
175
|
+
for attribute in tree_body.children:
|
176
|
+
if not isinstance(attribute, Tree):
|
177
|
+
continue
|
178
|
+
if attribute.data != "attribute":
|
179
|
+
continue
|
180
|
+
attr_identifier, _, attr_value = attribute.children
|
181
|
+
if token_name(attr_identifier.children[0]) != attribute_name:
|
182
|
+
continue
|
183
|
+
return attr_value
|
184
|
+
|
185
|
+
|
186
|
+
def _is_object(tree_body: Tree) -> bool:
|
187
|
+
if not isinstance(tree_body, Tree):
|
188
|
+
return False
|
189
|
+
if len(tree_body.children) != 1:
|
190
|
+
return False
|
191
|
+
if not isinstance(tree_body.children[0], Tree):
|
192
|
+
return False
|
193
|
+
return tree_body.children[0].data == "object"
|
194
|
+
|
195
|
+
|
196
|
+
def _read_object_elems(tree_body: Tree) -> list[Tree]:
|
197
|
+
object_elements = []
|
198
|
+
for obj_child in tree_body.children[0].children:
|
199
|
+
if not isinstance(obj_child, Tree):
|
200
|
+
continue
|
201
|
+
if obj_child.data != "object_elem":
|
202
|
+
continue
|
203
|
+
object_elements.append(obj_child)
|
204
|
+
return object_elements
|
205
|
+
|
206
|
+
|
207
|
+
def _read_object_elem_key(tree_body: Tree) -> str:
|
208
|
+
name_tree, _, _ = tree_body.children
|
209
|
+
return token_name(name_tree.children[0])
|
210
|
+
|
211
|
+
|
212
|
+
def read_block_attribute_object_keys(tf_path: Path, block_type: str, block_name: str, block_key: str) -> list[str]:
|
213
|
+
try:
|
214
|
+
tree = hcl2.parses(tf_path.read_text()) # type: ignore
|
215
|
+
except UnexpectedToken as e:
|
216
|
+
logger.warning(f"failed to parse {tf_path}: {e}")
|
217
|
+
return []
|
218
|
+
env_vars = []
|
219
|
+
|
220
|
+
def extract_env_vars(tree: Tree) -> bool:
|
221
|
+
if not is_block_type(tree, block_type):
|
222
|
+
return False
|
223
|
+
name, body = _block_name_body(tree)
|
224
|
+
if name != block_name:
|
225
|
+
return False
|
226
|
+
attribute_value = _read_attribute(body, block_key)
|
227
|
+
if not attribute_value:
|
228
|
+
return False
|
229
|
+
if not _is_object(attribute_value):
|
230
|
+
return False
|
231
|
+
object_elements = _read_object_elems(attribute_value)
|
232
|
+
for obj_elem in object_elements:
|
233
|
+
key = _read_object_elem_key(obj_elem)
|
234
|
+
env_vars.append(key)
|
235
|
+
return False
|
236
|
+
|
237
|
+
def tree_call(tree: Tree) -> Tree:
|
238
|
+
return tree
|
239
|
+
|
240
|
+
process_generic(
|
241
|
+
tree,
|
242
|
+
extract_env_vars,
|
243
|
+
tree_call,
|
244
|
+
)
|
245
|
+
return env_vars
|
atlas_init/cli_tf/mock_tf_log.py
CHANGED
@@ -33,7 +33,7 @@ from atlas_init.repos.go_sdk import (
|
|
33
33
|
download_admin_api,
|
34
34
|
parse_api_spec_paths,
|
35
35
|
)
|
36
|
-
from atlas_init.settings.
|
36
|
+
from atlas_init.settings.env_vars import init_settings
|
37
37
|
|
38
38
|
logger = logging.getLogger(__name__)
|
39
39
|
|
@@ -164,7 +164,8 @@ def resolve_admin_api_path(sdk_repo_path_str: str, sdk_branch: str, admin_api_pa
|
|
164
164
|
assert sdk_repo_path.exists(), f"not found sdk_repo_path={sdk_repo_path}"
|
165
165
|
resolved_admin_api_path = api_spec_path_transformed(sdk_repo_path)
|
166
166
|
else:
|
167
|
-
|
167
|
+
settings = init_settings()
|
168
|
+
resolved_admin_api_path = settings.atlas_atlas_api_transformed_yaml
|
168
169
|
if not is_cache_up_to_date(resolved_admin_api_path, 3600):
|
169
170
|
download_admin_api(resolved_admin_api_path, sdk_branch)
|
170
171
|
assert resolved_admin_api_path.exists(), f"unable to resolve admin_api_path={resolved_admin_api_path}"
|
atlas_init/cli_tf/schema_v3.py
CHANGED
@@ -210,12 +210,12 @@ class Schema(BaseModelLocal):
|
|
210
210
|
|
211
211
|
|
212
212
|
class Resource(BaseModelLocal):
|
213
|
-
|
213
|
+
local_schema: Schema = Field(alias="schema")
|
214
214
|
name: SnakeCaseString
|
215
215
|
|
216
216
|
@property
|
217
217
|
def use_timeout(self) -> bool:
|
218
|
-
return any(a.timeouts for a in self.
|
218
|
+
return any(a.timeouts for a in self.local_schema.attributes)
|
219
219
|
|
220
220
|
|
221
221
|
ResourceSchemaV3 = Resource
|
@@ -41,7 +41,7 @@ def schema_attributes(root: object) -> list[Attribute]:
|
|
41
41
|
|
42
42
|
@schema_attributes.register
|
43
43
|
def _resource_attributes(root: Resource) -> list[Attribute]:
|
44
|
-
return root.
|
44
|
+
return root.local_schema.attributes
|
45
45
|
|
46
46
|
|
47
47
|
@schema_attributes.register
|
atlas_init/settings/env_vars.py
CHANGED
@@ -7,11 +7,10 @@ from functools import cached_property
|
|
7
7
|
from pathlib import Path
|
8
8
|
from typing import Any, NamedTuple, TypeVar
|
9
9
|
|
10
|
-
from model_lib import parse_payload
|
11
|
-
from pydantic import ValidationError, field_validator
|
12
|
-
from
|
10
|
+
from model_lib import StaticSettings, parse_payload
|
11
|
+
from pydantic import BaseModel, ValidationError, field_validator
|
12
|
+
from zero_3rdparty import iter_utils
|
13
13
|
|
14
|
-
from atlas_init.cloud.aws import AwsRegion
|
15
14
|
from atlas_init.settings.config import (
|
16
15
|
AtlasInitConfig,
|
17
16
|
TestSuite,
|
@@ -19,12 +18,10 @@ from atlas_init.settings.config import (
|
|
19
18
|
from atlas_init.settings.config import (
|
20
19
|
active_suites as config_active_suites,
|
21
20
|
)
|
22
|
-
from atlas_init.settings.env_vars_generated import AtlasSettings
|
23
21
|
from atlas_init.settings.path import (
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
DEFAULT_TF_PATH,
|
22
|
+
DEFAULT_ATLAS_INIT_CONFIG_PATH,
|
23
|
+
DEFAULT_ATLAS_INIT_SCHEMA_CONFIG_PATH,
|
24
|
+
DEFAULT_TF_SRC_PATH,
|
28
25
|
dump_dotenv,
|
29
26
|
load_dotenv,
|
30
27
|
repo_path_rel_path,
|
@@ -33,66 +30,59 @@ from atlas_init.settings.path import (
|
|
33
30
|
logger = logging.getLogger(__name__)
|
34
31
|
ENV_PREFIX = "ATLAS_INIT_"
|
35
32
|
DEFAULT_PROFILE = "default"
|
33
|
+
DEFAULT_PROJECT_NAME = "atlas-init"
|
36
34
|
ENV_S3_PROFILE_BUCKET = f"{ENV_PREFIX}S3_PROFILE_BUCKET"
|
37
35
|
ENV_PROJECT_NAME = f"{ENV_PREFIX}PROJECT_NAME"
|
38
36
|
ENV_PROFILE = f"{ENV_PREFIX}PROFILE"
|
39
|
-
ENV_PROFILES_PATH = f"{ENV_PREFIX}PROFILES_PATH"
|
40
37
|
ENV_TEST_SUITES = f"{ENV_PREFIX}TEST_SUITES"
|
41
38
|
ENV_CLIPBOARD_COPY = f"{ENV_PREFIX}CLIPBOARD_COPY"
|
42
|
-
REQUIRED_FIELDS = [
|
43
|
-
"MONGODB_ATLAS_ORG_ID",
|
44
|
-
"MONGODB_ATLAS_PRIVATE_KEY",
|
45
|
-
"MONGODB_ATLAS_PUBLIC_KEY",
|
46
|
-
]
|
47
39
|
FILENAME_ENV_MANUAL = ".env-manual"
|
48
40
|
T = TypeVar("T")
|
49
41
|
|
50
42
|
|
51
|
-
|
52
|
-
|
43
|
+
def read_from_env(env_key: str, default: str = "") -> str:
|
44
|
+
return next(
|
45
|
+
(os.environ[name] for name in [env_key, env_key.lower(), env_key.upper()] if name in os.environ),
|
46
|
+
default,
|
47
|
+
)
|
53
48
|
|
54
|
-
TF_CLI_CONFIG_FILE: str = ""
|
55
|
-
AWS_PROFILE: str = ""
|
56
|
-
AWS_REGION: AwsRegion = "us-east-1"
|
57
|
-
non_interactive: bool = False
|
58
49
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
50
|
+
class AtlasInitSettings(StaticSettings):
|
51
|
+
atlas_init_profile: str = DEFAULT_PROFILE # override this for different env, e.g. dev, prod
|
52
|
+
atlas_init_project_name: str = DEFAULT_PROJECT_NAME # used in the atlas cloud
|
53
|
+
atlas_init_config_path: Path = DEFAULT_ATLAS_INIT_CONFIG_PATH # /atlas_init.yaml
|
54
|
+
atlas_init_tf_src_path: Path = DEFAULT_TF_SRC_PATH # /tf directory of repo
|
55
|
+
atlas_init_tf_schema_config_path: Path = DEFAULT_ATLAS_INIT_SCHEMA_CONFIG_PATH # /terraform.yaml
|
56
|
+
atlas_init_schema_out_path: Path | None = None # override this for the generated schema
|
57
|
+
|
58
|
+
atlas_init_cfn_profile: str = ""
|
59
|
+
atlas_init_cfn_region: str = ""
|
60
|
+
atlas_init_cfn_use_kms_key: bool = False
|
61
|
+
atlas_init_cliboard_copy: str = ""
|
62
|
+
atlas_init_test_suites: str = ""
|
63
|
+
atlas_init_s3_profile_bucket: str = ""
|
64
|
+
|
65
|
+
non_interactive: bool = False
|
65
66
|
|
66
67
|
@property
|
67
68
|
def is_interactive(self) -> bool:
|
68
69
|
return not self.non_interactive
|
69
70
|
|
70
71
|
@property
|
71
|
-
def
|
72
|
-
return "
|
72
|
+
def profiles_path(self) -> Path:
|
73
|
+
return self.static_root / "profiles"
|
73
74
|
|
75
|
+
@property
|
76
|
+
def project_name(self) -> str:
|
77
|
+
return self.atlas_init_project_name
|
74
78
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
return os.environ[name]
|
79
|
-
return default
|
80
|
-
|
81
|
-
|
82
|
-
class AtlasInitPaths(BaseSettings):
|
83
|
-
model_config = SettingsConfigDict(env_prefix=ENV_PREFIX)
|
84
|
-
|
85
|
-
profile: str = DEFAULT_PROFILE
|
86
|
-
config_path: Path = DEFAULT_CONFIG_PATH
|
87
|
-
tf_path: Path = DEFAULT_TF_PATH
|
88
|
-
profiles_path: Path = DEFAULT_PROFILES_PATH
|
89
|
-
tf_schema_config_path: Path = DEFAULT_SCHEMA_CONFIG_PATH
|
90
|
-
schema_out_path: Path | None = None
|
91
|
-
s3_profile_bucket: str = ""
|
79
|
+
@property
|
80
|
+
def profile(self) -> str:
|
81
|
+
return self.atlas_init_profile
|
92
82
|
|
93
83
|
@property
|
94
84
|
def schema_out_path_computed(self) -> Path:
|
95
|
-
return self.
|
85
|
+
return self.atlas_init_schema_out_path or self.static_root / "schema"
|
96
86
|
|
97
87
|
@property
|
98
88
|
def profile_dir(self) -> Path:
|
@@ -135,103 +125,42 @@ class AtlasInitPaths(BaseSettings):
|
|
135
125
|
def tf_outputs_path(self) -> Path:
|
136
126
|
return self.profile_dir / "tf_outputs.json"
|
137
127
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
return
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
# sourcery skip: dict-assign-update-to-union
|
157
|
-
manual_env_vars = self.manual_env_vars
|
158
|
-
if manual_env_vars:
|
159
|
-
if skip_os_update:
|
160
|
-
return manual_env_vars
|
161
|
-
if new_updates := {k: v for k, v in manual_env_vars.items() if k not in os.environ}:
|
162
|
-
logger.info(f"loading manual env-vars {','.join(new_updates)}")
|
163
|
-
os.environ.update(new_updates)
|
164
|
-
else:
|
165
|
-
logger.warning(f"no {self.env_file_manual} exists")
|
166
|
-
return manual_env_vars
|
128
|
+
@property
|
129
|
+
def github_ci_run_logs(self) -> Path:
|
130
|
+
return self.cache_root / "github_ci_run_logs"
|
131
|
+
|
132
|
+
@property
|
133
|
+
def github_ci_summary_dir(self) -> Path:
|
134
|
+
return self.cache_root / "github_ci_summary"
|
135
|
+
|
136
|
+
@property
|
137
|
+
def go_test_logs_dir(self) -> Path:
|
138
|
+
return self.cache_root / "go_test_logs"
|
139
|
+
|
140
|
+
@property
|
141
|
+
def atlas_atlas_api_transformed_yaml(self) -> Path:
|
142
|
+
return self.cache_root / "atlas_api_transformed.yaml"
|
143
|
+
|
144
|
+
def cfn_region(self, default: str) -> str:
|
145
|
+
return self.atlas_init_cfn_region or default
|
167
146
|
|
168
147
|
def include_extra_env_vars_in_vscode(self, extra_env_vars: dict[str, str]) -> None:
|
169
148
|
extra_name = ", ".join(extra_env_vars.keys())
|
170
|
-
original_env_vars =
|
149
|
+
original_env_vars = load_dotenv(self.env_vars_vs_code)
|
171
150
|
new_env_vars = original_env_vars | extra_env_vars
|
172
151
|
dump_dotenv(self.env_vars_vs_code, new_env_vars)
|
173
152
|
logger.info(f"done {self.env_vars_vs_code} updated with {extra_name} env-vars ✅")
|
174
153
|
|
175
|
-
|
176
|
-
class EnvVarsCheck(NamedTuple):
|
177
|
-
missing: list[str]
|
178
|
-
ambiguous: list[str]
|
179
|
-
|
180
|
-
|
181
|
-
class AtlasInitSettings(AtlasInitPaths, ExternalSettings):
|
182
|
-
model_config = SettingsConfigDict(env_prefix=ENV_PREFIX)
|
183
|
-
|
184
|
-
cfn_profile: str = ""
|
185
|
-
cfn_region: str = ""
|
186
|
-
cfn_use_kms_key: bool = False
|
187
|
-
project_name: str = ""
|
188
|
-
|
189
|
-
cliboard_copy: str = ""
|
190
|
-
test_suites: str = ""
|
191
|
-
|
192
|
-
@classmethod
|
193
|
-
def check_env_vars(
|
194
|
-
cls,
|
195
|
-
profile: str = DEFAULT_PROFILE,
|
196
|
-
required_env_vars: list[str] | None = None,
|
197
|
-
) -> EnvVarsCheck:
|
198
|
-
required_env_vars = required_env_vars or []
|
199
|
-
path_settings = AtlasInitPaths(profile=profile)
|
200
|
-
manual_env_vars = path_settings.manual_env_vars
|
201
|
-
ambiguous: list[str] = []
|
202
|
-
for env_name, manual_value in manual_env_vars.items():
|
203
|
-
env_value = read_from_env(env_name)
|
204
|
-
if env_value and manual_value != env_value:
|
205
|
-
ambiguous.append(env_name)
|
206
|
-
missing_env_vars = sorted(
|
207
|
-
env_name
|
208
|
-
for env_name in REQUIRED_FIELDS + required_env_vars
|
209
|
-
if read_from_env(env_name) == "" and env_name not in manual_env_vars
|
210
|
-
)
|
211
|
-
return EnvVarsCheck(missing=missing_env_vars, ambiguous=sorted(ambiguous))
|
212
|
-
|
213
|
-
@classmethod
|
214
|
-
def safe_settings(cls, profile: str, *, ext_settings: ExternalSettings | None = None) -> AtlasInitSettings:
|
215
|
-
"""side effect of loading manual env-vars and set profile"""
|
216
|
-
os.environ[ENV_PROFILE] = profile
|
217
|
-
AtlasInitPaths(profile=profile).load_profile_manual_env_vars()
|
218
|
-
ext_settings = ext_settings or ExternalSettings() # type: ignore
|
219
|
-
path_settings = AtlasInitPaths()
|
220
|
-
return cls(**path_settings.model_dump(), **ext_settings.model_dump())
|
221
|
-
|
222
|
-
@field_validator("test_suites", mode="after")
|
154
|
+
@field_validator(ENV_TEST_SUITES.lower(), mode="after")
|
223
155
|
@classmethod
|
224
156
|
def ensure_whitespace_replaced_with_commas(cls, value: str) -> str:
|
225
157
|
return value.strip().replace(" ", ",")
|
226
158
|
|
227
|
-
@model_validator(mode="after")
|
228
|
-
def post_init(self):
|
229
|
-
self.cfn_region = self.cfn_region or self.AWS_REGION
|
230
|
-
return self
|
231
|
-
|
232
159
|
@cached_property
|
233
160
|
def config(self) -> AtlasInitConfig:
|
234
|
-
config_path =
|
161
|
+
config_path = (
|
162
|
+
Path(self.atlas_init_config_path) if self.atlas_init_config_path else DEFAULT_ATLAS_INIT_CONFIG_PATH
|
163
|
+
)
|
235
164
|
assert config_path.exists(), f"no config path found @ {config_path}"
|
236
165
|
yaml_parsed = parse_payload(config_path)
|
237
166
|
assert isinstance(yaml_parsed, dict), f"config must be a dictionary, got {yaml_parsed}"
|
@@ -239,30 +168,35 @@ class AtlasInitSettings(AtlasInitPaths, ExternalSettings):
|
|
239
168
|
|
240
169
|
@property
|
241
170
|
def test_suites_parsed(self) -> list[str]:
|
242
|
-
return [t for t in self.
|
171
|
+
return [t for t in self.atlas_init_test_suites.split(",") if t]
|
243
172
|
|
244
|
-
def tf_vars(self) -> dict[str, Any]:
|
173
|
+
def tf_vars(self, default_aws_region: str) -> dict[str, Any]:
|
245
174
|
variables = {}
|
246
|
-
if self.
|
175
|
+
if self.atlas_init_cfn_profile:
|
247
176
|
variables["cfn_config"] = {
|
248
|
-
"profile": self.
|
249
|
-
"region": self.
|
250
|
-
"use_kms_key": self.
|
177
|
+
"profile": self.atlas_init_cfn_profile,
|
178
|
+
"region": self.atlas_init_cfn_region or default_aws_region,
|
179
|
+
"use_kms_key": self.atlas_init_cfn_use_kms_key,
|
251
180
|
}
|
252
|
-
if self.
|
181
|
+
if self.atlas_init_s3_profile_bucket:
|
253
182
|
variables["use_aws_s3"] = True
|
254
183
|
return variables
|
255
184
|
|
256
185
|
|
186
|
+
class EnvVarsCheck(NamedTuple):
|
187
|
+
missing: list[str]
|
188
|
+
ambiguous: list[str]
|
189
|
+
|
190
|
+
@property
|
191
|
+
def is_ok(self) -> bool:
|
192
|
+
return not self.missing and not self.ambiguous
|
193
|
+
|
194
|
+
|
257
195
|
def active_suites(settings: AtlasInitSettings) -> list[TestSuite]: # type: ignore
|
258
196
|
repo_path, cwd_rel_path = repo_path_rel_path()
|
259
197
|
return config_active_suites(settings.config, repo_path, cwd_rel_path, settings.test_suites_parsed)
|
260
198
|
|
261
199
|
|
262
|
-
_sentinel = object()
|
263
|
-
PLACEHOLDER_VALUE = "PLACEHOLDER"
|
264
|
-
|
265
|
-
|
266
200
|
class EnvVarsError(Exception):
|
267
201
|
def __init__(self, missing: list[str], ambiguous: list[str]):
|
268
202
|
self.missing = missing
|
@@ -273,31 +207,61 @@ class EnvVarsError(Exception):
|
|
273
207
|
return f"missing: {self.missing}, ambiguous: {self.ambiguous}"
|
274
208
|
|
275
209
|
|
210
|
+
def collect_required_env_vars(settings_classes: list[type[BaseModel]]) -> list[str]:
|
211
|
+
cls_required_env_vars: dict[str, list[str]] = {}
|
212
|
+
for cls in settings_classes:
|
213
|
+
try:
|
214
|
+
cls()
|
215
|
+
except ValidationError as error:
|
216
|
+
cls_required_env_vars[cls.__name__] = [".".join(str(loc) for loc in e["loc"]) for e in error.errors()]
|
217
|
+
return list(iter_utils.flat_map(cls_required_env_vars.values()))
|
218
|
+
|
219
|
+
|
220
|
+
def detect_ambiguous_env_vars(manual_env_vars: dict[str, str]) -> list[str]:
|
221
|
+
ambiguous: list[str] = []
|
222
|
+
for env_name, manual_value in manual_env_vars.items():
|
223
|
+
env_value = read_from_env(env_name)
|
224
|
+
if env_value and manual_value != env_value:
|
225
|
+
ambiguous.append(env_name)
|
226
|
+
return ambiguous
|
227
|
+
|
228
|
+
|
229
|
+
def find_missing_env_vars(required_env_vars: list[str], manual_env_vars: dict[str, str]) -> list[str]:
|
230
|
+
return sorted(
|
231
|
+
env_name for env_name in required_env_vars if read_from_env(env_name) == "" and env_name not in manual_env_vars
|
232
|
+
)
|
233
|
+
|
234
|
+
|
276
235
|
def init_settings(
|
277
|
-
|
278
|
-
*,
|
279
|
-
non_required: bool = False,
|
236
|
+
*settings_classes: type[BaseModel],
|
280
237
|
) -> AtlasInitSettings:
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
if
|
291
|
-
logger.warning(f"missing env_vars: {missing_env_vars}")
|
292
|
-
if ambiguous_env_vars:
|
238
|
+
settings = AtlasInitSettings.from_env()
|
239
|
+
profile_env_vars = settings.manual_env_vars
|
240
|
+
vscode_env_vars = settings.env_vars_vs_code
|
241
|
+
if vscode_env_vars.exists():
|
242
|
+
profile_env_vars |= load_dotenv(vscode_env_vars)
|
243
|
+
required_env_vars = collect_required_env_vars(list(settings_classes))
|
244
|
+
ambiguous = detect_ambiguous_env_vars(profile_env_vars)
|
245
|
+
missing_env_vars = find_missing_env_vars(required_env_vars, profile_env_vars)
|
246
|
+
|
247
|
+
if ambiguous:
|
293
248
|
logger.warning(
|
294
|
-
f"
|
249
|
+
f"ambiguous env_vars: {ambiguous} (specified both in cli/env & in .env-(manual|vscode) file with different values)"
|
295
250
|
)
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
251
|
+
if missing_env_vars or ambiguous:
|
252
|
+
raise EnvVarsError(missing_env_vars, ambiguous)
|
253
|
+
|
254
|
+
if new_updates := {k: v for k, v in profile_env_vars.items() if k not in os.environ}:
|
255
|
+
logger.info(f"loading env-vars {','.join(sorted(new_updates))}")
|
256
|
+
os.environ |= new_updates
|
257
|
+
for cls in settings_classes:
|
258
|
+
cls() # ensure any errors are raised
|
259
|
+
return AtlasInitSettings.from_env()
|
260
|
+
|
261
|
+
|
262
|
+
def env_vars_cls_or_none(t: type[T], *, dotenv_path: Path | None = None) -> T | None:
|
263
|
+
explicit_vars: dict[str, str] = {}
|
264
|
+
if dotenv_path and dotenv_path.exists():
|
265
|
+
explicit_vars = load_dotenv(dotenv_path)
|
266
|
+
with suppress(ValidationError):
|
267
|
+
return t(**explicit_vars)
|
@@ -1,18 +1,55 @@
|
|
1
1
|
import random
|
2
|
+
from typing import Self
|
2
3
|
|
3
|
-
from pydantic import ConfigDict, Field
|
4
|
+
from pydantic import ConfigDict, Field, model_validator
|
4
5
|
from pydantic_settings import BaseSettings
|
5
6
|
|
6
7
|
|
7
8
|
class _EnvVarsGenerated(BaseSettings):
|
8
9
|
model_config = ConfigDict(extra="ignore") # type: ignore
|
9
10
|
|
11
|
+
@classmethod
|
12
|
+
def from_env(cls) -> Self:
|
13
|
+
return cls()
|
14
|
+
|
10
15
|
|
11
16
|
class AtlasSettings(_EnvVarsGenerated):
|
12
17
|
MONGODB_ATLAS_ORG_ID: str
|
13
18
|
MONGODB_ATLAS_PRIVATE_KEY: str
|
14
19
|
MONGODB_ATLAS_PUBLIC_KEY: str
|
15
|
-
MONGODB_ATLAS_BASE_URL: str
|
20
|
+
MONGODB_ATLAS_BASE_URL: str
|
21
|
+
|
22
|
+
@property
|
23
|
+
def realm_url(self) -> str:
|
24
|
+
assert not self.is_mongodbgov_cloud, "realm_url is not supported for mongodbgov cloud"
|
25
|
+
if "cloud-dev." in self.MONGODB_ATLAS_BASE_URL:
|
26
|
+
return "https://services.cloud-dev.mongodb.com/"
|
27
|
+
return "https://services.cloud.mongodb.com/"
|
28
|
+
|
29
|
+
@property
|
30
|
+
def is_mongodbgov_cloud(self) -> bool:
|
31
|
+
return "mongodbgov" in self.MONGODB_ATLAS_BASE_URL
|
32
|
+
|
33
|
+
|
34
|
+
class AWSSettings(_EnvVarsGenerated):
|
35
|
+
AWS_REGION: str
|
36
|
+
AWS_PROFILE: str = ""
|
37
|
+
AWS_ACCESS_KEY_ID: str = ""
|
38
|
+
AWS_SECRET_ACCESS_KEY: str = ""
|
39
|
+
|
40
|
+
@model_validator(mode="after")
|
41
|
+
def ensure_credentials_are_given(self) -> Self:
|
42
|
+
assert self.AWS_PROFILE or (self.AWS_ACCESS_KEY_ID and self.AWS_SECRET_ACCESS_KEY), (
|
43
|
+
"Either AWS_PROFILE or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be provided"
|
44
|
+
)
|
45
|
+
assert not (self.AWS_PROFILE and (self.AWS_ACCESS_KEY_ID and self.AWS_SECRET_ACCESS_KEY)), (
|
46
|
+
"Either AWS_PROFILE or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be provided, not both"
|
47
|
+
)
|
48
|
+
return self
|
49
|
+
|
50
|
+
|
51
|
+
class TerraformSettings(_EnvVarsGenerated):
|
52
|
+
TF_CLI_CONFIG_FILE: str
|
16
53
|
|
17
54
|
|
18
55
|
class RealmSettings(_EnvVarsGenerated):
|
@@ -24,11 +61,5 @@ class RealmSettings(_EnvVarsGenerated):
|
|
24
61
|
RANDOM_INT_100K: str = Field(default_factory=lambda: str(random.randint(0, 100_000))) # noqa: S311 # not used for cryptographic purposes # nosec
|
25
62
|
|
26
63
|
|
27
|
-
class
|
64
|
+
class AtlasSettingsWithProject(AtlasSettings):
|
28
65
|
MONGODB_ATLAS_PROJECT_ID: str
|
29
|
-
|
30
|
-
|
31
|
-
class TFModuleCluster(_EnvVarsGenerated):
|
32
|
-
MONGODB_ATLAS_CLUSTER_NAME: str
|
33
|
-
MONGODB_ATLAS_CONTAINER_ID: str
|
34
|
-
MONGODB_URL: str
|