atlas-init 0.1.1__py3-none-any.whl → 0.1.4__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 +3 -3
- atlas_init/atlas_init.yaml +18 -1
- atlas_init/cli.py +62 -70
- atlas_init/cli_cfn/app.py +40 -117
- atlas_init/cli_cfn/{cfn.py → aws.py} +129 -14
- atlas_init/cli_cfn/cfn_parameter_finder.py +89 -6
- atlas_init/cli_cfn/example.py +203 -0
- atlas_init/cli_cfn/files.py +63 -0
- atlas_init/cli_helper/run.py +18 -2
- atlas_init/cli_helper/tf_runner.py +4 -6
- atlas_init/cli_root/__init__.py +0 -0
- atlas_init/cli_root/trigger.py +153 -0
- atlas_init/cli_tf/app.py +211 -4
- atlas_init/cli_tf/changelog.py +103 -0
- atlas_init/cli_tf/debug_logs.py +221 -0
- atlas_init/cli_tf/debug_logs_test_data.py +253 -0
- atlas_init/cli_tf/github_logs.py +229 -0
- atlas_init/cli_tf/go_test_run.py +194 -0
- atlas_init/cli_tf/go_test_run_format.py +31 -0
- atlas_init/cli_tf/go_test_summary.py +144 -0
- atlas_init/cli_tf/hcl/__init__.py +0 -0
- atlas_init/cli_tf/hcl/cli.py +161 -0
- atlas_init/cli_tf/hcl/cluster_mig.py +348 -0
- atlas_init/cli_tf/hcl/parser.py +140 -0
- atlas_init/cli_tf/schema.py +222 -18
- atlas_init/cli_tf/schema_go_parser.py +236 -0
- atlas_init/cli_tf/schema_table.py +150 -0
- atlas_init/cli_tf/schema_table_models.py +155 -0
- atlas_init/cli_tf/schema_v2.py +599 -0
- atlas_init/cli_tf/schema_v2_api_parsing.py +298 -0
- atlas_init/cli_tf/schema_v2_sdk.py +361 -0
- atlas_init/cli_tf/schema_v3.py +222 -0
- atlas_init/cli_tf/schema_v3_sdk.py +279 -0
- atlas_init/cli_tf/schema_v3_sdk_base.py +68 -0
- atlas_init/cli_tf/schema_v3_sdk_create.py +216 -0
- atlas_init/humps.py +253 -0
- atlas_init/repos/cfn.py +6 -1
- atlas_init/repos/path.py +3 -3
- atlas_init/settings/config.py +14 -4
- atlas_init/settings/env_vars.py +16 -1
- atlas_init/settings/path.py +12 -1
- atlas_init/settings/rich_utils.py +2 -0
- atlas_init/terraform.yaml +77 -1
- atlas_init/tf/.terraform.lock.hcl +59 -83
- atlas_init/tf/always.tf +7 -0
- atlas_init/tf/main.tf +3 -0
- atlas_init/tf/modules/aws_s3/provider.tf +1 -1
- atlas_init/tf/modules/aws_vars/aws_vars.tf +2 -0
- atlas_init/tf/modules/aws_vpc/provider.tf +4 -1
- atlas_init/tf/modules/cfn/cfn.tf +47 -33
- atlas_init/tf/modules/cfn/kms.tf +54 -0
- atlas_init/tf/modules/cfn/resource_actions.yaml +1 -0
- atlas_init/tf/modules/cfn/variables.tf +31 -0
- atlas_init/tf/modules/cloud_provider/cloud_provider.tf +1 -0
- atlas_init/tf/modules/cloud_provider/provider.tf +1 -1
- atlas_init/tf/modules/cluster/cluster.tf +34 -24
- atlas_init/tf/modules/cluster/provider.tf +1 -1
- atlas_init/tf/modules/federated_vars/federated_vars.tf +3 -0
- atlas_init/tf/modules/federated_vars/provider.tf +1 -1
- atlas_init/tf/modules/project_extra/project_extra.tf +15 -1
- atlas_init/tf/modules/stream_instance/stream_instance.tf +1 -1
- atlas_init/tf/modules/vpc_peering/vpc_peering.tf +1 -1
- atlas_init/tf/modules/vpc_privatelink/versions.tf +1 -1
- atlas_init/tf/outputs.tf +11 -3
- atlas_init/tf/providers.tf +2 -1
- atlas_init/tf/variables.tf +12 -0
- atlas_init/typer_app.py +76 -0
- {atlas_init-0.1.1.dist-info → atlas_init-0.1.4.dist-info}/METADATA +36 -18
- atlas_init-0.1.4.dist-info/RECORD +91 -0
- {atlas_init-0.1.1.dist-info → atlas_init-0.1.4.dist-info}/WHEEL +1 -1
- atlas_init-0.1.1.dist-info/RECORD +0 -62
- /atlas_init/tf/modules/aws_vpc/{aws-vpc.tf → aws_vpc.tf} +0 -0
- {atlas_init-0.1.1.dist-info → atlas_init-0.1.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,599 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import re
|
6
|
+
from collections.abc import Iterable
|
7
|
+
from fnmatch import fnmatch
|
8
|
+
from pathlib import Path
|
9
|
+
from queue import Queue
|
10
|
+
from tempfile import TemporaryDirectory
|
11
|
+
from typing import Literal, TypeAlias
|
12
|
+
|
13
|
+
from model_lib import Entity, copy_and_validate, parse_model
|
14
|
+
from pydantic import ConfigDict, Field, model_validator
|
15
|
+
from zero_3rdparty.enum_utils import StrEnum
|
16
|
+
|
17
|
+
from atlas_init.cli_helper.run import run_binary_command_is_ok
|
18
|
+
from atlas_init.humps import decamelize, pascalize
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
INDENT = " "
|
22
|
+
|
23
|
+
|
24
|
+
class PlanModifier(StrEnum):
|
25
|
+
use_state_for_unknown = "UseStateForUnknown"
|
26
|
+
diff_suppress_json = "schemafunc.DiffSuppressJSON"
|
27
|
+
|
28
|
+
|
29
|
+
class SchemaAttributeValidator(StrEnum):
|
30
|
+
string_is_json = "validate.StringIsJSON"
|
31
|
+
|
32
|
+
|
33
|
+
class SchemaAttribute(Entity):
|
34
|
+
model_config = ConfigDict(
|
35
|
+
frozen=False,
|
36
|
+
validate_assignment=True,
|
37
|
+
populate_by_name=True,
|
38
|
+
extra="ignore",
|
39
|
+
) # type: ignore
|
40
|
+
|
41
|
+
type: str = ""
|
42
|
+
name: str = "" # populated by the key of the attributes dict
|
43
|
+
description: str = ""
|
44
|
+
schema_ref: str = ""
|
45
|
+
alias: str = "" # comma separated list of aliases
|
46
|
+
is_required: bool = False
|
47
|
+
is_optional: bool = False
|
48
|
+
is_computed: bool = False
|
49
|
+
plan_modifiers: list[PlanModifier] = Field(default_factory=list)
|
50
|
+
validators: list[SchemaAttributeValidator] = Field(default_factory=list)
|
51
|
+
# not used during dumping but backtrace which parameters are used in the api spec
|
52
|
+
parameter_ref: str = ""
|
53
|
+
|
54
|
+
@property
|
55
|
+
def tf_name(self) -> str:
|
56
|
+
return decamelize(self.name) # type: ignore
|
57
|
+
|
58
|
+
@property
|
59
|
+
def schema_ref_name(self) -> str:
|
60
|
+
return self.schema_ref.split("/")[-1] if "/" in self.schema_ref else ""
|
61
|
+
|
62
|
+
@property
|
63
|
+
def tf_struct_name(self) -> str:
|
64
|
+
return pascalize(self.schema_ref_name) or pascalize(self.name)
|
65
|
+
|
66
|
+
@property
|
67
|
+
def aliases(self) -> list[str]:
|
68
|
+
return self.alias.split(",") if self.alias else []
|
69
|
+
|
70
|
+
@property
|
71
|
+
def is_nested(self) -> bool:
|
72
|
+
return self.schema_ref != ""
|
73
|
+
|
74
|
+
def merge(self, other: SchemaAttribute) -> SchemaAttribute:
|
75
|
+
# avoid schema_ref when type is different, e.g., setting `pipeline` to a string
|
76
|
+
schema_ref = (
|
77
|
+
self.schema_ref or other.schema_ref if not self.type or self.type == other.type else self.schema_ref
|
78
|
+
)
|
79
|
+
return SchemaAttribute(
|
80
|
+
type=self.type or other.type,
|
81
|
+
name=self.name or other.name,
|
82
|
+
description=self.description or other.description,
|
83
|
+
schema_ref=schema_ref,
|
84
|
+
alias=self.alias or other.alias,
|
85
|
+
is_required=self.is_required or other.is_required,
|
86
|
+
is_optional=self.is_optional or other.is_optional,
|
87
|
+
is_computed=self.is_computed or other.is_computed,
|
88
|
+
plan_modifiers=self.plan_modifiers + other.plan_modifiers,
|
89
|
+
validators=self.validators + other.validators,
|
90
|
+
parameter_ref=self.parameter_ref or other.parameter_ref,
|
91
|
+
)
|
92
|
+
|
93
|
+
def set_attribute_type(
|
94
|
+
self,
|
95
|
+
attr_type: Literal["required", "optional", "computed", "computed_optional"],
|
96
|
+
) -> None:
|
97
|
+
match attr_type:
|
98
|
+
case "required":
|
99
|
+
self.is_required = True
|
100
|
+
self.is_computed = False
|
101
|
+
self.is_optional = False
|
102
|
+
case "optional":
|
103
|
+
self.is_optional = True
|
104
|
+
self.is_computed = False
|
105
|
+
self.is_required = False
|
106
|
+
case "computed":
|
107
|
+
self.is_computed = True
|
108
|
+
self.is_required = False
|
109
|
+
self.is_optional = False
|
110
|
+
case "computed_optional":
|
111
|
+
self.is_optional = True
|
112
|
+
self.is_computed = True
|
113
|
+
self.is_required = False
|
114
|
+
case _:
|
115
|
+
raise ValueError(f"Unknown attribute type {attr_type}")
|
116
|
+
|
117
|
+
|
118
|
+
class SkipAttribute(Exception): # noqa: N818
|
119
|
+
def __init__(self, attr_name: str):
|
120
|
+
self.attr_name = attr_name
|
121
|
+
super().__init__(f"Skipping attribute {attr_name}")
|
122
|
+
|
123
|
+
|
124
|
+
class NewAttribute(Exception): # noqa: N818
|
125
|
+
def __init__(self, attr_name: str):
|
126
|
+
self.attr_name = attr_name
|
127
|
+
super().__init__(f"New attribute {attr_name}")
|
128
|
+
|
129
|
+
|
130
|
+
def attr_path_matches(attr_path: str, path: str) -> bool:
|
131
|
+
return fnmatch(attr_path, path)
|
132
|
+
|
133
|
+
|
134
|
+
class AttributeTypeModifiers(Entity):
|
135
|
+
required: list[str] = Field(default_factory=list)
|
136
|
+
optional: list[str] = Field(default_factory=list)
|
137
|
+
computed: list[str] = Field(default_factory=list)
|
138
|
+
computed_optional: set[str] = Field(default_factory=set)
|
139
|
+
|
140
|
+
def set_attribute_type(self, attr: SchemaAttribute, parent_path: str = "") -> SchemaAttribute:
|
141
|
+
attr_path = f"{parent_path}.{attr.tf_name}" if parent_path else attr.tf_name
|
142
|
+
for attr_type, names in self.model_dump().items():
|
143
|
+
for path_matcher in names:
|
144
|
+
if attr_path_matches(attr_path, path_matcher):
|
145
|
+
logger.info(f"Setting attribute {attr_path} to {attr_type}")
|
146
|
+
attr.set_attribute_type(attr_type) # type: ignore
|
147
|
+
return attr
|
148
|
+
if attr.name in self.required:
|
149
|
+
attr.is_required = True
|
150
|
+
attr.is_computed = False
|
151
|
+
attr.is_optional = False
|
152
|
+
if attr.is_computed and attr.is_required:
|
153
|
+
logger.warning(f"Attribute {attr.name} cannot be both computed and required, using required")
|
154
|
+
attr.is_computed = False
|
155
|
+
if attr.is_optional and attr.is_required:
|
156
|
+
raise ValueError(f"Attribute {attr.name} cannot be both optional and required")
|
157
|
+
if not attr.is_computed and not attr.is_required and not attr.is_optional:
|
158
|
+
logger.warning(
|
159
|
+
f"Attribute {attr.name} is neither read_only, required, nor optional, using computed-optional"
|
160
|
+
)
|
161
|
+
attr.is_optional = True
|
162
|
+
attr.is_computed = True
|
163
|
+
return attr
|
164
|
+
|
165
|
+
|
166
|
+
class SDKModelExample(Entity):
|
167
|
+
name: str
|
168
|
+
examples: list[str] = Field(default_factory=list)
|
169
|
+
|
170
|
+
|
171
|
+
class SDKConversion(Entity):
|
172
|
+
sdk_start_refs: list[SDKModelExample] = Field(default_factory=list)
|
173
|
+
|
174
|
+
def __bool__(self) -> bool:
|
175
|
+
return bool(self.sdk_start_refs)
|
176
|
+
|
177
|
+
|
178
|
+
class SchemaResource(Entity):
|
179
|
+
name: str = "" # populated by the key of the resources dict
|
180
|
+
description: str = ""
|
181
|
+
attributes: dict[str, SchemaAttribute] = Field(default_factory=dict)
|
182
|
+
attributes_skip: set[str] = Field(default_factory=set)
|
183
|
+
paths: list[str] = Field(default_factory=list)
|
184
|
+
attribute_type_modifiers: AttributeTypeModifiers = Field(default_factory=AttributeTypeModifiers)
|
185
|
+
conversion: SDKConversion = Field(default_factory=SDKConversion)
|
186
|
+
|
187
|
+
@model_validator(mode="after")
|
188
|
+
def set_attribute_names(self):
|
189
|
+
for name, attr in self.attributes.items():
|
190
|
+
attr.name = name
|
191
|
+
return self
|
192
|
+
|
193
|
+
@property
|
194
|
+
def nested_refs(self) -> set[str]:
|
195
|
+
return {attr.schema_ref for attr in self.attributes.values() if attr.is_nested}
|
196
|
+
|
197
|
+
def lookup_tf_name(self, tf_name: str) -> SchemaAttribute:
|
198
|
+
for name, attr in self.attributes.items():
|
199
|
+
if tf_name == decamelize(name):
|
200
|
+
return attr
|
201
|
+
for alias in attr.aliases:
|
202
|
+
if tf_name == decamelize(alias):
|
203
|
+
return attr
|
204
|
+
raise ValueError(f"Attribute {tf_name} not found in resource {self.name}")
|
205
|
+
|
206
|
+
def lookup_attribute(self, name: str) -> SchemaAttribute:
|
207
|
+
if name in self.attributes_skip:
|
208
|
+
raise SkipAttribute(name)
|
209
|
+
if found := self.attributes.get(name):
|
210
|
+
return found
|
211
|
+
for attr in self.attributes.values():
|
212
|
+
if name in attr.aliases:
|
213
|
+
return attr
|
214
|
+
raise NewAttribute(name)
|
215
|
+
|
216
|
+
def sorted_attributes(self) -> list[SchemaAttribute]:
|
217
|
+
return sorted(self.attributes.values(), key=lambda a: a.name)
|
218
|
+
|
219
|
+
|
220
|
+
class OpenAPIChanges(Entity):
|
221
|
+
schema_prefix_removal: list[str] = Field(default_factory=list)
|
222
|
+
|
223
|
+
|
224
|
+
class SchemaV2(Entity):
|
225
|
+
attributes_skip: set[str] = Field(default_factory=set)
|
226
|
+
openapi_changes: OpenAPIChanges = Field(default_factory=OpenAPIChanges)
|
227
|
+
resources: dict[str, SchemaResource] = Field(default_factory=dict)
|
228
|
+
ref_resources: dict[str, SchemaResource] = Field(default_factory=dict)
|
229
|
+
|
230
|
+
def ref_resource(self, ref: str, use_name: str = "") -> SchemaResource:
|
231
|
+
if ref not in self.ref_resources:
|
232
|
+
raise ValueError(f"Resource {ref} not found in ref_resources")
|
233
|
+
resource = self.ref_resources[ref]
|
234
|
+
return copy_and_validate(resource, name=use_name) if use_name else resource
|
235
|
+
|
236
|
+
@model_validator(mode="after")
|
237
|
+
def set_resource_names(self):
|
238
|
+
for name, resource in self.resources.items():
|
239
|
+
resource.name = name
|
240
|
+
return self
|
241
|
+
|
242
|
+
@model_validator(mode="after")
|
243
|
+
def add_ignored_attributes(self):
|
244
|
+
for resource in self.resources.values():
|
245
|
+
resource.attributes_skip |= self.attributes_skip
|
246
|
+
return self
|
247
|
+
|
248
|
+
def reset_attributes_skip(self) -> None:
|
249
|
+
self.attributes_skip.clear()
|
250
|
+
for resource in self.resources.values():
|
251
|
+
resource.attributes_skip.clear()
|
252
|
+
|
253
|
+
|
254
|
+
def parse_schema(path: Path) -> SchemaV2:
|
255
|
+
return parse_model(path, t=SchemaV2)
|
256
|
+
|
257
|
+
|
258
|
+
def generate_resource_go_resource_schema(schema: SchemaV2, resource_name: str) -> str:
|
259
|
+
if resource_name not in schema.resources:
|
260
|
+
raise ValueError(f"Resource {resource_name} not found in schema")
|
261
|
+
resource = schema.resources[resource_name]
|
262
|
+
return generate_go_resource_schema(schema, resource)
|
263
|
+
|
264
|
+
|
265
|
+
def generate_resource_go_schemas(schema: SchemaV2) -> Iterable[str]:
|
266
|
+
for name, resource in schema.resources.items():
|
267
|
+
logger.info(f"Generating Go schema for {name}")
|
268
|
+
yield generate_go_resource_schema(schema, resource)
|
269
|
+
|
270
|
+
|
271
|
+
def package_name(resource_name: str) -> str:
|
272
|
+
return resource_name.replace("_", "").lower()
|
273
|
+
|
274
|
+
|
275
|
+
def indent(level: int, line: str) -> str:
|
276
|
+
return INDENT * level + line
|
277
|
+
|
278
|
+
|
279
|
+
admin_version = os.getenv("ATLAS_SDK_VERSION", "v20241023001")
|
280
|
+
|
281
|
+
_import_urls = [
|
282
|
+
"context",
|
283
|
+
"github.com/hashicorp/terraform-plugin-framework/attr",
|
284
|
+
"github.com/hashicorp/terraform-plugin-framework/diag",
|
285
|
+
"github.com/hashicorp/terraform-plugin-framework/resource/schema",
|
286
|
+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier",
|
287
|
+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier",
|
288
|
+
"github.com/hashicorp/terraform-plugin-framework/schema/validator",
|
289
|
+
"github.com/hashicorp/terraform-plugin-framework/types",
|
290
|
+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes",
|
291
|
+
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts",
|
292
|
+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion",
|
293
|
+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/schemafunc",
|
294
|
+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate",
|
295
|
+
f"go.mongodb.org/atlas-sdk/{admin_version}/admin",
|
296
|
+
]
|
297
|
+
_import_urls_dict = {url.split("/")[-1]: url for url in _import_urls}
|
298
|
+
|
299
|
+
package_usage_pattern = re.compile(r"(?P<package_name>[\w\d_]+)\.(?P<package_func>[\w\d_]+)")
|
300
|
+
|
301
|
+
_variable_names = set()
|
302
|
+
|
303
|
+
|
304
|
+
def add_go_variable_names(names: Iterable[str]) -> None:
|
305
|
+
_variable_names.update(names)
|
306
|
+
|
307
|
+
|
308
|
+
_variable_suffixes = (
|
309
|
+
"Model",
|
310
|
+
"ObjectType",
|
311
|
+
"ObjType",
|
312
|
+
)
|
313
|
+
|
314
|
+
|
315
|
+
def extend_import_urls(import_urls: set[str], code_lines: list[str]) -> None:
|
316
|
+
for line in code_lines:
|
317
|
+
for match in package_usage_pattern.finditer(line):
|
318
|
+
package_name = match.group("package_name")
|
319
|
+
if package_name in _variable_names:
|
320
|
+
continue
|
321
|
+
if package_name.endswith(_variable_suffixes):
|
322
|
+
continue
|
323
|
+
if package_name in _import_urls_dict:
|
324
|
+
import_urls.add(_import_urls_dict[package_name])
|
325
|
+
else:
|
326
|
+
err_msg = f"Unknown package '{package_name}' used in line {line}"
|
327
|
+
raise ValueError(err_msg)
|
328
|
+
|
329
|
+
|
330
|
+
def import_lines(import_urls: set[str]) -> list[str]:
|
331
|
+
stdlib_imports = {url for url in import_urls if "." not in url}
|
332
|
+
pkg_imports = import_urls - stdlib_imports
|
333
|
+
imports = sorted(stdlib_imports)
|
334
|
+
if imports:
|
335
|
+
imports.append("")
|
336
|
+
imports.extend(sorted(pkg_imports))
|
337
|
+
return [
|
338
|
+
"import (",
|
339
|
+
*[indent(1, f'"{url}"') if url else "" for url in imports],
|
340
|
+
")",
|
341
|
+
"",
|
342
|
+
]
|
343
|
+
|
344
|
+
|
345
|
+
def generate_go_resource_schema(schema: SchemaV2, resource: SchemaResource) -> str:
|
346
|
+
func_lines = resource_schema_func(schema, resource)
|
347
|
+
object_type_lines = resource_object_type_lines(schema, resource)
|
348
|
+
import_urls = set()
|
349
|
+
extend_import_urls(import_urls, func_lines)
|
350
|
+
extend_import_urls(import_urls, object_type_lines)
|
351
|
+
unformatted = "\n".join(
|
352
|
+
[
|
353
|
+
f"package {package_name(resource.name)}",
|
354
|
+
"",
|
355
|
+
*import_lines(import_urls),
|
356
|
+
"",
|
357
|
+
*func_lines,
|
358
|
+
"",
|
359
|
+
*object_type_lines,
|
360
|
+
]
|
361
|
+
)
|
362
|
+
return go_fmt(resource.name, unformatted)
|
363
|
+
|
364
|
+
|
365
|
+
def go_fmt(name: str, unformatted: str) -> str:
|
366
|
+
with TemporaryDirectory() as temp_dir:
|
367
|
+
filename = f"{name}.go"
|
368
|
+
result_file = Path(temp_dir) / filename
|
369
|
+
result_file.write_text(unformatted)
|
370
|
+
if not run_binary_command_is_ok("go", f"fmt {filename}", cwd=Path(temp_dir), logger=logger):
|
371
|
+
logger.warning(f"go file unformatted:\n{unformatted}")
|
372
|
+
raise ValueError(f"Failed to format {result_file}")
|
373
|
+
return result_file.read_text()
|
374
|
+
|
375
|
+
|
376
|
+
def resource_schema_func(schema: SchemaV2, resource: SchemaResource) -> list[str]:
|
377
|
+
func_lines = [
|
378
|
+
"func ResourceSchema(ctx context.Context) schema.Schema {",
|
379
|
+
indent(1, "return schema.Schema{"),
|
380
|
+
indent(2, "Attributes: map[string]schema.Attribute{"),
|
381
|
+
]
|
382
|
+
for attr in resource.sorted_attributes():
|
383
|
+
func_lines.extend(generate_go_attribute_schema_lines(schema, attr, 3, [resource]))
|
384
|
+
func_lines.extend((indent(2, "},"), indent(1, "}"), "}"))
|
385
|
+
return func_lines
|
386
|
+
|
387
|
+
|
388
|
+
_attr_schema_types = {
|
389
|
+
"string": "schema.StringAttribute",
|
390
|
+
}
|
391
|
+
_attr_nested_schema_types = {
|
392
|
+
"object": "schema.SingleNestedAttribute",
|
393
|
+
"array": "schema.ListNestedAttribute",
|
394
|
+
}
|
395
|
+
|
396
|
+
|
397
|
+
def attribute_header(attr: SchemaAttribute) -> str:
|
398
|
+
if attr.is_nested:
|
399
|
+
err_msg = f"Unknown nested attribute type: {attr.type}"
|
400
|
+
schema_types = _attr_nested_schema_types
|
401
|
+
else:
|
402
|
+
err_msg = f"Unknown attribute type: {attr.type}"
|
403
|
+
schema_types = _attr_schema_types
|
404
|
+
header = schema_types.get(attr.type)
|
405
|
+
if header is None:
|
406
|
+
raise NotImplementedError(err_msg)
|
407
|
+
return header
|
408
|
+
|
409
|
+
|
410
|
+
def plan_modifier_call(modifier: PlanModifier, default_pkg_name: str) -> str:
|
411
|
+
if "." in modifier:
|
412
|
+
return f"{modifier}(),"
|
413
|
+
return f"{default_pkg_name}.{pascalize(modifier)}(),"
|
414
|
+
|
415
|
+
|
416
|
+
def plan_modifiers_lines(attr: SchemaAttribute, line_indent: int) -> list[str]:
|
417
|
+
# sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression
|
418
|
+
plan_modifiers = attr.plan_modifiers
|
419
|
+
if not plan_modifiers:
|
420
|
+
return []
|
421
|
+
modifier_package = {"string": "stringplanmodifier"}.get(attr.type)
|
422
|
+
if not modifier_package:
|
423
|
+
raise NotImplementedError
|
424
|
+
modifier_header = {
|
425
|
+
"string": "planmodifier.String",
|
426
|
+
}.get(attr.type)
|
427
|
+
if not modifier_header:
|
428
|
+
raise NotImplementedError
|
429
|
+
return [
|
430
|
+
indent(line_indent, f"PlanModifiers: []{modifier_header}{{"),
|
431
|
+
*[indent(line_indent + 1, plan_modifier_call(modifier, modifier_package)) for modifier in plan_modifiers],
|
432
|
+
indent(line_indent, "},"),
|
433
|
+
]
|
434
|
+
|
435
|
+
|
436
|
+
def validate_call(validator: SchemaAttributeValidator) -> str:
|
437
|
+
if "." not in validator:
|
438
|
+
raise NotImplementedError
|
439
|
+
return f"{validator}(),"
|
440
|
+
|
441
|
+
|
442
|
+
def validate_attribute_lines(attr: SchemaAttribute, line_indent: int) -> list[str]:
|
443
|
+
# sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression
|
444
|
+
if not attr.validators or attr.is_computed:
|
445
|
+
return []
|
446
|
+
validator_header = {
|
447
|
+
"string": "validator.String",
|
448
|
+
}.get(attr.type)
|
449
|
+
if not validator_header:
|
450
|
+
raise NotImplementedError
|
451
|
+
return [
|
452
|
+
indent(line_indent, f"Validators: []{validator_header}{{"),
|
453
|
+
*[indent(line_indent + 1, validate_call(validator)) for validator in attr.validators],
|
454
|
+
indent(line_indent, "},"),
|
455
|
+
]
|
456
|
+
|
457
|
+
|
458
|
+
def generate_go_attribute_schema_lines(
|
459
|
+
schema: SchemaV2,
|
460
|
+
attr: SchemaAttribute,
|
461
|
+
line_indent: int,
|
462
|
+
parent_resources: list[SchemaResource],
|
463
|
+
) -> list[str]:
|
464
|
+
parent_path = ".".join(parent.name for parent in parent_resources[1:])
|
465
|
+
parent_resources[0].attribute_type_modifiers.set_attribute_type(attr, parent_path)
|
466
|
+
attr_name = attr.tf_name
|
467
|
+
lines = [indent(line_indent, f'"{attr_name}": {attribute_header(attr)}{{')]
|
468
|
+
if desc := attr.description or attr.is_nested and (desc := schema.ref_resource(attr.schema_ref).description):
|
469
|
+
lines.append(indent(line_indent + 1, f'Description: "{desc.replace('\n', '\\n')}",'))
|
470
|
+
lines.append(indent(line_indent + 1, f'MarkdownDescription: "{desc.replace('\n', '\\n')}",'))
|
471
|
+
if attr.is_required:
|
472
|
+
lines.append(indent(line_indent + 1, "Required: true,"))
|
473
|
+
if attr.is_optional:
|
474
|
+
lines.append(indent(line_indent + 1, "Optional: true,"))
|
475
|
+
if attr.is_computed:
|
476
|
+
lines.append(indent(line_indent + 1, "Computed: true,"))
|
477
|
+
if attr.validators:
|
478
|
+
lines.extend(validate_attribute_lines(attr, line_indent + 1))
|
479
|
+
if attr.plan_modifiers:
|
480
|
+
lines.extend(plan_modifiers_lines(attr, line_indent + 1))
|
481
|
+
if attr.is_nested:
|
482
|
+
nested_attr = schema.ref_resource(attr.schema_ref, use_name=attr_name)
|
483
|
+
if attr.type == "array":
|
484
|
+
lines.append(indent(line_indent + 1, "NestedObject: schema.NestedAttributeObject{"))
|
485
|
+
lines.extend(generate_nested_attribute_schema_lines(schema, line_indent + 1, parent_resources, nested_attr))
|
486
|
+
lines.append(indent(line_indent + 1, "},"))
|
487
|
+
else:
|
488
|
+
lines.extend(generate_nested_attribute_schema_lines(schema, line_indent + 1, parent_resources, nested_attr))
|
489
|
+
lines.append(indent(line_indent, "},"))
|
490
|
+
return lines
|
491
|
+
|
492
|
+
|
493
|
+
def generate_nested_attribute_schema_lines(
|
494
|
+
schema: SchemaV2, line_indent: int, parent_resources: list[SchemaResource], nested_attr: SchemaResource
|
495
|
+
) -> list[str]:
|
496
|
+
lines = [indent(line_indent, "Attributes: map[string]schema.Attribute{")]
|
497
|
+
for nes in nested_attr.attributes.values():
|
498
|
+
lines.extend(generate_go_attribute_schema_lines(schema, nes, line_indent + 1, [*parent_resources, nested_attr]))
|
499
|
+
lines.append(indent(line_indent, "},"))
|
500
|
+
return lines
|
501
|
+
|
502
|
+
|
503
|
+
ResourceTypes: TypeAlias = Literal["rs", "ds", "dsp", ""]
|
504
|
+
|
505
|
+
|
506
|
+
def as_struct_name(resource_name: str, resource_type: ResourceTypes) -> str:
|
507
|
+
return f"TF{pascalize(resource_name)}{resource_type.upper()}Model"
|
508
|
+
|
509
|
+
|
510
|
+
def struct_def(tf_name: str, resource_type: ResourceTypes) -> str:
|
511
|
+
name = as_struct_name(tf_name, resource_type)
|
512
|
+
return f"type {name} struct {{"
|
513
|
+
|
514
|
+
|
515
|
+
_tpf_types = {
|
516
|
+
"string": "String",
|
517
|
+
"int": "Int",
|
518
|
+
"bool": "Bool",
|
519
|
+
"map": "Map",
|
520
|
+
"list": "List",
|
521
|
+
"array": "List",
|
522
|
+
"object": "Object",
|
523
|
+
}
|
524
|
+
|
525
|
+
|
526
|
+
def as_tpf_type(type_name: str) -> str:
|
527
|
+
if tpf_type := _tpf_types.get(type_name):
|
528
|
+
return tpf_type
|
529
|
+
raise ValueError(f"Don't know how to convert {type_name} to TPF type")
|
530
|
+
|
531
|
+
|
532
|
+
def struct_field_line(attr: SchemaAttribute) -> str:
|
533
|
+
tpf_type = as_tpf_type(attr.type)
|
534
|
+
struct_field_name = pascalize(attr.tf_name).replace("Id", "ID").replace("Db", "DB") # type: ignore
|
535
|
+
return f'{struct_field_name} types.{tpf_type} `tfsdk:"{attr.tf_name}"`'
|
536
|
+
|
537
|
+
|
538
|
+
def as_object_type_name(attr: SchemaAttribute) -> str:
|
539
|
+
tpf_type = as_tpf_type(attr.type)
|
540
|
+
return f"{tpf_type}Type"
|
541
|
+
|
542
|
+
|
543
|
+
def custom_object_type_name(attr: SchemaAttribute) -> str:
|
544
|
+
return f"{pascalize(attr.tf_struct_name)}ObjectType"
|
545
|
+
|
546
|
+
|
547
|
+
def object_type_def(attr: SchemaAttribute) -> str:
|
548
|
+
return f"var {custom_object_type_name(attr)} = types.ObjectType{{AttrTypes: map[string]attr.Type{{"
|
549
|
+
|
550
|
+
|
551
|
+
def object_type_field_line(attr: SchemaAttribute) -> str:
|
552
|
+
if attr.is_nested:
|
553
|
+
return f'"{attr.tf_name}": {custom_object_type_name(attr)},'
|
554
|
+
object_type_name = as_object_type_name(attr)
|
555
|
+
return f'"{attr.tf_name}": types.{object_type_name},'
|
556
|
+
|
557
|
+
|
558
|
+
_used_refs: set[str] = set()
|
559
|
+
|
560
|
+
|
561
|
+
def resource_object_type_lines(schema: SchemaV2, resource: SchemaResource) -> list[str]:
|
562
|
+
nested_attributes: Queue[SchemaAttribute] = Queue()
|
563
|
+
lines = [
|
564
|
+
struct_def(resource.name, "rs"),
|
565
|
+
*[indent(1, struct_field_line(attr)) for attr in resource.sorted_attributes()],
|
566
|
+
"}",
|
567
|
+
"",
|
568
|
+
]
|
569
|
+
for attr in resource.sorted_attributes():
|
570
|
+
if attr.is_nested:
|
571
|
+
nested_attributes.put(attr)
|
572
|
+
while not nested_attributes.empty():
|
573
|
+
nested_attr = nested_attributes.get()
|
574
|
+
schema_ref = nested_attr.schema_ref
|
575
|
+
if schema_ref in _used_refs:
|
576
|
+
continue
|
577
|
+
_used_refs.add(schema_ref)
|
578
|
+
nested_resource = schema.ref_resource(schema_ref, use_name=nested_attr.tf_name)
|
579
|
+
logger.info(f"creating struct for nested attribute %s, ref={schema_ref}", nested_attr.name)
|
580
|
+
lines.extend(
|
581
|
+
[
|
582
|
+
struct_def(nested_attr.tf_struct_name, ""),
|
583
|
+
*[indent(1, struct_field_line(attr)) for attr in nested_resource.sorted_attributes()],
|
584
|
+
"}",
|
585
|
+
"",
|
586
|
+
]
|
587
|
+
)
|
588
|
+
lines.extend(
|
589
|
+
[
|
590
|
+
object_type_def(nested_attr),
|
591
|
+
*[indent(1, object_type_field_line(attr)) for attr in nested_resource.sorted_attributes()],
|
592
|
+
"}}",
|
593
|
+
"",
|
594
|
+
]
|
595
|
+
)
|
596
|
+
for attr in nested_resource.sorted_attributes():
|
597
|
+
if attr.is_nested:
|
598
|
+
nested_attributes.put(attr)
|
599
|
+
return lines
|