Skip to content

Commit 13c421e

Browse files
authored
Merge pull request #305 from breml/generate-data-sources
Generate data sources for network and storage (and more)
2 parents 0fba2a0 + b534425 commit 13c421e

Some content is hidden

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

48 files changed

+5512
-51
lines changed

.gitattributes

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto
3+
4+
# Collapse generated and vendored files on GitHub
5+
*_gen.* linguist-generated merge=ours
6+
*_gen_test.* linguist-generated merge=ours
7+
go.sum linguist-generated merge=ours
8+
go.mod linguist-generated
9+
10+
# Reduce conflicts on markdown files
11+
*.md merge=union
12+
13+
# A set of files you probably don't want in distribution
14+
/.github export-ignore
15+
.gitattributes export-ignore
16+
.gitignore export-ignore

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ Separate commits should be used for:
4545

4646
- Documentation (`docs: Update XYZ` for files in `docs/`)
4747
- Resources (`<resource>: Add XYZ` for changes to resources in `internal/`)
48+
- Generated (`<resource>: Generated XYZ` for changes to resources in `internal/`)
4849
- Tests (`tests: Add test for XYZ` for changes to `tests/`)
4950

50-
This structure makes it easiser for contributions to be reviewed.
51+
This structure makes it easier for contributions to be reviewed.
5152

5253
### Developer Certificate of Origin
5354

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ testacc:
1515
build:
1616
$(GO) build -v
1717

18+
generate:
19+
$(GO) generate ./...
20+
1821
targets:
1922
gox -osarch='$(TARGETS)' -output="dist/{{.OS}}_{{.Arch}}/terraform-provider-incus_${TRAVIS_TAG}_x4"
2023
find dist -maxdepth 1 -mindepth 1 -type d -print0 | \

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,29 @@ provider_installation {
5252
}
5353
```
5454

55+
#### Generated code
56+
57+
Most of the data sources are generated using the tool located in
58+
`./cmd/generate-datasources`. The generated files have the suffix `_gen.go` as
59+
well as a comment `// Code generated by generate-datasources; DO NOT EDIT.`.
60+
Do not edit these files manually but instead change the respective template in
61+
`./cmd/generate-datasources/tmpl` and the generator if necessary.
62+
63+
The generator tool is controlled by the config file `generate-datasources.yaml`.
64+
65+
If the generator code, the templates of the generator's config file are changed,
66+
the generated files need to be regenerated.
67+
68+
Use the following command to regenerate the data sources:
69+
70+
```shell
71+
make generate
72+
```
73+
74+
For more details about the inner working of the generator tool as well as the
75+
settings in the config file please refer to
76+
[`cmd/generate-datasources/README.md`](cmd/generate-datasources/README.md).
77+
5578
#### Testing
5679

5780
There are two test suites, unit and acceptance. By default the acceptance tests are not run as they require a functional

cmd/generate-datasources/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# generate-datasources
2+
3+
`generate-datasources` is a tool to generate data sources for the the Terraform
4+
provider. It is controlled by the config file `generate-datasources.yaml`.
5+
6+
## Usage
7+
8+
```shell
9+
go run ./cmd/generate-datasources
10+
```
11+
12+
For development or debugging of `generate-datasources`, the following flags might be helpful:
13+
14+
```none
15+
Usage of generate-datasources:
16+
--config string filename of the configfile (default "generate-datasources.yaml")
17+
-d, --debug Show all debug messages
18+
--only-entity string Limit code generation to this entity
19+
--only-template string Limit code generation to this template
20+
-v, --verbose Show all information messages
21+
```
22+
23+
With `--only-*`, the generator can be instructed to only generate some defined parts:
24+
25+
* `entity` is the name of the entity as defined in the config file, e.g. `network`.
26+
* `template` is the name of the template file in `./cmd/generate-datasources/tmpl`, e.g. `datasource.go.gotmpl`.
27+
28+
## Config `generate-datasources.yaml`
29+
30+
The config settings available per entity, for which the data source should be
31+
generated, are defined and documented in [config.go](./config.go).
32+
33+
## Templates
34+
35+
The templates used by `generate-datasources` are [Go templates](https://pkg.go.dev/text/template).
36+
37+
Templating is used for two purposes:
38+
39+
* target file name and path
40+
* content of the generated file
41+
42+
There are two kinds of template files:
43+
44+
* regular: these templates are executed for each entity (resource).
45+
* global: these templates are only executed once for all entities.
46+
47+
### Arguments
48+
49+
For the *regular* templates, the arguments defined in `entityArgs` for the
50+
currently generated entity are available.
51+
52+
For *global* templates, a map with all entities is passed, where the key is
53+
the name of the entity (as provided in the config file) and the value is the
54+
respective `entityArgs` instance.
55+
56+
The type `entityArgs` is defined and documented in [main.go](./main.go).
57+
58+
### Functions
59+
60+
Additionally to the functions and operators provided by [Go templates](https://pkg.go.dev/text/template) also the complete set of functions provided by the [sprig](https://masterminds.github.io/sprig/) library is available, with some functions being replaced with equivalents
61+
(see below).
62+
63+
The following specialized functions are provided:
64+
65+
* `pascalcase`: Convert a string in snake case to pascal case (`some_name` -> `SomeName`)
66+
* `camelcase`: Convert a string in snake case to camel case (`some_name` -> `someName`)
67+
* `kebabcase`: Convert a string in snake case to kebab case (`some_name` -> `some-name`)
68+
* `titlecase`: Convert a string in snake case to title case (`some_name` -> `Some Name`)
69+
* `words`: Split a string in snake case into words (`some_name` -> `some name`)
70+
71+
These functions do value acronyms and will convert them to the appropriate case as well.
72+
For the list of supported acronyms, please refer to [funcs.go](./funcs.go).

cmd/generate-datasources/config.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"gopkg.in/yaml.v3"
7+
)
8+
9+
// Config contains a map of the specification for all entities (resources),
10+
// for which the code for the data source should be generated.
11+
// The key of the map is the `name` of the resource, e.g. `network`.
12+
// The key MUST be singular and in snake case, e.g. `network_forward`.
13+
type Config map[string]*Entity
14+
15+
type Entity struct {
16+
// Additional description about the resource. This is added to the
17+
// introduction section of resource in the documentation.
18+
Description string `yaml:"description"`
19+
20+
// Content for the optinal notes section at the end of the data source
21+
// documention.
22+
Notes string `yaml:"notes"`
23+
24+
// Name of the package, this data source belongs to.
25+
// This also defines the path of the package in `internal/`.
26+
PackageName string `yaml:"package-name"`
27+
28+
// Name property of the resource. If not set, this defaults to `name`.
29+
// But some entities do not have a `name` property and therefore an other
30+
// property is name defining.
31+
ObjectNamePropertyName string `yaml:"object-name-property-name"`
32+
33+
// Default value used in the documentation for the name defining property.
34+
// Defaults to: `default`.
35+
ObjectNamePropertyDefaultValue string `yaml:"object-name-property-default-value"`
36+
37+
// Method of the Incus client to get the resource, e.g. `GetNetwork`.
38+
//
39+
// E.g. from https://github.com/lxc/incus/blob/3da8fcd06c4f7ee3cb9388127e6071244db7ac8f/client/incus_networks.go#L104
40+
IncusGetMethod string `yaml:"incus-get-method"`
41+
42+
// Name of the parent entity, if any.
43+
// Resources like network forwards have a parent, in this case a network.
44+
// If this is the case, the name of the parent needs to be specidied.
45+
ParentName string `yaml:"parent"`
46+
47+
// If a resource has no project attribute.
48+
// Most resources do have a project attribute. If this is not the case,
49+
// `has-no-project` needs to be set to `true`.
50+
HasNoProject bool `yaml:"has-no-project"`
51+
52+
// If a resource has no status attribute.
53+
// Most resources do have a status attribute. If this is not the case,
54+
// `has-no-status` needs to be set to `true`.
55+
HasNoStatus bool `yaml:"has-no-status"`
56+
57+
// If a resource has a location, mutual exclusive with has-locations.
58+
// Some resources are location aware. If this is the case for a resource,
59+
// `has-location` needs to be set to `true`.
60+
HasLocation bool `yaml:"has-location"`
61+
62+
// If a resource has multiple locations, mutual exclusive with has-location.
63+
// Some resources can be assigned to multiple locations. If this is the case
64+
// for a resource, `has-locations` needs to be set to `true`.
65+
HasLocations bool `yaml:"has-locations"`
66+
67+
// Name in snake case of an extra ID attribute, which is specific to the
68+
// respective resource type.
69+
// As of now, the only resource requiring an extra ID defining attribute is
70+
// storage volume, where the `type` is also part of the ID.
71+
ExtraIDAttribute ExtraAttribute `yaml:"extra-id-attribute"`
72+
73+
// List of extra attributes, which are specific to the respective resource type.
74+
//
75+
// See definition of type ExtraAttribute for details.
76+
//
77+
// The following attributes are handled automatically be the code generator
78+
// and must therefore not be listed in `extra-attributes`:
79+
//
80+
// * `name` (or if the resource does not have a `name` attribute, the attribute referenced in `object-name-property-name`)
81+
// * The attributes mentioned in `extra-id-attribute`, if any
82+
// * `parent` (if `parent` is not empty)
83+
// * `project`
84+
// * `target`
85+
// * `remote`
86+
// * `description`
87+
// * `config`
88+
// * `status` (if `has-no-status` is not set to `true`)
89+
// * `location` (if `has-location` is set to `true`)
90+
// * `locations` (if `has-locations` is set to `true`)
91+
ExtraAttributes []ExtraAttribute `yaml:"extra-attributes"`
92+
93+
// Map of additional description added to the documentation for the
94+
// automatically handled attributes (see list above).
95+
// Is added to the documentation "as-is", may contain markdown.
96+
ExtraDescriptions map[string]string `yaml:"extra-descriptions"`
97+
}
98+
99+
// ExtraAttribute contains the specification for an attribute of a resource.
100+
type ExtraAttribute struct {
101+
// Name of the attribute in snake case, e.g. `type`.
102+
Name string `yaml:"name"`
103+
104+
// Data type of the attribute, e.g. `string`.
105+
// Supported types are (mapping directly to the corresponding types from Terraform):
106+
//
107+
// * `bool`
108+
// * `list`
109+
// * `map`
110+
// * `object`
111+
// * `string`
112+
//
113+
// Additionally, the following special types are supported:
114+
// (the internal types are recognizable by the leading `_`)
115+
//
116+
// * `_device`: represents devices as used in instances and profiles. On the
117+
// API, these are represented as map[string]map[string]string,
118+
// which is mapped to a set of blocks of device information
119+
// consisting of name, type and properties.
120+
// This is a special type with the names and the logic hard
121+
// coded. With this, it can only be used to represent devices.
122+
Type string `yaml:"type"`
123+
124+
// If the `type` is `list` or `map`, `element-type` defines the Terraform type
125+
// of the elements contained in the list or map.
126+
// Supported types for list are:
127+
//
128+
// * `bool`
129+
// * `list`
130+
// * `map`
131+
// * `object`
132+
// * `string`
133+
//
134+
// Supported types for `map`:
135+
//
136+
// * `bool`
137+
// * `list`
138+
// * `object`
139+
// * `string`
140+
//
141+
// Be aware, that nesting of map in map is not supported by Terraform.
142+
ElementType *ExtraAttribute `yaml:"element-type"`
143+
144+
// If the `type` or the `element-type` is `object`, the type of the attribtues of
145+
// the object need to be defined. This is a list, listing each possible attribute of
146+
// the object.
147+
AttrTypes []*ExtraAttribute `yaml:"attr-types"`
148+
149+
// Description of the extra attribute. This is added to the documentation.
150+
Description string `yaml:"description"`
151+
}
152+
153+
func (c *Config) LoadConfig(path string) error {
154+
contents, err := os.ReadFile(path)
155+
if err != nil {
156+
return err
157+
}
158+
159+
return yaml.Unmarshal(contents, c)
160+
}

cmd/generate-datasources/funcs.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package main
2+
3+
import (
4+
"strings"
5+
"unicode"
6+
7+
"golang.org/x/text/cases"
8+
"golang.org/x/text/language"
9+
)
10+
11+
// Capital capitalizes the given string ("foo" -> "Foo").
12+
func Capital(s string) string {
13+
return cases.Title(language.English, cases.NoLower).String(s)
14+
}
15+
16+
var acronyms = map[string]struct{}{
17+
"acl": {},
18+
"url": {},
19+
"snat": {},
20+
}
21+
22+
// CamelCase converts to camel case ("foo_bar" -> "fooBar").
23+
// If a segment (with the exception of the first one) is a known acronym,
24+
// it is returned in all upper case.
25+
func CamelCase(s string) string {
26+
words := capitalizedWords(s)
27+
words[0] = strings.ToLower(words[0])
28+
return strings.Join(words, "")
29+
}
30+
31+
// PascalCase converts to pascal case ("foo_bar" -> "FooBar").
32+
// If a segment is a known acronym, it is returned in all upper case.
33+
func PascalCase(s string) string {
34+
return strings.Join(capitalizedWords(s), "")
35+
}
36+
37+
// KebabCase converts to kebab case ("foo_bar" -> "foo-bar").
38+
func KebabCase(s string) string {
39+
return strings.ToLower(strings.Join(capitalizedWords(s), "-"))
40+
}
41+
42+
// TitleCase converts to title case ("foo_bar" -> "Foo Bar").
43+
// If a segment is a known acronym, it is returned in all upper case.
44+
func TitleCase(s string) string {
45+
return strings.Join(capitalizedWords(s), " ")
46+
}
47+
48+
// Words converts to space delimited words ("foo_bar" -> "foo bar").
49+
// If a segment is a known acronym, it is returned in all upper case.
50+
func Words(s string) string {
51+
words := capitalizedWords(s)
52+
for i, w := range words {
53+
runes := []rune(w)
54+
if len(runes) > 1 && unicode.IsUpper(runes[1]) {
55+
continue
56+
}
57+
58+
words[i] = strings.ToLower(w)
59+
}
60+
61+
return strings.Join(words, " ")
62+
}
63+
64+
func capitalizedWords(s string) []string {
65+
words := strings.Split(s, "_")
66+
for i := range words {
67+
_, ok := acronyms[strings.ToLower(words[i])]
68+
if ok {
69+
words[i] = strings.ToUpper(words[i])
70+
continue
71+
}
72+
73+
words[i] = Capital(words[i])
74+
}
75+
76+
return words
77+
}

0 commit comments

Comments
 (0)