atlas-init 0.1.1__py3-none-any.whl → 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. atlas_init/__init__.py +3 -3
  2. atlas_init/atlas_init.yaml +18 -1
  3. atlas_init/cli.py +62 -70
  4. atlas_init/cli_cfn/app.py +40 -117
  5. atlas_init/cli_cfn/{cfn.py → aws.py} +129 -14
  6. atlas_init/cli_cfn/cfn_parameter_finder.py +89 -6
  7. atlas_init/cli_cfn/example.py +203 -0
  8. atlas_init/cli_cfn/files.py +63 -0
  9. atlas_init/cli_helper/run.py +18 -2
  10. atlas_init/cli_helper/tf_runner.py +4 -6
  11. atlas_init/cli_root/__init__.py +0 -0
  12. atlas_init/cli_root/trigger.py +153 -0
  13. atlas_init/cli_tf/app.py +211 -4
  14. atlas_init/cli_tf/changelog.py +103 -0
  15. atlas_init/cli_tf/debug_logs.py +221 -0
  16. atlas_init/cli_tf/debug_logs_test_data.py +253 -0
  17. atlas_init/cli_tf/github_logs.py +229 -0
  18. atlas_init/cli_tf/go_test_run.py +194 -0
  19. atlas_init/cli_tf/go_test_run_format.py +31 -0
  20. atlas_init/cli_tf/go_test_summary.py +144 -0
  21. atlas_init/cli_tf/hcl/__init__.py +0 -0
  22. atlas_init/cli_tf/hcl/cli.py +161 -0
  23. atlas_init/cli_tf/hcl/cluster_mig.py +348 -0
  24. atlas_init/cli_tf/hcl/parser.py +140 -0
  25. atlas_init/cli_tf/schema.py +222 -18
  26. atlas_init/cli_tf/schema_go_parser.py +236 -0
  27. atlas_init/cli_tf/schema_table.py +150 -0
  28. atlas_init/cli_tf/schema_table_models.py +155 -0
  29. atlas_init/cli_tf/schema_v2.py +599 -0
  30. atlas_init/cli_tf/schema_v2_api_parsing.py +298 -0
  31. atlas_init/cli_tf/schema_v2_sdk.py +361 -0
  32. atlas_init/cli_tf/schema_v3.py +222 -0
  33. atlas_init/cli_tf/schema_v3_sdk.py +279 -0
  34. atlas_init/cli_tf/schema_v3_sdk_base.py +68 -0
  35. atlas_init/cli_tf/schema_v3_sdk_create.py +216 -0
  36. atlas_init/humps.py +253 -0
  37. atlas_init/repos/cfn.py +6 -1
  38. atlas_init/repos/path.py +3 -3
  39. atlas_init/settings/config.py +14 -4
  40. atlas_init/settings/env_vars.py +16 -1
  41. atlas_init/settings/path.py +12 -1
  42. atlas_init/settings/rich_utils.py +2 -0
  43. atlas_init/terraform.yaml +77 -1
  44. atlas_init/tf/.terraform.lock.hcl +59 -83
  45. atlas_init/tf/always.tf +7 -0
  46. atlas_init/tf/main.tf +3 -0
  47. atlas_init/tf/modules/aws_s3/provider.tf +1 -1
  48. atlas_init/tf/modules/aws_vars/aws_vars.tf +2 -0
  49. atlas_init/tf/modules/aws_vpc/provider.tf +4 -1
  50. atlas_init/tf/modules/cfn/cfn.tf +47 -33
  51. atlas_init/tf/modules/cfn/kms.tf +54 -0
  52. atlas_init/tf/modules/cfn/resource_actions.yaml +1 -0
  53. atlas_init/tf/modules/cfn/variables.tf +31 -0
  54. atlas_init/tf/modules/cloud_provider/cloud_provider.tf +1 -0
  55. atlas_init/tf/modules/cloud_provider/provider.tf +1 -1
  56. atlas_init/tf/modules/cluster/cluster.tf +34 -24
  57. atlas_init/tf/modules/cluster/provider.tf +1 -1
  58. atlas_init/tf/modules/federated_vars/federated_vars.tf +3 -0
  59. atlas_init/tf/modules/federated_vars/provider.tf +1 -1
  60. atlas_init/tf/modules/project_extra/project_extra.tf +15 -1
  61. atlas_init/tf/modules/stream_instance/stream_instance.tf +1 -1
  62. atlas_init/tf/modules/vpc_peering/vpc_peering.tf +1 -1
  63. atlas_init/tf/modules/vpc_privatelink/versions.tf +1 -1
  64. atlas_init/tf/outputs.tf +11 -3
  65. atlas_init/tf/providers.tf +2 -1
  66. atlas_init/tf/variables.tf +12 -0
  67. atlas_init/typer_app.py +76 -0
  68. {atlas_init-0.1.1.dist-info → atlas_init-0.1.8.dist-info}/METADATA +36 -18
  69. atlas_init-0.1.8.dist-info/RECORD +91 -0
  70. {atlas_init-0.1.1.dist-info → atlas_init-0.1.8.dist-info}/WHEEL +1 -1
  71. atlas_init-0.1.1.dist-info/RECORD +0 -62
  72. /atlas_init/tf/modules/aws_vpc/{aws-vpc.tf → aws_vpc.tf} +0 -0
  73. {atlas_init-0.1.1.dist-info → atlas_init-0.1.8.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