Skip to content

Commit 152a76c

Browse files
authored
Merge pull request #2 from terraform-tencentcloud/gendoc
add terraform docs generator
2 parents 28ae5c1 + 386a71c commit 152a76c

9 files changed

+616
-174
lines changed

gendoc/README.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Terraform docs generator
2+
3+
## Why
4+
5+
经过观察,大部分友商的 Terraform Plugins 文档都是人肉编写的,它们或多或少都有这样的问题:
6+
7+
* 风格难以统一,比如:章节顺序、空行数量、命名风格在不同产品之间有明显差异
8+
* 细节存在问题,比如:空格数量、缩进多少、中下划线出现不统一,甚至还夹带中文符号
9+
* 内容存在问题,比如:参数必须或选填跟代码不一致,列表中漏写参数或属性
10+
11+
然而,最大的问题是写文档需要消耗大量的时间和精力去整理内容整理格式,最后还发现总有这样那样的问题,甚至有时还会文档更新不及时。
12+
机器可以一如始终,完全无误差地完成各项有规律的重复性工作,而且不会出错,自动地生成文档也是 golang 所推动的标准做法。
13+
14+
## How
15+
16+
Terraform Plugins 文档,不管是 resource 还是 data_source,主要都分以下这几个主题:
17+
18+
* name
19+
* description
20+
* example usage
21+
* argument reference
22+
* attributes reference
23+
24+
### name
25+
26+
name 是 resource 及 data_source 的完整命名,它来源于 Provider 的 DataSourcesMap(data_source) 及 ResourcesMap(resource) 定义。
27+
例如以下 DataSourcesMap 中的 tencentcloud_vpc 与 tencentcloud_mysql_instance 就是一个标准的 name(for resource or data_source):
28+
29+
```go
30+
DataSourcesMap: map[string]*schema.Resource{
31+
"tencentcloud_vpc": dataSourceTencentCloudVpc(),
32+
"tencentcloud_mysql_instance": dataSourceTencentCloudMysqlInstance(),
33+
}
34+
```
35+
36+
### description & example usage
37+
38+
description 包括一个用于表头的一句话描述,与一个用于正文的详细说明。
39+
example usage 则是一个或几个使用示例。
40+
41+
description & example usage 需要在对应 resource 及 data_source 定义的文件中出现,它是符合 golang 标准文档注释的写法。例如:
42+
43+
/*
44+
Use this data source to get information about a MySQL instance.
45+
\n
46+
~> **NOTE:** The terminate operation of mysql does NOT take effect immediately,maybe takes for several hours.
47+
\n
48+
Example Usage
49+
\n
50+
```hcl
51+
data "tencentcloud_mysql_instance" "database"{
52+
mysql_id = "my-test-database"
53+
result_output_file = "mytestpath"
54+
}
55+
```
56+
*/
57+
package tencentcloud
58+
59+
以上注释的格式要求如下:
60+
61+
/*
62+
一句话描述
63+
\n
64+
在一句话描述基础上的补充描述,可以比较详细地说明各项内容,可以有多个段落。
65+
\n
66+
Example Usage
67+
\n
68+
Example Usage 是 必须的,在 Example Usage 以下的内容都会当成 Example Usage 填充到文档中。
69+
*/
70+
package tencentcloud
71+
72+
符合以上要求的注释将会自动提取并填写到文档中的对应位置。
73+
74+
### argument reference & attributes reference
75+
76+
Terraform 用 schema.Schema 来描述 argument reference & attributes reference,每个 schema.Schema 都会有一个 Description 字段。
77+
如果 Description 的内容不为空,那么这个 schema.Schema 将会被认为是需要写到文档里面的,如果 Optional 或 Required 设置了,它会被认为是一个参数,如果 Computed 为 true 则认为是一个属性。例如:
78+
79+
#### argument
80+
81+
```go
82+
map[string]*schema.Schema{
83+
"instance_name": {
84+
Type: schema.TypeString,
85+
Required: true,
86+
ValidateFunc: validateStringLengthInRange(1, 100),
87+
Description: "The name of a mysql instance.",
88+
},
89+
}
90+
```
91+
92+
#### attributes
93+
94+
```go
95+
map[string]*schema.Schema{
96+
"mysql_id": {
97+
Type: schema.TypeString,
98+
Computed: true,
99+
Description: "Instance ID, such as cdb-c1nl9rpv. It is identical to the instance ID displayed in the database console page.",
100+
},
101+
}
102+
```
103+
104+
#### attributes list
105+
106+
属性中 Type 为 schema.TypeList 的 schema.Schema 也是支持的,它会被认为是一个列表,里面的子 schema.Schema 会依次列出填充到文档中。
107+
108+
```go
109+
map[string]*schema.Schema{
110+
"instance_list": {
111+
Type: schema.TypeList,
112+
Computed: true,
113+
Description: "A list of instances. Each element contains the following attributes:",
114+
Elem: &schema.Resource{
115+
Schema: map[string]*schema.Schema{
116+
"mysql_id": {
117+
Type: schema.TypeString,
118+
Computed: true,
119+
Description: "Instance ID, such as cdb-c1nl9rpv. It is identical to the instance ID displayed in the database console page.",
120+
},
121+
"instance_name": {
122+
Type: schema.TypeString,
123+
Computed: true,
124+
Description: "Name of mysql instance.",
125+
},
126+
}
127+
}
128+
}
129+
}
130+
```

gendoc/main.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"github.com/hashicorp/terraform/helper/schema"
6+
cloud "github.com/terraform-providers/terraform-provider-tencentcloud/tencentcloud"
7+
"go/parser"
8+
"go/token"
9+
"log"
10+
"os"
11+
"path/filepath"
12+
"reflect"
13+
"runtime"
14+
"sort"
15+
"strings"
16+
"text/template"
17+
)
18+
19+
const (
20+
cloudMark = "tencentcloud"
21+
cloudTitle = "TencentCloud"
22+
cloudMarkShort = "tc"
23+
docRoot = "../website/docs"
24+
)
25+
26+
func main() {
27+
provider := cloud.Provider()
28+
vProvider := runtime.FuncForPC(reflect.ValueOf(cloud.Provider).Pointer())
29+
30+
fname, _ := vProvider.FileLine(0)
31+
fpath := filepath.Dir(fname)
32+
log.Printf("generating doc from: %s\n", fpath)
33+
34+
// DataSourcesMap
35+
for k, v := range provider.DataSourcesMap {
36+
genDoc("data_source", fpath, k, v)
37+
}
38+
39+
// ResourcesMap
40+
for k, v := range provider.ResourcesMap {
41+
genDoc("resource", fpath, k, v)
42+
}
43+
}
44+
45+
// genDoc generating doc for resource
46+
func genDoc(dtype, fpath, name string, resource *schema.Resource) {
47+
data := map[string]string{
48+
"name": name,
49+
"dtype": strings.Replace(dtype, "_", "", -1),
50+
"resource": name[len(cloudMark)+1:],
51+
"cloud_mark": cloudMark,
52+
"cloud_title": cloudTitle,
53+
"example": "",
54+
"description": "",
55+
"description_short": "",
56+
}
57+
58+
fname := fmt.Sprintf("%s_%s_%s.go", dtype, cloudMarkShort, data["resource"])
59+
log.Printf("get description from file: %s\n", fname)
60+
61+
description, err := getFileDescription(fmt.Sprintf("%s/%s", fpath, fname))
62+
if err != nil {
63+
log.Printf("[SKIP]get description failed, skip: %s", err)
64+
return
65+
}
66+
67+
description = strings.TrimSpace(description)
68+
if description == "" {
69+
log.Printf("[SKIP]description empty, skip: %s\n", fname)
70+
return
71+
}
72+
73+
pos := strings.Index(description, "\nExample Usage\n")
74+
if pos != -1 {
75+
data["example"] = strings.TrimSpace(description[pos+15:])
76+
description = strings.TrimSpace(description[:pos])
77+
} else {
78+
log.Printf("[SKIP]example usage missing, skip: %s\n", fname)
79+
return
80+
}
81+
82+
data["description"] = description
83+
pos = strings.Index(description, "\n\n")
84+
if pos != -1 {
85+
data["description_short"] = strings.TrimSpace(description[:pos])
86+
} else {
87+
data["description_short"] = description
88+
}
89+
90+
requiredArgs := []string{}
91+
optionalArgs := []string{}
92+
attributes := []string{}
93+
94+
for k, v := range resource.Schema {
95+
if v.Description == "" {
96+
continue
97+
}
98+
if v.Required {
99+
opt := "Required"
100+
if v.ForceNew {
101+
opt += ", ForceNew"
102+
}
103+
requiredArgs = append(requiredArgs, fmt.Sprintf("* `%s` - (%s) %s", k, opt, v.Description))
104+
} else if v.Optional {
105+
opt := "Optional"
106+
if v.ForceNew {
107+
opt += ", ForceNew"
108+
}
109+
optionalArgs = append(optionalArgs, fmt.Sprintf("* `%s` - (%s) %s", k, opt, v.Description))
110+
} else if v.Computed {
111+
if v.Type == schema.TypeList {
112+
listAttributes := []string{}
113+
for kk, vv := range v.Elem.(*schema.Resource).Schema {
114+
if vv.Description == "" {
115+
continue
116+
}
117+
if v.Computed {
118+
listAttributes = append(listAttributes, fmt.Sprintf(" * `%s` - %s", kk, vv.Description))
119+
}
120+
}
121+
slistAttributes := ""
122+
sort.Strings(listAttributes)
123+
if len(listAttributes) > 0 {
124+
slistAttributes = "\n" + strings.Join(listAttributes, "\n")
125+
}
126+
attributes = append(attributes, fmt.Sprintf("* `%s` - %s%s", k, v.Description, slistAttributes))
127+
} else {
128+
attributes = append(attributes, fmt.Sprintf("* `%s` - %s", k, v.Description))
129+
}
130+
}
131+
}
132+
133+
sort.Strings(requiredArgs)
134+
sort.Strings(optionalArgs)
135+
sort.Strings(attributes)
136+
137+
requiredArgs = append(requiredArgs, optionalArgs...)
138+
data["arguments"] = strings.Join(requiredArgs, "\n")
139+
data["attributes"] = strings.Join(attributes, "\n")
140+
141+
fname = fmt.Sprintf("%s/%s/%s.html.markdown", docRoot, dtype[0:1], data["resource"])
142+
fd, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
143+
if err != nil {
144+
log.Printf("[FAIL]open file %s failed: %s", fname, err)
145+
return
146+
}
147+
148+
defer fd.Close()
149+
t := template.Must(template.New("t").Parse(docTPL))
150+
err = t.Execute(fd, data)
151+
if err != nil {
152+
log.Printf("[FAIL]write file %s failed: %s", fname, err)
153+
return
154+
}
155+
156+
log.Printf("[SUCC]write doc to file success: %s", fname)
157+
}
158+
159+
// getFileDescription get description from go file
160+
func getFileDescription(fname string) (string, error) {
161+
fset := token.NewFileSet()
162+
163+
parsedAst, err := parser.ParseFile(fset, fname, nil, parser.ParseComments)
164+
if err != nil {
165+
return "", err
166+
}
167+
168+
return parsedAst.Doc.Text(), nil
169+
}

gendoc/template.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
const (
4+
docTPL = `---
5+
layout: "{{.cloud_mark}}"
6+
page_title: "{{.cloud_title}}: {{.name}}"
7+
sidebar_current: "docs-{{.cloud_mark}}-{{.dtype}}-{{.resource}}"
8+
description: |-
9+
{{.description_short}}
10+
---
11+
12+
# {{.name}}
13+
14+
{{.description}}
15+
16+
## Example Usage
17+
18+
{{.example}}
19+
20+
## Argument Reference
21+
22+
The following arguments are supported:
23+
24+
{{.arguments}}
25+
26+
## Attributes Reference
27+
28+
In addition to all arguments above, the following attributes are exported:
29+
30+
{{.attributes}}
31+
32+
`
33+
)

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ require (
2323
github.com/zqfan/tencentcloud-sdk-go v0.0.0-20181105105106-4c76f78ff2e6
2424
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
2525
golang.org/x/net v0.0.0-20190313220215-9f648a60d977 // indirect
26-
gopkg.in/natefinch/lumberjack.v2 v2.0.0
2726
labix.org/v2/mgo v0.0.0-20140701140051-000000000287 // indirect
2827
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect
2928
)

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
446446
gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
447447
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
448448
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
449-
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
450-
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
451449
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
452450
gopkg.in/yaml.v2 v2.0.0-20170407172122-cd8b52f8269e/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
453451
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

0 commit comments

Comments
 (0)