mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-15 02:57:17 +02:00
Add JSON over TCP support. Add support for multiple loggers. Make logging configurable. Signed-off-by: Alexey Palazhchenko <alexey.palazhchenko@talos-systems.com>
498 lines
10 KiB
Go
498 lines
10 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"text/template"
|
|
|
|
yaml "gopkg.in/yaml.v3"
|
|
"mvdan.cc/gofumpt/format"
|
|
)
|
|
|
|
var tpl = `// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
// Code generated by hack/docgen tool. DO NOT EDIT.
|
|
|
|
package {{ .Package }}
|
|
|
|
import (
|
|
"github.com/talos-systems/talos/pkg/machinery/config/encoder"
|
|
)
|
|
|
|
{{ $tick := "` + "`" + `" -}}
|
|
var (
|
|
{{ range $struct := .Structs -}}
|
|
{{ $struct.Name }}Doc encoder.Doc
|
|
{{ end -}}
|
|
)
|
|
|
|
func init() {
|
|
{{ range $struct := .Structs -}}
|
|
{{ $docVar := printf "%v%v" $struct.Name "Doc" }}
|
|
{{ $docVar }}.Type = "{{ $struct.Name }}"
|
|
{{ $docVar }}.Comments[encoder.LineComment] = "{{ $struct.Text.Comment }}"
|
|
{{ $docVar }}.Description = "{{ $struct.Text.Description }}"
|
|
{{ range $example := $struct.Text.Examples }}
|
|
{{ if $example.Value }}
|
|
{{ $docVar }}.AddExample("{{ $example.Name }}", {{ $example.Value }})
|
|
{{ end -}}
|
|
{{ end -}}
|
|
{{ if $struct.AppearsIn -}}
|
|
{{ $docVar }}.AppearsIn = []encoder.Appearance{
|
|
{{ range $value := $struct.AppearsIn -}}
|
|
{
|
|
TypeName: "{{ $value.Struct.Name }}",
|
|
FieldName: "{{ $value.FieldName }}",
|
|
},
|
|
{{ end -}}
|
|
}
|
|
{{ end -}}
|
|
{{ $docVar }}.Fields = make([]encoder.Doc,{{ len $struct.Fields }})
|
|
{{ range $index, $field := $struct.Fields }}{{ if $field.Tag -}}
|
|
{{ $docVar }}.Fields[{{ $index }}].Name = "{{ $field.Tag }}"
|
|
{{ $docVar }}.Fields[{{ $index }}].Type = "{{ $field.Type }}"
|
|
{{ $docVar }}.Fields[{{ $index }}].Note = "{{ $field.Note }}"
|
|
{{ $docVar }}.Fields[{{ $index }}].Description = "{{ $field.Text.Description }}"
|
|
{{ $docVar }}.Fields[{{ $index }}].Comments[encoder.LineComment] = "{{ $field.Text.Comment }}"
|
|
{{ range $example := $field.Text.Examples }}
|
|
{{ if $example.Value }}
|
|
{{ $docVar }}.Fields[{{ $index }}].AddExample("{{ $example.Name }}", {{ $example.Value }})
|
|
{{ end -}}
|
|
{{ end -}}
|
|
{{ if $field.Text.Values -}}
|
|
{{ $docVar }}.Fields[{{ $index }}].Values = []string{
|
|
{{ range $value := $field.Text.Values -}}
|
|
"{{ $value }}",
|
|
{{ end -}}
|
|
}
|
|
{{ end -}}
|
|
{{ end -}}
|
|
{{- end }}
|
|
{{ end }}
|
|
}
|
|
|
|
{{ range $struct := .Structs -}}
|
|
func (_ {{ $struct.Name }}) Doc() *encoder.Doc {
|
|
return &{{ $struct.Name }}Doc
|
|
}
|
|
{{ end -}}
|
|
|
|
// Get{{ .Name }}Doc returns documentation for the file {{ .File }}.
|
|
func Get{{ .Name }}Doc() *encoder.FileDoc {
|
|
return &encoder.FileDoc{
|
|
Name: "{{ .Name }}",
|
|
Description: "{{ .Header }}",
|
|
Structs: []*encoder.Doc{
|
|
{{ range $struct := .Structs -}}
|
|
&{{ $struct.Name }}Doc,
|
|
{{ end -}}
|
|
},
|
|
}
|
|
}
|
|
`
|
|
|
|
type Doc struct {
|
|
Name string
|
|
Package string
|
|
Title string
|
|
Header string
|
|
File string
|
|
Structs []*Struct
|
|
}
|
|
|
|
type Struct struct {
|
|
Name string
|
|
Text *Text
|
|
Fields []*Field
|
|
AppearsIn []Appearance
|
|
}
|
|
|
|
type Appearance struct {
|
|
Struct *Struct
|
|
FieldName string
|
|
}
|
|
|
|
type Example struct {
|
|
Name string `yaml:"name"`
|
|
Value string `yaml:"value"`
|
|
}
|
|
|
|
type Field struct {
|
|
Name string
|
|
Type string
|
|
TypeRef string
|
|
Text *Text
|
|
Tag string
|
|
Note string
|
|
}
|
|
|
|
type Text struct {
|
|
Comment string `json:"-"`
|
|
Description string `json:"description"`
|
|
Examples []*Example `json:"examples"`
|
|
Values []string `json:"values"`
|
|
}
|
|
|
|
func in(p string) (string, error) {
|
|
return filepath.Abs(p)
|
|
}
|
|
|
|
func out(p string) (*os.File, error) {
|
|
abs, err := filepath.Abs(p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return os.Create(abs)
|
|
}
|
|
|
|
type structType struct {
|
|
name string
|
|
text *Text
|
|
pos token.Pos
|
|
node *ast.StructType
|
|
}
|
|
|
|
func collectStructs(node ast.Node) []*structType {
|
|
structs := []*structType{}
|
|
|
|
collectStructs := func(n ast.Node) bool {
|
|
g, ok := n.(*ast.GenDecl)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
if g.Doc != nil {
|
|
for _, comment := range g.Doc.List {
|
|
if strings.Contains(comment.Text, "docgen:nodoc") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, spec := range g.Specs {
|
|
t, ok := spec.(*ast.TypeSpec)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
if t.Type == nil {
|
|
return true
|
|
}
|
|
|
|
x, ok := t.Type.(*ast.StructType)
|
|
if !ok {
|
|
return true
|
|
}
|
|
|
|
structName := t.Name.Name
|
|
|
|
text := &Text{}
|
|
|
|
if t.Doc != nil {
|
|
text = parseComment([]byte(t.Doc.Text()))
|
|
} else if g.Doc != nil {
|
|
text = parseComment([]byte(g.Doc.Text()))
|
|
}
|
|
|
|
s := &structType{
|
|
name: structName,
|
|
text: text,
|
|
node: x,
|
|
pos: x.Pos(),
|
|
}
|
|
|
|
structs = append(structs, s)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
ast.Inspect(node, collectStructs)
|
|
|
|
return structs
|
|
}
|
|
|
|
func parseComment(comment []byte) *Text {
|
|
text := &Text{}
|
|
if err := yaml.Unmarshal(comment, text); err != nil {
|
|
// not yaml, fallback
|
|
text.Description = string(comment)
|
|
// take only the first line from the Description for the comment
|
|
text.Comment = strings.Split(text.Description, "\n")[0]
|
|
|
|
// try to parse everything except for the first line as yaml
|
|
if err = yaml.Unmarshal([]byte(strings.Join(strings.Split(text.Description, "\n")[1:], "\n")), text); err == nil {
|
|
// if parsed, remove it from the description
|
|
text.Description = text.Comment
|
|
}
|
|
} else {
|
|
text.Description = strings.TrimSpace(text.Description)
|
|
// take only the first line from the Description for the comment
|
|
text.Comment = strings.Split(text.Description, "\n")[0]
|
|
}
|
|
|
|
text.Comment = escape(text.Comment)
|
|
|
|
text.Description = escape(text.Description)
|
|
for _, example := range text.Examples {
|
|
example.Name = escape(example.Name)
|
|
example.Value = strings.TrimSpace(example.Value)
|
|
}
|
|
|
|
return text
|
|
}
|
|
|
|
func getFieldType(p interface{}) string {
|
|
if m, ok := p.(*ast.MapType); ok {
|
|
return getFieldType(m.Value)
|
|
}
|
|
|
|
switch t := p.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.ArrayType:
|
|
return getFieldType(p.(*ast.ArrayType).Elt)
|
|
case *ast.StarExpr:
|
|
return getFieldType(t.X)
|
|
case *ast.SelectorExpr:
|
|
return getFieldType(t.Sel)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func formatFieldType(p interface{}) string {
|
|
if m, ok := p.(*ast.MapType); ok {
|
|
return fmt.Sprintf("map[%s]%s", formatFieldType(m.Key), formatFieldType(m.Value))
|
|
}
|
|
|
|
switch t := p.(type) {
|
|
case *ast.Ident:
|
|
return t.Name
|
|
case *ast.ArrayType:
|
|
return "[]" + formatFieldType(p.(*ast.ArrayType).Elt)
|
|
case *ast.StructType:
|
|
return "struct"
|
|
case *ast.StarExpr:
|
|
return formatFieldType(t.X)
|
|
case *ast.SelectorExpr:
|
|
return formatFieldType(t.Sel)
|
|
default:
|
|
log.Printf("unknown: %#v", t)
|
|
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func escape(value string) string {
|
|
value = strings.ReplaceAll(value, `"`, `\"`)
|
|
value = strings.ReplaceAll(value, "\n", `\n`)
|
|
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func collectFields(s *structType) (fields []*Field) {
|
|
fields = []*Field{}
|
|
|
|
for _, f := range s.node.Fields.List {
|
|
if f.Names == nil {
|
|
// This is an embedded struct.
|
|
continue
|
|
}
|
|
|
|
name := f.Names[0].Name
|
|
|
|
if f.Doc == nil {
|
|
log.Fatalf("field %q is missing a documentation", name)
|
|
}
|
|
|
|
if strings.Contains(f.Doc.Text(), "docgen:nodoc") {
|
|
fields = append(fields, &Field{Type: "unknown"})
|
|
|
|
continue
|
|
}
|
|
|
|
if f.Tag == nil {
|
|
log.Fatalf("field %q is missing a tag", name)
|
|
}
|
|
|
|
fieldType := formatFieldType(f.Type)
|
|
fieldTypeRef := getFieldType(f.Type)
|
|
|
|
tag := reflect.StructTag(strings.Trim(f.Tag.Value, "`"))
|
|
yamlTag := tag.Get("yaml")
|
|
yamlTag = strings.Split(yamlTag, ",")[0]
|
|
|
|
if yamlTag == "" {
|
|
log.Fatalf("field %q is missing a `yaml` tag or name in it", name)
|
|
}
|
|
|
|
text := parseComment([]byte(f.Doc.Text()))
|
|
|
|
field := &Field{
|
|
Name: name,
|
|
Tag: yamlTag,
|
|
Type: fieldType,
|
|
TypeRef: fieldTypeRef,
|
|
Text: text,
|
|
}
|
|
|
|
if f.Comment != nil {
|
|
field.Note = escape(f.Comment.Text())
|
|
}
|
|
|
|
fields = append(fields, field)
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
func render(doc *Doc, dest string) {
|
|
t := template.Must(template.New("docfile.tpl").Parse(tpl))
|
|
buf := bytes.Buffer{}
|
|
|
|
err := t.Execute(&buf, doc)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
formatted, err := format.Source(buf.Bytes(), format.Options{})
|
|
if err != nil {
|
|
log.Printf("data: %s", buf.Bytes())
|
|
panic(err)
|
|
}
|
|
|
|
out, err := out(dest)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer out.Close()
|
|
_, err = out.Write(formatted)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func processFile(inputFile, outputFile, typeName string) {
|
|
abs, err := in(inputFile)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
fmt.Printf("creating package file set: %q\n", abs)
|
|
|
|
fset := token.NewFileSet()
|
|
|
|
node, err := parser.ParseFile(fset, abs, nil, parser.ParseComments)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
var structs []*structType
|
|
|
|
packageName := node.Name.Name
|
|
|
|
tokenFile := fset.File(node.Pos())
|
|
if tokenFile == nil {
|
|
log.Fatalf("No token")
|
|
}
|
|
|
|
fmt.Printf("parsing file in package %q: %s\n", packageName, tokenFile.Name())
|
|
|
|
structs = append(structs, collectStructs(node)...)
|
|
|
|
if len(structs) == 0 {
|
|
log.Fatalf("failed to find types that could be documented in %s", abs)
|
|
}
|
|
|
|
doc := &Doc{
|
|
Package: packageName,
|
|
Structs: []*Struct{},
|
|
}
|
|
|
|
extraExamples := map[string][]*Example{}
|
|
backReferences := map[string][]Appearance{}
|
|
|
|
for _, s := range structs {
|
|
fmt.Printf("generating docs for type: %q\n", s.name)
|
|
|
|
fields := collectFields(s)
|
|
|
|
s := &Struct{
|
|
Name: s.name,
|
|
Text: s.text,
|
|
Fields: fields,
|
|
}
|
|
|
|
for _, field := range fields {
|
|
if field.TypeRef == "" {
|
|
continue
|
|
}
|
|
|
|
if len(field.Text.Examples) > 0 {
|
|
extraExamples[field.TypeRef] = append(extraExamples[field.TypeRef], field.Text.Examples...)
|
|
}
|
|
|
|
backReferences[field.TypeRef] = append(backReferences[field.TypeRef], Appearance{
|
|
Struct: s,
|
|
FieldName: field.Tag,
|
|
})
|
|
}
|
|
|
|
doc.Structs = append(doc.Structs, s)
|
|
}
|
|
|
|
for _, s := range doc.Structs {
|
|
if extra, ok := extraExamples[s.Name]; ok {
|
|
s.Text.Examples = append(s.Text.Examples, extra...)
|
|
}
|
|
|
|
if ref, ok := backReferences[s.Name]; ok {
|
|
s.AppearsIn = append(s.AppearsIn, ref...)
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
doc.Package = node.Name.Name
|
|
doc.Name = typeName
|
|
|
|
if node.Doc != nil {
|
|
doc.Header = escape(node.Doc.Text())
|
|
}
|
|
}
|
|
|
|
doc.File = outputFile
|
|
render(doc, outputFile)
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if flag.NArg() != 3 {
|
|
log.Fatalf("expected 3 args, got %d", flag.NArg())
|
|
}
|
|
|
|
inputFile := flag.Arg(0)
|
|
outputFile := flag.Arg(1)
|
|
typeName := flag.Arg(2)
|
|
|
|
processFile(inputFile, outputFile, typeName)
|
|
}
|