-
Notifications
You must be signed in to change notification settings - Fork 550
/
Copy pathgenerate.go
265 lines (243 loc) · 7.9 KB
/
generate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package codegen
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/framework"
)
// generatedDirPerms uses 0775 because it is the same as for
// the "vault" directory, which is at "drwxrwxr-x".
const generatedDirPerms os.FileMode = 0o775
var errUnsupported = errors.New("code and doc generation for this item is unsupported")
// Run accepts a map of endpoint paths and generates both code and documentation
// for NEW endpoints in the endpoint registry.
func Run(logger hclog.Logger, paths map[string]*framework.OASPathItem) error {
// Read in the templates we'll be using.
h, err := newTemplateHandler(logger)
if err != nil {
return err
}
// Use a file creator so the logger can always be available without having
// to awkwardly pass it in everywhere.
fCreator := &fileCreator{
logger: logger,
templateHandler: h,
}
createdCount := 0
skippedCount := 0
for endpoint, addedInfo := range endpointRegistry {
if err := fCreator.GenerateCode(endpoint, paths[endpoint], addedInfo); err != nil {
if err == errUnsupported {
logger.Warn(fmt.Sprintf("couldn't generate %s, continuing", endpoint))
continue
}
return err
}
logger.Info(fmt.Sprintf("generated %s for %s", addedInfo.Type.String(), endpoint))
createdCount++
created, err := fCreator.GenerateDoc(endpoint, paths[endpoint], addedInfo)
if err != nil {
return err
}
if created {
logger.Info(fmt.Sprintf("generated doc for %s", endpoint))
createdCount++
} else {
skippedCount++
}
}
logger.Info(fmt.Sprintf("generated %d files", createdCount))
logger.Info(fmt.Sprintf("skipped generating %d docs because they already existed", skippedCount))
return nil
}
type fileCreator struct {
logger hclog.Logger
templateHandler *templateHandler
}
// GenerateCode is exported because it's the only method intended to be used by
// other objects. Unexported methods may be available to other code in this package,
// but they're not intended to be used by anything but the fileCreator.
func (c *fileCreator) GenerateCode(endpoint string, endpointInfo *framework.OASPathItem, addedInfo *additionalInfo) error {
pathToFile, err := codeFilePath(addedInfo.Type, endpoint)
if err != nil {
return err
}
tmplType := templateTypeResource
if addedInfo.Type == tfTypeDataSource {
tmplType = templateTypeDataSource
}
return c.writeFile(pathToFile, tmplType, endpoint, endpointInfo, addedInfo)
}
// GenerateDoc is exported to indicate it's intended to be directly used.
// It will return:
// - true, nil: if a new doc is generated
// - false, nil: if a doc already exists so a new one is not generated
// - false, err: in error conditions
func (c *fileCreator) GenerateDoc(endpoint string, endpointInfo *framework.OASPathItem, addedInfo *additionalInfo) (bool, error) {
pathToFile, err := docFilePath(addedInfo.Type, endpoint)
if err != nil {
return false, err
}
// If the doc already exists, no need to generate a new one, especially
// since these get hand-edited after being first created.
if _, err := os.Stat(pathToFile); err == nil {
// The file already exists, nothing further to do here.
return false, nil
}
return true, c.writeFile(pathToFile, templateTypeDoc, endpoint, endpointInfo, addedInfo)
}
func (c *fileCreator) writeFile(pathToFile string, tmplTp templateType, endpoint string, endpointInfo *framework.OASPathItem, addedInfo *additionalInfo) error {
wr, closer, err := c.createFileWriter(pathToFile)
if err != nil {
return err
}
defer closer()
return c.templateHandler.Write(wr, tmplTp, endpoint, endpointInfo, addedInfo)
}
// createFileWriter creates a file and returns its writer for the caller to use in templating.
// The closer will only be populated if the err is nil.
func (c *fileCreator) createFileWriter(pathToFile string) (wr *bufio.Writer, closer func(), err error) {
var cleanups []func() error
closer = func() {
for _, cleanup := range cleanups {
if err := cleanup(); err != nil {
c.logger.Error(err.Error())
}
}
}
// Make the directory and file.
if err := os.MkdirAll(filepath.Dir(pathToFile), generatedDirPerms); err != nil {
return nil, nil, err
}
f, err := os.Create(pathToFile)
if err != nil {
return nil, nil, err
}
cleanups = []func() error{
f.Close,
}
// Open the file for writing.
wr = bufio.NewWriter(f)
cleanups = []func() error{
wr.Flush,
f.Close,
}
return wr, closer, nil
}
/*
codeFilePath creates a directory structure inside the "generated" folder that's
intended to make it easy to find the file for each endpoint in Vault, even if
we eventually cover all >500 of them and add tests.
terraform-provider-vault/generated$ tree
.
├── datasources
│ └── transform
│ ├── decode
│ │ └── role_name.go
│ └── encode
│ └── role_name.go
└── resources
└── transform
├── alphabet
│ └── name.go
├── alphabet.go
├── role
│ └── name.go
├── role.go
├── template
│ └── name.go
├── template.go
├── transformation
│ └── name.go
└── transformation.go
*/
func codeFilePath(tfTp tfType, endpoint string) (string, error) {
filename := fmt.Sprintf("%ss%s.go", tfTp.String(), endpoint)
repoRoot, err := getRepoRoot()
if err != nil {
return "", err
}
path := filepath.Join(repoRoot, "generated", filename)
return stripCurlyBraces(path), nil
}
/*
docFilePath creates a directory structure inside the "website/docs/generated" folder
that's intended to make it easy to find the file for each endpoint in Vault, even if
we eventually cover all >500 of them and add tests.
terraform-provider-vault/website/docs/generated$ tree
.
├── datasources
│ └── transform
│ ├── decode
│ │ └── role_name.md
│ └── encode
│ └── role_name.md
└── resources
└── transform
├── alphabet
│ └── name.md
├── alphabet.md
├── role
│ └── name.md
├── role.md
├── template
│ └── name.md
├── template.md
├── transformation
│ └── name.md
└── transformation.md
*/
func docFilePath(tfTp tfType, endpoint string) (string, error) {
endpoint = normalizeDocEndpoint(endpoint)
filename := fmt.Sprintf("%s/%s.html.md", tfTp.DocType(), endpoint)
repoRoot, err := getRepoRoot()
if err != nil {
return "", err
}
return filepath.Join(repoRoot, "website", "docs", filename), nil
}
// normalizeDocEndpoint changes the raw endpoint into the format we expect for
// using in generated documentation structure on registry.terraform.io.
// Example:
//
// endpoint: /transform/alphabet/{name}
// normalized: transform_alphabet
//
// endpoint: /transform/decode/{role_name}
// normalized: transform_decode
//
// endpoint: /transform/encode/{role_name}
// normalized: transform_encode
func normalizeDocEndpoint(endpoint string) string {
endpoint = stripCurlyBraces(endpoint)
endpoint = strings.TrimRight(endpoint, "name")
endpoint = strings.TrimRight(endpoint, "role_")
endpoint = strings.TrimRight(endpoint, "/")
endpoint = strings.ReplaceAll(endpoint, "/", "_")
endpoint = strings.TrimLeft(endpoint, "_")
return endpoint
}
// stripCurlyBraces converts a path like
// "generated/resources/transform-transformation-{name}.go"
// to "generated/resources/transform-transformation-name.go".
func stripCurlyBraces(path string) string {
path = strings.ReplaceAll(path, "{", "")
path = strings.ReplaceAll(path, "}", "")
return path
}
// getRepoRoot relative to CWD.
func getRepoRoot() (string, error) {
out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
if err != nil {
return "", err
}
return string(bytes.TrimRight(out, "\n")), nil
}