Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add all chia config values to the config struct + bundle default config + allow setting config values with env paths #132

Merged
merged 10 commits into from
Jun 20, 2024
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/samber/mo v1.11.0
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,5 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
458 changes: 358 additions & 100 deletions pkg/config/config.go

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions pkg/config/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package config

import (
"fmt"
"math/big"
"os"
"reflect"
"strconv"
"strings"

"github.com/chia-network/go-chia-libs/pkg/types"
)

// FillValuesFromEnvironment reads environment variables starting with `chia.` and edits the config based on the config path
// chia.selected_network=mainnet would set the top level `selected_network: mainnet`
// chia.full_node.port=8444 would set full_node.port to 8444
//
// # Complex data structures can be passed in as JSON strings and they will be parsed out into the datatype specified for the config prior to being inserted
//
// chia.network_overrides.constants.mainnet='{"GENESIS_CHALLENGE":"abc123","GENESIS_PRE_FARM_POOL_PUZZLE_HASH":"xyz789"}'
func (c *ChiaConfig) FillValuesFromEnvironment() error {
valuesToUpdate := getAllChiaVars()
for _, pAndV := range valuesToUpdate {
err := c.SetFieldByPath(pAndV.path, pAndV.value)
if err != nil {
return err
}
}

return nil
}

type pathAndValue struct {
path []string
value string
}

func getAllChiaVars() map[string]pathAndValue {
// Most shells don't allow `.` in env names, but docker will and its easier to visualize the `.`, so support both
// `.` and `__` as valid path segment separators
// chia.full_node.port
// chia__full_node__port
separators := []string{".", "__"}
envVars := os.Environ()
finalVars := map[string]pathAndValue{}

for _, sep := range separators {
prefix := fmt.Sprintf("chia%s", sep)
for _, env := range envVars {
if strings.HasPrefix(env, prefix) {
pair := strings.SplitN(env, "=", 2)
if len(pair) == 2 {
finalVars[pair[0][len(prefix):]] = pathAndValue{
path: strings.Split(pair[0], sep)[1:], // This is the path in the config to the value to edit minus the "chia" prefix
value: pair[1],
}
}
}
}
}

return finalVars
}

// SetFieldByPath iterates through each item in path to find the corresponding `yaml` tag in the struct
// Once found, we move to the next item in path and look for that key within the first element
// If any element is not found, an error will be returned
func (c *ChiaConfig) SetFieldByPath(path []string, value any) error {
v := reflect.ValueOf(c).Elem()
return setFieldByPath(v, path, value)
}

func setFieldByPath(v reflect.Value, path []string, value any) error {
if len(path) == 0 {
return fmt.Errorf("invalid path")
}

for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
yamlTagRaw := field.Tag.Get("yaml")
yamlTag := strings.Split(yamlTagRaw, ",")[0]

if yamlTagRaw == ",inline" && field.Anonymous {
// Check the inline struct
if err := setFieldByPath(v.Field(i), path, value); err != nil {
return err
}
} else if yamlTag == path[0] {
// We found a match for the current level of "paths"
// If we only have 1 element left in paths, then we can set the value
// Otherwise, we can recursively call setFieldByPath again, with the remaining elements of path
fieldValue := v.Field(i)
if len(path) > 1 {
if fieldValue.Kind() == reflect.Map {
mapKey := reflect.ValueOf(path[1])
if !mapKey.Type().ConvertibleTo(fieldValue.Type().Key()) {
return fmt.Errorf("invalid map key type %s", mapKey.Type())
}
mapValue := fieldValue.MapIndex(mapKey)
if mapValue.IsValid() {
if !mapValue.CanSet() {
// Create a new writable map and copy over the existing data
newMapValue := reflect.New(fieldValue.Type().Elem()).Elem()
newMapValue.Set(mapValue)
mapValue = newMapValue
}
err := setFieldByPath(mapValue, path[2:], value)
if err != nil {
return err
}
fieldValue.SetMapIndex(mapKey, mapValue)
return nil
}
} else {
return setFieldByPath(fieldValue, path[1:], value)
}
}

if !fieldValue.CanSet() {
return fmt.Errorf("cannot set field %s", path[0])
}

// Special Cases
if fieldValue.Type() == reflect.TypeOf(types.Uint128{}) {
strValue, ok := value.(string)
if !ok {
return fmt.Errorf("expected string for Uint128 field, got %T", value)
}
bigIntValue := new(big.Int)
_, ok = bigIntValue.SetString(strValue, 10)
if !ok {
return fmt.Errorf("invalid string for big.Int: %s", strValue)
}
fieldValue.Set(reflect.ValueOf(types.Uint128FromBig(bigIntValue)))
return nil
}

val := reflect.ValueOf(value)

if fieldValue.Type() != val.Type() {
if val.Type().ConvertibleTo(fieldValue.Type()) {
val = val.Convert(fieldValue.Type())
} else {
convertedVal, err := convertValue(value, fieldValue.Type())
if err != nil {
return err
}
val = reflect.ValueOf(convertedVal)
}
}

fieldValue.Set(val)

return nil
}
}

return nil
}

func convertValue(value interface{}, targetType reflect.Type) (interface{}, error) {
switch targetType.Kind() {
case reflect.Uint8:
v, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 8)
if err != nil {
return nil, err
}
return uint8(v), nil
case reflect.Uint16:
v, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 16)
if err != nil {
return nil, err
}
return uint16(v), nil
case reflect.Uint32:
v, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 32)
if err != nil {
return nil, err
}
return uint32(v), nil
case reflect.Uint64:
v, err := strconv.ParseUint(fmt.Sprintf("%v", value), 10, 64)
if err != nil {
return nil, err
}
return v, nil
case reflect.Bool:
v, err := strconv.ParseBool(fmt.Sprintf("%v", value))
if err != nil {
return nil, err
}
return v, nil
default:
return nil, fmt.Errorf("unsupported conversion to %s", targetType.Kind())
}
}
69 changes: 69 additions & 0 deletions pkg/config/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package config_test

import (
"os"
"testing"

"github.com/stretchr/testify/assert"

"github.com/chia-network/go-chia-libs/pkg/config"
"github.com/chia-network/go-chia-libs/pkg/types"
)

func TestChiaConfig_SetFieldByPath(t *testing.T) {
defaultConfig, err := config.LoadDefaultConfig()
assert.NoError(t, err)
// Make assertions about the default state, to ensure the assumed initial values are correct
assert.Equal(t, uint16(8444), defaultConfig.FullNode.Port)
assert.Equal(t, uint16(8555), defaultConfig.FullNode.RPCPort)
assert.NotNil(t, defaultConfig.NetworkOverrides.Constants["mainnet"])
assert.Equal(t, defaultConfig.NetworkOverrides.Constants["mainnet"].DifficultyConstantFactor, types.Uint128{})
assert.Equal(t, defaultConfig.SelectedNetwork, "mainnet")
assert.Equal(t, defaultConfig.Logging.LogLevel, "WARNING")

err = defaultConfig.SetFieldByPath([]string{"full_node", "port"}, "1234")
assert.NoError(t, err)
assert.Equal(t, uint16(1234), defaultConfig.FullNode.Port)

err = defaultConfig.SetFieldByPath([]string{"full_node", "rpc_port"}, "5678")
assert.NoError(t, err)
assert.Equal(t, uint16(5678), defaultConfig.FullNode.RPCPort)

err = defaultConfig.SetFieldByPath([]string{"network_overrides", "constants", "mainnet", "DIFFICULTY_CONSTANT_FACTOR"}, "44445555")
assert.NoError(t, err)
assert.NotNil(t, defaultConfig.NetworkOverrides.Constants["mainnet"])
assert.Equal(t, types.Uint128From64(44445555), defaultConfig.NetworkOverrides.Constants["mainnet"].DifficultyConstantFactor)

err = defaultConfig.SetFieldByPath([]string{"selected_network"}, "unittestnet")
assert.NoError(t, err)
assert.Equal(t, defaultConfig.SelectedNetwork, "unittestnet")

err = defaultConfig.SetFieldByPath([]string{"logging", "log_level"}, "INFO")
assert.NoError(t, err)
assert.Equal(t, defaultConfig.Logging.LogLevel, "INFO")
}

func TestChiaConfig_FillValuesFromEnvironment(t *testing.T) {
defaultConfig, err := config.LoadDefaultConfig()
assert.NoError(t, err)
// Make assertions about the default state, to ensure the assumed initial values are correct
assert.Equal(t, uint16(8444), defaultConfig.FullNode.Port)
assert.Equal(t, uint16(8555), defaultConfig.FullNode.RPCPort)
assert.NotNil(t, defaultConfig.NetworkOverrides.Constants["mainnet"])
assert.Equal(t, defaultConfig.NetworkOverrides.Constants["mainnet"].DifficultyConstantFactor, types.Uint128{})
assert.Equal(t, defaultConfig.SelectedNetwork, "mainnet")
assert.Equal(t, defaultConfig.Logging.LogLevel, "WARNING")

assert.NoError(t, os.Setenv("chia.full_node.port", "1234"))
assert.NoError(t, os.Setenv("chia__full_node__rpc_port", "5678"))
assert.NoError(t, os.Setenv("chia.network_overrides.constants.mainnet.DIFFICULTY_CONSTANT_FACTOR", "44445555"))
assert.NoError(t, os.Setenv("chia.selected_network", "unittestnet"))
assert.NoError(t, os.Setenv("chia__logging__log_level", "INFO"))

assert.NoError(t, defaultConfig.FillValuesFromEnvironment())
assert.Equal(t, uint16(1234), defaultConfig.FullNode.Port)
assert.Equal(t, uint16(5678), defaultConfig.FullNode.RPCPort)
assert.Equal(t, types.Uint128From64(44445555), defaultConfig.NetworkOverrides.Constants["mainnet"].DifficultyConstantFactor)
assert.Equal(t, defaultConfig.SelectedNetwork, "unittestnet")
assert.Equal(t, defaultConfig.Logging.LogLevel, "INFO")
}
Loading