2019-07-03 16:59:49 +00:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
2019-10-30 16:38:39 +00:00
|
|
|
"bytes"
|
2020-05-22 14:51:18 +00:00
|
|
|
"fmt"
|
2019-07-03 16:59:49 +00:00
|
|
|
"io"
|
|
|
|
"os"
|
2021-07-23 12:55:19 +00:00
|
|
|
"reflect"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"unicode"
|
2019-07-03 16:59:49 +00:00
|
|
|
|
|
|
|
"github.com/BurntSushi/toml"
|
2020-05-22 14:51:18 +00:00
|
|
|
"github.com/kelseyhightower/envconfig"
|
2019-10-30 16:38:39 +00:00
|
|
|
"golang.org/x/xerrors"
|
2019-07-03 16:59:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// FromFile loads config from a specified file overriding defaults specified in
|
2020-06-09 23:30:30 +00:00
|
|
|
// the def parameter. If file does not exist or is empty defaults are assumed.
|
2019-10-30 16:38:39 +00:00
|
|
|
func FromFile(path string, def interface{}) (interface{}, error) {
|
2019-07-03 16:59:49 +00:00
|
|
|
file, err := os.Open(path)
|
|
|
|
switch {
|
|
|
|
case os.IsNotExist(err):
|
2019-10-30 16:38:39 +00:00
|
|
|
return def, nil
|
2019-07-03 16:59:49 +00:00
|
|
|
case err != nil:
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer file.Close() //nolint:errcheck // The file is RO
|
2019-10-30 16:38:39 +00:00
|
|
|
return FromReader(file, def)
|
2019-07-03 16:59:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FromReader loads config from a reader instance.
|
2019-10-30 16:38:39 +00:00
|
|
|
func FromReader(reader io.Reader, def interface{}) (interface{}, error) {
|
|
|
|
cfg := def
|
2019-07-03 16:59:49 +00:00
|
|
|
_, err := toml.DecodeReader(reader, cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-05-22 14:51:18 +00:00
|
|
|
err = envconfig.Process("LOTUS", cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("processing env vars overrides: %s", err)
|
|
|
|
}
|
|
|
|
|
2019-07-03 16:59:49 +00:00
|
|
|
return cfg, nil
|
|
|
|
}
|
2019-10-30 16:38:39 +00:00
|
|
|
|
2021-07-23 12:55:19 +00:00
|
|
|
func ConfigUpdate(cfgCur, cfgDef interface{}, comment bool) ([]byte, error) {
|
|
|
|
var nodeStr, defStr string
|
|
|
|
if cfgDef != nil {
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
e := toml.NewEncoder(buf)
|
|
|
|
if err := e.Encode(cfgDef); err != nil {
|
|
|
|
return nil, xerrors.Errorf("encoding default config: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defStr = buf.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
e := toml.NewEncoder(buf)
|
|
|
|
if err := e.Encode(cfgCur); err != nil {
|
|
|
|
return nil, xerrors.Errorf("encoding node config: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
nodeStr = buf.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
if comment {
|
|
|
|
// create a map of default lines so we can comment those out later
|
|
|
|
defLines := strings.Split(defStr, "\n")
|
|
|
|
defaults := map[string]struct{}{}
|
|
|
|
for i := range defLines {
|
|
|
|
l := strings.TrimSpace(defLines[i])
|
|
|
|
if len(l) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if l[0] == '#' || l[0] == '[' {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
defaults[l] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
nodeLines := strings.Split(nodeStr, "\n")
|
|
|
|
var outLines []string
|
|
|
|
|
2021-07-23 13:16:07 +00:00
|
|
|
sectionRx := regexp.MustCompile(`\[(.+)]`)
|
2021-07-23 12:55:19 +00:00
|
|
|
var section string
|
|
|
|
|
|
|
|
for i, line := range nodeLines {
|
|
|
|
// if this is a section, track it
|
|
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
if len(trimmed) > 0 {
|
|
|
|
if trimmed[0] == '[' {
|
|
|
|
m := sectionRx.FindSubmatch([]byte(trimmed))
|
|
|
|
if len(m) != 2 {
|
|
|
|
return nil, xerrors.Errorf("section didn't match (line %d)", i)
|
|
|
|
}
|
|
|
|
section = string(m[1])
|
|
|
|
|
|
|
|
// never comment sections
|
|
|
|
outLines = append(outLines, line)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-23 13:16:07 +00:00
|
|
|
pad := strings.Repeat(" ", len(line)-len(strings.TrimLeftFunc(line, unicode.IsSpace)))
|
2021-07-23 12:55:19 +00:00
|
|
|
|
|
|
|
// see if we have docs for this field
|
|
|
|
{
|
|
|
|
lf := strings.Fields(line)
|
|
|
|
if len(lf) > 1 {
|
2021-07-23 13:16:07 +00:00
|
|
|
doc := findDoc(cfgCur, section, lf[0])
|
2021-07-23 12:55:19 +00:00
|
|
|
|
|
|
|
if doc != nil {
|
|
|
|
// found docfield, emit doc comment
|
|
|
|
if len(doc.Comment) > 0 {
|
|
|
|
for _, docLine := range strings.Split(doc.Comment, "\n") {
|
2021-07-23 13:16:07 +00:00
|
|
|
outLines = append(outLines, pad+"# "+docLine)
|
2021-07-23 12:55:19 +00:00
|
|
|
}
|
2021-07-23 13:16:07 +00:00
|
|
|
outLines = append(outLines, pad+"#")
|
2021-07-23 12:55:19 +00:00
|
|
|
}
|
|
|
|
|
2021-07-23 13:16:07 +00:00
|
|
|
outLines = append(outLines, pad+"# type: "+doc.Type)
|
2021-07-23 12:55:19 +00:00
|
|
|
}
|
2021-09-30 16:23:05 +00:00
|
|
|
|
|
|
|
outLines = append(outLines, pad+"# env var: LOTUS_"+strings.ToUpper(strings.ReplaceAll(section, ".", "_"))+"_"+strings.ToUpper(lf[0]))
|
2021-07-23 12:55:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if there is the same line in the default config, comment it out it output
|
|
|
|
if _, found := defaults[strings.TrimSpace(nodeLines[i])]; (cfgDef == nil || found) && len(line) > 0 {
|
|
|
|
line = pad + "#" + line[len(pad):]
|
2021-07-23 14:59:55 +00:00
|
|
|
}
|
|
|
|
outLines = append(outLines, line)
|
|
|
|
if len(line) > 0 {
|
|
|
|
outLines = append(outLines, "")
|
2021-07-23 12:55:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nodeStr = strings.Join(outLines, "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
// sanity-check that the updated config parses the same way as the current one
|
|
|
|
if cfgDef != nil {
|
|
|
|
cfgUpdated, err := FromReader(strings.NewReader(nodeStr), cfgDef)
|
|
|
|
if err != nil {
|
|
|
|
return nil, xerrors.Errorf("parsing updated config: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(cfgCur, cfgUpdated) {
|
|
|
|
return nil, xerrors.Errorf("updated config didn't match current config")
|
|
|
|
}
|
2019-10-30 16:38:39 +00:00
|
|
|
}
|
2021-07-23 12:55:19 +00:00
|
|
|
|
|
|
|
return []byte(nodeStr), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func ConfigComment(t interface{}) ([]byte, error) {
|
|
|
|
return ConfigUpdate(t, nil, true)
|
2019-10-30 16:38:39 +00:00
|
|
|
}
|