atlas-init 0.6.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atlas_init/__init__.py +1 -1
- atlas_init/atlas_init.yaml +1 -0
- atlas_init/cli_args.py +19 -1
- atlas_init/cli_tf/ci_tests.py +116 -24
- atlas_init/cli_tf/example_update.py +20 -8
- atlas_init/cli_tf/go_test_run.py +14 -2
- atlas_init/cli_tf/go_test_summary.py +334 -82
- atlas_init/cli_tf/go_test_tf_error.py +20 -12
- atlas_init/cli_tf/hcl/modifier.py +22 -8
- atlas_init/cli_tf/hcl/modifier2.py +120 -0
- atlas_init/cli_tf/openapi.py +10 -6
- atlas_init/html_out/__init__.py +0 -0
- atlas_init/html_out/md_export.py +143 -0
- atlas_init/sdk_ext/__init__.py +0 -0
- atlas_init/sdk_ext/go.py +102 -0
- atlas_init/sdk_ext/typer_app.py +18 -0
- atlas_init/settings/env_vars.py +25 -3
- atlas_init/settings/env_vars_generated.py +2 -0
- atlas_init/tf/.terraform.lock.hcl +33 -33
- atlas_init/tf/modules/aws_s3/provider.tf +1 -1
- atlas_init/tf/modules/aws_vpc/provider.tf +1 -1
- atlas_init/tf/modules/cloud_provider/provider.tf +1 -1
- atlas_init/tf/modules/cluster/provider.tf +1 -1
- atlas_init/tf/modules/encryption_at_rest/provider.tf +1 -1
- atlas_init/tf/modules/federated_vars/federated_vars.tf +1 -2
- atlas_init/tf/modules/federated_vars/provider.tf +1 -1
- atlas_init/tf/modules/project_extra/provider.tf +1 -1
- atlas_init/tf/modules/stream_instance/provider.tf +1 -1
- atlas_init/tf/modules/vpc_peering/provider.tf +1 -1
- atlas_init/tf/modules/vpc_privatelink/versions.tf +1 -1
- atlas_init/tf/providers.tf +1 -1
- atlas_init/tf_ext/__init__.py +0 -0
- atlas_init/tf_ext/__main__.py +3 -0
- atlas_init/tf_ext/api_call.py +325 -0
- atlas_init/tf_ext/args.py +32 -0
- atlas_init/tf_ext/constants.py +3 -0
- atlas_init/tf_ext/gen_examples.py +141 -0
- atlas_init/tf_ext/gen_module_readme.py +131 -0
- atlas_init/tf_ext/gen_resource_main.py +195 -0
- atlas_init/tf_ext/gen_resource_output.py +71 -0
- atlas_init/tf_ext/gen_resource_variables.py +159 -0
- atlas_init/tf_ext/gen_versions.py +10 -0
- atlas_init/tf_ext/models.py +106 -0
- atlas_init/tf_ext/models_module.py +454 -0
- atlas_init/tf_ext/newres.py +90 -0
- atlas_init/tf_ext/paths.py +126 -0
- atlas_init/tf_ext/plan_diffs.py +140 -0
- atlas_init/tf_ext/provider_schema.py +199 -0
- atlas_init/tf_ext/py_gen.py +294 -0
- atlas_init/tf_ext/schema_to_dataclass.py +522 -0
- atlas_init/tf_ext/settings.py +188 -0
- atlas_init/tf_ext/tf_dep.py +324 -0
- atlas_init/tf_ext/tf_desc_gen.py +53 -0
- atlas_init/tf_ext/tf_desc_update.py +0 -0
- atlas_init/tf_ext/tf_mod_gen.py +263 -0
- atlas_init/tf_ext/tf_mod_gen_provider.py +124 -0
- atlas_init/tf_ext/tf_modules.py +395 -0
- atlas_init/tf_ext/tf_vars.py +158 -0
- atlas_init/tf_ext/typer_app.py +28 -0
- {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/METADATA +5 -3
- {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/RECORD +64 -31
- atlas_init-0.8.0.dist-info/entry_points.txt +5 -0
- atlas_init-0.6.0.dist-info/entry_points.txt +0 -2
- {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/WHEEL +0 -0
- {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,9 @@
|
|
1
|
+
from __future__ import annotations
|
1
2
|
import logging
|
2
3
|
from collections import defaultdict
|
3
4
|
from copy import deepcopy
|
4
5
|
from pathlib import Path
|
5
|
-
from typing import Callable
|
6
|
+
from typing import Callable, Protocol
|
6
7
|
|
7
8
|
import hcl2
|
8
9
|
from lark import Token, Tree
|
@@ -40,14 +41,16 @@ def is_block_type(tree: Tree, block_type: str) -> bool:
|
|
40
41
|
return False
|
41
42
|
|
42
43
|
|
43
|
-
def update_description(
|
44
|
+
def update_description(
|
45
|
+
path: Path, tree: Tree, get_new_description: NewDescription, existing_names: dict[str, list[str]]
|
46
|
+
) -> Tree:
|
44
47
|
new_children = tree.children.copy()
|
45
48
|
variable_body = new_children[2]
|
46
49
|
assert variable_body.data == "body"
|
47
50
|
name = token_name(new_children[1])
|
48
51
|
old_description = read_description_attribute(variable_body)
|
49
52
|
existing_names[name].append(old_description)
|
50
|
-
new_description =
|
53
|
+
new_description = get_new_description(name, old_description, path)
|
51
54
|
if not new_description:
|
52
55
|
debug_log(f"no description found for variable {name}", 0)
|
53
56
|
return tree
|
@@ -60,6 +63,8 @@ def token_name(token: Token | Tree) -> str:
|
|
60
63
|
return token.value.strip('"')
|
61
64
|
if isinstance(token, Tree) and token.data == "identifier":
|
62
65
|
return token.children[0].value.strip('"') # type: ignore
|
66
|
+
if isinstance(token, Tree) and isinstance(token.data, Token) and token.data.value == "heredoc_template_trim":
|
67
|
+
return token.children[0].value.strip('"') # type: ignore
|
63
68
|
err_msg = f"unexpected token type {type(token)} for token name"
|
64
69
|
raise ValueError(err_msg)
|
65
70
|
|
@@ -103,10 +108,11 @@ def read_description_attribute(tree: Tree) -> str:
|
|
103
108
|
|
104
109
|
|
105
110
|
def create_description_attribute(description_value: str) -> Tree:
|
111
|
+
token_value = f"<<-EOT\n{description_value}\nEOT\n" if "\n" in description_value else f'"{description_value}"'
|
106
112
|
children = [
|
107
113
|
Tree(Token("RULE", "identifier"), [Token("NAME", "description")]),
|
108
114
|
Token("EQ", " ="),
|
109
|
-
Tree(Token("RULE", "expr_term"), [Token("STRING_LIT",
|
115
|
+
Tree(Token("RULE", "expr_term"), [Token("STRING_LIT", token_value)]),
|
110
116
|
]
|
111
117
|
return Tree(Token("RULE", "attribute"), children)
|
112
118
|
|
@@ -129,9 +135,14 @@ def process_generic(
|
|
129
135
|
return Tree(node.data, new_children)
|
130
136
|
|
131
137
|
|
138
|
+
class NewDescription(Protocol):
|
139
|
+
def __call__(self, name: str, old_description: str, path: Path) -> str: ...
|
140
|
+
|
141
|
+
|
132
142
|
def process_descriptions(
|
143
|
+
path: Path,
|
133
144
|
node: Tree,
|
134
|
-
|
145
|
+
new_description: NewDescription,
|
135
146
|
existing_names: dict[str, list[str]],
|
136
147
|
depth=0,
|
137
148
|
*,
|
@@ -141,7 +152,7 @@ def process_descriptions(
|
|
141
152
|
return is_block_type(tree, block_type)
|
142
153
|
|
143
154
|
def tree_call(tree: Tree) -> Tree:
|
144
|
-
return update_description(tree,
|
155
|
+
return update_description(path, tree, new_description, existing_names)
|
145
156
|
|
146
157
|
return process_generic(
|
147
158
|
node,
|
@@ -151,14 +162,17 @@ def process_descriptions(
|
|
151
162
|
)
|
152
163
|
|
153
164
|
|
154
|
-
def update_descriptions(
|
165
|
+
def update_descriptions(
|
166
|
+
tf_path: Path, new_description: NewDescription, block_type: str
|
167
|
+
) -> tuple[str, dict[str, list[str]]]:
|
155
168
|
tree = safe_parse(tf_path)
|
156
169
|
if tree is None:
|
157
170
|
return "", {}
|
158
171
|
existing_descriptions = defaultdict(list)
|
159
172
|
new_tree = process_descriptions(
|
173
|
+
tf_path,
|
160
174
|
tree,
|
161
|
-
|
175
|
+
new_description,
|
162
176
|
existing_descriptions,
|
163
177
|
block_type=block_type,
|
164
178
|
)
|
@@ -1,3 +1,4 @@
|
|
1
|
+
from collections import defaultdict
|
1
2
|
import logging
|
2
3
|
from contextlib import suppress
|
3
4
|
from pathlib import Path
|
@@ -57,6 +58,110 @@ def attribute_transfomer(attr_name: str, obj_key: str, new_value: str) -> tuple[
|
|
57
58
|
return AttributeTransformer(with_meta=True), changes
|
58
59
|
|
59
60
|
|
61
|
+
def variable_reader(tree: Tree) -> dict[str, str | None]:
|
62
|
+
"""
|
63
|
+
Reads the variable names from a parsed HCL2 tree.
|
64
|
+
Returns a variable_name -> description, None if no description is found.
|
65
|
+
"""
|
66
|
+
variables: dict[str, str | None] = {}
|
67
|
+
|
68
|
+
class DescriptionReader(DictTransformer):
|
69
|
+
def __init__(self, with_meta: bool = False, *, name: str):
|
70
|
+
super().__init__(with_meta)
|
71
|
+
self.name = name
|
72
|
+
self.description: str | None = None
|
73
|
+
|
74
|
+
def attribute(self, args: list) -> Attribute:
|
75
|
+
name = args[0]
|
76
|
+
if name == "description":
|
77
|
+
description = _parse_attribute_value(args)
|
78
|
+
self.description = description
|
79
|
+
return super().attribute(args)
|
80
|
+
|
81
|
+
class BlockReader(Transformer):
|
82
|
+
@v_args(tree=True)
|
83
|
+
def block(self, block_tree: Tree) -> Tree:
|
84
|
+
current_block_name = _identifier_name(block_tree)
|
85
|
+
if current_block_name == "variable":
|
86
|
+
variable_name = token_name(block_tree.children[1])
|
87
|
+
reader = DescriptionReader(name=variable_name)
|
88
|
+
reader.transform(block_tree)
|
89
|
+
variables[variable_name] = reader.description
|
90
|
+
return block_tree
|
91
|
+
|
92
|
+
BlockReader().transform(tree)
|
93
|
+
return variables
|
94
|
+
|
95
|
+
|
96
|
+
def _parse_attribute_value(args: list) -> str:
|
97
|
+
description = args[-1]
|
98
|
+
return token_name(description) if isinstance(description, Token) else description.strip('"')
|
99
|
+
|
100
|
+
|
101
|
+
def resource_types_vars_usage(tree: Tree) -> dict[str, dict[str, str]]:
|
102
|
+
"""
|
103
|
+
Reads the resource types and their variable usages from a parsed HCL2 tree.
|
104
|
+
Returns a dictionary where keys are resource type names and values are dictionaries
|
105
|
+
of variable names and the attribute paths they are used in.
|
106
|
+
"""
|
107
|
+
resource_types: dict[str, dict[str, str]] = defaultdict(dict)
|
108
|
+
|
109
|
+
class ResourceBlockAttributeReader(DictTransformer):
|
110
|
+
def __init__(self, with_meta: bool = False, resource_type: str = ""):
|
111
|
+
self.resource_type = resource_type
|
112
|
+
resource_types.setdefault(self.resource_type, {})
|
113
|
+
super().__init__(with_meta)
|
114
|
+
|
115
|
+
def attribute(self, args: list) -> Attribute:
|
116
|
+
try:
|
117
|
+
value = _parse_attribute_value(args)
|
118
|
+
except AttributeError:
|
119
|
+
return super().attribute(args)
|
120
|
+
if value.startswith("var."):
|
121
|
+
variable_name = value[4:]
|
122
|
+
resource_types[self.resource_type][variable_name] = args[0]
|
123
|
+
return super().attribute(args)
|
124
|
+
|
125
|
+
class BlockReader(Transformer):
|
126
|
+
@v_args(tree=True)
|
127
|
+
def block(self, block_tree: Tree) -> Tree:
|
128
|
+
block_resource_name = _block_resource_name(block_tree)
|
129
|
+
if block_resource_name is not None:
|
130
|
+
ResourceBlockAttributeReader(with_meta=True, resource_type=block_resource_name).transform(block_tree)
|
131
|
+
return block_tree
|
132
|
+
|
133
|
+
BlockReader().transform(tree)
|
134
|
+
return resource_types
|
135
|
+
|
136
|
+
|
137
|
+
def variable_usages(variable_names: set[str], tree: Tree) -> dict[str, set[str]]:
|
138
|
+
usages = defaultdict(set)
|
139
|
+
current_resource_type = None
|
140
|
+
|
141
|
+
class ResourceBlockAttributeReader(DictTransformer):
|
142
|
+
def attribute(self, args: list) -> Attribute:
|
143
|
+
attr_value = args[-1]
|
144
|
+
if isinstance(attr_value, str) and attr_value.startswith("var."):
|
145
|
+
variable_name = attr_value[4:]
|
146
|
+
if variable_name in variable_names:
|
147
|
+
assert current_resource_type is not None, "current_resource_type should not be None"
|
148
|
+
usages[variable_name].add(current_resource_type)
|
149
|
+
return super().attribute(args)
|
150
|
+
|
151
|
+
class BlockReader(Transformer):
|
152
|
+
@v_args(tree=True)
|
153
|
+
def block(self, block_tree: Tree) -> Tree:
|
154
|
+
block_resource_name = _block_resource_name(block_tree)
|
155
|
+
if block_resource_name is not None and block_resource_name.startswith("mongodbatlas_"):
|
156
|
+
nonlocal current_resource_type
|
157
|
+
current_resource_type = block_resource_name
|
158
|
+
ResourceBlockAttributeReader().transform(block_tree)
|
159
|
+
return block_tree
|
160
|
+
|
161
|
+
BlockReader().transform(tree)
|
162
|
+
return usages
|
163
|
+
|
164
|
+
|
60
165
|
def _identifier_name(tree: Tree) -> str | None:
|
61
166
|
with suppress(Exception):
|
62
167
|
identifier_tree = tree.children[0]
|
@@ -67,6 +172,21 @@ def _identifier_name(tree: Tree) -> str | None:
|
|
67
172
|
return name_token.value
|
68
173
|
|
69
174
|
|
175
|
+
def _block_resource_name(tree: Tree) -> str | None:
|
176
|
+
block_name = _identifier_name(tree)
|
177
|
+
if block_name != "resource":
|
178
|
+
return None
|
179
|
+
token = tree.children[1]
|
180
|
+
return token_name(token)
|
181
|
+
|
182
|
+
|
183
|
+
def token_name(token):
|
184
|
+
assert isinstance(token, Token)
|
185
|
+
token_value = token.value
|
186
|
+
assert isinstance(token_value, str)
|
187
|
+
return token_value.strip('"')
|
188
|
+
|
189
|
+
|
70
190
|
def write_tree(tree: Tree) -> str:
|
71
191
|
return writes(tree)
|
72
192
|
|
atlas_init/cli_tf/openapi.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import datetime
|
3
4
|
import logging
|
4
5
|
import re
|
5
6
|
from collections.abc import Iterable
|
@@ -76,7 +77,7 @@ class OpenapiSchema(Entity):
|
|
76
77
|
def create_method(self, path: str) -> dict | None:
|
77
78
|
return self.paths.get(path, {}).get("post")
|
78
79
|
|
79
|
-
def
|
80
|
+
def get_method(self, path: str) -> dict | None:
|
80
81
|
return self.paths.get(path, {}).get("get")
|
81
82
|
|
82
83
|
def delete_method(self, path: str) -> dict | None:
|
@@ -88,10 +89,13 @@ class OpenapiSchema(Entity):
|
|
88
89
|
def put_method(self, path: str) -> dict | None:
|
89
90
|
return self.paths.get(path, {}).get("patch")
|
90
91
|
|
91
|
-
def
|
92
|
+
def methods_with_name(self, path: str) -> Iterable[tuple[str, dict]]:
|
92
93
|
for method_name in ["post", "get", "delete", "patch", "put"]:
|
93
94
|
if method := self.paths.get(path, {}).get(method_name):
|
94
|
-
yield method
|
95
|
+
yield method_name, method
|
96
|
+
|
97
|
+
def methods(self, path: str) -> Iterable[dict]:
|
98
|
+
yield from (method for _, method in self.methods_with_name(path))
|
95
99
|
|
96
100
|
def method_refs(self, path: str) -> Iterable[str]:
|
97
101
|
for method in self.methods(path):
|
@@ -145,7 +149,7 @@ class OpenapiSchema(Entity):
|
|
145
149
|
if ref := value.get("schema", {}).get("$ref"):
|
146
150
|
yield ref
|
147
151
|
|
148
|
-
def _unpack_schema_versions(self, response: dict) -> list[
|
152
|
+
def _unpack_schema_versions(self, response: dict) -> list[datetime.date]:
|
149
153
|
content: dict[str, dict] = {**response.get("content", {})}
|
150
154
|
versions = []
|
151
155
|
while content:
|
@@ -159,7 +163,7 @@ class OpenapiSchema(Entity):
|
|
159
163
|
versions.append(version)
|
160
164
|
return versions
|
161
165
|
|
162
|
-
def path_method_api_versions(self) -> Iterable[tuple[PathMethodCode, list[
|
166
|
+
def path_method_api_versions(self) -> Iterable[tuple[PathMethodCode, list[datetime.date]]]:
|
163
167
|
for path, methods in self.paths.items():
|
164
168
|
for method_name, method_dict in methods.items():
|
165
169
|
if not isinstance(method_dict, dict):
|
@@ -302,7 +306,7 @@ def add_api_spec_info(schema: SchemaV2, api_spec_path: Path, *, minimal_refs: bo
|
|
302
306
|
for property_dict in api_spec.schema_properties(req_ref):
|
303
307
|
parse_api_spec_param(api_spec, property_dict, resource)
|
304
308
|
for path in resource.paths:
|
305
|
-
read_method = api_spec.
|
309
|
+
read_method = api_spec.get_method(path)
|
306
310
|
if not read_method:
|
307
311
|
continue
|
308
312
|
for param in read_method.get("parameters", []):
|
File without changes
|
@@ -0,0 +1,143 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import logging
|
3
|
+
from concurrent.futures import Future
|
4
|
+
from contextlib import suppress
|
5
|
+
from datetime import datetime
|
6
|
+
from typing import ClassVar
|
7
|
+
from ask_shell import ShellRun, confirm, kill, run, run_and_wait
|
8
|
+
from ask_shell.models import ShellRunEventT, ShellRunStdOutput
|
9
|
+
from zero_3rdparty import str_utils
|
10
|
+
from zero_3rdparty.file_utils import copy, ensure_parents_write_text
|
11
|
+
from zero_3rdparty.future import chain_future
|
12
|
+
from atlas_init.settings.env_vars import AtlasInitSettings
|
13
|
+
from pathlib import Path
|
14
|
+
from model_lib import Event
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class MonthlyReportPaths(Event):
|
20
|
+
summary_path: Path
|
21
|
+
error_only_path: Path
|
22
|
+
details_dir: Path
|
23
|
+
summary_name: str
|
24
|
+
daily_path: Path
|
25
|
+
|
26
|
+
ERROR_ONLY_SUFFIX: ClassVar[str] = "_error-only.md"
|
27
|
+
DAILY_SUFFIX: ClassVar[str] = "_daily.md"
|
28
|
+
|
29
|
+
def export_to_dir(self, out_dir: Path) -> None:
|
30
|
+
for path in [self.summary_path, self.error_only_path, self.daily_path]:
|
31
|
+
if path.exists():
|
32
|
+
ensure_parents_write_text(out_dir / path.name, path.read_text())
|
33
|
+
if self.details_dir.exists():
|
34
|
+
copy(self.details_dir, out_dir / self.details_dir.name, clean_dest=True)
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def from_settings(cls, settings: AtlasInitSettings, summary_name: str) -> MonthlyReportPaths:
|
38
|
+
summary_path = settings.github_ci_summary_dir / str_utils.ensure_suffix(summary_name, ".md")
|
39
|
+
return cls(
|
40
|
+
summary_path=summary_path,
|
41
|
+
error_only_path=settings.github_ci_summary_dir
|
42
|
+
/ str_utils.ensure_suffix(summary_name, MonthlyReportPaths.ERROR_ONLY_SUFFIX),
|
43
|
+
details_dir=settings.github_ci_summary_details_path(summary_name, "dummy").parent,
|
44
|
+
summary_name=summary_name,
|
45
|
+
daily_path=settings.github_ci_summary_dir / f"{summary_path.stem}{MonthlyReportPaths.DAILY_SUFFIX}",
|
46
|
+
)
|
47
|
+
|
48
|
+
|
49
|
+
CI_TESTS_DIR_NAME = "ci-tests"
|
50
|
+
MKDOCS_SERVE_TIMEOUT = 120
|
51
|
+
MKDOCS_SERVE_URL = "http://127.0.0.1:8000"
|
52
|
+
|
53
|
+
|
54
|
+
def export_ci_tests_markdown_to_html(settings: AtlasInitSettings, report_paths: MonthlyReportPaths) -> None:
|
55
|
+
html_out = settings.atlas_init_static_html_path
|
56
|
+
if not html_out or not html_out.exists():
|
57
|
+
return
|
58
|
+
ci_tests_dir = html_out / CI_TESTS_DIR_NAME
|
59
|
+
docs_out_dir = ci_tests_dir / "docs"
|
60
|
+
report_paths.export_to_dir(docs_out_dir)
|
61
|
+
index_md_content = create_index_md(docs_out_dir)
|
62
|
+
ensure_parents_write_text(docs_out_dir / "index.md", index_md_content)
|
63
|
+
server_url, run_event = start_mkdocs_serve(ci_tests_dir)
|
64
|
+
try:
|
65
|
+
if confirm(f"do you want to open the html docs? {server_url}", default=False):
|
66
|
+
run_and_wait(f'open -a "Google Chrome" {server_url}')
|
67
|
+
if confirm("Finished testing html docs?", default=False):
|
68
|
+
pass
|
69
|
+
except BaseException as e:
|
70
|
+
raise e
|
71
|
+
finally:
|
72
|
+
kill(run_event, reason="Done with html docs check")
|
73
|
+
if confirm("Are docs ok to build and push?", default=False):
|
74
|
+
build_and_push(ci_tests_dir, report_paths.summary_name)
|
75
|
+
|
76
|
+
|
77
|
+
def create_index_md(docs_out_dir: Path) -> str:
|
78
|
+
"""
|
79
|
+
tree -L 1 docs
|
80
|
+
docs
|
81
|
+
├── 2025-06-26_details
|
82
|
+
├── 2025-06-26_.md
|
83
|
+
├── 2025-06-26.md
|
84
|
+
├── 2025-06-26_error-only.md
|
85
|
+
├── index.md
|
86
|
+
├── javascript
|
87
|
+
└── stylesheets
|
88
|
+
"""
|
89
|
+
md_files = {f.name: f for f in docs_out_dir.glob("*.md") if f.name != "index.md"}
|
90
|
+
parsed_dates = []
|
91
|
+
for md_file in md_files.values():
|
92
|
+
with suppress(ValueError):
|
93
|
+
parsed_dates.append(datetime.strptime(md_file.stem, "%Y-%m-%d"))
|
94
|
+
parsed_dates.sort(reverse=True)
|
95
|
+
|
96
|
+
def date_row(date: datetime) -> str:
|
97
|
+
summary_filename = f"{date.strftime('%Y-%m-%d')}.md"
|
98
|
+
error_only_filename = f"{date.strftime('%Y-%m-%d')}{MonthlyReportPaths.ERROR_ONLY_SUFFIX}"
|
99
|
+
daily_filename = f"{date.strftime('%Y-%m-%d')}{MonthlyReportPaths.DAILY_SUFFIX}"
|
100
|
+
line_links = [f"[{date.strftime('%Y-%m-%d')}](./{summary_filename})"]
|
101
|
+
if error_only_filename in md_files:
|
102
|
+
line_links.append(f"[{date.strftime('%Y-%m-%d')} Error Only](./{error_only_filename})")
|
103
|
+
if daily_filename in md_files:
|
104
|
+
line_links.append(f"[{date.strftime('%Y-%m-%d')} Daily Errors](./{daily_filename})")
|
105
|
+
return f"- {', '.join(line_links)}"
|
106
|
+
|
107
|
+
md_content = [
|
108
|
+
"# Welcome to CI Tests",
|
109
|
+
"",
|
110
|
+
*[date_row(dt) for dt in parsed_dates],
|
111
|
+
"",
|
112
|
+
]
|
113
|
+
return "\n".join(md_content)
|
114
|
+
|
115
|
+
|
116
|
+
def start_mkdocs_serve(ci_tests_dir: Path) -> tuple[str, ShellRun]:
|
117
|
+
future = Future()
|
118
|
+
|
119
|
+
def on_message(event: ShellRunEventT) -> bool:
|
120
|
+
match event:
|
121
|
+
case ShellRunStdOutput(_, content) if f"Serving on {MKDOCS_SERVE_URL}" in content:
|
122
|
+
logger.info(f"Docs server ready @ {MKDOCS_SERVE_URL}")
|
123
|
+
future.set_result(None)
|
124
|
+
return True
|
125
|
+
return False
|
126
|
+
|
127
|
+
run_event = run(
|
128
|
+
"uv run mkdocs serve", cwd=ci_tests_dir, message_callbacks=[on_message], print_prefix="mkdocs serve"
|
129
|
+
)
|
130
|
+
chain_future(run_event._complete_flag, future)
|
131
|
+
try:
|
132
|
+
future.result(timeout=MKDOCS_SERVE_TIMEOUT)
|
133
|
+
except BaseException as e:
|
134
|
+
kill(run_event, reason=f"Failed to start mkdocs serve, timeout after {MKDOCS_SERVE_TIMEOUT} seconds")
|
135
|
+
raise e
|
136
|
+
return MKDOCS_SERVE_URL, run_event
|
137
|
+
|
138
|
+
|
139
|
+
def build_and_push(ci_tests_dir: Path, summary_name: str) -> None:
|
140
|
+
run_and_wait("uv run mkdocs build", cwd=ci_tests_dir, print_prefix="build")
|
141
|
+
run_and_wait("git add .", cwd=ci_tests_dir, print_prefix="add")
|
142
|
+
run_and_wait(f"git commit -m 'update ci tests {summary_name}'", cwd=ci_tests_dir, print_prefix="commit")
|
143
|
+
run_and_wait("git push", cwd=ci_tests_dir, print_prefix="push")
|
File without changes
|
atlas_init/sdk_ext/go.py
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
from contextlib import suppress
|
2
|
+
import logging
|
3
|
+
from pathlib import Path
|
4
|
+
from ask_shell import confirm, run_and_wait
|
5
|
+
from model_lib import dump, parse_model
|
6
|
+
import typer
|
7
|
+
from zero_3rdparty.file_utils import clean_dir, copy, ensure_parents_write_text
|
8
|
+
from atlas_init.cli_args import ParsedPaths, option_sdk_repo_path, option_mms_repo_path
|
9
|
+
from atlas_init.cli_tf.openapi import OpenapiSchema
|
10
|
+
|
11
|
+
_go_mod_line = "replace go.mongodb.org/atlas-sdk/v20250312005 v20250312005.0.0 => ../atlas-sdk-go"
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
def go(
|
16
|
+
mms_path_str: str = option_mms_repo_path,
|
17
|
+
sdk_repo_path_str: str = option_sdk_repo_path,
|
18
|
+
mms_branch: str = typer.Option("master", "--mms-branch", help="Branch to use for mms"),
|
19
|
+
skip_mms_openapi: bool = typer.Option(
|
20
|
+
False, "-smms", "--skip-mms-openapi", help="Skip mms openapi generation, use existing file instead"
|
21
|
+
),
|
22
|
+
):
|
23
|
+
paths = ParsedPaths.from_strings(sdk_repo_path_str=sdk_repo_path_str, mms_path=mms_path_str)
|
24
|
+
mms_path = paths.mms_repo_path
|
25
|
+
assert mms_path, "mms_path is required"
|
26
|
+
sdk_path = paths.sdk_repo_path
|
27
|
+
assert sdk_path, "sdk_path is required"
|
28
|
+
openapi_path = safe_openapi_path(mms_path) if skip_mms_openapi else generate_openapi_spec(mms_path, mms_branch)
|
29
|
+
openapi_path = transform_openapi(openapi_path, sdk_path / "openapi/openapi-mms.yaml")
|
30
|
+
generate_go_sdk(sdk_path, openapi_path)
|
31
|
+
confirm(f"Have you remembered to add to your go.mod file: {_go_mod_line}")
|
32
|
+
|
33
|
+
|
34
|
+
def transform_openapi(old: Path, dest_path: Path) -> Path:
|
35
|
+
api_spec = parse_model(old, t=OpenapiSchema)
|
36
|
+
new_api_spec = api_spec.model_dump()
|
37
|
+
for path in api_spec.paths.keys():
|
38
|
+
for method_name, method in api_spec.methods_with_name(path):
|
39
|
+
responses = method.get("responses", {})
|
40
|
+
for code, multi_responses in responses.items():
|
41
|
+
with suppress(AlreadySingleVersion):
|
42
|
+
new_api_spec["paths"][path][method_name]["responses"][code]["content"] = use_a_single_version(
|
43
|
+
multi_responses, api_spec, path
|
44
|
+
)
|
45
|
+
if request_body := method.get("requestBody", {}):
|
46
|
+
with suppress(AlreadySingleVersion):
|
47
|
+
new_api_spec["paths"][path][method_name]["requestBody"]["content"] = use_a_single_version(
|
48
|
+
request_body, api_spec, path
|
49
|
+
)
|
50
|
+
dest_yaml = dump(new_api_spec, "yaml")
|
51
|
+
ensure_parents_write_text(dest_path, dest_yaml)
|
52
|
+
return dest_path
|
53
|
+
|
54
|
+
|
55
|
+
class AlreadySingleVersion(Exception):
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
def use_a_single_version(multi_content: dict, api_spec: OpenapiSchema, path: str) -> dict[str, dict]:
|
60
|
+
if api_versions := api_spec._unpack_schema_versions(multi_content):
|
61
|
+
if len(api_versions) > 1:
|
62
|
+
latest_version = max(api_versions)
|
63
|
+
last_header = f"application/vnd.atlas.{latest_version}+json"
|
64
|
+
old_content = multi_content["content"]
|
65
|
+
assert last_header in old_content, f"failed to find {last_header} for {path} in {old_content.keys()}"
|
66
|
+
return {last_header: old_content[last_header]}
|
67
|
+
raise AlreadySingleVersion
|
68
|
+
|
69
|
+
|
70
|
+
def generate_openapi_spec(mms_path: Path, mms_branch: str) -> Path:
|
71
|
+
run_and_wait(f"git stash && git checkout {mms_branch}", cwd=mms_path)
|
72
|
+
bazelisk_bin_run = run_and_wait("mise which bazelisk", cwd=mms_path)
|
73
|
+
bazelisk_bin = bazelisk_bin_run.stdout_one_line
|
74
|
+
assert Path(bazelisk_bin).exists(), f"not found {bazelisk_bin}"
|
75
|
+
openapi_run = run_and_wait(f"{bazelisk_bin} run //server:mms-openapi", cwd=mms_path, print_prefix="mms-openapi")
|
76
|
+
assert openapi_run.clean_complete, f"failed to run {openapi_run}"
|
77
|
+
return safe_openapi_path(mms_path)
|
78
|
+
|
79
|
+
|
80
|
+
def safe_openapi_path(mms_path: Path) -> Path:
|
81
|
+
openapi_path = mms_path / "server/openapi/services/openapi-mms.json"
|
82
|
+
assert openapi_path.exists(), f"not found {openapi_path}"
|
83
|
+
return openapi_path
|
84
|
+
|
85
|
+
|
86
|
+
def generate_go_sdk(repo_path: Path, openapi_path: Path) -> None:
|
87
|
+
SDK_FOLDER = repo_path / "admin"
|
88
|
+
clean_dir(SDK_FOLDER, recreate=True)
|
89
|
+
generate_script = repo_path / "tools/scripts/generate.sh"
|
90
|
+
assert generate_script.exists(), f"not found {generate_script}"
|
91
|
+
openapi_folder = repo_path / "openapi"
|
92
|
+
openapi_dest_path = openapi_folder / openapi_path.name
|
93
|
+
if openapi_path != openapi_dest_path:
|
94
|
+
copy(openapi_path, openapi_dest_path)
|
95
|
+
generate_env = {
|
96
|
+
"OPENAPI_FOLDER": str(openapi_folder),
|
97
|
+
"OPENAPI_FILE_NAME": openapi_path.name,
|
98
|
+
"SDK_FOLDER": str(SDK_FOLDER),
|
99
|
+
}
|
100
|
+
run_and_wait(f"{generate_script}", cwd=repo_path / "tools", env=generate_env, print_prefix="go sdk create")
|
101
|
+
mockery_script = repo_path / "tools/scripts/generate_mocks.sh"
|
102
|
+
run_and_wait(f"{mockery_script}", cwd=repo_path / "tools", print_prefix="go sdk mockery")
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from ask_shell import configure_logging
|
2
|
+
from typer import Typer
|
3
|
+
|
4
|
+
|
5
|
+
def typer_main():
|
6
|
+
from atlas_init.sdk_ext import go
|
7
|
+
|
8
|
+
app = Typer(
|
9
|
+
name="sdk-ext",
|
10
|
+
help="SDK extension commands for Atlas Init",
|
11
|
+
)
|
12
|
+
app.command(name="go")(go.go)
|
13
|
+
configure_logging(app)
|
14
|
+
app()
|
15
|
+
|
16
|
+
|
17
|
+
if __name__ == "__main__":
|
18
|
+
typer_main()
|
atlas_init/settings/env_vars.py
CHANGED
@@ -9,7 +9,7 @@ from typing import Any, NamedTuple, TypeVar
|
|
9
9
|
|
10
10
|
from model_lib import StaticSettings, parse_payload
|
11
11
|
from pydantic import BaseModel, ValidationError, field_validator
|
12
|
-
from zero_3rdparty import iter_utils
|
12
|
+
from zero_3rdparty import iter_utils, str_utils
|
13
13
|
|
14
14
|
from atlas_init.settings.config import (
|
15
15
|
AtlasInitConfig,
|
@@ -54,6 +54,7 @@ class AtlasInitSettings(StaticSettings):
|
|
54
54
|
atlas_init_tf_src_path: Path = DEFAULT_TF_SRC_PATH # /tf directory of repo
|
55
55
|
atlas_init_tf_schema_config_path: Path = DEFAULT_ATLAS_INIT_SCHEMA_CONFIG_PATH # /terraform.yaml
|
56
56
|
atlas_init_schema_out_path: Path | None = None # override this for the generated schema
|
57
|
+
atlas_init_static_html_path: Path | None = None
|
57
58
|
|
58
59
|
atlas_init_cfn_profile: str = ""
|
59
60
|
atlas_init_cfn_region: str = ""
|
@@ -136,6 +137,17 @@ class AtlasInitSettings(StaticSettings):
|
|
136
137
|
def github_ci_summary_dir(self) -> Path:
|
137
138
|
return self.cache_root / "github_ci_summary"
|
138
139
|
|
140
|
+
def github_ci_summary_path(self, summary_name: str) -> Path:
|
141
|
+
return self.github_ci_summary_dir / str_utils.ensure_suffix(summary_name, ".md")
|
142
|
+
|
143
|
+
def github_ci_summary_details_path(self, summary_name: str, test_name: str) -> Path:
|
144
|
+
return self.github_ci_summary_path(summary_name).parent / self.github_ci_summary_details_rel_path(
|
145
|
+
summary_name, test_name
|
146
|
+
)
|
147
|
+
|
148
|
+
def github_ci_summary_details_rel_path(self, summary_name: str, test_name: str) -> str:
|
149
|
+
return f"{summary_name.removesuffix('.md')}_details/{test_name}.md"
|
150
|
+
|
139
151
|
@property
|
140
152
|
def go_test_logs_dir(self) -> Path:
|
141
153
|
return self.cache_root / "go_test_logs"
|
@@ -231,7 +243,9 @@ def detect_ambiguous_env_vars(manual_env_vars: dict[str, str]) -> list[str]:
|
|
231
243
|
|
232
244
|
def find_missing_env_vars(required_env_vars: list[str], manual_env_vars: dict[str, str]) -> list[str]:
|
233
245
|
return sorted(
|
234
|
-
env_name
|
246
|
+
env_name
|
247
|
+
for env_name in required_env_vars
|
248
|
+
if read_from_env(env_name) == "" and env_name not in manual_env_vars and env_name
|
235
249
|
)
|
236
250
|
|
237
251
|
|
@@ -243,7 +257,15 @@ def init_settings(
|
|
243
257
|
profile_env_vars = settings.manual_env_vars
|
244
258
|
vscode_env_vars = settings.env_vars_vs_code
|
245
259
|
if vscode_env_vars.exists():
|
246
|
-
|
260
|
+
skip_generated_vars: set[str] = set()
|
261
|
+
if "AWS_PROFILE" in profile_env_vars:
|
262
|
+
skip_generated_vars |= {
|
263
|
+
"AWS_ACCESS_KEY_ID",
|
264
|
+
"AWS_SECRET_ACCESS_KEY",
|
265
|
+
} # avoid generated env-vars overwriting AWS PROFILE
|
266
|
+
profile_env_vars |= {
|
267
|
+
key: value for key, value in load_dotenv(vscode_env_vars).items() if key not in skip_generated_vars
|
268
|
+
}
|
247
269
|
required_env_vars = collect_required_env_vars(list(settings_classes))
|
248
270
|
ambiguous = [] if skip_ambiguous_check else detect_ambiguous_env_vars(profile_env_vars)
|
249
271
|
missing_env_vars = find_missing_env_vars(required_env_vars, profile_env_vars)
|