atlas-init 0.4.2__py3-none-any.whl → 0.4.4__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 +3 -7
- atlas_init/cli.py +2 -1
- atlas_init/cli_cfn/app.py +3 -5
- atlas_init/cli_cfn/cfn_parameter_finder.py +1 -28
- atlas_init/cli_cfn/contract.py +5 -3
- atlas_init/cli_cfn/example.py +8 -6
- atlas_init/cli_helper/go.py +18 -14
- atlas_init/cli_helper/tf_runner.py +14 -11
- atlas_init/cli_root/trigger.py +21 -8
- atlas_init/cli_tf/app.py +1 -1
- atlas_init/cli_tf/debug_logs.py +4 -4
- atlas_init/cli_tf/example_update.py +3 -3
- 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 +4 -3
- atlas_init/cli_tf/schema_v2.py +2 -2
- atlas_init/cli_tf/schema_v2_api_parsing.py +3 -3
- 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 +119 -142
- atlas_init/settings/env_vars_generated.py +40 -9
- atlas_init/settings/env_vars_modules.py +71 -0
- atlas_init/settings/path.py +4 -10
- atlas_init/typer_app.py +3 -3
- {atlas_init-0.4.2.dist-info → atlas_init-0.4.4.dist-info}/METADATA +6 -5
- {atlas_init-0.4.2.dist-info → atlas_init-0.4.4.dist-info}/RECORD +29 -32
- atlas_init-0.4.4.dist-info/licenses/LICENSE +21 -0
- 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.2.dist-info → atlas_init-0.4.4.dist-info}/WHEEL +0 -0
- {atlas_init-0.4.2.dist-info → atlas_init-0.4.4.dist-info}/entry_points.txt +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
|
|
@@ -149,7 +149,7 @@ def is_cache_up_to_date(cache_path: Path, cache_ttl: int) -> bool:
|
|
149
149
|
if cache_path.exists():
|
150
150
|
modified_ts = file_utils.file_modified_time(cache_path)
|
151
151
|
if modified_ts > time.time() - cache_ttl:
|
152
|
-
logger.info(f"using cached admin api: {cache_path} downloaded {time.time()-modified_ts:.0f}s ago")
|
152
|
+
logger.info(f"using cached admin api: {cache_path} downloaded {time.time() - modified_ts:.0f}s ago")
|
153
153
|
return True
|
154
154
|
return False
|
155
155
|
|
@@ -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_v2.py
CHANGED
@@ -466,8 +466,8 @@ def generate_go_attribute_schema_lines(
|
|
466
466
|
attr_name = attr.tf_name
|
467
467
|
lines = [indent(line_indent, f'"{attr_name}": {attribute_header(attr)}{{')]
|
468
468
|
if desc := attr.description or attr.is_nested and (desc := schema.ref_resource(attr.schema_ref).description):
|
469
|
-
lines.append(indent(line_indent + 1, f'Description: "{desc.replace(
|
470
|
-
lines.append(indent(line_indent + 1, f'MarkdownDescription: "{desc.replace(
|
469
|
+
lines.append(indent(line_indent + 1, f'Description: "{desc.replace("\n", "\\n")}",'))
|
470
|
+
lines.append(indent(line_indent + 1, f'MarkdownDescription: "{desc.replace("\n", "\\n")}",'))
|
471
471
|
if attr.is_required:
|
472
472
|
lines.append(indent(line_indent + 1, "Required: true,"))
|
473
473
|
if attr.is_optional:
|
@@ -32,9 +32,9 @@ def api_spec_text_changes(schema: SchemaV2, api_spec_parsed: OpenapiSchema) -> O
|
|
32
32
|
if name.startswith(prefix):
|
33
33
|
schema_to_update.pop(name)
|
34
34
|
name_no_prefix = name.removeprefix(prefix)
|
35
|
-
assert (
|
36
|
-
|
37
|
-
)
|
35
|
+
assert name_no_prefix not in schema_to_update, (
|
36
|
+
f"removed {prefix} from {name} in schema but {name_no_prefix} already exists"
|
37
|
+
)
|
38
38
|
schema_to_update[name_no_prefix] = value
|
39
39
|
openapi_yaml = dump(api_spec_parsed, "yaml")
|
40
40
|
for prefix in openapi_changes.schema_prefix_removal:
|
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,8 +125,24 @@ class AtlasInitPaths(BaseSettings):
|
|
135
125
|
def tf_outputs_path(self) -> Path:
|
136
126
|
return self.profile_dir / "tf_outputs.json"
|
137
127
|
|
138
|
-
|
139
|
-
|
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
|
140
146
|
|
141
147
|
def load_env_vars_full(self) -> dict[str, str]:
|
142
148
|
env_path = self.env_vars_vs_code
|
@@ -149,7 +155,7 @@ class AtlasInitPaths(BaseSettings):
|
|
149
155
|
|
150
156
|
def env_vars_cls(self, t: type[T], *, path: Path | None = None) -> T:
|
151
157
|
path = path or self.env_vars_vs_code
|
152
|
-
env_vars =
|
158
|
+
env_vars = load_dotenv(path) if path.exists() else {}
|
153
159
|
return t(**env_vars)
|
154
160
|
|
155
161
|
def load_profile_manual_env_vars(self, *, skip_os_update: bool = False) -> dict[str, str]:
|
@@ -161,77 +167,25 @@ class AtlasInitPaths(BaseSettings):
|
|
161
167
|
if new_updates := {k: v for k, v in manual_env_vars.items() if k not in os.environ}:
|
162
168
|
logger.info(f"loading manual env-vars {','.join(new_updates)}")
|
163
169
|
os.environ.update(new_updates)
|
164
|
-
else:
|
165
|
-
logger.warning(f"no {self.env_file_manual} exists")
|
166
170
|
return manual_env_vars
|
167
171
|
|
168
172
|
def include_extra_env_vars_in_vscode(self, extra_env_vars: dict[str, str]) -> None:
|
169
173
|
extra_name = ", ".join(extra_env_vars.keys())
|
170
|
-
original_env_vars =
|
174
|
+
original_env_vars = load_dotenv(self.env_vars_vs_code)
|
171
175
|
new_env_vars = original_env_vars | extra_env_vars
|
172
176
|
dump_dotenv(self.env_vars_vs_code, new_env_vars)
|
173
177
|
logger.info(f"done {self.env_vars_vs_code} updated with {extra_name} env-vars ✅")
|
174
178
|
|
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")
|
179
|
+
@field_validator(ENV_TEST_SUITES.lower(), mode="after")
|
223
180
|
@classmethod
|
224
181
|
def ensure_whitespace_replaced_with_commas(cls, value: str) -> str:
|
225
182
|
return value.strip().replace(" ", ",")
|
226
183
|
|
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
184
|
@cached_property
|
233
185
|
def config(self) -> AtlasInitConfig:
|
234
|
-
config_path =
|
186
|
+
config_path = (
|
187
|
+
Path(self.atlas_init_config_path) if self.atlas_init_config_path else DEFAULT_ATLAS_INIT_CONFIG_PATH
|
188
|
+
)
|
235
189
|
assert config_path.exists(), f"no config path found @ {config_path}"
|
236
190
|
yaml_parsed = parse_payload(config_path)
|
237
191
|
assert isinstance(yaml_parsed, dict), f"config must be a dictionary, got {yaml_parsed}"
|
@@ -239,30 +193,35 @@ class AtlasInitSettings(AtlasInitPaths, ExternalSettings):
|
|
239
193
|
|
240
194
|
@property
|
241
195
|
def test_suites_parsed(self) -> list[str]:
|
242
|
-
return [t for t in self.
|
196
|
+
return [t for t in self.atlas_init_test_suites.split(",") if t]
|
243
197
|
|
244
|
-
def tf_vars(self) -> dict[str, Any]:
|
198
|
+
def tf_vars(self, default_aws_region: str) -> dict[str, Any]:
|
245
199
|
variables = {}
|
246
|
-
if self.
|
200
|
+
if self.atlas_init_cfn_profile:
|
247
201
|
variables["cfn_config"] = {
|
248
|
-
"profile": self.
|
249
|
-
"region": self.
|
250
|
-
"use_kms_key": self.
|
202
|
+
"profile": self.atlas_init_cfn_profile,
|
203
|
+
"region": self.atlas_init_cfn_region or default_aws_region,
|
204
|
+
"use_kms_key": self.atlas_init_cfn_use_kms_key,
|
251
205
|
}
|
252
|
-
if self.
|
206
|
+
if self.atlas_init_s3_profile_bucket:
|
253
207
|
variables["use_aws_s3"] = True
|
254
208
|
return variables
|
255
209
|
|
256
210
|
|
211
|
+
class EnvVarsCheck(NamedTuple):
|
212
|
+
missing: list[str]
|
213
|
+
ambiguous: list[str]
|
214
|
+
|
215
|
+
@property
|
216
|
+
def is_ok(self) -> bool:
|
217
|
+
return not self.missing and not self.ambiguous
|
218
|
+
|
219
|
+
|
257
220
|
def active_suites(settings: AtlasInitSettings) -> list[TestSuite]: # type: ignore
|
258
221
|
repo_path, cwd_rel_path = repo_path_rel_path()
|
259
222
|
return config_active_suites(settings.config, repo_path, cwd_rel_path, settings.test_suites_parsed)
|
260
223
|
|
261
224
|
|
262
|
-
_sentinel = object()
|
263
|
-
PLACEHOLDER_VALUE = "PLACEHOLDER"
|
264
|
-
|
265
|
-
|
266
225
|
class EnvVarsError(Exception):
|
267
226
|
def __init__(self, missing: list[str], ambiguous: list[str]):
|
268
227
|
self.missing = missing
|
@@ -273,31 +232,49 @@ class EnvVarsError(Exception):
|
|
273
232
|
return f"missing: {self.missing}, ambiguous: {self.ambiguous}"
|
274
233
|
|
275
234
|
|
235
|
+
def collect_required_env_vars(settings_classes: list[type[BaseModel]]) -> list[str]:
|
236
|
+
cls_required_env_vars: dict[str, list[str]] = {}
|
237
|
+
for cls in settings_classes:
|
238
|
+
try:
|
239
|
+
cls()
|
240
|
+
except ValidationError as error:
|
241
|
+
cls_required_env_vars[cls.__name__] = [".".join(str(loc) for loc in e["loc"]) for e in error.errors()]
|
242
|
+
return list(iter_utils.flat_map(cls_required_env_vars.values()))
|
243
|
+
|
244
|
+
|
245
|
+
def detect_ambiguous_env_vars(manual_env_vars: dict[str, str]) -> list[str]:
|
246
|
+
ambiguous: list[str] = []
|
247
|
+
for env_name, manual_value in manual_env_vars.items():
|
248
|
+
env_value = read_from_env(env_name)
|
249
|
+
if env_value and manual_value != env_value:
|
250
|
+
ambiguous.append(env_name)
|
251
|
+
return ambiguous
|
252
|
+
|
253
|
+
|
254
|
+
def find_missing_env_vars(required_env_vars: list[str], manual_env_vars: dict[str, str]) -> list[str]:
|
255
|
+
return sorted(
|
256
|
+
env_name for env_name in required_env_vars if read_from_env(env_name) == "" and env_name not in manual_env_vars
|
257
|
+
)
|
258
|
+
|
259
|
+
|
276
260
|
def init_settings(
|
277
|
-
|
278
|
-
*,
|
279
|
-
non_required: bool = False,
|
261
|
+
*settings_classes: type[BaseModel],
|
280
262
|
) -> AtlasInitSettings:
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
missing_env_vars
|
287
|
-
|
288
|
-
|
289
|
-
)
|
290
|
-
if missing_env_vars and not non_required:
|
291
|
-
logger.warning(f"missing env_vars: {missing_env_vars}")
|
292
|
-
if ambiguous_env_vars:
|
263
|
+
settings = AtlasInitSettings.from_env()
|
264
|
+
manual_env_vars = settings.manual_env_vars
|
265
|
+
|
266
|
+
required_env_vars = collect_required_env_vars(list(settings_classes))
|
267
|
+
ambiguous = detect_ambiguous_env_vars(manual_env_vars)
|
268
|
+
missing_env_vars = find_missing_env_vars(required_env_vars, manual_env_vars)
|
269
|
+
|
270
|
+
if ambiguous:
|
293
271
|
logger.warning(
|
294
|
-
f"
|
272
|
+
f"ambiguous env_vars: {ambiguous} (specified both in cli/env & in .env-manual file with different values)"
|
295
273
|
)
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
return AtlasInitSettings.safe_settings(profile, ext_settings=ext_settings)
|
274
|
+
if missing_env_vars or ambiguous:
|
275
|
+
raise EnvVarsError(missing_env_vars, ambiguous)
|
276
|
+
|
277
|
+
settings.load_profile_manual_env_vars()
|
278
|
+
for cls in settings_classes:
|
279
|
+
cls() # ensure any errors are raised
|
280
|
+
return AtlasInitSettings.from_env()
|