mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
337 lines
8.0 KiB
Go
337 lines
8.0 KiB
Go
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
|
/// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License.AGPL.txt in the project root for license information.
|
|
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/doc"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/fatih/structtag"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
configDir = "./pkg/config" // todo(nvn): better ways to handle the config path
|
|
)
|
|
|
|
var version string
|
|
|
|
type configDoc struct {
|
|
configName string
|
|
doc string
|
|
fields map[string][]fieldSpec
|
|
}
|
|
|
|
type fieldSpec struct {
|
|
name string
|
|
required bool
|
|
doc string
|
|
value string
|
|
allowedValues string
|
|
}
|
|
|
|
// extractTags strips the tags of each struct field and returns json name of the
|
|
// field and if the field is a mandatory one
|
|
func extractTags(tag string) (result fieldSpec, err error) {
|
|
|
|
// unfortunately structtag doesn't support multiple keys,
|
|
// so we have to handle this manually
|
|
tag = strings.Trim(tag, "`")
|
|
|
|
tagObj, err := structtag.Parse(tag) // we assume at least JSON tag is always present
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
metadata, err := tagObj.Get("json")
|
|
if err != nil {
|
|
// There is no "json" tag in this key - move on
|
|
err = nil
|
|
return
|
|
}
|
|
|
|
result.name = metadata.Name
|
|
|
|
reqInfo, err := tagObj.Get("validate")
|
|
if err != nil {
|
|
// bit of a hack to overwrite the value of error since we do
|
|
// not care if `validate` field is absent
|
|
err = nil
|
|
result.required = false
|
|
} else {
|
|
result.required = reqInfo.Name == "required"
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func extractPkg(name string, dir string) (config configDoc, err error) {
|
|
fset := token.NewFileSet()
|
|
|
|
pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
pkgInfo, ok := pkgs[name]
|
|
|
|
if !ok {
|
|
err = fmt.Errorf("Could not extract pkg %s", name)
|
|
return
|
|
}
|
|
|
|
pkgData := doc.New(pkgInfo, "./", 0)
|
|
|
|
return extractStructInfo(pkgData.Types)
|
|
}
|
|
|
|
func extractStructFields(structType *ast.StructType) (specs []fieldSpec, err error) {
|
|
var fieldInfo fieldSpec
|
|
if structType != nil && structType.Fields != nil {
|
|
|
|
for _, field := range structType.Fields.List {
|
|
// we extract all the tags of the struct
|
|
if field.Tag != nil {
|
|
fieldInfo, err = extractTags(field.Tag.Value)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// we document experimental section separately
|
|
if fieldInfo.name == "experimental" {
|
|
continue
|
|
}
|
|
}
|
|
|
|
switch xv := field.Type.(type) {
|
|
case *ast.StarExpr:
|
|
if si, ok := xv.X.(*ast.Ident); ok {
|
|
fieldInfo.value = si.Name
|
|
}
|
|
case *ast.Ident:
|
|
fieldInfo.value = xv.Name
|
|
case *ast.ArrayType:
|
|
fieldInfo.value = fmt.Sprintf("[]%s", xv.Elt)
|
|
}
|
|
|
|
// Doc about the field can be provided as a comment
|
|
// above the field
|
|
if field.Doc != nil {
|
|
var comment string = ""
|
|
|
|
// sometimes the comments are multi-line
|
|
for _, line := range field.Doc.List {
|
|
comment = fmt.Sprintf("%s %s", comment, strings.Trim(line.Text, "//"))
|
|
}
|
|
|
|
fieldInfo.doc = comment
|
|
}
|
|
|
|
specs = append(specs, fieldInfo)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func extractStructInfo(structTypes []*doc.Type) (configSpec configDoc, err error) {
|
|
configSpec.fields = map[string][]fieldSpec{}
|
|
for _, t := range structTypes {
|
|
|
|
typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
|
|
|
|
structType, ok := typeSpec.Type.(*ast.StructType)
|
|
if !ok {
|
|
typename, aok := typeSpec.Type.(*ast.Ident)
|
|
if !aok {
|
|
continue
|
|
}
|
|
|
|
allowed := []string{}
|
|
for _, con := range t.Consts[0].Decl.Specs {
|
|
value, ok := con.(*ast.ValueSpec)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, val := range value.Values {
|
|
bslit := val.(*ast.BasicLit)
|
|
|
|
allowed = append(allowed, fmt.Sprintf("`%s`", strings.Trim(bslit.Value, "\"")))
|
|
}
|
|
}
|
|
|
|
configSpec.fields[typeSpec.Name.Name] = []fieldSpec{
|
|
{
|
|
name: typeSpec.Name.Name,
|
|
allowedValues: strings.Join(allowed, ", "),
|
|
value: typename.Name,
|
|
doc: t.Consts[0].Doc,
|
|
},
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
structSpecs, err := extractStructFields(structType)
|
|
if err != nil {
|
|
return configSpec, err
|
|
}
|
|
|
|
if t.Name == "Config" {
|
|
if strings.Contains(t.Doc, "experimental") {
|
|
// if we are dealing with experimental pkg we rename the config title
|
|
configSpec.configName = "Experimental config parameters"
|
|
configSpec.doc = "Additional config parameters that are in experimental state"
|
|
} else {
|
|
configSpec.configName = t.Name
|
|
configSpec.doc = t.Doc
|
|
// we hardcode the value for apiVersion since it is not present in
|
|
// Config struct
|
|
structSpecs = append(structSpecs,
|
|
fieldSpec{
|
|
name: "apiVersion",
|
|
required: true,
|
|
value: "string",
|
|
doc: fmt.Sprintf("API version of the Gitpod config defintion."+
|
|
" `%s` in this version of Config", version)})
|
|
}
|
|
}
|
|
|
|
configSpec.fields[typeSpec.Name.Name] = structSpecs
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// parseConfigDir parses the AST of the config package and returns metadata
|
|
// about the `Config` struct
|
|
func parseConfigDir(fileDir string) (configSpec []configDoc, err error) {
|
|
// we basically parse the AST of the config package
|
|
configStruct, err := extractPkg("config", fileDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
experimentalDir := fmt.Sprintf("%s/%s", fileDir, "experimental")
|
|
// we parse the AST of the experimental package since we have additional
|
|
// Config there
|
|
experimentalStruct, err := extractPkg("experimental", experimentalDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
configSpec = []configDoc{configStruct, experimentalStruct}
|
|
|
|
return
|
|
}
|
|
|
|
func recurse(configSpec configDoc, field fieldSpec, parent string) []fieldSpec {
|
|
// check if field has type array
|
|
var arrayString, valuename string
|
|
if strings.Contains(field.value, "[]") {
|
|
arrayString = "[ ]"
|
|
valuename = strings.Trim(field.value, "[]")
|
|
} else {
|
|
valuename = field.value
|
|
}
|
|
|
|
field.name = fmt.Sprintf("%s%s%s", parent, field.name, arrayString)
|
|
// results := []fieldSpec{field}
|
|
results := []fieldSpec{}
|
|
subFields := configSpec.fields[valuename]
|
|
|
|
if len(subFields) < 1 {
|
|
// this means that this is a leaf node, terminating condition
|
|
return []fieldSpec{field}
|
|
}
|
|
|
|
for _, sub := range subFields {
|
|
results = append(results, recurse(configSpec, sub, field.name+".")...)
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
func generateMarkdown(configSpec configDoc, mddoc *strings.Builder) {
|
|
|
|
var prefix string = ""
|
|
if strings.Contains(configSpec.configName, "Experimental") {
|
|
prefix = "experimental."
|
|
}
|
|
|
|
mddoc.WriteString(fmt.Sprintf("# %s %s\n\n%s\n", configSpec.configName, version, configSpec.doc))
|
|
mddoc.WriteString("\n## Supported parameters\n")
|
|
mddoc.WriteString("| Property | Type | Required | Allowed| Description |\n")
|
|
mddoc.WriteString("| --- | --- | --- | --- | --- |\n")
|
|
|
|
results := []fieldSpec{}
|
|
fieldLists := configSpec.fields["Config"]
|
|
for _, field := range fieldLists {
|
|
results = append(results, recurse(configSpec, field, "")...)
|
|
}
|
|
|
|
for _, res := range results {
|
|
reqd := "N"
|
|
if res.required {
|
|
reqd = "Y"
|
|
}
|
|
|
|
if res.allowedValues != "" {
|
|
lastInd := strings.LastIndex(res.name, ".")
|
|
res.name = res.name[:lastInd]
|
|
|
|
}
|
|
|
|
mddoc.WriteString(fmt.Sprintf("|`%s%s`|%s|%s| %s |%s|\n", prefix,
|
|
res.name, res.value, reqd, res.allowedValues, strings.TrimSuffix(res.doc,
|
|
"\n")))
|
|
}
|
|
|
|
mddoc.WriteString("\n\n")
|
|
}
|
|
|
|
func main() {
|
|
versionFlag := flag.String("version", "v1", "Config version for doc creation")
|
|
flag.Parse()
|
|
|
|
version = *versionFlag
|
|
|
|
log.Infof("Generating doc for config version %s", version)
|
|
|
|
fileDir := fmt.Sprintf("%s/%s", configDir, version)
|
|
|
|
// get the `Config` struct field info from `config` pkg
|
|
configSpec, err := parseConfigDir(fileDir)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// generate markdown for the doc
|
|
|
|
mddoc := &strings.Builder{}
|
|
for _, spec := range configSpec {
|
|
generateMarkdown(spec, mddoc)
|
|
}
|
|
|
|
// write the md file of name config.md in the same directory as config
|
|
mdfilename := filepath.Join(fileDir, "config.md")
|
|
|
|
err = ioutil.WriteFile(mdfilename, []byte(mddoc.String()), 0644)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
log.Infof("The doc is written to the file %s", mdfilename)
|
|
}
|