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.
Files changed (37) hide show
  1. atlas_init/__init__.py +4 -7
  2. atlas_init/atlas_init.yaml +3 -0
  3. atlas_init/cli_helper/go.py +103 -57
  4. atlas_init/cli_root/go_test.py +13 -10
  5. atlas_init/cli_tf/app.py +4 -0
  6. atlas_init/cli_tf/debug_logs.py +4 -4
  7. atlas_init/cli_tf/example_update.py +142 -0
  8. atlas_init/cli_tf/example_update_test/test_update_example.tf +23 -0
  9. atlas_init/cli_tf/example_update_test.py +96 -0
  10. atlas_init/cli_tf/github_logs.py +4 -1
  11. atlas_init/cli_tf/go_test_run.py +23 -0
  12. atlas_init/cli_tf/go_test_summary.py +7 -1
  13. atlas_init/cli_tf/hcl/modifier.py +144 -0
  14. atlas_init/cli_tf/hcl/modifier_test/test_process_variables_output_.tf +25 -0
  15. atlas_init/cli_tf/hcl/modifier_test/test_process_variables_variable_.tf +24 -0
  16. atlas_init/cli_tf/hcl/modifier_test.py +95 -0
  17. atlas_init/cli_tf/log_clean.py +29 -0
  18. atlas_init/cli_tf/mock_tf_log.py +1 -1
  19. atlas_init/cli_tf/schema_v2.py +2 -2
  20. atlas_init/cli_tf/schema_v2_api_parsing.py +3 -3
  21. atlas_init/repos/path.py +14 -0
  22. atlas_init/settings/config.py +24 -13
  23. atlas_init/settings/path.py +1 -1
  24. atlas_init/settings/rich_utils.py +1 -1
  25. atlas_init/tf/.terraform.lock.hcl +16 -16
  26. atlas_init/tf/main.tf +25 -1
  27. atlas_init/tf/modules/aws_kms/aws_kms.tf +100 -0
  28. atlas_init/tf/modules/aws_kms/provider.tf +7 -0
  29. atlas_init/tf/modules/cloud_provider/cloud_provider.tf +8 -1
  30. atlas_init/tf/modules/encryption_at_rest/main.tf +29 -0
  31. atlas_init/tf/modules/encryption_at_rest/provider.tf +9 -0
  32. atlas_init/tf/variables.tf +5 -0
  33. {atlas_init-0.4.1.dist-info → atlas_init-0.4.3.dist-info}/METADATA +12 -9
  34. {atlas_init-0.4.1.dist-info → atlas_init-0.4.3.dist-info}/RECORD +37 -24
  35. atlas_init-0.4.3.dist-info/licenses/LICENSE +21 -0
  36. {atlas_init-0.4.1.dist-info → atlas_init-0.4.3.dist-info}/WHEEL +0 -0
  37. {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.1"
3
+ VERSION = "0.4.3"
4
4
 
5
5
 
6
6
  def running_in_repo() -> bool:
7
- py_directory = Path(__file__).parent.parent
8
- if py_directory.name != "py":
9
- return False
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()
@@ -13,6 +13,9 @@ test_suites:
13
13
  - name: clusterm10
14
14
  vars:
15
15
  cluster_info_m10: true
16
+ - name: encryption_at_rest
17
+ vars:
18
+ use_encryption_at_rest: true
16
19
  - name: federated
17
20
  repo_go_packages:
18
21
  tf:
@@ -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
- use_replay_mode: bool = False,
87
+ capture_mode: GoTestCaptureMode = GoTestCaptureMode.capture,
88
+ use_old_schema: bool = False,
71
89
  ) -> GoTestResult:
72
- test_env = _resolve_env_vars(settings, env_vars, use_replay_mode=use_replay_mode)
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"pooped CI={ci_value}")
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
- package_paths = group.repo_go_packages.get(repo_alias, [])
79
- packages = ",".join(f"{package_prefix}/{pkg}" for pkg in package_paths)
80
- if not packages:
81
- logger.warning(f"no go packages for suite: {group}")
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
- if mode == GoTestMode.individual:
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 _resolve_env_vars(settings: AtlasInitSettings, env_vars: GoEnvVars, *, use_replay_mode: bool) -> dict[str, str]:
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
- extra_vars = settings.load_profile_manual_env_vars(skip_os_update=True)
171
+ test_env_vars = settings.load_profile_manual_env_vars(skip_os_update=True)
122
172
  elif env_vars == GoEnvVars.vscode:
123
- extra_vars = settings.load_env_vars(settings.env_vars_vs_code)
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
- mocker_env_name = "HTTP_MOCKER_REPLAY" if use_replay_mode else "HTTP_MOCKER_CAPTURE"
127
- extra_vars |= {"TF_ACC": "1", "TF_LOG": "DEBUG", mocker_env_name: "true"}
128
- test_env = os.environ | extra_vars
129
- logger.info(f"go test env-vars-extra: {sorted(extra_vars)}")
130
- return test_env
131
-
132
-
133
- def find_individual_tests(repo_path: Path, package_paths: list[str]) -> dict[str, Path]:
134
- tests = {}
135
- for package_path in package_paths:
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() and not re_run:
162
- logger.info(f"skipping {name} because log exists")
163
- continue
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
- move_failed_logs_to_error_dir(failure_names)
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 move_failed_logs_to_error_dir(failures: set[str]):
213
- error_dir = DEFAULT_DOWNLOADS_DIR / "failures"
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 failures:
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(error_dir / f"{ts}.{log.name}")
266
+ log.rename(new_dir / f"{ts}.{log.name}")
221
267
 
222
268
 
223
269
  def _log_path(name: str) -> Path:
@@ -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: GoTestMode = typer.Option("package", "-m", "--mode", help="package|individual"),
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", help="|".join(list(GoEnvVars))),
25
+ env_method: GoEnvVars = typer.Option(GoEnvVars.manual, "--env"),
26
26
  names: list[str] = typer.Option(
27
- ..., "-n", "--names", default_factory=list, help="run only the tests with these names"
27
+ ...,
28
+ "-n",
29
+ "--names",
30
+ default_factory=list,
31
+ help="run only the tests with these names",
28
32
  ),
29
- use_replay_mode: bool = typer.Option(False, "--replay", help="use replay mode and stored responses"),
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 repo_alias := current_repo():
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
- use_replay_mode=use_replay_mode,
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
 
@@ -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 == response_count
265
- ), f"Mismatch in request and response count: {request_count} != {response_count}"
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
@@ -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
- return list(parse(logs_lines, job, step))
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: