Path: blob/main/install/installer/scripts/structdoc.go
2498 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1/// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package main56import (7"flag"8"fmt"9"go/ast"10"go/doc"11"go/parser"12"go/token"13"io/ioutil"14"path/filepath"15"strings"1617"github.com/fatih/structtag"18log "github.com/sirupsen/logrus"19)2021const (22configDir = "./pkg/config" // todo(nvn): better ways to handle the config path23)2425var version string2627type configDoc struct {28configName string29doc string30fields map[string][]fieldSpec31}3233type fieldSpec struct {34name string35required bool36doc string37value string38allowedValues string39}4041// extractTags strips the tags of each struct field and returns json name of the42// field and if the field is a mandatory one43func extractTags(tag string) (result fieldSpec, err error) {4445// unfortunately structtag doesn't support multiple keys,46// so we have to handle this manually47tag = strings.Trim(tag, "`")4849tagObj, err := structtag.Parse(tag) // we assume at least JSON tag is always present50if err != nil {51return52}5354metadata, err := tagObj.Get("json")55if err != nil {56// There is no "json" tag in this key - move on57err = nil58return59}6061result.name = metadata.Name6263reqInfo, err := tagObj.Get("validate")64if err != nil {65// bit of a hack to overwrite the value of error since we do66// not care if `validate` field is absent67err = nil68result.required = false69} else {70result.required = reqInfo.Name == "required"71}7273return74}7576func extractPkg(name string, dir string) (config configDoc, err error) {77fset := token.NewFileSet()7879pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments)80if err != nil {81return82}8384pkgInfo, ok := pkgs[name]8586if !ok {87err = fmt.Errorf("Could not extract pkg %s", name)88return89}9091pkgData := doc.New(pkgInfo, "./", 0)9293return extractStructInfo(pkgData.Types)94}9596func extractStructFields(structType *ast.StructType) (specs []fieldSpec, err error) {97var fieldInfo fieldSpec98if structType != nil && structType.Fields != nil {99100for _, field := range structType.Fields.List {101// we extract all the tags of the struct102if field.Tag != nil {103fieldInfo, err = extractTags(field.Tag.Value)104if err != nil {105return106}107108// we document experimental section separately109if fieldInfo.name == "experimental" {110continue111}112}113114switch xv := field.Type.(type) {115case *ast.StarExpr:116if si, ok := xv.X.(*ast.Ident); ok {117fieldInfo.value = si.Name118}119case *ast.Ident:120fieldInfo.value = xv.Name121case *ast.ArrayType:122fieldInfo.value = fmt.Sprintf("[]%s", xv.Elt)123}124125// Doc about the field can be provided as a comment126// above the field127if field.Doc != nil {128var comment string = ""129130// sometimes the comments are multi-line131for _, line := range field.Doc.List {132comment = fmt.Sprintf("%s %s", comment, strings.Trim(line.Text, "//"))133}134135fieldInfo.doc = comment136}137138specs = append(specs, fieldInfo)139}140}141142return143}144145func extractStructInfo(structTypes []*doc.Type) (configSpec configDoc, err error) {146configSpec.fields = map[string][]fieldSpec{}147for _, t := range structTypes {148149typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)150151structType, ok := typeSpec.Type.(*ast.StructType)152if !ok {153typename, aok := typeSpec.Type.(*ast.Ident)154if !aok {155continue156}157158allowed := []string{}159for _, con := range t.Consts[0].Decl.Specs {160value, ok := con.(*ast.ValueSpec)161if !ok {162continue163}164165for _, val := range value.Values {166bslit := val.(*ast.BasicLit)167168allowed = append(allowed, fmt.Sprintf("`%s`", strings.Trim(bslit.Value, "\"")))169}170}171172configSpec.fields[typeSpec.Name.Name] = []fieldSpec{173{174name: typeSpec.Name.Name,175allowedValues: strings.Join(allowed, ", "),176value: typename.Name,177doc: t.Consts[0].Doc,178},179}180181continue182183}184185structSpecs, err := extractStructFields(structType)186if err != nil {187return configSpec, err188}189190if t.Name == "Config" {191if strings.Contains(t.Doc, "experimental") {192// if we are dealing with experimental pkg we rename the config title193configSpec.configName = "Experimental config parameters"194configSpec.doc = "Additional config parameters that are in experimental state"195} else {196configSpec.configName = t.Name197configSpec.doc = t.Doc198// we hardcode the value for apiVersion since it is not present in199// Config struct200structSpecs = append(structSpecs,201fieldSpec{202name: "apiVersion",203required: true,204value: "string",205doc: fmt.Sprintf("API version of the Gitpod config defintion."+206" `%s` in this version of Config", version)})207}208}209210configSpec.fields[typeSpec.Name.Name] = structSpecs211}212213return214}215216// parseConfigDir parses the AST of the config package and returns metadata217// about the `Config` struct218func parseConfigDir(fileDir string) (configSpec []configDoc, err error) {219// we basically parse the AST of the config package220configStruct, err := extractPkg("config", fileDir)221if err != nil {222return223}224225experimentalDir := fmt.Sprintf("%s/%s", fileDir, "experimental")226// we parse the AST of the experimental package since we have additional227// Config there228experimentalStruct, err := extractPkg("experimental", experimentalDir)229if err != nil {230return231}232233configSpec = []configDoc{configStruct, experimentalStruct}234235return236}237238func recurse(configSpec configDoc, field fieldSpec, parent string) []fieldSpec {239// check if field has type array240var arrayString, valuename string241if strings.Contains(field.value, "[]") {242arrayString = "[ ]"243valuename = strings.Trim(field.value, "[]")244} else {245valuename = field.value246}247248field.name = fmt.Sprintf("%s%s%s", parent, field.name, arrayString)249// results := []fieldSpec{field}250results := []fieldSpec{}251subFields := configSpec.fields[valuename]252253if len(subFields) < 1 {254// this means that this is a leaf node, terminating condition255return []fieldSpec{field}256}257258for _, sub := range subFields {259results = append(results, recurse(configSpec, sub, field.name+".")...)260}261262return results263}264265func generateMarkdown(configSpec configDoc, mddoc *strings.Builder) {266267var prefix string = ""268if strings.Contains(configSpec.configName, "Experimental") {269prefix = "experimental."270}271272mddoc.WriteString(fmt.Sprintf("# %s %s\n\n%s\n", configSpec.configName, version, configSpec.doc))273mddoc.WriteString("\n## Supported parameters\n")274mddoc.WriteString("| Property | Type | Required | Allowed| Description |\n")275mddoc.WriteString("| --- | --- | --- | --- | --- |\n")276277results := []fieldSpec{}278fieldLists := configSpec.fields["Config"]279for _, field := range fieldLists {280results = append(results, recurse(configSpec, field, "")...)281}282283for _, res := range results {284reqd := "N"285if res.required {286reqd = "Y"287}288289if res.allowedValues != "" {290lastInd := strings.LastIndex(res.name, ".")291res.name = res.name[:lastInd]292293}294295mddoc.WriteString(fmt.Sprintf("|`%s%s`|%s|%s| %s |%s|\n", prefix,296res.name, res.value, reqd, res.allowedValues, strings.TrimSuffix(res.doc,297"\n")))298}299300mddoc.WriteString("\n\n")301}302303func main() {304versionFlag := flag.String("version", "v1", "Config version for doc creation")305flag.Parse()306307version = *versionFlag308309log.Infof("Generating doc for config version %s", version)310311fileDir := fmt.Sprintf("%s/%s", configDir, version)312313// get the `Config` struct field info from `config` pkg314configSpec, err := parseConfigDir(fileDir)315if err != nil {316log.Fatal(err)317}318319// generate markdown for the doc320321mddoc := &strings.Builder{}322for _, spec := range configSpec {323generateMarkdown(spec, mddoc)324}325326// write the md file of name config.md in the same directory as config327mdfilename := filepath.Join(fileDir, "config.md")328329err = ioutil.WriteFile(mdfilename, []byte(mddoc.String()), 0644)330if err != nil {331log.Fatal(err)332}333334log.Infof("The doc is written to the file %s", mdfilename)335}336337338