Skip to content

Commit

Permalink
Add all chia config values to the config struct + bundle default conf…
Browse files Browse the repository at this point in the history
…ig + allow setting config values with env paths (#132)

* Specifically dont marshall ChiaRoot in the config

* Add remaining config fields

* Move config loading funcs to their own file

* Add LoadDefaultConfig function for chia config

* Parse out env vars that use . or __ as a separator

* Set fields by path

* Add tests for setting by path

* Remove debug code

* Add tests for setting with the environment variable paths

* Fix harvester yaml tag
  • Loading branch information
cmmarslender authored Jun 20, 2024
1 parent a822a9a commit 593fee6
Show file tree
Hide file tree
Showing 9 changed files with 1,506 additions and 104 deletions.
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

0 comments on commit 593fee6

Please sign in to comment.