atlas-init 0.1.0__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.
Files changed (74) hide show
  1. atlas_init/__init__.py +3 -3
  2. atlas_init/atlas_init.yaml +51 -34
  3. atlas_init/cli.py +76 -72
  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/go.py +6 -3
  10. atlas_init/cli_helper/run.py +18 -2
  11. atlas_init/cli_helper/tf_runner.py +12 -21
  12. atlas_init/cli_root/__init__.py +0 -0
  13. atlas_init/cli_root/trigger.py +153 -0
  14. atlas_init/cli_tf/app.py +211 -4
  15. atlas_init/cli_tf/changelog.py +103 -0
  16. atlas_init/cli_tf/debug_logs.py +221 -0
  17. atlas_init/cli_tf/debug_logs_test_data.py +253 -0
  18. atlas_init/cli_tf/github_logs.py +229 -0
  19. atlas_init/cli_tf/go_test_run.py +194 -0
  20. atlas_init/cli_tf/go_test_run_format.py +31 -0
  21. atlas_init/cli_tf/go_test_summary.py +144 -0
  22. atlas_init/cli_tf/hcl/__init__.py +0 -0
  23. atlas_init/cli_tf/hcl/cli.py +161 -0
  24. atlas_init/cli_tf/hcl/cluster_mig.py +348 -0
  25. atlas_init/cli_tf/hcl/parser.py +140 -0
  26. atlas_init/cli_tf/schema.py +222 -18
  27. atlas_init/cli_tf/schema_go_parser.py +236 -0
  28. atlas_init/cli_tf/schema_table.py +150 -0
  29. atlas_init/cli_tf/schema_table_models.py +155 -0
  30. atlas_init/cli_tf/schema_v2.py +599 -0
  31. atlas_init/cli_tf/schema_v2_api_parsing.py +298 -0
  32. atlas_init/cli_tf/schema_v2_sdk.py +361 -0
  33. atlas_init/cli_tf/schema_v3.py +222 -0
  34. atlas_init/cli_tf/schema_v3_sdk.py +279 -0
  35. atlas_init/cli_tf/schema_v3_sdk_base.py +68 -0
  36. atlas_init/cli_tf/schema_v3_sdk_create.py +216 -0
  37. atlas_init/humps.py +253 -0
  38. atlas_init/repos/cfn.py +6 -1
  39. atlas_init/repos/path.py +3 -3
  40. atlas_init/settings/config.py +30 -11
  41. atlas_init/settings/env_vars.py +29 -3
  42. atlas_init/settings/path.py +12 -1
  43. atlas_init/settings/rich_utils.py +39 -2
  44. atlas_init/terraform.yaml +77 -1
  45. atlas_init/tf/.terraform.lock.hcl +125 -0
  46. atlas_init/tf/always.tf +11 -2
  47. atlas_init/tf/main.tf +3 -0
  48. atlas_init/tf/modules/aws_s3/provider.tf +1 -1
  49. atlas_init/tf/modules/aws_vars/aws_vars.tf +2 -0
  50. atlas_init/tf/modules/aws_vpc/provider.tf +4 -1
  51. atlas_init/tf/modules/cfn/cfn.tf +47 -33
  52. atlas_init/tf/modules/cfn/kms.tf +54 -0
  53. atlas_init/tf/modules/cfn/resource_actions.yaml +1 -0
  54. atlas_init/tf/modules/cfn/variables.tf +31 -0
  55. atlas_init/tf/modules/cloud_provider/cloud_provider.tf +1 -0
  56. atlas_init/tf/modules/cloud_provider/provider.tf +1 -1
  57. atlas_init/tf/modules/cluster/cluster.tf +34 -24
  58. atlas_init/tf/modules/cluster/provider.tf +1 -1
  59. atlas_init/tf/modules/federated_vars/federated_vars.tf +3 -0
  60. atlas_init/tf/modules/federated_vars/provider.tf +1 -1
  61. atlas_init/tf/modules/project_extra/project_extra.tf +15 -1
  62. atlas_init/tf/modules/stream_instance/stream_instance.tf +1 -1
  63. atlas_init/tf/modules/vpc_peering/vpc_peering.tf +1 -1
  64. atlas_init/tf/modules/vpc_privatelink/versions.tf +1 -1
  65. atlas_init/tf/outputs.tf +11 -3
  66. atlas_init/tf/providers.tf +2 -1
  67. atlas_init/tf/variables.tf +17 -0
  68. atlas_init/typer_app.py +76 -0
  69. {atlas_init-0.1.0.dist-info → atlas_init-0.1.4.dist-info}/METADATA +58 -21
  70. atlas_init-0.1.4.dist-info/RECORD +91 -0
  71. {atlas_init-0.1.0.dist-info → atlas_init-0.1.4.dist-info}/WHEEL +1 -1
  72. atlas_init-0.1.0.dist-info/RECORD +0 -61
  73. /atlas_init/tf/modules/aws_vpc/{aws-vpc.tf → aws_vpc.tf} +0 -0
  74. {atlas_init-0.1.0.dist-info → atlas_init-0.1.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import re
5
+ from collections.abc import Iterable
6
+ from pathlib import Path
7
+ from queue import Queue
8
+ from typing import ClassVar
9
+
10
+ from model_lib import Entity, dump
11
+ from pydantic import Field
12
+
13
+ from atlas_init.cli_tf.schema_v2 import (
14
+ NewAttribute,
15
+ SchemaAttribute,
16
+ SchemaResource,
17
+ SchemaV2,
18
+ SkipAttribute,
19
+ parse_model,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def api_spec_text_changes(schema: SchemaV2, api_spec_parsed: OpenapiSchema) -> OpenapiSchema:
26
+ openapi_changes = schema.openapi_changes
27
+ schema_to_update = api_spec_parsed.components.get("schemas", {})
28
+ original_schema = {**api_spec_parsed.components.get("schemas", {})} # copy used for iteration
29
+ assert isinstance(original_schema, dict), "Expected a dict @ components.schemas"
30
+ for prefix in openapi_changes.schema_prefix_removal:
31
+ for name, value in original_schema.items():
32
+ if name.startswith(prefix):
33
+ schema_to_update.pop(name)
34
+ name_no_prefix = name.removeprefix(prefix)
35
+ assert (
36
+ name_no_prefix not in schema_to_update
37
+ ), f"removed {prefix} from {name} in schema but {name_no_prefix} already exists"
38
+ schema_to_update[name_no_prefix] = value
39
+ openapi_yaml = dump(api_spec_parsed, "yaml")
40
+ for prefix in openapi_changes.schema_prefix_removal:
41
+ pattern = re.compile(rf"{OpenapiSchema.SCHEMAS_PREFIX}(?P<prefix>{prefix})(?P<name>\w+)")
42
+ openapi_yaml = pattern.sub(rf"{OpenapiSchema.SCHEMAS_PREFIX}\g<name>", openapi_yaml)
43
+ return parse_model(openapi_yaml, t=OpenapiSchema, format="yaml")
44
+
45
+
46
+ def parse_openapi_schema_after_modifications(schema: SchemaV2, api_spec_path: Path) -> OpenapiSchema:
47
+ original = parse_model(api_spec_path, t=OpenapiSchema)
48
+ return api_spec_text_changes(schema, original)
49
+
50
+
51
+ class OpenapiSchema(Entity):
52
+ PARAMETERS_PREFIX: ClassVar[str] = "#/components/parameters/"
53
+ SCHEMAS_PREFIX: ClassVar[str] = "#/components/schemas/"
54
+
55
+ openapi: str
56
+ info: dict
57
+ paths: dict
58
+ components: dict
59
+ tags: list = Field(default_factory=list)
60
+
61
+ def create_method(self, path: str) -> dict | None:
62
+ return self.paths.get(path, {}).get("post")
63
+
64
+ def read_method(self, path: str) -> dict | None:
65
+ return self.paths.get(path, {}).get("get")
66
+
67
+ def method_refs(self, path: str) -> Iterable[str]:
68
+ for method in [self.create_method(path), self.read_method(path)]:
69
+ if method:
70
+ if req_ref := self.method_request_body_ref(method):
71
+ yield req_ref
72
+ if resp_ref := self.method_response_ref(method):
73
+ yield resp_ref
74
+
75
+ def parameter(self, ref: str) -> dict:
76
+ assert ref.startswith(OpenapiSchema.PARAMETERS_PREFIX)
77
+ parameter_name = ref.split("/")[-1]
78
+ param_dict = self.components["parameters"][parameter_name]
79
+ assert isinstance(param_dict, dict), f"Expected a dict @ {ref}"
80
+ return param_dict
81
+
82
+ def schema_properties(self, ref: str) -> Iterable[dict]:
83
+ assert ref.startswith(OpenapiSchema.SCHEMAS_PREFIX)
84
+ schema_name = ref.split("/")[-1]
85
+ schema_dict = self.components["schemas"][schema_name]
86
+ assert isinstance(schema_dict, dict), f"Expected a dict @ {ref}"
87
+ properties = schema_dict.get("properties", {})
88
+ assert isinstance(properties, dict), f"Expected a dict @ {ref}.properties"
89
+ for name, prop in properties.items():
90
+ assert isinstance(prop, dict), f"Expected a dict @ {ref}.properties.{name}"
91
+ prop["name"] = name
92
+ yield prop
93
+
94
+ def method_request_body_ref(self, method: dict) -> str | None:
95
+ request_body = method.get("requestBody", {})
96
+ return self._unpack_schema_ref(request_body)
97
+
98
+ def method_response_ref(self, method: dict) -> str | None:
99
+ responses = method.get("responses", {})
100
+ ok_response = responses.get("200", {})
101
+ return self._unpack_schema_ref(ok_response)
102
+
103
+ def _unpack_schema_ref(self, response: dict) -> str | None:
104
+ content = {**response.get("content", {})} # avoid side effects
105
+ if not content:
106
+ return None
107
+ key, value = content.popitem()
108
+ if not isinstance(key, str) or not key.endswith("json"):
109
+ return None
110
+ return value.get("schema", {}).get("$ref")
111
+
112
+ def schema_ref_component(self, ref: str, attributes_skip: set[str]) -> SchemaResource:
113
+ schemas = self.components.get("schemas", {})
114
+ assert isinstance(schemas, dict), "Expected a dict @ components.schemas"
115
+ schema = schemas.get(ref.split("/")[-1])
116
+ assert isinstance(schema, dict), f"Expected a dict @ components.schemas.{ref}"
117
+ return self._as_schema_resource(attributes_skip, schema, ref)
118
+
119
+ def schema_ref_components(self, attributes_skip: set[str]) -> Iterable[SchemaResource]:
120
+ schemas = self.components.get("schemas", {})
121
+ assert isinstance(schemas, dict), "Expected a dict @ components.schemas"
122
+ for name, schema in schemas.items():
123
+ ref = f"#/components/schemas/{name}"
124
+ yield self._as_schema_resource(attributes_skip, schema, ref)
125
+
126
+ def _as_schema_resource(self, attributes_skip: set[str], schema: dict, ref: str):
127
+ schema_resource = SchemaResource(
128
+ name=ref,
129
+ description=schema.get("description", ""),
130
+ attributes_skip=attributes_skip,
131
+ )
132
+ required_names = schema.get("required", [])
133
+ for prop in self.schema_properties(ref):
134
+ if attr := parse_api_spec_param(self, prop, schema_resource):
135
+ attr.is_required = prop["name"] in required_names
136
+ schema_resource.attributes[attr.name] = attr
137
+ return schema_resource
138
+
139
+ def add_schema_ref(self, ref: str, ref_value: dict) -> None:
140
+ if ref.startswith(self.PARAMETERS_PREFIX):
141
+ prefix = self.PARAMETERS_PREFIX
142
+ parent_dict = self.components["parameters"]
143
+ elif ref.startswith(self.SCHEMAS_PREFIX):
144
+ prefix = self.SCHEMAS_PREFIX
145
+ parent_dict = self.components["schemas"]
146
+ else:
147
+ err_msg = f"Unknown schema_ref {ref}"
148
+ raise ValueError(err_msg)
149
+ parent_dict[ref.removeprefix(prefix)] = ref_value
150
+
151
+ def resolve_ref(self, ref: str) -> dict:
152
+ if ref.startswith(self.PARAMETERS_PREFIX):
153
+ return self.parameter(ref)
154
+ if ref.startswith(self.SCHEMAS_PREFIX):
155
+ return self.components["schemas"][ref.split("/")[-1]]
156
+ err_msg = f"Unknown ref {ref}"
157
+ raise ValueError(err_msg)
158
+
159
+
160
+ def parse_api_spec_param(api_spec: OpenapiSchema, param: dict, resource: SchemaResource) -> SchemaAttribute | None:
161
+ match param:
162
+ case {"$ref": ref} if ref.startswith(OpenapiSchema.PARAMETERS_PREFIX):
163
+ param_root = api_spec.parameter(ref)
164
+ found_attribute = parse_api_spec_param(api_spec, param_root, resource)
165
+ if found_attribute:
166
+ found_attribute.parameter_ref = ref
167
+ return found_attribute
168
+ case {"$ref": ref, "name": name} if ref.startswith(OpenapiSchema.SCHEMAS_PREFIX):
169
+ # nested attribute
170
+ attribute = SchemaAttribute(
171
+ type="object",
172
+ name=name,
173
+ schema_ref=ref,
174
+ )
175
+ case {"type": "array", "items": {"$ref": ref}, "name": name}:
176
+ attribute = SchemaAttribute(
177
+ type="array",
178
+ name=name,
179
+ schema_ref=ref,
180
+ description=param.get("description", ""),
181
+ is_computed=param.get("readOnly", False),
182
+ is_required=param.get("required", False),
183
+ )
184
+ case {"name": name, "schema": schema}:
185
+ attribute = SchemaAttribute(
186
+ type=schema["type"],
187
+ name=name,
188
+ description=param.get("description", ""),
189
+ is_computed=schema.get("readOnly", False),
190
+ is_required=param.get("required", False),
191
+ )
192
+ case {"name": name, "type": type_}:
193
+ attribute = SchemaAttribute(
194
+ type=type_,
195
+ name=name,
196
+ description=param.get("description", ""),
197
+ is_computed=param.get("readOnly", False),
198
+ is_required=param.get("required", False),
199
+ )
200
+ case _:
201
+ raise NotImplementedError
202
+ try:
203
+ existing = resource.lookup_attribute(attribute.name)
204
+ logger.info(f"Merging attribute {attribute.name} into {existing.name}")
205
+ attribute = existing.merge(attribute)
206
+ except NewAttribute:
207
+ logger.info("Adding new attribute %s to %s", attribute.name, resource.name)
208
+ except SkipAttribute:
209
+ return None
210
+ resource.attributes[attribute.name] = attribute
211
+ return attribute
212
+
213
+
214
+ def add_api_spec_info(schema: SchemaV2, api_spec_path: Path, *, minimal_refs: bool = False) -> None:
215
+ api_spec = parse_openapi_schema_after_modifications(schema, api_spec_path)
216
+ for resource in schema.resources.values():
217
+ for path in resource.paths:
218
+ create_method = api_spec.create_method(path)
219
+ if not create_method:
220
+ continue
221
+ for param in create_method.get("parameters", []):
222
+ parse_api_spec_param(api_spec, param, resource)
223
+ if req_ref := api_spec.method_request_body_ref(create_method):
224
+ for property_dict in api_spec.schema_properties(req_ref):
225
+ parse_api_spec_param(api_spec, property_dict, resource)
226
+ for path in resource.paths:
227
+ read_method = api_spec.read_method(path)
228
+ if not read_method:
229
+ continue
230
+ for param in read_method.get("parameters", []):
231
+ parse_api_spec_param(api_spec, param, resource)
232
+ if response_ref := api_spec.method_response_ref(read_method):
233
+ for property_dict in api_spec.schema_properties(response_ref):
234
+ parse_api_spec_param(api_spec, property_dict, resource)
235
+ if minimal_refs:
236
+ minimal_ref_resources(schema, api_spec)
237
+ else:
238
+ for resource in api_spec.schema_ref_components(schema.attributes_skip):
239
+ schema.ref_resources[resource.name] = resource
240
+
241
+
242
+ def minimal_ref_resources(schema: SchemaV2, api_spec: OpenapiSchema) -> None:
243
+ include_refs = Queue()
244
+ seen_refs = set()
245
+ for resource in schema.resources.values():
246
+ for attribute in resource.attributes.values():
247
+ if attribute.schema_ref:
248
+ include_refs.put(attribute.schema_ref)
249
+ while not include_refs.empty():
250
+ ref = include_refs.get()
251
+ logger.info(f"Adding ref {ref}")
252
+ seen_refs.add(ref)
253
+ ref_resource = api_spec.schema_ref_component(ref, schema.attributes_skip)
254
+ schema.ref_resources[ref_resource.name] = ref_resource
255
+ for attribute in ref_resource.attributes.values():
256
+ if attribute.schema_ref and attribute.schema_ref not in seen_refs:
257
+ include_refs.put(attribute.schema_ref)
258
+
259
+
260
+ def minimal_api_spec(schema: SchemaV2, original_api_spec_path: Path) -> OpenapiSchema:
261
+ schema.reset_attributes_skip()
262
+ full_spec = parse_openapi_schema_after_modifications(schema, original_api_spec_path)
263
+ add_api_spec_info(schema, original_api_spec_path, minimal_refs=True)
264
+ minimal_spec = OpenapiSchema(
265
+ openapi=full_spec.openapi,
266
+ info=full_spec.info,
267
+ paths={},
268
+ components={"schemas": {}, "parameters": {}},
269
+ )
270
+ include_refs = Queue()
271
+ seen_refs = set()
272
+
273
+ def add_from_resource(resource: SchemaResource) -> None:
274
+ for path in resource.paths:
275
+ minimal_spec.paths[path] = full_spec.paths[path]
276
+ for ref in full_spec.method_refs(path):
277
+ minimal_spec.add_schema_ref(ref, full_spec.resolve_ref(ref))
278
+ include_refs.put(ref)
279
+ for attribute in resource.attributes.values():
280
+ if attribute.schema_ref:
281
+ minimal_spec.add_schema_ref(attribute.schema_ref, full_spec.resolve_ref(attribute.schema_ref))
282
+ include_refs.put(attribute.schema_ref)
283
+ if attribute.parameter_ref:
284
+ minimal_spec.add_schema_ref(
285
+ attribute.parameter_ref,
286
+ full_spec.resolve_ref(attribute.parameter_ref),
287
+ )
288
+
289
+ for resource in schema.resources.values():
290
+ add_from_resource(resource)
291
+ while not include_refs.empty():
292
+ ref = include_refs.get()
293
+ if ref in seen_refs:
294
+ continue
295
+ seen_refs.add(ref)
296
+ ref_resource = full_spec.schema_ref_component(ref, schema.attributes_skip)
297
+ add_from_resource(ref_resource)
298
+ return minimal_spec
@@ -0,0 +1,361 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import re
5
+ from functools import total_ordering
6
+ from pathlib import Path
7
+ from queue import Queue
8
+ from typing import NamedTuple
9
+
10
+ from model_lib import Entity
11
+ from pydantic import Field
12
+ from zero_3rdparty.enum_utils import StrEnum
13
+
14
+ from atlas_init.cli_tf.schema_v2 import (
15
+ SchemaAttribute,
16
+ SchemaResource,
17
+ SchemaV2,
18
+ add_go_variable_names,
19
+ as_struct_name,
20
+ custom_object_type_name,
21
+ extend_import_urls,
22
+ go_fmt,
23
+ import_lines,
24
+ package_name,
25
+ )
26
+ from atlas_init.humps import camelize, decamelize, pascalize
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class GoStdlibType(StrEnum):
32
+ STRING = "string"
33
+ STRING_POINTER = "*string"
34
+ TIME = "time.Time"
35
+ TIME_POINTER = "*time.Time"
36
+ BOOL = "bool"
37
+ BOOL_POINTER = "*bool"
38
+ INT = "int"
39
+ INT_POINTER = "*int"
40
+ INT64 = "int64"
41
+ INT64_POINTER = "*int64"
42
+ FLOAT64 = "float64"
43
+ FLOAT64_POINTER = "*float64"
44
+ MAP_STRING = "map[string]string"
45
+ MAP_STRING_POINTER = "*map[string]string"
46
+
47
+
48
+ _go_types = set(GoStdlibType)
49
+
50
+
51
+ def is_go_type(value: str) -> bool:
52
+ return value in _go_types
53
+
54
+
55
+ class GoVarName(StrEnum):
56
+ INPUT = "input"
57
+ ITEM = "item"
58
+ DIAGS = "diags"
59
+ CTX = "ctx"
60
+ ELEMENTS = "elements"
61
+ RESP = "resp"
62
+
63
+
64
+ add_go_variable_names(list(GoVarName))
65
+
66
+
67
+ _sdk_to_tf_funcs = {
68
+ ("*string", "string"): lambda sdk_ref: f"types.StringPointerValue({sdk_ref})",
69
+ ("string", "string"): lambda sdk_ref: f"types.StringValue({sdk_ref})",
70
+ (
71
+ "*time.Time",
72
+ "string",
73
+ ): lambda sdk_ref: f"types.StringPointerValue(conversion.TimePtrToStringPtr({sdk_ref}))",
74
+ }
75
+ # sdk_name -> tf_name
76
+ _sdk_attribute_aliases = {
77
+ "group_id": "project_id",
78
+ "mongo_dbemployee_access_grant": "mongo_db_employee_access_grant",
79
+ "mongo_dbmajor_version": "mongo_db_major_version",
80
+ "mongo_dbversion": "mongo_db_version",
81
+ }
82
+
83
+
84
+ @total_ordering
85
+ class SDKAttribute(Entity):
86
+ go_type: GoStdlibType | str
87
+ json_name: str
88
+ struct_name: str
89
+ nested_attributes: dict[str, SDKAttribute] = Field(default_factory=dict)
90
+
91
+ @property
92
+ def tf_name(self) -> str:
93
+ default = decamelize(self.json_name)
94
+ if override := _sdk_attribute_aliases.get(default):
95
+ return override
96
+ return default
97
+
98
+ @property
99
+ def is_nested(self) -> bool:
100
+ return not is_go_type(self.go_type) or bool(self.nested_attributes)
101
+
102
+ @property
103
+ def struct_type_name(self) -> str:
104
+ assert self.is_nested
105
+ return self.go_type.removeprefix("*").removeprefix("[]")
106
+
107
+ def list_nested_attributes(self) -> list[SDKAttribute]:
108
+ return [attribute for attribute in sorted(self.nested_attributes.values()) if attribute.is_nested]
109
+
110
+ def __lt__(self, other) -> bool:
111
+ if not isinstance(other, SDKAttribute):
112
+ raise TypeError
113
+ return self.json_name < other.json_name
114
+
115
+ def as_sdk_model(self) -> SDKModel:
116
+ assert self.is_nested
117
+ return SDKModel(
118
+ name=self.struct_type_name,
119
+ attributes=self.nested_attributes,
120
+ )
121
+
122
+
123
+ SDKAttribute.model_rebuild()
124
+
125
+
126
+ class SDKModel(Entity):
127
+ name: str
128
+ attributes: dict[str, SDKAttribute] = Field(default_factory=dict)
129
+ example_files: list[str] = Field(default_factory=list)
130
+
131
+ def lookup_tf_name(self, tf_name: str) -> SDKAttribute:
132
+ for attribute in self.attributes.values():
133
+ if attribute.tf_name == tf_name:
134
+ return attribute
135
+ raise ValueError(f"Could not find SDK attribute for {self.name}.{tf_name}")
136
+
137
+ def list_nested_attributes(self) -> list[SDKAttribute]:
138
+ return [attribute for attribute in sorted(self.attributes.values()) if attribute.is_nested]
139
+
140
+
141
+ json_attribute_line = re.compile(
142
+ r"^\s+(?P<struct_name>\w+)\s+(?P<go_type>[\w\*\.\[\]]+)\s+`json:\"(?P<json_name>\w+)",
143
+ re.M,
144
+ )
145
+
146
+ _ignored_sdk_attributes = {"href", "links"}
147
+
148
+
149
+ def parse_sdk_model(repo_path: Path, model_name: str) -> SDKModel:
150
+ model_path = repo_path / "admin" / f"model_{decamelize(model_name)}.go"
151
+ model = SDKModel(name=model_name)
152
+ for match in json_attribute_line.finditer(model_path.read_text()):
153
+ struct_name = match.group("struct_name")
154
+ go_type = match.group("go_type")
155
+ json_name = match.group("json_name")
156
+ if json_name in _ignored_sdk_attributes:
157
+ continue
158
+ sdk_attribute = model.attributes[struct_name] = SDKAttribute(
159
+ go_type=go_type, json_name=json_name, struct_name=struct_name
160
+ )
161
+ if sdk_attribute.is_nested:
162
+ nested_model = parse_sdk_model(repo_path, sdk_attribute.struct_type_name)
163
+ sdk_attribute.nested_attributes = nested_model.attributes
164
+ return model
165
+
166
+
167
+ def generate_model_go(schema: SchemaV2, resource: SchemaResource, sdk_model: SDKModel) -> str:
168
+ func_lines = sdk_to_tf_func(schema, resource, sdk_model)
169
+ import_urls = set()
170
+ extend_import_urls(import_urls, func_lines)
171
+ unformatted = "\n".join(
172
+ [
173
+ f"package {package_name(resource.name)}",
174
+ "",
175
+ *import_lines(import_urls),
176
+ "",
177
+ *func_lines,
178
+ ]
179
+ )
180
+ return go_fmt(resource.name, unformatted)
181
+
182
+
183
+ def find_schema_attribute(schema: SchemaV2, parent: SchemaResource, sdk_attribute: SDKAttribute) -> SchemaAttribute:
184
+ err_msg = "we might need the schema to lookup sdk_attribute as an escape hatch"
185
+ assert schema, err_msg
186
+ return parent.lookup_tf_name(sdk_attribute.tf_name)
187
+
188
+
189
+ def find_sdk_attribute(schema_attribute: SchemaAttribute, sdk_model: SDKModel) -> SDKAttribute:
190
+ return sdk_model.lookup_tf_name(schema_attribute.tf_name)
191
+
192
+
193
+ class SDKAndSchemaAttribute(NamedTuple):
194
+ sdk_attribute: SDKAttribute
195
+ schema_attribute: SchemaAttribute
196
+
197
+
198
+ def sdk_to_tf_attribute_value(
199
+ schema_attribute: SchemaAttribute,
200
+ sdk_attribute: SDKAttribute,
201
+ variable_name: GoVarName = GoVarName.INPUT,
202
+ ) -> str:
203
+ key = (sdk_attribute.go_type, schema_attribute.type)
204
+ if key in _sdk_to_tf_funcs:
205
+ return _sdk_to_tf_funcs[key](f"{variable_name}.{sdk_attribute.struct_name}")
206
+ raise ValueError(f"Could not find conversion function for {key}")
207
+
208
+
209
+ def sdk_to_tf_func(schema: SchemaV2, resource: SchemaResource, sdk_model: SDKModel) -> list[str]:
210
+ struct_name = as_struct_name(resource.name, "")
211
+ lines = [
212
+ f"func New{struct_name}(ctx context.Context, {GoVarName.INPUT} *admin.{sdk_model.name}) (*{struct_name}, diag.Diagnostics) {{"
213
+ ]
214
+ nested_attributes, call_lines = call_nested_functions(schema, resource, sdk_model.list_nested_attributes())
215
+ lines.extend(call_lines)
216
+
217
+ lines.append(f" return &{struct_name}{{")
218
+ lines.extend(tf_struct_create(resource, sdk_model))
219
+ lines.append(" }, nil") # close return
220
+ lines.append("}\n") # close function
221
+
222
+ lines.extend(process_nested_attributes(schema, nested_attributes))
223
+ return lines
224
+
225
+
226
+ def process_nested_attributes(schema: SchemaV2, nested_attributes: Queue[SDKAndSchemaAttribute]) -> list[str]:
227
+ lines = []
228
+ while not nested_attributes.empty():
229
+ sdk_attribute, schema_attribute = nested_attributes.get()
230
+ lines.extend(sdk_to_tf_func_nested(schema, schema_attribute, sdk_attribute))
231
+ return lines
232
+
233
+
234
+ def as_tf_struct_field_name(schema_attribute: SchemaAttribute) -> str:
235
+ default = pascalize(schema_attribute.name)
236
+ if default.endswith("Id"):
237
+ return default[:-2] + "ID"
238
+ return default
239
+
240
+
241
+ def tf_struct_create(
242
+ resource: SchemaResource,
243
+ sdk_model: SDKModel,
244
+ sdk_var_name: GoVarName = GoVarName.INPUT,
245
+ ) -> list[str]:
246
+ lines = []
247
+ for schema_attribute in resource.sorted_attributes():
248
+ sdk_attribute = find_sdk_attribute(schema_attribute, sdk_model)
249
+ tf_struct_field_name = as_tf_struct_field_name(schema_attribute)
250
+ if sdk_attribute.is_nested:
251
+ var_name = camelize(sdk_attribute.struct_name)
252
+ lines.append(f" {tf_struct_field_name}: {var_name},")
253
+ else:
254
+ lines.append(
255
+ f" {tf_struct_field_name}: {sdk_to_tf_attribute_value(schema_attribute, sdk_attribute, sdk_var_name)},"
256
+ )
257
+ return lines
258
+
259
+
260
+ def call_nested_functions(
261
+ schema: SchemaV2, resource: SchemaResource, sdk_attributes: list[SDKAttribute]
262
+ ) -> tuple[Queue[SDKAndSchemaAttribute], list[str]]:
263
+ nested_attributes: Queue[SDKAndSchemaAttribute] = Queue()
264
+ lines = []
265
+ for sdk_attribute in sdk_attributes:
266
+ schema_attribute = find_schema_attribute(schema, resource, sdk_attribute)
267
+ var_name = camelize(sdk_attribute.struct_name)
268
+ lines.append(
269
+ f" {var_name} := New{custom_object_type_name(schema_attribute)}(ctx, {GoVarName.INPUT}.{sdk_attribute.struct_name}, {GoVarName.DIAGS})"
270
+ )
271
+ nested_attributes.put(SDKAndSchemaAttribute(sdk_attribute, schema_attribute))
272
+ if lines:
273
+ lines.insert(0, " diags := &diag.Diagnostics{}")
274
+ lines.extend(
275
+ [
276
+ " if diags.HasError() {",
277
+ " return nil, *diags",
278
+ " }",
279
+ ]
280
+ )
281
+ return nested_attributes, lines
282
+
283
+
284
+ _used_refs: set[str] = set()
285
+
286
+
287
+ def sdk_to_tf_func_nested(
288
+ schema: SchemaV2, schema_attribute: SchemaAttribute, sdk_attribute: SDKAttribute
289
+ ) -> list[str]:
290
+ if schema_attribute.schema_ref in _used_refs:
291
+ return []
292
+ _used_refs.add(schema_attribute.schema_ref)
293
+ is_object = schema_attribute.type == "object"
294
+ if is_object:
295
+ return sdk_to_tf_func_object(schema, schema_attribute, sdk_attribute)
296
+ return sdk_to_tf_func_list(schema, schema_attribute, sdk_attribute)
297
+
298
+
299
+ def sdk_to_tf_func_object(
300
+ schema: SchemaV2, schema_attribute: SchemaAttribute, sdk_attribute: SDKAttribute
301
+ ) -> list[str]:
302
+ object_type_name = custom_object_type_name(schema_attribute)
303
+ lines: list[str] = [
304
+ f"func New{object_type_name}(ctx context.Context, {GoVarName.INPUT} *admin.{sdk_attribute.struct_type_name}, diags *diag.Diagnostics) types.Object {{",
305
+ f" var nilPointer *admin.{sdk_attribute.struct_type_name}",
306
+ f" if {GoVarName.INPUT} == nilPointer {{",
307
+ f" return types.ObjectNull({object_type_name}.AttrTypes)",
308
+ " }",
309
+ ]
310
+ resource = schema.ref_resource(schema_attribute.schema_ref, use_name=schema_attribute.schema_ref_name)
311
+ nested_attributes, call_lines = call_nested_functions(schema, resource, sdk_attribute.list_nested_attributes())
312
+ lines.extend(call_lines)
313
+ struct_name = as_struct_name(resource.name, "")
314
+
315
+ lines.extend(
316
+ [
317
+ f" tfModel := {struct_name}{{",
318
+ *tf_struct_create(resource, sdk_attribute.as_sdk_model()),
319
+ " }",
320
+ f" objType, diagsLocal := types.ObjectValueFrom(ctx, {object_type_name}.AttrTypes, tfModel)",
321
+ f" {GoVarName.DIAGS}.Append(diagsLocal...)",
322
+ " return objType",
323
+ "}\n",
324
+ ]
325
+ )
326
+ lines.extend(process_nested_attributes(schema, nested_attributes))
327
+ return lines
328
+
329
+
330
+ def sdk_to_tf_func_list(schema: SchemaV2, schema_attribute: SchemaAttribute, sdk_attribute: SDKAttribute) -> list[str]:
331
+ list_object_type = custom_object_type_name(schema_attribute)
332
+ nested_resource = schema.ref_resource(schema_attribute.schema_ref, use_name=schema_attribute.schema_ref_name)
333
+ lines: list[str] = [
334
+ f"func New{list_object_type}(ctx context.Context, {GoVarName.INPUT} *[]admin.{sdk_attribute.struct_type_name}, diags *diag.Diagnostics) types.List {{",
335
+ f" var nilPointer *[]admin.{sdk_attribute.struct_type_name}",
336
+ f" if {GoVarName.INPUT} == nilPointer {{",
337
+ f" return types.ListNull({list_object_type})",
338
+ " }",
339
+ ]
340
+ struct_name = as_struct_name(nested_resource.name, "")
341
+ if nested_list_attributes := [attr for attr in sdk_attribute.list_nested_attributes() if attr.is_nested]:
342
+ logger.warning(f"Nested list attributes: {nested_list_attributes}, are not supported yet.")
343
+ lines.extend(
344
+ [
345
+ f" tfModels := make([]{struct_name}, len(*{GoVarName.INPUT}))",
346
+ f" for i, item := range *{GoVarName.INPUT} {{",
347
+ f" tfModels[i] = {struct_name}{{",
348
+ *tf_struct_create(
349
+ nested_resource,
350
+ sdk_attribute.as_sdk_model(),
351
+ sdk_var_name=GoVarName.ITEM,
352
+ ),
353
+ " }",
354
+ " }",
355
+ f" listType, diagsLocal := types.ListValueFrom(ctx, {list_object_type}, tfModels)",
356
+ f" {GoVarName.DIAGS}.Append(diagsLocal...)",
357
+ " return listType",
358
+ "}\n",
359
+ ]
360
+ )
361
+ return lines