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.
2023-03-20 16:19:14 +00:00
func FromFile ( path string , opts ... LoadCfgOpt ) ( interface { } , error ) {
2024-03-08 07:43:39 +00:00
loadOpts , err := applyOpts ( opts ... )
if err != nil {
return nil , err
2023-03-20 16:19:14 +00:00
}
var def interface { }
if loadOpts . defaultCfg != nil {
def , err = loadOpts . defaultCfg ( )
if err != nil {
return nil , xerrors . Errorf ( "no config found" )
}
}
// check for loadability
2019-07-03 16:59:49 +00:00
file , err := os . Open ( path )
switch {
case os . IsNotExist ( err ) :
2023-03-20 16:19:14 +00:00
if loadOpts . canFallbackOnDefault != nil {
if err := loadOpts . canFallbackOnDefault ( ) ; err != nil {
return nil , 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
}
2023-03-20 16:19:14 +00:00
defer file . Close ( ) //nolint:errcheck,staticcheck // The file is RO
2023-03-29 19:24:07 +00:00
cfgBs , err := io . ReadAll ( file )
2023-03-20 16:19:14 +00:00
if err != nil {
return nil , xerrors . Errorf ( "failed to read config for validation checks %w" , err )
}
buf := bytes . NewBuffer ( cfgBs )
if loadOpts . validate != nil {
if err := loadOpts . validate ( buf . String ( ) ) ; err != nil {
return nil , xerrors . Errorf ( "config failed validation: %w" , err )
}
}
2024-03-08 07:43:39 +00:00
return FromReader ( buf , def , opts ... )
2019-07-03 16:59:49 +00:00
}
// FromReader loads config from a reader instance.
2024-03-08 07:43:39 +00:00
func FromReader ( reader io . Reader , def interface { } , opts ... LoadCfgOpt ) ( interface { } , error ) {
loadOpts , err := applyOpts ( opts ... )
if err != nil {
return nil , err
}
2019-10-30 16:38:39 +00:00
cfg := def
2024-03-08 07:43:39 +00:00
md , err := toml . NewDecoder ( reader ) . Decode ( cfg )
2019-07-03 16:59:49 +00:00
if err != nil {
return nil , err
}
2024-03-08 07:43:39 +00:00
// find any fields with a tag: `moved:"New.Config.Location"` and move any set values there over to
// the new location if they are not already set there.
movedFields := findMovedFields ( nil , cfg )
var warningOut io . Writer = os . Stderr
if loadOpts . warningWriter != nil {
warningOut = loadOpts . warningWriter
}
for _ , d := range movedFields {
if md . IsDefined ( d . Field ... ) {
fmt . Fprintf (
warningOut ,
"WARNING: Use of deprecated configuration option '%s' will be removed in a future release, use '%s' instead\n" ,
strings . Join ( d . Field , "." ) ,
strings . Join ( d . NewField , "." ) )
if ! md . IsDefined ( d . NewField ... ) {
// new value isn't set but old is, we should move what the user set there
if err := moveFieldValue ( cfg , d . Field , d . NewField ) ; err != nil {
return nil , fmt . Errorf ( "failed to move field value: %w" , 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
2024-03-08 07:43:39 +00:00
// move a value from the location in the valPtr struct specified by oldPath, to the location
// specified by newPath; where the path is an array of nested field names.
func moveFieldValue ( valPtr interface { } , oldPath [ ] string , newPath [ ] string ) error {
oldValue , err := getFieldValue ( valPtr , oldPath )
if err != nil {
return err
}
val := reflect . ValueOf ( valPtr ) . Elem ( )
for {
field := val . FieldByName ( newPath [ 0 ] )
if ! field . IsValid ( ) {
return fmt . Errorf ( "unexpected error fetching field value" )
}
if len ( newPath ) == 1 {
if field . Kind ( ) != oldValue . Kind ( ) {
return fmt . Errorf ( "unexpected error, old kind != new kind" )
}
// set field on val to be the new one, and we're done
field . Set ( oldValue )
return nil
}
if field . Kind ( ) != reflect . Struct {
return fmt . Errorf ( "unexpected error fetching field value, is not a struct" )
}
newPath = newPath [ 1 : ]
val = field
}
}
// recursively iterate into `path` to find the terminal value
func getFieldValue ( val interface { } , path [ ] string ) ( reflect . Value , error ) {
if reflect . ValueOf ( val ) . Kind ( ) == reflect . Ptr {
val = reflect . ValueOf ( val ) . Elem ( ) . Interface ( )
}
field := reflect . ValueOf ( val ) . FieldByName ( path [ 0 ] )
if ! field . IsValid ( ) {
return reflect . Value { } , fmt . Errorf ( "unexpected error fetching field value" )
}
if len ( path ) > 1 {
if field . Kind ( ) != reflect . Struct {
return reflect . Value { } , fmt . Errorf ( "unexpected error fetching field value, is not a struct" )
}
return getFieldValue ( field . Interface ( ) , path [ 1 : ] )
}
return field , nil
}
type movedField struct {
Field [ ] string
NewField [ ] string
}
// inspect the fields recursively within a struct and find any with "moved" tags
func findMovedFields ( path [ ] string , val interface { } ) [ ] movedField {
dep := make ( [ ] movedField , 0 )
if reflect . ValueOf ( val ) . Kind ( ) == reflect . Ptr {
val = reflect . ValueOf ( val ) . Elem ( ) . Interface ( )
}
t := reflect . TypeOf ( val )
if t . Kind ( ) != reflect . Struct {
return nil
}
for i := 0 ; i < t . NumField ( ) ; i ++ {
field := t . Field ( i )
// could also do a "deprecated" in here
if idx := field . Tag . Get ( "moved" ) ; idx != "" && idx != "-" {
dep = append ( dep , movedField {
Field : append ( path , field . Name ) ,
NewField : strings . Split ( idx , "." ) ,
} )
}
if field . Type . Kind ( ) == reflect . Struct && reflect . ValueOf ( val ) . FieldByName ( field . Name ) . IsValid ( ) {
deps := findMovedFields ( append ( path , field . Name ) , reflect . ValueOf ( val ) . FieldByName ( field . Name ) . Interface ( ) )
dep = append ( dep , deps ... )
}
}
return dep
}
2023-03-20 16:19:14 +00:00
type cfgLoadOpts struct {
defaultCfg func ( ) ( interface { } , error )
canFallbackOnDefault func ( ) error
validate func ( string ) error
2024-03-08 07:43:39 +00:00
warningWriter io . Writer
2023-03-20 16:19:14 +00:00
}
type LoadCfgOpt func ( opts * cfgLoadOpts ) error
2024-03-08 07:43:39 +00:00
func applyOpts ( opts ... LoadCfgOpt ) ( cfgLoadOpts , error ) {
var loadOpts cfgLoadOpts
var err error
for _ , opt := range opts {
if err = opt ( & loadOpts ) ; err != nil {
return loadOpts , fmt . Errorf ( "failed to apply load cfg option: %w" , err )
}
}
return loadOpts , nil
}
2023-03-20 16:19:14 +00:00
func SetDefault ( f func ( ) ( interface { } , error ) ) LoadCfgOpt {
return func ( opts * cfgLoadOpts ) error {
opts . defaultCfg = f
return nil
}
}
func SetCanFallbackOnDefault ( f func ( ) error ) LoadCfgOpt {
return func ( opts * cfgLoadOpts ) error {
opts . canFallbackOnDefault = f
return nil
}
}
func SetValidate ( f func ( string ) error ) LoadCfgOpt {
return func ( opts * cfgLoadOpts ) error {
opts . validate = f
return nil
}
}
2024-03-08 07:43:39 +00:00
func SetWarningWriter ( w io . Writer ) LoadCfgOpt {
return func ( opts * cfgLoadOpts ) error {
opts . warningWriter = w
return nil
}
}
2023-03-20 16:19:14 +00:00
func NoDefaultForSplitstoreTransition ( ) error {
return xerrors . Errorf ( "FullNode config not found and fallback to default disallowed while we transition to splitstore discard default. Use `lotus config default` to set this repo up with a default config. Be sure to set `EnableSplitstore` to `false` if you are running a full archive node" )
}
// Match the EnableSplitstore field
func MatchEnableSplitstoreField ( s string ) bool {
enableSplitstoreRx := regexp . MustCompile ( ` (?m)^\s*EnableSplitstore\s*= ` )
return enableSplitstoreRx . MatchString ( s )
}
func ValidateSplitstoreSet ( cfgRaw string ) error {
if ! MatchEnableSplitstoreField ( cfgRaw ) {
return xerrors . Errorf ( "Config does not contain explicit set of EnableSplitstore field, refusing to load. Please explicitly set EnableSplitstore. Set it to false if you are running a full archival node" )
}
return nil
}
type cfgUpdateOpts struct {
comment bool
keepUncommented func ( string ) bool
2023-09-20 21:17:51 +00:00
noEnv bool
2023-03-20 16:19:14 +00:00
}
// UpdateCfgOpt is a functional option for updating the config
type UpdateCfgOpt func ( opts * cfgUpdateOpts ) error
// KeepUncommented sets a function for matching default valeus that should remain uncommented during
// a config update that comments out default values.
func KeepUncommented ( f func ( string ) bool ) UpdateCfgOpt {
return func ( opts * cfgUpdateOpts ) error {
opts . keepUncommented = f
return nil
}
}
func Commented ( commented bool ) UpdateCfgOpt {
return func ( opts * cfgUpdateOpts ) error {
opts . comment = commented
return nil
}
}
func DefaultKeepUncommented ( ) UpdateCfgOpt {
return KeepUncommented ( MatchEnableSplitstoreField )
}
2023-09-20 21:17:51 +00:00
func NoEnv ( ) UpdateCfgOpt {
return func ( opts * cfgUpdateOpts ) error {
opts . noEnv = true
return nil
}
}
2023-03-20 16:19:14 +00:00
// ConfigUpdate takes in a config and a default config and optionally comments out default values
func ConfigUpdate ( cfgCur , cfgDef interface { } , opts ... UpdateCfgOpt ) ( [ ] byte , error ) {
var updateOpts cfgUpdateOpts
for _ , opt := range opts {
if err := opt ( & updateOpts ) ; err != nil {
return nil , xerrors . Errorf ( "failed to apply update cfg option to ConfigUpdate's config: %w" , err )
}
}
2021-07-23 12:55:19 +00:00
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 ( )
}
2023-03-20 16:19:14 +00:00
if updateOpts . comment {
2022-03-10 10:58:31 +00:00
// create a map of default lines, so we can comment those out later
2021-07-23 12:55:19 +00:00
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
2023-09-20 21:17:51 +00:00
if ! updateOpts . noEnv {
outLines = append ( outLines , pad + "# env var: LOTUS_" + strings . ToUpper ( strings . ReplaceAll ( section , "." , "_" ) ) + "_" + strings . ToUpper ( lf [ 0 ] ) )
}
2021-07-23 12:55:19 +00:00
}
}
2023-03-20 16:19:14 +00:00
// filter lines from options
optsFilter := updateOpts . keepUncommented != nil && updateOpts . keepUncommented ( line )
// if there is the same line in the default config, comment it out in output
if _ , found := defaults [ strings . TrimSpace ( nodeLines [ i ] ) ] ; ( cfgDef == nil || found ) && len ( line ) > 0 && ! optsFilter {
2021-07-23 12:55:19 +00:00
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 ) {
2023-03-20 16:19:14 +00:00
return ConfigUpdate ( t , nil , Commented ( true ) , DefaultKeepUncommented ( ) )
2019-10-30 16:38:39 +00:00
}