scripts/generate-docs/custom_doc.go (258 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
"gopkg.in/yaml.v2"
"github.com/pkg/errors"
)
type customDocEvent struct {
filePath string
dataStream string
fileSubPath string
doc eventDoc
}
type eventOverview struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
}
type StringArray []string
type eventIdentification struct {
Os []string `yaml:"os"`
DataStream string `yaml:"data_stream"`
Filter map[string]StringArray `yaml:"filter"`
}
func (a *StringArray) UnmarshalYAML(unmarshal func(interface{}) error) error {
var multi []string
err := unmarshal(&multi)
if err != nil {
var single string
err := unmarshal(&single)
if err != nil {
return err
}
*a = []string{single}
} else {
*a = multi
}
return nil
}
type eventFieldDetails struct {
Description string `yaml:"description"`
}
type eventFields struct {
Endpoint []string `yaml:"endpoint"`
Details map[string]eventFieldDetails `yaml:"details"`
}
type eventDoc struct {
Overview eventOverview `yaml:"overview"`
Identification eventIdentification `yaml:"identification"`
Fields eventFields `yaml:"fields"`
}
func findCustomDocFiles(root string) ([]customDocEvent, error) {
// Return all the custom doc files under root. Intelligently understand the directory structure
// and interpret the data_stream name from it for each file.
var customFiles []customDocEvent
dataStreamRoot := filepath.Clean(filepath.Join(root, "data_stream"))
dataStreamNames, err := os.ReadDir(dataStreamRoot)
if err != nil {
return nil, errors.Wrapf(err, "Failed to list directory %s", dataStreamRoot)
}
for _, dataStreamName := range dataStreamNames {
dataStreamNameRoot := filepath.Join(dataStreamRoot, dataStreamName.Name())
err := filepath.WalkDir(dataStreamNameRoot, func(path string, info os.DirEntry, err error) error {
if !info.IsDir() {
cleanPath := filepath.Clean(path)
event := customDocEvent{filePath: path, dataStream: dataStreamName.Name(), fileSubPath: cleanPath[len(dataStreamRoot):]}
customFiles = append(customFiles, event)
}
return nil
})
if err != nil {
return nil, errors.Wrapf(err, "failed to load data_stream directory %s", dataStreamName.Name())
}
}
return customFiles, err
}
func loadCustomDocFile(path string) (eventDoc, error) {
var result eventDoc
body, err := ioutil.ReadFile(path)
if err != nil {
return result, errors.Wrapf(err, "reading file failed (path: %s)", path)
}
err = yaml.UnmarshalStrict(body, &result)
if err != nil {
return result, errors.Wrapf(err, "parsing yaml failed (path: %s)", path)
}
// do some light validation
if result.Overview.Name == "" {
return result, fmt.Errorf("missing overview.name in %s", path)
}
if result.Overview.Description == "" {
return result, fmt.Errorf("missing overview.description in %s", path)
}
if len(result.Identification.Os) == 0 {
return result, fmt.Errorf("missing identification.os in %s", path)
}
for _, _os := range result.Identification.Os {
low := strings.ToLower(_os)
if low != "linux" && low != "macos" && low != "windows" {
return result, fmt.Errorf("invalid identification.os value %s", path)
}
}
if result.Identification.DataStream == "" {
return result, fmt.Errorf("missing identification.data_stream in %s", path)
}
if len(result.Identification.Filter) == 0 {
return result, fmt.Errorf("missing identification.filter in %s", path)
}
if len(result.Fields.Endpoint) == 0 {
return result, fmt.Errorf("missing fields.endpoint in %s", path)
}
return result, nil
}
func loadDataStreamFields(options generateOptions, packageName string, dataStreamName string) ([]fieldsTableRecord, error) {
dataStreamPath := filepath.Join(options.packagesSourceDir, packageName, "data_stream", dataStreamName)
fieldFiles, err := listFieldFields(dataStreamPath)
if err != nil {
return nil, errors.Wrapf(err, "listing field files failed (dataStreamPath: %s)", dataStreamPath)
}
fields, err := loadFields(fieldFiles)
if err != nil {
return nil, errors.Wrap(err, "loading fields files failed")
}
collected, err := collectFieldsFromDefinitions(fields)
if err != nil {
return nil, errors.Wrap(err, "collecting fields files failed")
}
return collected, nil
}
func renderCustomDocumentationReadme(options generateOptions, packageName string) error {
readmePath := filepath.Join(options.docTemplatesDir, fmt.Sprintf("%s/docs", packageName), "CustomDocumentationREADME.md")
content, err := ioutil.ReadFile(readmePath)
if err != nil {
return errors.Wrapf(err, "failed to read readme file %s", readmePath)
}
outputPath := filepath.Join(options.customDocDir, "doc", packageName, "README.md")
// Write data to dst
err = ioutil.WriteFile(outputPath, content, 0644)
if err != nil {
return errors.Wrapf(err, "write readme (path: %s)", outputPath)
}
return nil
}
func renderCustomDocumentationEvent(options generateOptions, packageName string, event customDocEvent) error {
templatePath := filepath.Join(options.docTemplatesDir, fmt.Sprintf("%s/docs", packageName), "CustomDocumentation.md")
_, err := os.Stat(templatePath)
if err != nil {
return errors.Wrapf(err, "failed to find or stat template file %s", templatePath)
}
t := template.New("CustomDocumentation.md")
t, err = t.Funcs(template.FuncMap{
"overview_name": func() (string, error) {
return event.doc.Overview.Name, nil
},
"overview_description": func() (string, error) {
return event.doc.Overview.Description, nil
},
"identification_os": func() (string, error) {
var styleOses []string
for _, _os := range event.doc.Identification.Os {
if strings.ToLower(_os) == "linux" {
styleOses = append(styleOses, "Linux")
} else if strings.ToLower(_os) == "windows" {
styleOses = append(styleOses, "Windows")
} else if strings.ToLower(_os) == "macos" {
styleOses = append(styleOses, "macOS")
} else {
styleOses = append(styleOses, _os)
}
}
sort.Strings(styleOses)
return strings.Join(styleOses, ", "), nil
},
"identification_data_stream": func() (string, error) {
return event.doc.Identification.DataStream, nil
},
"identification_kql": func() (string, error) {
var terms []string
var keys []string
for key := range event.doc.Identification.Filter {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if len(event.doc.Identification.Filter[key]) == 1 {
terms = append(terms, fmt.Sprintf("%s : \"%s\"", key, event.doc.Identification.Filter[key][0]))
} else {
term := fmt.Sprintf("%s : (\"%s\"", key, event.doc.Identification.Filter[key][0])
for i := 1; i < len(event.doc.Identification.Filter[key]); i++ {
term += fmt.Sprintf(" or \"%s\"", event.doc.Identification.Filter[key][i])
}
term += ")"
terms = append(terms, term)
}
}
return strings.Join(terms, " and "), nil
},
"fields": func() (string, error) {
var builder strings.Builder
builder.WriteString("| Field |\n")
builder.WriteString("|---|\n")
for _, f := range event.doc.Fields.Endpoint {
detail, ok := event.doc.Fields.Details[f]
if ok {
f += "<br /><br />" + strings.TrimSpace(strings.ReplaceAll(detail.Description, "\n", " "))
}
builder.WriteString(fmt.Sprintf("| %s |\n", f))
}
return builder.String(), nil
},
}).ParseFiles(templatePath)
if err != nil {
return errors.Wrapf(err, "parsing CustomDocumentation template failed (path: %s)", templatePath)
}
subPath := event.fileSubPath
subPathLower := strings.ToLower(subPath)
if strings.HasSuffix(subPathLower, "yaml") {
subPath = subPath[:len(subPath)-4] + "md"
} else if strings.HasSuffix(subPathLower, "yml") {
subPath = subPath[:len(subPath)-3] + "md"
} else {
subPath = subPath + ".md"
}
outputPath := filepath.Join(options.customDocDir, "doc", packageName, subPath)
f, err := os.OpenFile(outputPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return errors.Wrapf(err, "opening %s for writing failed, does the directory exist?", outputPath)
}
defer f.Close()
err = t.Execute(f, nil)
if err != nil {
return errors.Wrapf(err, "rendering custom documentation failed (path: %s)", templatePath)
}
return nil
}
func renderCustomDocumentation(options generateOptions, packageName string) error {
customDocPackageDir := filepath.Join(options.customDocDir, "src", packageName)
customFiles, err := findCustomDocFiles(customDocPackageDir)
if err != nil {
return errors.Wrapf(err, "failed to find custom documentation (path: %s", customDocPackageDir)
}
for _, event := range customFiles {
doc, err := loadCustomDocFile(event.filePath)
if err != nil {
return err
}
event.doc = doc
err = renderCustomDocumentationEvent(options, packageName, event)
if err != nil {
return errors.Wrapf(err, "failed to render %s", event.filePath)
}
}
err = renderCustomDocumentationReadme(options, packageName)
if err != nil {
return errors.Wrapf(err, "failed to render readme after rendering all other documentation")
}
return nil
}