atlas-init 0.4.1__py3-none-any.whl → 0.4.3__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 +4 -7
- atlas_init/atlas_init.yaml +3 -0
- atlas_init/cli_helper/go.py +103 -57
- atlas_init/cli_root/go_test.py +13 -10
- atlas_init/cli_tf/app.py +4 -0
- atlas_init/cli_tf/debug_logs.py +4 -4
- atlas_init/cli_tf/example_update.py +142 -0
- atlas_init/cli_tf/example_update_test/test_update_example.tf +23 -0
- atlas_init/cli_tf/example_update_test.py +96 -0
- atlas_init/cli_tf/github_logs.py +4 -1
- atlas_init/cli_tf/go_test_run.py +23 -0
- atlas_init/cli_tf/go_test_summary.py +7 -1
- atlas_init/cli_tf/hcl/modifier.py +144 -0
- atlas_init/cli_tf/hcl/modifier_test/test_process_variables_output_.tf +25 -0
- atlas_init/cli_tf/hcl/modifier_test/test_process_variables_variable_.tf +24 -0
- atlas_init/cli_tf/hcl/modifier_test.py +95 -0
- atlas_init/cli_tf/log_clean.py +29 -0
- atlas_init/cli_tf/mock_tf_log.py +1 -1
- atlas_init/cli_tf/schema_v2.py +2 -2
- atlas_init/cli_tf/schema_v2_api_parsing.py +3 -3
- atlas_init/repos/path.py +14 -0
- atlas_init/settings/config.py +24 -13
- atlas_init/settings/path.py +1 -1
- atlas_init/settings/rich_utils.py +1 -1
- atlas_init/tf/.terraform.lock.hcl +16 -16
- atlas_init/tf/main.tf +25 -1
- atlas_init/tf/modules/aws_kms/aws_kms.tf +100 -0
- atlas_init/tf/modules/aws_kms/provider.tf +7 -0
- atlas_init/tf/modules/cloud_provider/cloud_provider.tf +8 -1
- atlas_init/tf/modules/encryption_at_rest/main.tf +29 -0
- atlas_init/tf/modules/encryption_at_rest/provider.tf +9 -0
- atlas_init/tf/variables.tf +5 -0
- {atlas_init-0.4.1.dist-info → atlas_init-0.4.3.dist-info}/METADATA +12 -9
- {atlas_init-0.4.1.dist-info → atlas_init-0.4.3.dist-info}/RECORD +37 -24
- atlas_init-0.4.3.dist-info/licenses/LICENSE +21 -0
- {atlas_init-0.4.1.dist-info → atlas_init-0.4.3.dist-info}/WHEEL +0 -0
- {atlas_init-0.4.1.dist-info → atlas_init-0.4.3.dist-info}/entry_points.txt +0 -0
atlas_init/__init__.py
CHANGED
@@ -1,12 +1,9 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
|
3
|
-
VERSION = "0.4.
|
3
|
+
VERSION = "0.4.3"
|
4
4
|
|
5
5
|
|
6
6
|
def running_in_repo() -> bool:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
git_directory = py_directory.parent / ".git"
|
11
|
-
git_config = git_directory / "config"
|
12
|
-
return git_directory.exists() and git_config.exists() and "atlas-init" in git_config.read_text()
|
7
|
+
maybe_git_directory = Path(__file__).parent.parent / ".git"
|
8
|
+
git_config = maybe_git_directory / "config"
|
9
|
+
return git_config.exists() and "atlas-init" in git_config.read_text()
|
atlas_init/atlas_init.yaml
CHANGED
atlas_init/cli_helper/go.py
CHANGED
@@ -24,6 +24,7 @@ logger = logging.getLogger(__name__)
|
|
24
24
|
class GoTestMode(StrEnum):
|
25
25
|
package = "package"
|
26
26
|
individual = "individual"
|
27
|
+
regex = "regex"
|
27
28
|
|
28
29
|
|
29
30
|
class GoEnvVars(StrEnum):
|
@@ -31,6 +32,24 @@ class GoEnvVars(StrEnum):
|
|
31
32
|
vscode = "vscode"
|
32
33
|
|
33
34
|
|
35
|
+
class GoTestCaptureMode(StrEnum):
|
36
|
+
capture = "capture"
|
37
|
+
replay = "replay"
|
38
|
+
replay_and_update = "replay-and-update"
|
39
|
+
no_capture = "no-capture"
|
40
|
+
|
41
|
+
|
42
|
+
def env_vars_for_capture(mode: GoTestCaptureMode) -> dict[str, str]:
|
43
|
+
env = {}
|
44
|
+
if mode == GoTestCaptureMode.capture:
|
45
|
+
env["HTTP_MOCKER_CAPTURE"] = "true"
|
46
|
+
if mode in {GoTestCaptureMode.replay, GoTestCaptureMode.replay_and_update}:
|
47
|
+
env["HTTP_MOCKER_REPLAY"] = "true"
|
48
|
+
if mode == GoTestCaptureMode.replay_and_update:
|
49
|
+
env["HTTP_MOCKER_DATA_UPDATE"] = "true"
|
50
|
+
return env
|
51
|
+
|
52
|
+
|
34
53
|
class GoTestResult(Entity):
|
35
54
|
runs: dict[str, list[GoTestRun]] = Field(default_factory=dict)
|
36
55
|
failure_names: set[str] = Field(default_factory=set)
|
@@ -55,11 +74,9 @@ class GoTestResult(Entity):
|
|
55
74
|
|
56
75
|
def run_go_tests(
|
57
76
|
repo_path: Path,
|
58
|
-
repo_alias: str,
|
59
|
-
package_prefix: str,
|
60
77
|
settings: AtlasInitSettings,
|
61
78
|
groups: list[TestSuite], # type: ignore
|
62
|
-
mode: GoTestMode = GoTestMode.package,
|
79
|
+
mode: GoTestMode | str = GoTestMode.package,
|
63
80
|
*,
|
64
81
|
dry_run: bool = False,
|
65
82
|
timeout_minutes: int = 300,
|
@@ -67,37 +84,30 @@ def run_go_tests(
|
|
67
84
|
re_run: bool = False,
|
68
85
|
env_vars: GoEnvVars = GoEnvVars.vscode,
|
69
86
|
names: set[str] | None = None,
|
70
|
-
|
87
|
+
capture_mode: GoTestCaptureMode = GoTestCaptureMode.capture,
|
88
|
+
use_old_schema: bool = False,
|
71
89
|
) -> GoTestResult:
|
72
|
-
test_env =
|
90
|
+
test_env = resolve_env_vars(
|
91
|
+
settings,
|
92
|
+
env_vars,
|
93
|
+
capture_mode=capture_mode,
|
94
|
+
use_old_schema=use_old_schema,
|
95
|
+
)
|
73
96
|
if ci_value := test_env.pop("CI", None):
|
74
|
-
logger.warning(f"
|
97
|
+
logger.warning(f"popped CI={ci_value}")
|
75
98
|
results = GoTestResult()
|
76
99
|
commands_to_run: dict[str, str] = {}
|
77
100
|
for group in groups:
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
101
|
+
if group.sequential_tests:
|
102
|
+
logger.info(f"running individual tests sequentially as {group.name} is set to sequential_tests")
|
103
|
+
concurrent_runs = 1
|
104
|
+
group_commands_to_run = group_commands_for_mode(
|
105
|
+
repo_path, mode, concurrent_runs, timeout_minutes, names, results, group
|
106
|
+
)
|
107
|
+
if not group_commands_to_run:
|
108
|
+
logger.warning(f"no tests for suite: {group.name}")
|
82
109
|
continue
|
83
|
-
|
84
|
-
if group.sequential_tests:
|
85
|
-
logger.info(f"running individual tests sequentially as {group.name} is set to sequential_tests")
|
86
|
-
concurrent_runs = 1
|
87
|
-
test_names = find_individual_tests(repo_path, package_paths)
|
88
|
-
for name, pkg_path in test_names.items():
|
89
|
-
if names and name not in names:
|
90
|
-
continue
|
91
|
-
results.add_test_package_path(name, pkg_path)
|
92
|
-
commands_to_run[name] = f"go test {packages} -v -run ^{name}$ -timeout {timeout_minutes}m"
|
93
|
-
elif mode == GoTestMode.package:
|
94
|
-
name_regex = f'^({"|".join(names)})$' if names else "^TestAcc*"
|
95
|
-
command = f"go test {packages} -v -run {name_regex} -timeout {timeout_minutes}m"
|
96
|
-
if not group.sequential_tests:
|
97
|
-
command = f"{command} -parallel {concurrent_runs}"
|
98
|
-
commands_to_run[group.name] = command
|
99
|
-
else:
|
100
|
-
raise NotImplementedError(f"mode={mode}")
|
110
|
+
commands_to_run |= group_commands_to_run
|
101
111
|
commands_str = "\n".join(f"'{name}': '{command}'" for name, command in sorted(commands_to_run.items()))
|
102
112
|
logger.info(f"will run the following commands:\n{commands_str}")
|
103
113
|
if dry_run:
|
@@ -116,31 +126,63 @@ def run_go_tests(
|
|
116
126
|
)
|
117
127
|
|
118
128
|
|
119
|
-
def
|
129
|
+
def group_commands_for_mode(
|
130
|
+
repo_path: Path,
|
131
|
+
mode: GoTestMode | str,
|
132
|
+
concurrent_runs: int,
|
133
|
+
timeout_minutes: int,
|
134
|
+
names: set[str] | None,
|
135
|
+
results: GoTestResult,
|
136
|
+
group: TestSuite, # type: ignore
|
137
|
+
) -> dict[str, str]:
|
138
|
+
commands_to_run: dict[str, str] = {}
|
139
|
+
if mode == GoTestMode.package:
|
140
|
+
name_regex = f"^({'|'.join(names)})$" if names else "^TestAcc*"
|
141
|
+
for pkg_url in group.package_url_tests(repo_path):
|
142
|
+
command = f"go test {pkg_url} -v -run {name_regex} -timeout {timeout_minutes}m"
|
143
|
+
if not group.sequential_tests:
|
144
|
+
command = f"{command} -parallel {concurrent_runs}"
|
145
|
+
pkg_name = pkg_url.rsplit("/")[-1]
|
146
|
+
commands_to_run[f"{group.name}-{pkg_name}"] = command
|
147
|
+
return commands_to_run
|
148
|
+
if mode == GoTestMode.individual:
|
149
|
+
prefix = "TestAcc"
|
150
|
+
else:
|
151
|
+
logger.info(f"using {GoTestMode.regex} with {mode}")
|
152
|
+
prefix = mode
|
153
|
+
for pkg_url, tests in group.package_url_tests(repo_path, prefix=prefix).items():
|
154
|
+
for name, pkg_path in tests.items():
|
155
|
+
if names and name not in names:
|
156
|
+
continue
|
157
|
+
results.add_test_package_path(name, pkg_path)
|
158
|
+
commands_to_run[name] = f"go test {pkg_url} -v -run ^{name}$ -timeout {timeout_minutes}m"
|
159
|
+
return commands_to_run
|
160
|
+
|
161
|
+
|
162
|
+
def resolve_env_vars(
|
163
|
+
settings: AtlasInitSettings,
|
164
|
+
env_vars: GoEnvVars,
|
165
|
+
*,
|
166
|
+
capture_mode: GoTestCaptureMode,
|
167
|
+
use_old_schema: bool,
|
168
|
+
skip_os: bool = False,
|
169
|
+
) -> dict[str, str]:
|
120
170
|
if env_vars == GoEnvVars.manual:
|
121
|
-
|
171
|
+
test_env_vars = settings.load_profile_manual_env_vars(skip_os_update=True)
|
122
172
|
elif env_vars == GoEnvVars.vscode:
|
123
|
-
|
173
|
+
test_env_vars = settings.load_env_vars(settings.env_vars_vs_code)
|
124
174
|
else:
|
125
175
|
raise NotImplementedError(f"don't know how to load env_vars={env_vars}")
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
package_abs_path = repo_path / package_path.lstrip(".").lstrip("/")
|
137
|
-
for go_file in package_abs_path.glob("*.go"):
|
138
|
-
with go_file.open() as f:
|
139
|
-
for line in f:
|
140
|
-
if line.startswith("func TestAcc"):
|
141
|
-
test_name = line.split("(")[0].strip().removeprefix("func ")
|
142
|
-
tests[test_name] = package_abs_path
|
143
|
-
return tests
|
176
|
+
test_env_vars |= {
|
177
|
+
"TF_ACC": "1",
|
178
|
+
"TF_LOG": "DEBUG",
|
179
|
+
"MONGODB_ATLAS_PREVIEW_PROVIDER_V2_ADVANCED_CLUSTER": "false" if use_old_schema else "true",
|
180
|
+
}
|
181
|
+
test_env_vars |= env_vars_for_capture(capture_mode)
|
182
|
+
logger.info(f"go test env-vars-extra: {sorted(test_env_vars)}")
|
183
|
+
if not skip_os:
|
184
|
+
test_env_vars = os.environ | test_env_vars # os.environ on the left side, prefer explicit args
|
185
|
+
return test_env_vars
|
144
186
|
|
145
187
|
|
146
188
|
def _run_tests(
|
@@ -158,9 +200,13 @@ def _run_tests(
|
|
158
200
|
with ThreadPoolExecutor(max_workers=actual_workers) as pool:
|
159
201
|
for name, command in sorted(commands_to_run.items()):
|
160
202
|
log_path = _log_path(name)
|
161
|
-
if log_path.exists() and log_path.read_text()
|
162
|
-
|
163
|
-
|
203
|
+
if log_path.exists() and log_path.read_text():
|
204
|
+
if re_run:
|
205
|
+
logger.info(f"moving existing logs of {name} to old dir")
|
206
|
+
move_logs_to_dir({name}, dir_name="old")
|
207
|
+
else:
|
208
|
+
logger.info(f"skipping {name} because log exists")
|
209
|
+
continue
|
164
210
|
command_env = {**test_env, "TF_LOG_PATH": str(log_path)}
|
165
211
|
future = pool.submit(
|
166
212
|
run_command_is_ok_output,
|
@@ -204,20 +250,20 @@ def _run_tests(
|
|
204
250
|
if not results.add_test_results_all_pass(name, parsed_tests):
|
205
251
|
results.failure_names.add(name)
|
206
252
|
if failure_names := results.failure_names:
|
207
|
-
|
253
|
+
move_logs_to_dir(failure_names)
|
208
254
|
logger.error(f"failed to run tests: {sorted(failure_names)}")
|
209
255
|
return results
|
210
256
|
|
211
257
|
|
212
|
-
def
|
213
|
-
|
258
|
+
def move_logs_to_dir(names: set[str], dir_name: str = "failures"):
|
259
|
+
new_dir = DEFAULT_DOWNLOADS_DIR / dir_name
|
214
260
|
for log in DEFAULT_DOWNLOADS_DIR.glob("*.log"):
|
215
|
-
if log.stem in
|
261
|
+
if log.stem in names:
|
216
262
|
text = log.read_text()
|
217
263
|
assert "\n" in text
|
218
264
|
first_line = text.split("\n", maxsplit=1)[0]
|
219
265
|
ts = first_line.split(" ")[0]
|
220
|
-
log.rename(
|
266
|
+
log.rename(new_dir / f"{ts}.{log.name}")
|
221
267
|
|
222
268
|
|
223
269
|
def _log_path(name: str) -> Path:
|
atlas_init/cli_root/go_test.py
CHANGED
@@ -2,7 +2,7 @@ import logging
|
|
2
2
|
|
3
3
|
import typer
|
4
4
|
|
5
|
-
from atlas_init.cli_helper.go import GoEnvVars, GoTestMode, GoTestResult, run_go_tests
|
5
|
+
from atlas_init.cli_helper.go import GoEnvVars, GoTestCaptureMode, GoTestMode, GoTestResult, run_go_tests
|
6
6
|
from atlas_init.cli_tf.mock_tf_log import MockTFLog, mock_tf_log, resolve_admin_api_path
|
7
7
|
from atlas_init.repos.path import Repo, current_repo, current_repo_path
|
8
8
|
from atlas_init.settings.env_vars import active_suites, init_settings
|
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
|
13
13
|
|
14
14
|
@app_command()
|
15
15
|
def go_test(
|
16
|
-
mode:
|
16
|
+
mode: str = typer.Option("package", "-m", "--mode", help="package|individual or a prefix"),
|
17
17
|
dry_run: bool = typer.Option(False, help="only log out the commands to be run"),
|
18
18
|
timeout_minutes: int = typer.Option(300, "-t", "--timeout", help="timeout in minutes"),
|
19
19
|
concurrent_runs: int = typer.Option(20, "-c", "--concurrent", help="number of concurrent runs"),
|
@@ -22,11 +22,16 @@ def go_test(
|
|
22
22
|
export_mock_tf_log_verbose: bool = typer.Option(
|
23
23
|
False, "--export-verbose", help="log roundtrips when exporting the mock-tf-log"
|
24
24
|
),
|
25
|
-
env_method: GoEnvVars = typer.Option(GoEnvVars.manual, "--env"
|
25
|
+
env_method: GoEnvVars = typer.Option(GoEnvVars.manual, "--env"),
|
26
26
|
names: list[str] = typer.Option(
|
27
|
-
...,
|
27
|
+
...,
|
28
|
+
"-n",
|
29
|
+
"--names",
|
30
|
+
default_factory=list,
|
31
|
+
help="run only the tests with these names",
|
28
32
|
),
|
29
|
-
|
33
|
+
capture_mode: GoTestCaptureMode = typer.Option(GoTestCaptureMode.capture, "--capture"),
|
34
|
+
use_old_schema: bool = typer.Option(False, "--old-schema", help="use the old schema for the tests"),
|
30
35
|
):
|
31
36
|
if export_mock_tf_log and mode != GoTestMode.individual:
|
32
37
|
err_msg = "exporting mock-tf-log is only supported for individual tests"
|
@@ -36,16 +41,13 @@ def go_test(
|
|
36
41
|
sorted_suites = sorted(suite.name for suite in suites)
|
37
42
|
logger.info(f"running go tests for {len(suites)} test-suites: {sorted_suites}")
|
38
43
|
results: GoTestResult | None = None
|
39
|
-
match
|
44
|
+
match current_repo():
|
40
45
|
case Repo.CFN:
|
41
46
|
raise NotImplementedError
|
42
47
|
case Repo.TF:
|
43
48
|
repo_path = current_repo_path()
|
44
|
-
package_prefix = settings.config.go_package_prefix(repo_alias)
|
45
49
|
results = run_go_tests(
|
46
50
|
repo_path,
|
47
|
-
repo_alias,
|
48
|
-
package_prefix,
|
49
51
|
settings,
|
50
52
|
suites,
|
51
53
|
mode,
|
@@ -55,7 +57,8 @@ def go_test(
|
|
55
57
|
re_run=re_run,
|
56
58
|
env_vars=env_method,
|
57
59
|
names=set(names),
|
58
|
-
|
60
|
+
capture_mode=capture_mode,
|
61
|
+
use_old_schema=use_old_schema,
|
59
62
|
)
|
60
63
|
case _:
|
61
64
|
raise NotImplementedError
|
atlas_init/cli_tf/app.py
CHANGED
@@ -17,6 +17,7 @@ from atlas_init.cli_helper.run import (
|
|
17
17
|
run_command_receive_result,
|
18
18
|
)
|
19
19
|
from atlas_init.cli_tf.changelog import convert_to_changelog
|
20
|
+
from atlas_init.cli_tf.example_update import update_example_cmd
|
20
21
|
from atlas_init.cli_tf.github_logs import (
|
21
22
|
GH_TOKEN_ENV_NAME,
|
22
23
|
find_test_runs,
|
@@ -28,6 +29,7 @@ from atlas_init.cli_tf.go_test_summary import (
|
|
28
29
|
create_detailed_summary,
|
29
30
|
create_short_summary,
|
30
31
|
)
|
32
|
+
from atlas_init.cli_tf.log_clean import log_clean
|
31
33
|
from atlas_init.cli_tf.mock_tf_log import mock_tf_log_cmd
|
32
34
|
from atlas_init.cli_tf.schema import (
|
33
35
|
dump_generator_config,
|
@@ -48,6 +50,8 @@ from atlas_init.settings.interactive import confirm
|
|
48
50
|
|
49
51
|
app = typer.Typer(no_args_is_help=True)
|
50
52
|
app.command(name="mock-tf-log")(mock_tf_log_cmd)
|
53
|
+
app.command(name="example-update")(update_example_cmd)
|
54
|
+
app.command(name="log-clean")(log_clean)
|
51
55
|
logger = logging.getLogger(__name__)
|
52
56
|
|
53
57
|
|
atlas_init/cli_tf/debug_logs.py
CHANGED
@@ -225,7 +225,7 @@ def match_request(
|
|
225
225
|
step_number=step_number,
|
226
226
|
)
|
227
227
|
remaining_responses = [resp for i, resp in enumerate(responses_list) if i not in used_responses]
|
228
|
-
err_msg = f"Could not match request {request.path} ({ref}) with any response\n\n{request}\n\n\nThere are #{len(remaining_responses)} responses left that doesn't match\n{'-'*80}\n{'\n'.join(r.text for r in remaining_responses)}"
|
228
|
+
err_msg = f"Could not match request {request.path} ({ref}) with any response\n\n{request}\n\n\nThere are #{len(remaining_responses)} responses left that doesn't match\n{'-' * 80}\n{'\n'.join(r.text for r in remaining_responses)}"
|
229
229
|
raise ValueError(err_msg)
|
230
230
|
|
231
231
|
|
@@ -260,9 +260,9 @@ def parse_raw_req_responses(
|
|
260
260
|
in_response = False
|
261
261
|
assert not in_request, "Request not closed"
|
262
262
|
assert not in_response, "Response not closed"
|
263
|
-
assert (
|
264
|
-
request_count
|
265
|
-
)
|
263
|
+
assert request_count == response_count, (
|
264
|
+
f"Mismatch in request and response count: {request_count} != {response_count}"
|
265
|
+
)
|
266
266
|
parsed_requests = {}
|
267
267
|
for ref, request_lines in requests.items():
|
268
268
|
parsed_requests[ref] = parse_request(request_lines)
|
@@ -0,0 +1,142 @@
|
|
1
|
+
import logging
|
2
|
+
from collections import defaultdict
|
3
|
+
from functools import total_ordering
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
import typer
|
7
|
+
from model_lib import Entity, Event, dump, parse_payload
|
8
|
+
from pydantic import Field
|
9
|
+
|
10
|
+
from atlas_init.cli_helper.run import run_binary_command_is_ok
|
11
|
+
from atlas_init.cli_tf.hcl.modifier import (
|
12
|
+
BLOCK_TYPE_OUTPUT,
|
13
|
+
BLOCK_TYPE_VARIABLE,
|
14
|
+
update_descriptions,
|
15
|
+
)
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
class UpdateExamples(Entity):
|
21
|
+
examples_base_dir: Path
|
22
|
+
var_descriptions: dict[str, str]
|
23
|
+
output_descriptions: dict[str, str] = Field(default_factory=dict)
|
24
|
+
skip_tf_fmt: bool = False
|
25
|
+
|
26
|
+
|
27
|
+
@total_ordering
|
28
|
+
class TFConfigDescriptionChange(Event):
|
29
|
+
path: Path
|
30
|
+
name: str
|
31
|
+
before: str
|
32
|
+
after: str
|
33
|
+
block_type: str
|
34
|
+
|
35
|
+
@property
|
36
|
+
def changed(self) -> bool:
|
37
|
+
return self.after not in ("", self.before)
|
38
|
+
|
39
|
+
def __lt__(self, other) -> bool:
|
40
|
+
if not isinstance(other, TFConfigDescriptionChange):
|
41
|
+
raise TypeError
|
42
|
+
return (self.path, self.name) < (other.path, other.name)
|
43
|
+
|
44
|
+
|
45
|
+
class UpdateExamplesOutput(Entity):
|
46
|
+
before_var_descriptions: dict[str, str] = Field(default_factory=dict)
|
47
|
+
before_output_descriptions: dict[str, str] = Field(default_factory=dict)
|
48
|
+
changes: list[TFConfigDescriptionChange] = Field(default_factory=list)
|
49
|
+
|
50
|
+
|
51
|
+
def update_examples(event_in: UpdateExamples) -> UpdateExamplesOutput:
|
52
|
+
changes = []
|
53
|
+
existing_var_descriptions = update_block_descriptions(
|
54
|
+
event_in.examples_base_dir,
|
55
|
+
changes,
|
56
|
+
event_in.var_descriptions,
|
57
|
+
BLOCK_TYPE_VARIABLE,
|
58
|
+
)
|
59
|
+
existing_output_descriptions = update_block_descriptions(
|
60
|
+
event_in.examples_base_dir,
|
61
|
+
changes,
|
62
|
+
event_in.output_descriptions,
|
63
|
+
BLOCK_TYPE_OUTPUT,
|
64
|
+
)
|
65
|
+
if event_in.skip_tf_fmt:
|
66
|
+
logger.info("skipping terraform fmt")
|
67
|
+
else:
|
68
|
+
assert run_binary_command_is_ok("terraform", "fmt -recursive", cwd=event_in.examples_base_dir, logger=logger), (
|
69
|
+
"terraform fmt failed"
|
70
|
+
)
|
71
|
+
return UpdateExamplesOutput(
|
72
|
+
before_var_descriptions=flatten_descriptions(existing_var_descriptions),
|
73
|
+
before_output_descriptions=flatten_descriptions(existing_output_descriptions),
|
74
|
+
changes=sorted(changes),
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
def flatten_descriptions(descriptions: dict[str, list[str]]) -> dict[str, str]:
|
79
|
+
return {
|
80
|
+
key: "\n".join(desc for desc in sorted(set(descriptions)) if desc != "")
|
81
|
+
for key, descriptions in descriptions.items()
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
def update_block_descriptions(
|
86
|
+
base_dir: Path,
|
87
|
+
changes: list[TFConfigDescriptionChange],
|
88
|
+
new_names: dict[str, str],
|
89
|
+
block_type: str,
|
90
|
+
):
|
91
|
+
all_existing_descriptions = defaultdict(list)
|
92
|
+
in_files = sorted(base_dir.rglob("*.tf"))
|
93
|
+
for tf_file in in_files:
|
94
|
+
logger.info(f"looking for {block_type} in {tf_file}")
|
95
|
+
new_tf, existing_descriptions = update_descriptions(tf_file, new_names, block_type=block_type)
|
96
|
+
if not existing_descriptions: # probably no variables in the file
|
97
|
+
continue
|
98
|
+
for name, descriptions in existing_descriptions.items():
|
99
|
+
changes.extend(
|
100
|
+
TFConfigDescriptionChange(
|
101
|
+
path=tf_file,
|
102
|
+
name=name,
|
103
|
+
before=description,
|
104
|
+
after=new_names.get(name, ""),
|
105
|
+
block_type=block_type,
|
106
|
+
)
|
107
|
+
for description in descriptions
|
108
|
+
)
|
109
|
+
all_existing_descriptions[name].extend(descriptions)
|
110
|
+
if tf_file.read_text() == new_tf:
|
111
|
+
logger.debug(f"no {block_type} changes for {tf_file}")
|
112
|
+
continue
|
113
|
+
tf_file.write_text(new_tf)
|
114
|
+
return all_existing_descriptions
|
115
|
+
|
116
|
+
|
117
|
+
def update_example_cmd(
|
118
|
+
examples_base_dir: Path = typer.Argument(
|
119
|
+
..., help="Directory containing *.tf files (can have many subdirectories)"
|
120
|
+
),
|
121
|
+
var_descriptions: Path = typer.Option("", "--vars", help="Path to a JSON/yaml file with variable descriptions"),
|
122
|
+
output_descriptions: Path = typer.Option("", "--outputs", help="Path to a JSON/yaml file with output descriptions"),
|
123
|
+
skip_log_existing: bool = typer.Option(False, help="Log existing descriptions"),
|
124
|
+
skip_log_changes: bool = typer.Option(False, help="Log variable updates"),
|
125
|
+
):
|
126
|
+
var_descriptions_dict = parse_payload(var_descriptions) if var_descriptions else {}
|
127
|
+
output_descriptions_dict = parse_payload(output_descriptions) if output_descriptions else {}
|
128
|
+
event = UpdateExamples(
|
129
|
+
examples_base_dir=examples_base_dir,
|
130
|
+
var_descriptions=var_descriptions_dict, # type: ignore
|
131
|
+
output_descriptions=output_descriptions_dict, # type: ignore
|
132
|
+
)
|
133
|
+
output = update_examples(event)
|
134
|
+
if not skip_log_changes:
|
135
|
+
for change in output.changes:
|
136
|
+
if change.changed:
|
137
|
+
logger.info(f"{change.path}({change.block_type}) {change.name}: {change.before} -> {change.after}")
|
138
|
+
if not skip_log_existing:
|
139
|
+
existing_var_yaml = dump(output.before_var_descriptions, "yaml")
|
140
|
+
logger.info(f"Existing Variables:\n{existing_var_yaml}")
|
141
|
+
existing_output_yaml = dump(output.before_output_descriptions, "yaml")
|
142
|
+
logger.info(f"Existing Outputs:\n{existing_output_yaml}")
|
@@ -0,0 +1,23 @@
|
|
1
|
+
variable "cluster_name" {
|
2
|
+
description = "description of cluster name"
|
3
|
+
type = string
|
4
|
+
}
|
5
|
+
variable "replication_specs" {
|
6
|
+
description = "Updated description"
|
7
|
+
default = []
|
8
|
+
type = list(object({
|
9
|
+
num_shards = number
|
10
|
+
zone_name = string
|
11
|
+
regions_config = set(object({
|
12
|
+
region_name = string
|
13
|
+
electable_nodes = number
|
14
|
+
priority = number
|
15
|
+
read_only_nodes = optional(number, 0)
|
16
|
+
}))
|
17
|
+
}))
|
18
|
+
}
|
19
|
+
|
20
|
+
variable "provider_name" {
|
21
|
+
type = string
|
22
|
+
default = "" # optional in v3
|
23
|
+
}
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import pytest
|
5
|
+
|
6
|
+
from atlas_init.cli_tf.example_update import (
|
7
|
+
TFConfigDescriptionChange,
|
8
|
+
UpdateExamples,
|
9
|
+
update_examples,
|
10
|
+
)
|
11
|
+
from atlas_init.cli_tf.hcl.modifier import BLOCK_TYPE_VARIABLE, update_descriptions
|
12
|
+
|
13
|
+
|
14
|
+
def test_description_change(tmp_path):
|
15
|
+
assert TFConfigDescriptionChange(
|
16
|
+
block_type=BLOCK_TYPE_VARIABLE,
|
17
|
+
path=tmp_path,
|
18
|
+
name="cluster_name",
|
19
|
+
before="",
|
20
|
+
after="description of cluster name",
|
21
|
+
).changed
|
22
|
+
assert not TFConfigDescriptionChange(
|
23
|
+
block_type=BLOCK_TYPE_VARIABLE,
|
24
|
+
path=tmp_path,
|
25
|
+
name="cluster_name",
|
26
|
+
before="description of cluster name",
|
27
|
+
after="description of cluster name",
|
28
|
+
).changed
|
29
|
+
assert not TFConfigDescriptionChange(
|
30
|
+
block_type=BLOCK_TYPE_VARIABLE,
|
31
|
+
path=tmp_path,
|
32
|
+
name="cluster_name",
|
33
|
+
before="description of cluster name",
|
34
|
+
after="",
|
35
|
+
).changed
|
36
|
+
|
37
|
+
|
38
|
+
example_variables_tf = """variable "cluster_name" {
|
39
|
+
type = string
|
40
|
+
}
|
41
|
+
variable "replication_specs" {
|
42
|
+
description = "List of replication specifications in legacy mongodbatlas_cluster format"
|
43
|
+
default = []
|
44
|
+
type = list(object({
|
45
|
+
num_shards = number
|
46
|
+
zone_name = string
|
47
|
+
regions_config = set(object({
|
48
|
+
region_name = string
|
49
|
+
electable_nodes = number
|
50
|
+
priority = number
|
51
|
+
read_only_nodes = optional(number, 0)
|
52
|
+
}))
|
53
|
+
}))
|
54
|
+
}
|
55
|
+
|
56
|
+
variable "provider_name" {
|
57
|
+
type = string
|
58
|
+
default = "" # optional in v3
|
59
|
+
}
|
60
|
+
"""
|
61
|
+
|
62
|
+
|
63
|
+
def test_update_example(tmp_path, file_regression):
|
64
|
+
base_dir = tmp_path / "example_base"
|
65
|
+
base_dir.mkdir()
|
66
|
+
example_variables_tf_path = base_dir / "example_variables.tf"
|
67
|
+
example_variables_tf_path.write_text(example_variables_tf)
|
68
|
+
output = update_examples(
|
69
|
+
UpdateExamples(
|
70
|
+
examples_base_dir=base_dir,
|
71
|
+
var_descriptions={
|
72
|
+
"cluster_name": "description of cluster name",
|
73
|
+
"replication_specs": "Updated description",
|
74
|
+
},
|
75
|
+
)
|
76
|
+
)
|
77
|
+
assert output.before_var_descriptions == {
|
78
|
+
"cluster_name": "",
|
79
|
+
"provider_name": "",
|
80
|
+
"replication_specs": "List of replication specifications in legacy mongodbatlas_cluster format",
|
81
|
+
}
|
82
|
+
assert len(output.changes) == 3 # noqa: PLR2004
|
83
|
+
assert [
|
84
|
+
("cluster_name", True),
|
85
|
+
("provider_name", False),
|
86
|
+
("replication_specs", True),
|
87
|
+
] == [(change.name, change.changed) for change in output.changes]
|
88
|
+
file_regression.check(example_variables_tf_path.read_text(), extension=".tf")
|
89
|
+
|
90
|
+
|
91
|
+
@pytest.mark.skipif(os.environ.get("TF_FILE", "") == "", reason="needs os.environ[TF_FILE]")
|
92
|
+
def test_parsing_tf_file():
|
93
|
+
file = Path(os.environ["TF_FILE"])
|
94
|
+
assert file.exists()
|
95
|
+
response, _ = update_descriptions(file, {}, block_type=BLOCK_TYPE_VARIABLE)
|
96
|
+
assert response
|
atlas_init/cli_tf/github_logs.py
CHANGED
@@ -130,7 +130,10 @@ def parse_job_logs(job: WorkflowJob, logs_path: Path) -> list[GoTestRun]:
|
|
130
130
|
if job.conclusion in {"skipped", "cancelled", None}:
|
131
131
|
return []
|
132
132
|
step, logs_lines = select_step_and_log_content(job, logs_path)
|
133
|
-
|
133
|
+
test_runs = list(parse(logs_lines, job, step))
|
134
|
+
for run in test_runs:
|
135
|
+
run.log_path = logs_path
|
136
|
+
return test_runs
|
134
137
|
|
135
138
|
|
136
139
|
def download_job_safely(workflow_dir: Path, job: WorkflowJob) -> Path | None:
|