use comments of variables for help
This commit is contained in:
committed by
Jonathon Anderson
parent
facde98fb5
commit
68758e3e45
@@ -47,7 +47,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
if !NoHeader {
|
||||
yamlTemplate := node.UnmarshalConf(node.Node{}, nil)
|
||||
yamlTemplate := node.ConfToYaml(node.Node{}, nil)
|
||||
if _, err := tempFile.WriteString("#nodename:\n# " + strings.Join(yamlTemplate, "\n# ") + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@ package variables
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warewulf/warewulf/internal/app/wwctl/table"
|
||||
"github.com/warewulf/warewulf/internal/pkg/node"
|
||||
"github.com/warewulf/warewulf/internal/pkg/overlay"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
@@ -20,11 +23,71 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
vars := ov.ParseVars(filePath)
|
||||
sort.Strings(vars)
|
||||
commentMap := ov.ParseCommentVars(filePath)
|
||||
varMap := node.TemplateVarMap{}
|
||||
varMap.ConfToTemplateMap(node.Node{}, "")
|
||||
if vars == nil {
|
||||
return fmt.Errorf("could not parse variables for %s in overlay %s", filePath, overlayName)
|
||||
}
|
||||
t := table.New(cmd.OutOrStdout())
|
||||
t.AddHeader("OVERLAY VARIABLE", "HELP", "TYPE", "CMD OPTION")
|
||||
|
||||
fmt.Println(strings.Join(vars, "\n"))
|
||||
for _, v := range vars {
|
||||
found := false
|
||||
helpText, hasCommentHelp := commentMap[v]
|
||||
|
||||
for key, val := range varMap {
|
||||
// fuzzy match, ignore case and try to also match singular / plural
|
||||
textLower := strings.ToLower(v)
|
||||
keyLower := strings.ToLower(key)
|
||||
match := false
|
||||
if strings.Contains(textLower, keyLower) {
|
||||
match = true
|
||||
} else {
|
||||
keyParts := strings.Split(key, ".")
|
||||
newParts := make([]string, len(keyParts))
|
||||
for i, p := range keyParts {
|
||||
newParts[i] = strings.ToLower(p)
|
||||
}
|
||||
for i, part := range newParts {
|
||||
originalPart := part
|
||||
var variation string
|
||||
if strings.HasSuffix(part, "s") {
|
||||
variation = strings.TrimSuffix(part, "s")
|
||||
} else {
|
||||
variation = part + "s"
|
||||
}
|
||||
newParts[i] = variation
|
||||
variantKey := strings.Join(newParts, ".")
|
||||
if strings.Contains(textLower, variantKey) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
newParts[i] = originalPart // restore for next iteration
|
||||
}
|
||||
}
|
||||
if match {
|
||||
opt := ""
|
||||
if val.LongOpt != "" {
|
||||
opt = val.LongOpt
|
||||
}
|
||||
if hasCommentHelp {
|
||||
t.AddLine(v, helpText, val.Type, opt)
|
||||
} else {
|
||||
t.AddLine(v, val.Comment, val.Type, opt)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if hasCommentHelp {
|
||||
t.AddLine(v, helpText, "", "")
|
||||
} else if strings.Contains(v, "Tags") {
|
||||
t.AddLine(v, "", "", "", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Print()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,99 +2,66 @@ package variables
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
|
||||
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||
)
|
||||
|
||||
func Test_Overlay_Variables(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
conf := env.Configure()
|
||||
|
||||
// be quiet
|
||||
wwlog.SetLogFormatter(func(int, *wwlog.LogRecord) string { return "" })
|
||||
|
||||
overlayDir := path.Join(conf.Paths.SiteOverlaydir(), "test-overlay")
|
||||
err := os.MkdirAll(overlayDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create overlay dir: %v", err)
|
||||
}
|
||||
warewulfd.SetNoDaemon()
|
||||
|
||||
templateContent := `
|
||||
{{/* .Kernel.Tags.foo: "some help text" */}}
|
||||
{{ .Kernel.Tags.foo }}
|
||||
{{ .Node.Tags.bar }}
|
||||
{{ .Cluster.Tags.baz }}
|
||||
{{ .Kernel.Vars }}
|
||||
`
|
||||
templatePath := path.Join(overlayDir, "test.ww")
|
||||
err = os.WriteFile(templatePath, []byte(templateContent), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write template file: %v", err)
|
||||
}
|
||||
env.WriteFile("var/lib/warewulf/overlays/test-overlay/test.ww", templateContent)
|
||||
|
||||
// Redirect stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
t.Run("overlay variables", func(t *testing.T) {
|
||||
baseCmd := GetCommand()
|
||||
buf := new(bytes.Buffer)
|
||||
baseCmd.SetOut(buf)
|
||||
baseCmd.SetErr(buf)
|
||||
wwlog.SetLogWriter(buf)
|
||||
|
||||
baseCmd.SetArgs([]string{"test-overlay", "test.ww"})
|
||||
err = baseCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
baseCmd.SetArgs([]string{"test-overlay", "test.ww"})
|
||||
err := baseCmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "OVERLAY VARIABLE")
|
||||
assert.Contains(t, output, ".Kernel.Tags.foo")
|
||||
assert.Contains(t, output, "some help text")
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, err = io.Copy(&buf, r)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read stdout: %v", err)
|
||||
}
|
||||
t.Run("overlay variables no file", func(t *testing.T) {
|
||||
baseCmd := GetCommand()
|
||||
buf := new(bytes.Buffer)
|
||||
baseCmd.SetOut(buf)
|
||||
baseCmd.SetErr(buf)
|
||||
wwlog.SetLogWriter(buf)
|
||||
|
||||
output := strings.TrimSpace(buf.String())
|
||||
expected := []string{
|
||||
".Kernel.Tags.foo",
|
||||
".Node.Tags.bar",
|
||||
".Cluster.Tags.baz",
|
||||
}
|
||||
baseCmd.SetArgs([]string{"test-overlay", "no-file.ww"})
|
||||
err := baseCmd.Execute()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
outputLines := strings.Split(output, "\n")
|
||||
assert.ElementsMatch(t, expected, outputLines)
|
||||
}
|
||||
|
||||
func Test_Overlay_Variables_No_File(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
conf := env.Configure()
|
||||
|
||||
// be quiet
|
||||
wwlog.SetLogFormatter(func(int, *wwlog.LogRecord) string { return "" })
|
||||
overlayDir := path.Join(conf.Paths.SiteOverlaydir(), "test-overlay")
|
||||
err := os.MkdirAll(overlayDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("could not create overlay dir: %v", err)
|
||||
}
|
||||
|
||||
baseCmd.SetArgs([]string{"test-overlay", "test.ww"})
|
||||
err = baseCmd.Execute()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_Overlay_Variables_No_Overlay(t *testing.T) {
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
env.Configure()
|
||||
|
||||
// be quiet
|
||||
wwlog.SetLogFormatter(func(int, *wwlog.LogRecord) string { return "" })
|
||||
|
||||
baseCmd.SetArgs([]string{"no-overlay", "test.ww"})
|
||||
err := baseCmd.Execute()
|
||||
assert.Error(t, err)
|
||||
t.Run("overlay variables no overlay", func(t *testing.T) {
|
||||
baseCmd := GetCommand()
|
||||
buf := new(bytes.Buffer)
|
||||
baseCmd.SetOut(buf)
|
||||
baseCmd.SetErr(buf)
|
||||
wwlog.SetLogWriter(buf)
|
||||
|
||||
baseCmd.SetArgs([]string{"no-overlay", "test.ww"})
|
||||
err := baseCmd.Execute()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,4 +36,3 @@ var (
|
||||
func GetCommand() *cobra.Command {
|
||||
return baseCmd
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
if !NoHeader {
|
||||
yamlTemplate := node.UnmarshalConf(node.Profile{}, nil)
|
||||
yamlTemplate := node.ConfToYaml(node.Profile{}, nil)
|
||||
if _, err := tempFile.WriteString("#profilename:\n# " + strings.Join(yamlTemplate, "\n# ") + "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ func recursiveFlatten(obj interface{}) (hasContent bool) {
|
||||
Create a string slice, where every element represents a yaml entry, used for node/profile edit
|
||||
in order to get a summary of all available elements
|
||||
*/
|
||||
func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
|
||||
func ConfToYaml(obj interface{}, excludeList []string) (lines []string) {
|
||||
objType := reflect.TypeOf(obj)
|
||||
// now iterate of every field
|
||||
for i := 0; i < objType.NumField(); i++ {
|
||||
@@ -264,7 +264,7 @@ func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
|
||||
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
||||
}
|
||||
lines = append(lines, typeLine)
|
||||
nestedLine := UnmarshalConf(reflect.New(field.Type.Elem()).Elem().Interface(), excludeList)
|
||||
nestedLine := ConfToYaml(reflect.New(field.Type.Elem()).Elem().Interface(), excludeList)
|
||||
for _, ln := range nestedLine {
|
||||
lines = append(lines, " "+ln)
|
||||
}
|
||||
@@ -274,12 +274,12 @@ func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
|
||||
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
||||
}
|
||||
lines = append(lines, typeLine, " element:")
|
||||
nestedLine := UnmarshalConf(reflect.New(field.Type.Elem().Elem()).Elem().Interface(), excludeList)
|
||||
nestedLine := ConfToYaml(reflect.New(field.Type.Elem().Elem()).Elem().Interface(), excludeList)
|
||||
for _, ln := range nestedLine {
|
||||
lines = append(lines, " "+ln)
|
||||
}
|
||||
} else if field.Type.Kind() == reflect.Struct && field.Anonymous {
|
||||
nestedLine := UnmarshalConf(reflect.New(field.Type).Elem().Interface(), excludeList)
|
||||
nestedLine := ConfToYaml(reflect.New(field.Type).Elem().Interface(), excludeList)
|
||||
lines = append(lines, nestedLine...)
|
||||
}
|
||||
}
|
||||
@@ -316,9 +316,39 @@ func getYamlString(myType reflect.StructField, excludeList []string) ([]string,
|
||||
return []string{ymlStr}, true
|
||||
}
|
||||
|
||||
/*
|
||||
Getters for unexported fields
|
||||
*/
|
||||
// struectred type for variable
|
||||
type TemplateVarDetails struct {
|
||||
Name string
|
||||
Comment string
|
||||
Type string
|
||||
LongOpt string
|
||||
}
|
||||
|
||||
// Type to sore a map which looks and feels like the variables in a template
|
||||
type TemplateVarMap map[string]TemplateVarDetails
|
||||
|
||||
// Fill the map so that every key is like a template variable and the value is the comment field
|
||||
func (varMap TemplateVarMap) ConfToTemplateMap(obj interface{}, prefix string) {
|
||||
objType := reflect.TypeOf(obj)
|
||||
// now iterate of every field
|
||||
for i := 0; i < objType.NumField(); i++ {
|
||||
field := objType.Field(i)
|
||||
if field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct {
|
||||
varMap.ConfToTemplateMap(reflect.New(field.Type.Elem()).Elem().Interface(), field.Name)
|
||||
} else if field.Type.Kind() == reflect.Map && field.Type.Elem().Kind() == reflect.Ptr {
|
||||
varMap.ConfToTemplateMap(reflect.New(field.Type.Elem().Elem()).Elem().Interface(), field.Name)
|
||||
} else if field.Type.Kind() == reflect.Struct && field.Anonymous {
|
||||
varMap.ConfToTemplateMap(reflect.New(field.Type).Elem().Interface(), field.Name)
|
||||
} else {
|
||||
varMap[prefix+"."+field.Name] = TemplateVarDetails{
|
||||
Name: field.Name,
|
||||
Comment: field.Tag.Get("comment"),
|
||||
Type: field.Type.Name(),
|
||||
LongOpt: field.Tag.Get("lopt"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Returns the id of the node
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/coreos/go-systemd/v22/unit"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/warewulf/warewulf/internal/pkg/config"
|
||||
"github.com/warewulf/warewulf/internal/pkg/node"
|
||||
@@ -309,6 +310,44 @@ func (overlay Overlay) ParseVars(file string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseCommentVars parses a template file for comments that contain variable documentations.
|
||||
// The comments must be in the format `{{/* key: value */}}`. The content is parsed as YAML.
|
||||
func (overlay Overlay) ParseCommentVars(file string) map[string]string {
|
||||
if !strings.HasSuffix(file, ".ww") {
|
||||
return nil
|
||||
}
|
||||
fullPath := overlay.File(file)
|
||||
if !util.IsFile(fullPath) {
|
||||
wwlog.Error("Template file does not exist in overlay %s: %s", overlay.Name(), file)
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
wwlog.Error("Could not read template file %s: %s", fullPath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
vars := make(map[string]string)
|
||||
re := regexp.MustCompile(`{{\s*/\*(.*?)\*/\s*}}`)
|
||||
matches := re.FindAllStringSubmatch(string(content), -1)
|
||||
|
||||
for _, match := range matches {
|
||||
commentContent := strings.TrimSpace(match[1])
|
||||
var data map[string]string
|
||||
err := yaml.Unmarshal([]byte(commentContent), &data)
|
||||
if err == nil {
|
||||
for k, v := range data {
|
||||
vars[k] = v
|
||||
}
|
||||
} else {
|
||||
wwlog.Debug("Could not parse template comment as yaml in file %s: %s", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
return vars
|
||||
}
|
||||
|
||||
// walkParseTree recursively traverses the template's parse tree to find variables.
|
||||
func walkParseTree(node parse.Node, vars map[string]bool) {
|
||||
if node == nil {
|
||||
@@ -343,9 +382,7 @@ func walkParseTree(node parse.Node, vars map[string]bool) {
|
||||
}
|
||||
}
|
||||
case *parse.VariableNode, *parse.FieldNode:
|
||||
if strings.Contains(n.String(), "Tags") {
|
||||
vars[n.String()] = true
|
||||
}
|
||||
vars[n.String()] = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user