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,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
|