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
@@ -0,0 +1,159 @@
|
|
1
|
+
import logging
|
2
|
+
from contextlib import contextmanager
|
3
|
+
from dataclasses import Field, fields, is_dataclass
|
4
|
+
from typing import Dict, List, Set, Union, get_args, get_origin, get_type_hints
|
5
|
+
|
6
|
+
from model_lib import Entity
|
7
|
+
from pydantic import Field as PydanticField
|
8
|
+
|
9
|
+
from atlas_init.tf_ext.gen_resource_main import format_tf_content
|
10
|
+
from atlas_init.tf_ext.models_module import ResourceAbs, ResourceGenConfig, ResourceTypePythonModule
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class DefaultValueContext(Entity):
|
16
|
+
default_lines: list[str] = PydanticField(default_factory=list)
|
17
|
+
field_path: list[str] = PydanticField(default_factory=list)
|
18
|
+
ignored_names: set[str] = PydanticField(default_factory=set)
|
19
|
+
|
20
|
+
@property
|
21
|
+
def final_str(self) -> str:
|
22
|
+
if not self.default_lines:
|
23
|
+
return "null"
|
24
|
+
return "\n".join(self.default_lines)
|
25
|
+
|
26
|
+
@property
|
27
|
+
def current_field_path(self) -> str:
|
28
|
+
return ".".join(self.field_path)
|
29
|
+
|
30
|
+
def at_root(self, field_name: str) -> bool:
|
31
|
+
return field_name == self.current_field_path
|
32
|
+
|
33
|
+
@property
|
34
|
+
def _prefix(self) -> str:
|
35
|
+
return " " * len(self.field_path)
|
36
|
+
|
37
|
+
@contextmanager
|
38
|
+
def add_nested_field(self, field_name: str):
|
39
|
+
if not self.at_root(field_name):
|
40
|
+
self.field_path.append(field_name)
|
41
|
+
default_index_before = len(self.default_lines)
|
42
|
+
try:
|
43
|
+
yield
|
44
|
+
default_index_after = len(self.default_lines)
|
45
|
+
if (
|
46
|
+
default_index_before == default_index_after
|
47
|
+
): # no child had a default, so we don't need to add default lines
|
48
|
+
return
|
49
|
+
if self.at_root(field_name):
|
50
|
+
self.default_lines.insert(default_index_before, "{")
|
51
|
+
else:
|
52
|
+
self.default_lines.insert(default_index_before, f"{self._prefix}{field_name} = {{")
|
53
|
+
self.default_lines.append(f"{self._prefix}}}")
|
54
|
+
finally:
|
55
|
+
self.field_path.pop()
|
56
|
+
|
57
|
+
def add_default(self, field_name: str, default_value: str) -> None:
|
58
|
+
if self.at_root(field_name): # root field default value, no field name needed
|
59
|
+
self.default_lines.append(default_value)
|
60
|
+
else:
|
61
|
+
self.default_lines.append(f"{self._prefix}{field_name} = {default_value}")
|
62
|
+
|
63
|
+
|
64
|
+
def python_type_to_terraform_type(field: Field, py_type: type, context: DefaultValueContext) -> str:
|
65
|
+
# Unwrap Optional/Union
|
66
|
+
origin = get_origin(py_type)
|
67
|
+
args = get_args(py_type)
|
68
|
+
if origin is Union and type(None) in args:
|
69
|
+
# Optional[X] or Union[X, None] -> X
|
70
|
+
not_none = [a for a in args if a is not type(None)]
|
71
|
+
return python_type_to_terraform_type(field, not_none[0], context) if not_none else "any"
|
72
|
+
if origin is list or origin is List:
|
73
|
+
elem_type = python_type_to_terraform_type(field, args[0], context)
|
74
|
+
return f"list({elem_type})"
|
75
|
+
elif origin is set or origin is Set:
|
76
|
+
elem_type = python_type_to_terraform_type(field, args[0], context)
|
77
|
+
return f"set({elem_type})"
|
78
|
+
elif origin is dict or origin is Dict:
|
79
|
+
elem_type = python_type_to_terraform_type(field, args[1], context)
|
80
|
+
return f"map({elem_type})"
|
81
|
+
elif is_dataclass(py_type):
|
82
|
+
return dataclass_to_object_type(field.name, py_type, context)
|
83
|
+
elif py_type is str:
|
84
|
+
return "string"
|
85
|
+
elif py_type is int or py_type is float:
|
86
|
+
return "number"
|
87
|
+
elif py_type is bool:
|
88
|
+
return "bool"
|
89
|
+
else:
|
90
|
+
return "any"
|
91
|
+
|
92
|
+
|
93
|
+
def dataclass_to_object_type(name: str, cls: type, context: DefaultValueContext) -> str:
|
94
|
+
lines = ["object({"]
|
95
|
+
hints = get_type_hints(cls)
|
96
|
+
with context.add_nested_field(name):
|
97
|
+
for f in fields(cls):
|
98
|
+
# Skip ClassVars and internal fields
|
99
|
+
nested_field_name = f.name
|
100
|
+
is_computed_only = ResourceAbs.is_computed_only(nested_field_name, cls)
|
101
|
+
if is_computed_only or nested_field_name in context.ignored_names:
|
102
|
+
continue
|
103
|
+
tf_type = python_type_to_terraform_type(f, hints[nested_field_name], context)
|
104
|
+
is_required = ResourceAbs.is_required(nested_field_name, cls)
|
105
|
+
if default_value := ResourceAbs.default_hcl_string(nested_field_name, cls):
|
106
|
+
context.add_default(nested_field_name, default_value)
|
107
|
+
lines.append(f" {nested_field_name} = optional({tf_type}, {default_value})")
|
108
|
+
elif is_required:
|
109
|
+
lines.append(f" {nested_field_name} = {tf_type}")
|
110
|
+
else:
|
111
|
+
lines.append(f" {nested_field_name} = optional({tf_type})")
|
112
|
+
lines.append("})")
|
113
|
+
return "\n".join(lines)
|
114
|
+
|
115
|
+
|
116
|
+
def generate_module_variables(
|
117
|
+
python_module: ResourceTypePythonModule, resource_config: ResourceGenConfig
|
118
|
+
) -> tuple[str, str]:
|
119
|
+
base_resource = python_module.resource
|
120
|
+
assert base_resource is not None, f"{python_module} does not have a resource"
|
121
|
+
skipped_names_in_resource_ext = set(python_module.base_field_names)
|
122
|
+
return generate_resource_variables(base_resource, resource_config), generate_resource_variables(
|
123
|
+
python_module.resource_ext, resource_config, skipped_names_in_resource_ext
|
124
|
+
)
|
125
|
+
|
126
|
+
|
127
|
+
def generate_resource_variables(
|
128
|
+
resource: type[ResourceAbs] | None, resource_config: ResourceGenConfig, extra_skipped: set[str] | None = None
|
129
|
+
) -> str:
|
130
|
+
extra_skipped = extra_skipped or set()
|
131
|
+
required_variables = resource_config.required_variables
|
132
|
+
if resource is None:
|
133
|
+
return ""
|
134
|
+
out = []
|
135
|
+
hints = get_type_hints(resource)
|
136
|
+
ignored_names = (
|
137
|
+
resource_config.skip_variables_extra
|
138
|
+
| resource.COMPUTED_ONLY_ATTRIBUTES
|
139
|
+
| getattr(resource, ResourceAbs.SKIP_VARIABLES_NAME, set())
|
140
|
+
| extra_skipped
|
141
|
+
)
|
142
|
+
if resource_config.use_single_variable:
|
143
|
+
context = DefaultValueContext(field_path=[], ignored_names=ignored_names)
|
144
|
+
tf_type = dataclass_to_object_type(resource_config.name, resource, context)
|
145
|
+
return format_tf_content(f'''variable "{resource_config.name}" {{
|
146
|
+
type = {tf_type}
|
147
|
+
}}\n''')
|
148
|
+
for f in fields(resource): # type: ignore
|
149
|
+
field_name = f.name
|
150
|
+
if field_name.isupper() or field_name in ignored_names:
|
151
|
+
continue
|
152
|
+
context = DefaultValueContext(field_path=[field_name])
|
153
|
+
tf_type = python_type_to_terraform_type(f, hints[field_name], context)
|
154
|
+
default_line = f"\n default = {context.final_str}" if field_name not in required_variables else ""
|
155
|
+
nullable_line = "\n nullable = true" if field_name not in required_variables else ""
|
156
|
+
out.append(f'''variable "{field_name}" {{
|
157
|
+
type = {tf_type}{nullable_line}{default_line}
|
158
|
+
}}\n''')
|
159
|
+
return format_tf_content("\n".join(out))
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from atlas_init.tf_ext.provider_schema import get_providers_tf
|
3
|
+
from zero_3rdparty.file_utils import ensure_parents_write_text
|
4
|
+
|
5
|
+
|
6
|
+
def dump_versions_tf(module_path: Path, skip_python: bool = False, minimal: bool = False) -> Path:
|
7
|
+
provider_path = module_path / "versions.tf"
|
8
|
+
if not provider_path.exists():
|
9
|
+
ensure_parents_write_text(provider_path, get_providers_tf(skip_python=skip_python, minimal=minimal))
|
10
|
+
return provider_path
|
@@ -0,0 +1,106 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from typing import Self
|
3
|
+
|
4
|
+
from model_lib import Entity
|
5
|
+
from pydantic import Field, RootModel, model_validator
|
6
|
+
|
7
|
+
from atlas_init.tf_ext.tf_dep import AtlasGraph
|
8
|
+
|
9
|
+
_emojii_list = [
|
10
|
+
"1️⃣",
|
11
|
+
"2️⃣",
|
12
|
+
"3️⃣",
|
13
|
+
"4️⃣",
|
14
|
+
"5️⃣",
|
15
|
+
"6️⃣",
|
16
|
+
"7️⃣",
|
17
|
+
"8️⃣",
|
18
|
+
"9️⃣",
|
19
|
+
"🔟",
|
20
|
+
"1️⃣1️⃣",
|
21
|
+
"1️⃣2️⃣",
|
22
|
+
]
|
23
|
+
_emoji_counter = 0
|
24
|
+
|
25
|
+
|
26
|
+
def choose_next_emoji() -> str:
|
27
|
+
global _emoji_counter
|
28
|
+
emoji = _emojii_list[_emoji_counter]
|
29
|
+
_emoji_counter += 1
|
30
|
+
return emoji
|
31
|
+
|
32
|
+
|
33
|
+
class ModuleState(Entity):
|
34
|
+
resource_types: set[str] = Field(default_factory=set, description="Set of resource types in the module.")
|
35
|
+
|
36
|
+
|
37
|
+
def default_allowed_multi_parents() -> set[str]:
|
38
|
+
return {
|
39
|
+
"mongodbatlas_project",
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
class ModuleConfig(Entity):
|
44
|
+
name: str = Field(..., description="Name of the module.")
|
45
|
+
root_resource_types: list[str] = Field(..., description="List of root resource types for the module.")
|
46
|
+
force_include_children: list[str] = Field(
|
47
|
+
default_factory=list, description="List of resource types that should always be included as children."
|
48
|
+
)
|
49
|
+
emojii: str = Field(init=False, default_factory=choose_next_emoji)
|
50
|
+
allowed_multi_parents: set[str] = Field(
|
51
|
+
default_factory=default_allowed_multi_parents,
|
52
|
+
description="Set of parents that a child resource type can have in addition to the root_resource_type.",
|
53
|
+
)
|
54
|
+
allow_external_dependencies: bool = Field(
|
55
|
+
default=False, description="Whether to allow external dependencies for the module."
|
56
|
+
)
|
57
|
+
extra_nested_resource_types: list[str] = Field(
|
58
|
+
default_factory=list,
|
59
|
+
description="List of additional nested resource types that should be included in the module.",
|
60
|
+
)
|
61
|
+
|
62
|
+
state: ModuleState = Field(default_factory=ModuleState, description="Internal state of the module.")
|
63
|
+
|
64
|
+
@model_validator(mode="after")
|
65
|
+
def update_state(self) -> Self:
|
66
|
+
self.state.resource_types.update(self.root_resource_types)
|
67
|
+
return self
|
68
|
+
|
69
|
+
@property
|
70
|
+
def tree_label(self) -> str:
|
71
|
+
return f"{self.emojii} {self.name}"
|
72
|
+
|
73
|
+
def include_child(self, child: str, atlas_graph: AtlasGraph) -> bool:
|
74
|
+
if child in atlas_graph.deprecated_resource_types:
|
75
|
+
return False
|
76
|
+
if child in self.force_include_children or child in self.extra_nested_resource_types:
|
77
|
+
self.state.resource_types.add(child)
|
78
|
+
return True
|
79
|
+
has_external_dependencies = len(atlas_graph.external_parents.get(child, [])) > 0
|
80
|
+
if self.allow_external_dependencies and has_external_dependencies:
|
81
|
+
has_external_dependencies = False
|
82
|
+
is_a_parent = bool(atlas_graph.parent_child_edges.get(child))
|
83
|
+
extra_parents = (
|
84
|
+
set(atlas_graph.all_parents(child))
|
85
|
+
- self.allowed_multi_parents
|
86
|
+
- set(self.root_resource_types)
|
87
|
+
- set(self.extra_nested_resource_types)
|
88
|
+
)
|
89
|
+
has_extra_parents = len(extra_parents) > 0
|
90
|
+
if has_external_dependencies or is_a_parent or has_extra_parents:
|
91
|
+
return False
|
92
|
+
self.state.resource_types.add(child)
|
93
|
+
return True
|
94
|
+
|
95
|
+
|
96
|
+
class ModuleConfigs(RootModel[dict[str, ModuleConfig]]):
|
97
|
+
def module_emoji_prefix(self, resource_type: str) -> str:
|
98
|
+
"""Get the emoji prefix for a resource type based on its module."""
|
99
|
+
return next(
|
100
|
+
(
|
101
|
+
module_config.emojii
|
102
|
+
for module_config in self.root.values()
|
103
|
+
if resource_type in module_config.state.resource_types
|
104
|
+
),
|
105
|
+
"",
|
106
|
+
)
|