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 ) {
var loadOpts cfgLoadOpts
var err error
for _ , opt := range opts {
if err = opt ( & loadOpts ) ; err != nil {
return nil , xerrors . Errorf ( "failed to apply load cfg option: %w" , err )
}
}
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 )
}
}
return FromReader ( buf , 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
2022-06-29 14:53:15 +00:00
_ , err := toml . NewDecoder ( reader ) . Decode ( cfg )
2019-07-03 16:59:49 +00:00
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
2023-03-20 16:19:14 +00:00
type cfgLoadOpts struct {
defaultCfg func ( ) ( interface { } , error )
canFallbackOnDefault func ( ) error
validate func ( string ) error
}
type LoadCfgOpt func ( opts * cfgLoadOpts ) error
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
}
}
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
}