Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/install/installer/scripts/structdoc.go
2498 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
/// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package main
6
7
import (
8
"flag"
9
"fmt"
10
"go/ast"
11
"go/doc"
12
"go/parser"
13
"go/token"
14
"io/ioutil"
15
"path/filepath"
16
"strings"
17
18
"github.com/fatih/structtag"
19
log "github.com/sirupsen/logrus"
20
)
21
22
const (
23
configDir = "./pkg/config" // todo(nvn): better ways to handle the config path
24
)
25
26
var version string
27
28
type configDoc struct {
29
configName string
30
doc string
31
fields map[string][]fieldSpec
32
}
33
34
type fieldSpec struct {
35
name string
36
required bool
37
doc string
38
value string
39
allowedValues string
40
}
41
42
// extractTags strips the tags of each struct field and returns json name of the
43
// field and if the field is a mandatory one
44
func extractTags(tag string) (result fieldSpec, err error) {
45
46
// unfortunately structtag doesn't support multiple keys,
47
// so we have to handle this manually
48
tag = strings.Trim(tag, "`")
49
50
tagObj, err := structtag.Parse(tag) // we assume at least JSON tag is always present
51
if err != nil {
52
return
53
}
54
55
metadata, err := tagObj.Get("json")
56
if err != nil {
57
// There is no "json" tag in this key - move on
58
err = nil
59
return
60
}
61
62
result.name = metadata.Name
63
64
reqInfo, err := tagObj.Get("validate")
65
if err != nil {
66
// bit of a hack to overwrite the value of error since we do
67
// not care if `validate` field is absent
68
err = nil
69
result.required = false
70
} else {
71
result.required = reqInfo.Name == "required"
72
}
73
74
return
75
}
76
77
func extractPkg(name string, dir string) (config configDoc, err error) {
78
fset := token.NewFileSet()
79
80
pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments)
81
if err != nil {
82
return
83
}
84
85
pkgInfo, ok := pkgs[name]
86
87
if !ok {
88
err = fmt.Errorf("Could not extract pkg %s", name)
89
return
90
}
91
92
pkgData := doc.New(pkgInfo, "./", 0)
93
94
return extractStructInfo(pkgData.Types)
95
}
96
97
func extractStructFields(structType *ast.StructType) (specs []fieldSpec, err error) {
98
var fieldInfo fieldSpec
99
if structType != nil && structType.Fields != nil {
100
101
for _, field := range structType.Fields.List {
102
// we extract all the tags of the struct
103
if field.Tag != nil {
104
fieldInfo, err = extractTags(field.Tag.Value)
105
if err != nil {
106
return
107
}
108
109
// we document experimental section separately
110
if fieldInfo.name == "experimental" {
111
continue
112
}
113
}
114
115
switch xv := field.Type.(type) {
116
case *ast.StarExpr:
117
if si, ok := xv.X.(*ast.Ident); ok {
118
fieldInfo.value = si.Name
119
}
120
case *ast.Ident:
121
fieldInfo.value = xv.Name
122
case *ast.ArrayType:
123
fieldInfo.value = fmt.Sprintf("[]%s", xv.Elt)
124
}
125
126
// Doc about the field can be provided as a comment
127
// above the field
128
if field.Doc != nil {
129
var comment string = ""
130
131
// sometimes the comments are multi-line
132
for _, line := range field.Doc.List {
133
comment = fmt.Sprintf("%s %s", comment, strings.Trim(line.Text, "//"))
134
}
135
136
fieldInfo.doc = comment
137
}
138
139
specs = append(specs, fieldInfo)
140
}
141
}
142
143
return
144
}
145
146
func extractStructInfo(structTypes []*doc.Type) (configSpec configDoc, err error) {
147
configSpec.fields = map[string][]fieldSpec{}
148
for _, t := range structTypes {
149
150
typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
151
152
structType, ok := typeSpec.Type.(*ast.StructType)
153
if !ok {
154
typename, aok := typeSpec.Type.(*ast.Ident)
155
if !aok {
156
continue
157
}
158
159
allowed := []string{}
160
for _, con := range t.Consts[0].Decl.Specs {
161
value, ok := con.(*ast.ValueSpec)
162
if !ok {
163
continue
164
}
165
166
for _, val := range value.Values {
167
bslit := val.(*ast.BasicLit)
168
169
allowed = append(allowed, fmt.Sprintf("`%s`", strings.Trim(bslit.Value, "\"")))
170
}
171
}
172
173
configSpec.fields[typeSpec.Name.Name] = []fieldSpec{
174
{
175
name: typeSpec.Name.Name,
176
allowedValues: strings.Join(allowed, ", "),
177
value: typename.Name,
178
doc: t.Consts[0].Doc,
179
},
180
}
181
182
continue
183
184
}
185
186
structSpecs, err := extractStructFields(structType)
187
if err != nil {
188
return configSpec, err
189
}
190
191
if t.Name == "Config" {
192
if strings.Contains(t.Doc, "experimental") {
193
// if we are dealing with experimental pkg we rename the config title
194
configSpec.configName = "Experimental config parameters"
195
configSpec.doc = "Additional config parameters that are in experimental state"
196
} else {
197
configSpec.configName = t.Name
198
configSpec.doc = t.Doc
199
// we hardcode the value for apiVersion since it is not present in
200
// Config struct
201
structSpecs = append(structSpecs,
202
fieldSpec{
203
name: "apiVersion",
204
required: true,
205
value: "string",
206
doc: fmt.Sprintf("API version of the Gitpod config defintion."+
207
" `%s` in this version of Config", version)})
208
}
209
}
210
211
configSpec.fields[typeSpec.Name.Name] = structSpecs
212
}
213
214
return
215
}
216
217
// parseConfigDir parses the AST of the config package and returns metadata
218
// about the `Config` struct
219
func parseConfigDir(fileDir string) (configSpec []configDoc, err error) {
220
// we basically parse the AST of the config package
221
configStruct, err := extractPkg("config", fileDir)
222
if err != nil {
223
return
224
}
225
226
experimentalDir := fmt.Sprintf("%s/%s", fileDir, "experimental")
227
// we parse the AST of the experimental package since we have additional
228
// Config there
229
experimentalStruct, err := extractPkg("experimental", experimentalDir)
230
if err != nil {
231
return
232
}
233
234
configSpec = []configDoc{configStruct, experimentalStruct}
235
236
return
237
}
238
239
func recurse(configSpec configDoc, field fieldSpec, parent string) []fieldSpec {
240
// check if field has type array
241
var arrayString, valuename string
242
if strings.Contains(field.value, "[]") {
243
arrayString = "[ ]"
244
valuename = strings.Trim(field.value, "[]")
245
} else {
246
valuename = field.value
247
}
248
249
field.name = fmt.Sprintf("%s%s%s", parent, field.name, arrayString)
250
// results := []fieldSpec{field}
251
results := []fieldSpec{}
252
subFields := configSpec.fields[valuename]
253
254
if len(subFields) < 1 {
255
// this means that this is a leaf node, terminating condition
256
return []fieldSpec{field}
257
}
258
259
for _, sub := range subFields {
260
results = append(results, recurse(configSpec, sub, field.name+".")...)
261
}
262
263
return results
264
}
265
266
func generateMarkdown(configSpec configDoc, mddoc *strings.Builder) {
267
268
var prefix string = ""
269
if strings.Contains(configSpec.configName, "Experimental") {
270
prefix = "experimental."
271
}
272
273
mddoc.WriteString(fmt.Sprintf("# %s %s\n\n%s\n", configSpec.configName, version, configSpec.doc))
274
mddoc.WriteString("\n## Supported parameters\n")
275
mddoc.WriteString("| Property | Type | Required | Allowed| Description |\n")
276
mddoc.WriteString("| --- | --- | --- | --- | --- |\n")
277
278
results := []fieldSpec{}
279
fieldLists := configSpec.fields["Config"]
280
for _, field := range fieldLists {
281
results = append(results, recurse(configSpec, field, "")...)
282
}
283
284
for _, res := range results {
285
reqd := "N"
286
if res.required {
287
reqd = "Y"
288
}
289
290
if res.allowedValues != "" {
291
lastInd := strings.LastIndex(res.name, ".")
292
res.name = res.name[:lastInd]
293
294
}
295
296
mddoc.WriteString(fmt.Sprintf("|`%s%s`|%s|%s| %s |%s|\n", prefix,
297
res.name, res.value, reqd, res.allowedValues, strings.TrimSuffix(res.doc,
298
"\n")))
299
}
300
301
mddoc.WriteString("\n\n")
302
}
303
304
func main() {
305
versionFlag := flag.String("version", "v1", "Config version for doc creation")
306
flag.Parse()
307
308
version = *versionFlag
309
310
log.Infof("Generating doc for config version %s", version)
311
312
fileDir := fmt.Sprintf("%s/%s", configDir, version)
313
314
// get the `Config` struct field info from `config` pkg
315
configSpec, err := parseConfigDir(fileDir)
316
if err != nil {
317
log.Fatal(err)
318
}
319
320
// generate markdown for the doc
321
322
mddoc := &strings.Builder{}
323
for _, spec := range configSpec {
324
generateMarkdown(spec, mddoc)
325
}
326
327
// write the md file of name config.md in the same directory as config
328
mdfilename := filepath.Join(fileDir, "config.md")
329
330
err = ioutil.WriteFile(mdfilename, []byte(mddoc.String()), 0644)
331
if err != nil {
332
log.Fatal(err)
333
}
334
335
log.Infof("The doc is written to the file %s", mdfilename)
336
}
337
338