atlas-init 0.7.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_tf/example_update.py +20 -8
- atlas_init/cli_tf/hcl/modifier.py +22 -8
- atlas_init/settings/env_vars.py +12 -2
- atlas_init/tf_ext/api_call.py +9 -9
- atlas_init/tf_ext/args.py +16 -1
- 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_module.py +454 -0
- atlas_init/tf_ext/newres.py +90 -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 +151 -2
- atlas_init/tf_ext/tf_dep.py +5 -5
- 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 +5 -4
- atlas_init/tf_ext/tf_vars.py +13 -28
- atlas_init/tf_ext/typer_app.py +6 -2
- {atlas_init-0.7.0.dist-info → atlas_init-0.8.0.dist-info}/METADATA +4 -3
- {atlas_init-0.7.0.dist-info → atlas_init-0.8.0.dist-info}/RECORD +33 -17
- {atlas_init-0.7.0.dist-info → atlas_init-0.8.0.dist-info}/WHEEL +0 -0
- {atlas_init-0.7.0.dist-info → atlas_init-0.8.0.dist-info}/entry_points.txt +0 -0
- {atlas_init-0.7.0.dist-info → atlas_init-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,195 @@
|
|
1
|
+
import logging
|
2
|
+
from dataclasses import fields
|
3
|
+
from pathlib import Path
|
4
|
+
from tempfile import TemporaryDirectory
|
5
|
+
from typing import Iterable
|
6
|
+
|
7
|
+
from ask_shell import run_and_wait
|
8
|
+
|
9
|
+
from atlas_init.tf_ext.models_module import ModuleGenConfig, ResourceAbs, ResourceGenConfig
|
10
|
+
from atlas_init.tf_ext.schema_to_dataclass import ResourceTypePythonModule
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
def local_name_varsx(resource_type: str) -> str:
|
16
|
+
return f"{resource_type}_varsx"
|
17
|
+
|
18
|
+
|
19
|
+
def local_name_vars(resource_type: str) -> str:
|
20
|
+
return f"{resource_type}_vars"
|
21
|
+
|
22
|
+
|
23
|
+
def locals_def(module: ResourceTypePythonModule) -> str:
|
24
|
+
base_defs = "\n".join(f" {name} = var.{name}" for name in module.base_field_names_not_computed)
|
25
|
+
if extras := module.extra_fields_names:
|
26
|
+
extra_defs = "\n".join(f" {name} = var.{name}" for name in extras)
|
27
|
+
base_def = f" {local_name_varsx(module.resource_type)} = {{\n{base_defs}\n }}"
|
28
|
+
extra_def = f"\n {local_name_vars(module.resource_type)} = {{\n{extra_defs}\n }}"
|
29
|
+
else:
|
30
|
+
base_def = f" {local_name_vars(module.resource_type)} = {{\n{base_defs}\n }}"
|
31
|
+
extra_def = ""
|
32
|
+
return f"""
|
33
|
+
locals {{
|
34
|
+
{base_def}{extra_def}
|
35
|
+
}}
|
36
|
+
"""
|
37
|
+
|
38
|
+
|
39
|
+
def data_external(module: ResourceTypePythonModule, config: ModuleGenConfig) -> str:
|
40
|
+
input_json_parts = [
|
41
|
+
f"local.{local_name_vars(module.resource_type)}",
|
42
|
+
]
|
43
|
+
if module.extra_fields_names:
|
44
|
+
input_json_parts.append(f"local.{local_name_varsx(module.resource_type)}")
|
45
|
+
if extras := config.inputs_json_hcl_extras:
|
46
|
+
input_json_parts.extend(extras)
|
47
|
+
inputs_json_merge = input_json_parts[0] if len(input_json_parts) == 1 else f"merge({', '.join(input_json_parts)})"
|
48
|
+
return f"""
|
49
|
+
data "external" "{module.resource_type}" {{
|
50
|
+
program = ["python3", "${{path.module}}/{module.resource_type}.py"]
|
51
|
+
query = {{
|
52
|
+
input_json = jsonencode({inputs_json_merge})
|
53
|
+
}}
|
54
|
+
}}
|
55
|
+
"""
|
56
|
+
|
57
|
+
|
58
|
+
def resource_declare_direct(py_module: ResourceTypePythonModule, config: ResourceGenConfig) -> str:
|
59
|
+
parent_cls = py_module.resource
|
60
|
+
resource_type = py_module.resource_type
|
61
|
+
assert parent_cls, f"{resource_type} does not have a resource"
|
62
|
+
field_base = f"var.{resource_type}." if config.use_single_variable else "var."
|
63
|
+
field_values = "\n".join(
|
64
|
+
_field_value(parent_cls, name, field_base) for name in py_module.base_field_names_not_computed
|
65
|
+
)
|
66
|
+
|
67
|
+
return f"""
|
68
|
+
resource "{py_module.resource_type}" "this" {{
|
69
|
+
{field_values}
|
70
|
+
}}
|
71
|
+
"""
|
72
|
+
|
73
|
+
|
74
|
+
def _field_value(parent_cls: type[ResourceAbs], field_name: str, field_base: str = "var.") -> str:
|
75
|
+
if ResourceAbs.is_computed_only(field_name, parent_cls):
|
76
|
+
return ""
|
77
|
+
this_indent = " "
|
78
|
+
if ResourceAbs.is_block(field_name, parent_cls):
|
79
|
+
return "\n".join(f"{this_indent}{line}" for line in _handle_dynamic(parent_cls, field_name, field_base))
|
80
|
+
return this_indent + f"{field_name} = {field_base}{field_name}"
|
81
|
+
|
82
|
+
|
83
|
+
def _handle_dynamic(
|
84
|
+
parent_cls: type[ResourceAbs], dynamic_field_name: str, existing_ref: str = "var."
|
85
|
+
) -> Iterable[str]:
|
86
|
+
try:
|
87
|
+
container_type = next(
|
88
|
+
t for name, t in ResourceTypePythonModule.container_types(parent_cls) if name == dynamic_field_name
|
89
|
+
)
|
90
|
+
except StopIteration:
|
91
|
+
raise ValueError(f"Could not find container type for field {dynamic_field_name} in {parent_cls}")
|
92
|
+
hcl_ref = f"{dynamic_field_name}.value."
|
93
|
+
yield f'dynamic "{dynamic_field_name}" {{'
|
94
|
+
ref = existing_ref + dynamic_field_name
|
95
|
+
if container_type.is_list or container_type.is_set:
|
96
|
+
if container_type.is_optional:
|
97
|
+
yield f" for_each = {ref} == null ? [] : {ref}"
|
98
|
+
else:
|
99
|
+
yield f" for_each = {ref}"
|
100
|
+
elif container_type.is_dict:
|
101
|
+
raise NotImplementedError(f"Dict not supported for {dynamic_field_name} in {parent_cls}")
|
102
|
+
else: # singular
|
103
|
+
if container_type.is_optional:
|
104
|
+
yield f" for_each = {ref} == null ? [] : [{ref}]"
|
105
|
+
else:
|
106
|
+
yield f" for_each = [{ref}]"
|
107
|
+
yield " content {"
|
108
|
+
yield from [f" {line}" for line in _nested_fields(container_type.type, hcl_ref)]
|
109
|
+
yield " }"
|
110
|
+
yield "}"
|
111
|
+
|
112
|
+
|
113
|
+
def _nested_fields(cls: type[ResourceAbs], hcl_ref: str) -> Iterable[str]:
|
114
|
+
for field in fields(cls):
|
115
|
+
field_name = field.name
|
116
|
+
if ResourceAbs.is_computed_only(field_name, cls):
|
117
|
+
continue
|
118
|
+
if ResourceAbs.is_block(field_name, cls):
|
119
|
+
yield from _handle_dynamic(cls, field_name, hcl_ref)
|
120
|
+
else:
|
121
|
+
yield _field_value(cls, field_name, hcl_ref)
|
122
|
+
|
123
|
+
|
124
|
+
def resource_declare(
|
125
|
+
resource_type: str, required_fields: set[str], nested_fields: set[str], field_names: list[str]
|
126
|
+
) -> str:
|
127
|
+
def output_field(field_name: str) -> str:
|
128
|
+
return f"data.external.{resource_type}.result.{field_name}"
|
129
|
+
|
130
|
+
def as_output_field(field_name: str) -> str:
|
131
|
+
if field_name in nested_fields:
|
132
|
+
if field_name in required_fields:
|
133
|
+
return f"jsondecode({output_field(field_name)})"
|
134
|
+
return f'{output_field(field_name)} == "" ? null : jsondecode({output_field(field_name)})'
|
135
|
+
if field_name in required_fields:
|
136
|
+
return output_field(field_name)
|
137
|
+
return f'{output_field(field_name)} == "" ? null : {output_field(field_name)}'
|
138
|
+
|
139
|
+
required = [f" {field_name} = {as_output_field(field_name)}" for field_name in sorted(required_fields)]
|
140
|
+
non_required = [
|
141
|
+
f" {field_name} = {as_output_field(field_name)}"
|
142
|
+
for field_name in sorted(field_names)
|
143
|
+
if field_name not in required_fields
|
144
|
+
]
|
145
|
+
return f"""
|
146
|
+
resource "{resource_type}" "this" {{
|
147
|
+
lifecycle {{
|
148
|
+
precondition {{
|
149
|
+
condition = length({output_field("error_message")}) == 0
|
150
|
+
error_message = {output_field("error_message")}
|
151
|
+
}}
|
152
|
+
}}
|
153
|
+
|
154
|
+
{"\n".join(required)}
|
155
|
+
{"\n".join(non_required)}
|
156
|
+
}}
|
157
|
+
"""
|
158
|
+
|
159
|
+
|
160
|
+
def format_tf_content(content: str) -> str:
|
161
|
+
with TemporaryDirectory() as tmp_dir:
|
162
|
+
tmp_file = Path(tmp_dir) / "content.tf"
|
163
|
+
tmp_file.write_text(content)
|
164
|
+
try:
|
165
|
+
run_and_wait("terraform fmt .", cwd=tmp_dir)
|
166
|
+
except Exception as e:
|
167
|
+
logger.error(f"Failed to format tf content:\n{content}")
|
168
|
+
raise e
|
169
|
+
return tmp_file.read_text()
|
170
|
+
|
171
|
+
|
172
|
+
def generate_resource_main(python_module: ResourceTypePythonModule, config: ModuleGenConfig) -> str:
|
173
|
+
resource = python_module.resource_ext or python_module.resource
|
174
|
+
assert resource, f"{python_module} does not have a resource"
|
175
|
+
resource_hcl = (
|
176
|
+
resource_declare_direct(python_module, config.resource_config(python_module.resource_type))
|
177
|
+
if config.skip_python
|
178
|
+
else resource_declare(
|
179
|
+
resource_type=python_module.resource_type,
|
180
|
+
required_fields=resource.REQUIRED_ATTRIBUTES,
|
181
|
+
nested_fields=resource.NESTED_ATTRIBUTES,
|
182
|
+
field_names=python_module.base_field_names_not_computed,
|
183
|
+
)
|
184
|
+
)
|
185
|
+
return format_tf_content(
|
186
|
+
"\n".join(
|
187
|
+
[
|
188
|
+
*([] if config.skip_python else [locals_def(python_module)]),
|
189
|
+
*([] if config.skip_python else [data_external(python_module, config)]),
|
190
|
+
"",
|
191
|
+
resource_hcl,
|
192
|
+
"",
|
193
|
+
]
|
194
|
+
)
|
195
|
+
)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
from dataclasses import fields
|
2
|
+
from typing import Iterable
|
3
|
+
from atlas_init.tf_ext.models_module import ContainerType, ResourceTypePythonModule, ModuleGenConfig, ResourceAbs
|
4
|
+
|
5
|
+
|
6
|
+
def as_output(resource_type: str, field_name: str, output_name: str) -> str:
|
7
|
+
return _as_output(output_name, f"{resource_type}.this.{field_name}")
|
8
|
+
|
9
|
+
|
10
|
+
def _as_output(name: str, value: str) -> str:
|
11
|
+
return f"""\
|
12
|
+
output "{name}" {{
|
13
|
+
value = {value}
|
14
|
+
}}
|
15
|
+
"""
|
16
|
+
|
17
|
+
|
18
|
+
def as_nested_output(
|
19
|
+
resource_type: str,
|
20
|
+
parent_cls: type[ResourceAbs],
|
21
|
+
nested_types: dict[str, ContainerType[ResourceAbs]],
|
22
|
+
config: ModuleGenConfig,
|
23
|
+
) -> Iterable[str]:
|
24
|
+
resource_id = f"{resource_type}.this"
|
25
|
+
for field_name, container_type in nested_types.items():
|
26
|
+
if container_type.is_any:
|
27
|
+
continue
|
28
|
+
computed_nested_fields = [
|
29
|
+
nested_field.name
|
30
|
+
for nested_field in fields(container_type.type)
|
31
|
+
if ResourceAbs.is_computed_only(nested_field.name, container_type.type)
|
32
|
+
]
|
33
|
+
if container_type.is_list:
|
34
|
+
for computed_field_name in computed_nested_fields:
|
35
|
+
if container_type.is_optional and not ResourceAbs.is_required(field_name, parent_cls):
|
36
|
+
yield _as_output(
|
37
|
+
config.output_name(resource_type, field_name, computed_field_name),
|
38
|
+
f"{resource_id}.{field_name} == null ? null : {resource_id}.{field_name}[*].{computed_field_name}",
|
39
|
+
)
|
40
|
+
else:
|
41
|
+
yield _as_output(
|
42
|
+
config.output_name(resource_type, field_name, computed_field_name),
|
43
|
+
f"{resource_id}.{field_name}[*].{computed_field_name}",
|
44
|
+
)
|
45
|
+
elif container_type.is_set:
|
46
|
+
continue # block type "limits" is represented by a set of objects, and set elements do not have addressable keys. To find elements matching specific criteria, use a "for" expression with an "if" clause.
|
47
|
+
elif container_type.is_dict or container_type.is_set:
|
48
|
+
raise NotImplementedError("Dict and set container types not supported yet")
|
49
|
+
else:
|
50
|
+
for computed_field_name in computed_nested_fields:
|
51
|
+
if container_type.is_optional and not ResourceAbs.is_required(field_name, parent_cls):
|
52
|
+
yield _as_output(
|
53
|
+
config.output_name(resource_type, field_name, computed_field_name),
|
54
|
+
f"{resource_id}.{field_name} == null ? null : {resource_id}.{field_name}.{computed_field_name}",
|
55
|
+
)
|
56
|
+
else:
|
57
|
+
yield _as_output(
|
58
|
+
config.output_name(resource_type, field_name, computed_field_name),
|
59
|
+
f"{resource_id}.{field_name}.{computed_field_name}",
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
def generate_resource_output(py_module: ResourceTypePythonModule, config: ModuleGenConfig) -> str:
|
64
|
+
nested_types = dict(py_module.nested_field_types)
|
65
|
+
base_resource = py_module.resource
|
66
|
+
assert base_resource is not None, f"Resource {py_module.resource_type} has no base resource"
|
67
|
+
computed_field_names = [name for name in py_module.base_field_names_computed if name not in nested_types]
|
68
|
+
return "\n".join(
|
69
|
+
as_output(py_module.resource_type, field_name, config.output_name(py_module.resource_type, field_name))
|
70
|
+
for field_name in computed_field_names
|
71
|
+
) + "\n".join(as_nested_output(py_module.resource_type, base_resource, nested_types, config))
|
@@ -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
|