Skip to content

Commit bf60fad

Browse files
Merge branch 'master' into daniel.zhou/CWS-3394-tf-provider
2 parents 4c19fd8 + 3efffc3 commit bf60fad

File tree

243 files changed

+11926
-3510
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

243 files changed

+11926
-3510
lines changed

.generator/README.md

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Terraform Generation
2+
3+
The goal of this sub-project is to generate the scaffolding to create a Terraform resource.
4+
5+
> [!CAUTION]
6+
> This code is HIGHLY experimental and should stabilize over the next weeks/months. As such this code is NOT intended for production uses.
7+
8+
## How to use
9+
10+
### Requirements
11+
12+
- This project
13+
- Poetry
14+
- An OpenApi 3.0.x specification (Datadog's OpenApi spec can be found [here](https://github.com/DataDog/datadog-api-client-go/tree/master/.generator/schemas))
15+
- Go
16+
17+
### Install dependencies
18+
19+
Install the necessary dependencies by running `poetry install`
20+
21+
Install go as we use the `go fmt` command on the generated files to format them.
22+
23+
### Marking the resources to be generated
24+
25+
The generator reads a configuration file in order to generate the appropriate resources.
26+
The configuration file should look like the following:
27+
28+
```yaml
29+
resources:
30+
{ resource_name }:
31+
read:
32+
method: { read_method }
33+
path: { read_path }
34+
create:
35+
method: { create_method }
36+
path: { create_path }
37+
update:
38+
method: { update_method }
39+
path: { update_path }
40+
delete:
41+
method: { delete_method }
42+
path: { delete_path }
43+
...
44+
```
45+
46+
- `resource_name` is the name of the resource to be generated.
47+
- `xxx_method` should be the HTTP method used by the relevant route
48+
- `xxx_path` should be the HTTP route of the resource's CRUD operation
49+
50+
> [!NOTE]
51+
> An example using the `team` resource would look like this:
52+
>
53+
> ```yaml
54+
> resources:
55+
> team:
56+
> read:
57+
> method: get
58+
> path: /api/v2/team/{team_id}
59+
> create:
60+
> method: post
61+
> path: /api/v2/team
62+
> update:
63+
> method: patch
64+
> path: /api/v2/team/{team_id}
65+
> delete:
66+
> method: delete
67+
> path: /api/v2/team/{team_id}
68+
> ```
69+
70+
### Running the generator
71+
72+
Once the configuration file is written, you can run the following command to generate the Terraform resources:
73+
74+
```sh
75+
$ poetry run python -m generator <openapi_spec_path> <configuration_path>
76+
```
77+
78+
> [!NOTE]
79+
> The generated resources will be placed in `datadog/fwprovider/`

.generator/poetry.lock

+430
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.generator/pyproject.toml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[project]
2+
name = "generator"
3+
version = "0.1.0"
4+
description = ""
5+
authors = [{ name = "Datadog", email = "[email protected]"}]
6+
license = "Apache-2.0"
7+
requires-python = ">=3.10"
8+
dependencies = [
9+
"click (>=8.1.8,<9.0.0)",
10+
"PyYaml (>=6.0,<7.0)",
11+
"jsonref (>=1.1.0,<2.0.0)",
12+
"jinja2 (>=3.1.5,<4.0.0)",
13+
"pytest (>=8.3.4,<9.0.0)",
14+
"pytest-bdd (>=8.1.0,<9.0.0)",
15+
]
16+
17+
[build-system]
18+
requires = ["poetry-core>=1.0.0"]
19+
build-backend = "poetry.core.masonry.api"

.generator/src/generator/__init__.py

Whitespace-only changes.

.generator/src/generator/__main__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .cli import cli
2+
3+
cli()

.generator/src/generator/cli.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import os
2+
import pathlib
3+
import shlex
4+
import click
5+
6+
from jinja2 import Template
7+
8+
from . import setup
9+
from . import openapi
10+
11+
12+
@click.command()
13+
@click.argument(
14+
"spec_path",
15+
type=click.Path(
16+
exists=True, file_okay=True, dir_okay=False, path_type=pathlib.Path
17+
),
18+
)
19+
@click.argument(
20+
"config_path",
21+
type=click.Path(
22+
exists=True, file_okay=True, dir_okay=False, path_type=pathlib.Path
23+
),
24+
)
25+
@click.option(
26+
"-o",
27+
"--output",
28+
default="../datadog/",
29+
type=click.Path(path_type=pathlib.Path),
30+
)
31+
def cli(spec_path, config_path, output):
32+
"""
33+
Generate a terraform code snippet from OpenAPI specification.
34+
"""
35+
env = setup.load_environment(version=spec_path.parent.name)
36+
37+
templates = setup.load_templates(env=env)
38+
39+
spec = setup.load(spec_path)
40+
config = setup.load(config_path)
41+
42+
resources_to_generate = openapi.get_resources(spec, config)
43+
44+
for name, resource in resources_to_generate.items():
45+
generate_resource(
46+
name=name,
47+
resource=resource,
48+
output=output,
49+
templates=templates,
50+
)
51+
52+
53+
def generate_resource(
54+
name: str, resource: dict, output: pathlib.Path, templates: dict[str, Template]
55+
) -> None:
56+
"""
57+
Generates files related to a resource.
58+
59+
:param name: The name of the resource.
60+
:param operation: The intermediate representation of the resource.
61+
:param output: The root where the files will be generated.
62+
:param templates: The templates of the generated files.
63+
"""
64+
# TF resource file
65+
filename = output / f"fwprovider/resource_datadog_{name}.go"
66+
with filename.open("w") as fp:
67+
fp.write(templates["base"].render(name=name, operations=resource))
68+
os.system(shlex.quote(f"go fmt {filename}"))
69+
70+
# TF test file
71+
filename = output / "tests" / f"resource_datadog_{name}_test.go"
72+
with filename.open("w") as fp:
73+
fp.write(templates["test"].render(name=name, operations=resource))
74+
os.system(shlex.quote(f"go fmt {filename}"))
75+
76+
# TF resource example
77+
filename = output.parent / f"examples/resources/datadog_{name}/resource.tf"
78+
with filename.open("w") as fp:
79+
fp.write(templates["example"].render(name=name, operations=resource))
80+
81+
# TF import example
82+
filename = output.parent / f"examples/resources/datadog_{name}/import.sh"
83+
with filename.open("w") as fp:
84+
fp.write(templates["import"].render(name=name, operations=resource))

.generator/src/generator/formatter.py

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""Data formatter."""
2+
3+
from .utils import snake_case, camel_case, untitle_case, schema_name
4+
5+
PRIMITIVE_TYPES = ["string", "number", "boolean", "integer"]
6+
7+
KEYWORDS = {
8+
"break",
9+
"case",
10+
"chan",
11+
"const",
12+
"continue",
13+
"default",
14+
"defer",
15+
"else",
16+
"fallthrough",
17+
"for",
18+
"func",
19+
"go",
20+
"goto",
21+
"if",
22+
"import",
23+
"interface",
24+
"map",
25+
"meta",
26+
"package",
27+
"range",
28+
"return",
29+
"select",
30+
"struct",
31+
"switch",
32+
"type",
33+
"var",
34+
}
35+
36+
SUFFIXES = {
37+
# Test
38+
"test",
39+
# $GOOS
40+
"aix",
41+
"android",
42+
"darwin",
43+
"dragonfly",
44+
"freebsd",
45+
"illumos",
46+
"js",
47+
"linux",
48+
"netbsd",
49+
"openbsd",
50+
"plan9",
51+
"solaris",
52+
"windows",
53+
# $GOARCH
54+
"386",
55+
"amd64",
56+
"arm",
57+
"arm64",
58+
"mips",
59+
"mips64",
60+
"mips64le",
61+
"mipsle",
62+
"ppc64",
63+
"ppc64le",
64+
"s390x",
65+
"wasm",
66+
}
67+
68+
69+
def sanitize_description(description):
70+
escaped_description = description.replace('"', '\\"')
71+
return " ".join(escaped_description.splitlines())
72+
73+
74+
def escape_reserved_keyword(word):
75+
"""
76+
Escape reserved language keywords like openapi generator does it
77+
:param word: Word to escape
78+
:return: The escaped word if it was a reserved keyword, the word unchanged otherwise
79+
"""
80+
if word in KEYWORDS:
81+
return f"{word}Var"
82+
return word
83+
84+
85+
def attribute_name(attribute):
86+
return escape_reserved_keyword(camel_case(attribute))
87+
88+
89+
def variable_name(attribute):
90+
return escape_reserved_keyword(untitle_case(camel_case(attribute)))
91+
92+
93+
def simple_type(schema, render_nullable=False, render_new=False):
94+
"""Return the simple type of a schema.
95+
96+
:param schema: The schema to extract the type from
97+
:return: The simple type name
98+
"""
99+
type_name = schema.get("type")
100+
type_format = schema.get("format")
101+
nullable = render_nullable and schema.get("nullable", False)
102+
103+
nullable_prefix = "datadog.NewNullable" if render_new else "datadog.Nullable"
104+
105+
if type_name == "integer":
106+
return {
107+
"int32": "int32" if not nullable else f"{nullable_prefix}Int32",
108+
"int64": "int64" if not nullable else f"{nullable_prefix}Int64",
109+
None: "int32" if not nullable else f"{nullable_prefix}Int32",
110+
}[type_format]
111+
112+
if type_name == "number":
113+
return {
114+
"double": "float64" if not nullable else f"{nullable_prefix}Float64",
115+
None: "float" if not nullable else f"{nullable_prefix}Float",
116+
}[type_format]
117+
118+
if type_name == "string":
119+
return {
120+
"date": "time.Time" if not nullable else f"{nullable_prefix}Time",
121+
"date-time": "time.Time" if not nullable else f"{nullable_prefix}Time",
122+
"email": "string" if not nullable else f"{nullable_prefix}String",
123+
"binary": "*os.File",
124+
None: "string" if not nullable else f"{nullable_prefix}String",
125+
}[type_format]
126+
if type_name == "boolean":
127+
return "bool" if not nullable else f"{nullable_prefix}Bool"
128+
129+
return None
130+
131+
132+
def get_terraform_schema_type(schema):
133+
return {
134+
"string": "String",
135+
"boolean": "Bool",
136+
"integer": "Int64",
137+
"number": "Int64",
138+
"array": "List",
139+
"object": "Block",
140+
None: "String",
141+
}[schema.get("type")]

0 commit comments

Comments
 (0)