node: Add config file support (#3710)
* node: Add logic to read file config and bind flags Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: add guardian node config to node.yaml Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: fix path typo Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: modularize initFileConfig Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: update ethRPC to the correct url Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: update config file path Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: add initial config file testing data Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: add test for flag precedence over config file Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: add test cases for flag, env var and config file precedence Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: use backticks as expected output Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: update comments Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: handle binding errors Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: create separate test functions Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: absolute filepath -> relative filepath Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * docs: Add guardian config file usage Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> * node: update config file name and env var prefix Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com> --------- Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com>
This commit is contained in:
parent
3d16cca785
commit
7acbacd0ea
|
@ -60,6 +60,9 @@ spec:
|
|||
path: gwrelayerKey0
|
||||
- key: gwrelayerKey1
|
||||
path: gwrelayerKey1
|
||||
- name: node-config
|
||||
configMap:
|
||||
name: node-config
|
||||
containers:
|
||||
- name: guardiand
|
||||
image: guardiand-image
|
||||
|
@ -68,11 +71,13 @@ spec:
|
|||
name: node-rundir
|
||||
- mountPath: /tmp/mounted-keys/wormchain
|
||||
name: node-wormchain-key
|
||||
- mountPath: /app/node/config
|
||||
name: node-config
|
||||
command:
|
||||
- /guardiand
|
||||
- node
|
||||
- --ethRPC
|
||||
- ws://eth-devnet:8545
|
||||
# - --ethRPC
|
||||
# - ws://eth-devnet:8545
|
||||
# - --bscRPC
|
||||
# - ws://eth-devnet2:8545
|
||||
- --polygonRPC
|
||||
|
@ -219,3 +224,11 @@ data:
|
|||
accountantKey1: LS0tLS1CRUdJTiBURU5ERVJNSU5UIFBSSVZBVEUgS0VZLS0tLS0Ka2RmOiBiY3J5cHQKc2FsdDogNzc1M0NCQTBBMUQ0NTJCMkE2QzlERDM4ODc3MTg0NEEKdHlwZTogc2VjcDI1NmsxCgpSYnhRVWRnK2ZHcjMzZTAyVWFFQW1YTDFlNFkrTGJUMFdqbnl4RVR3OXBoL2JXOGI0MzdhWmErOWlCc3NBa0UyCnRScUwvb0J1NWFnQXJocHNnWUgxNlhOWjJHMXRwY0R3V0dQZ1VWVT0KPUd6YUwKLS0tLS1FTkQgVEVOREVSTUlOVCBQUklWQVRFIEtFWS0tLS0t
|
||||
gwrelayerKey0: LS0tLS1CRUdJTiBURU5ERVJNSU5UIFBSSVZBVEUgS0VZLS0tLS0KdHlwZTogc2VjcDI1NmsxCmtkZjogYmNyeXB0CnNhbHQ6IDc4OUYzRTBCMkVGNDcyNjAyQzNFMUE0OUI2OENFQzlBCgpGWHAvSllPS3E4WmZtOWxHZ3ZFNEM3NXFyUXFNZFp2RHNWRjhObTdMQU1oR2dHbXBnZnpoZjUrZ3IwZ1hjYjVWCmtSTXA2c0p0NkxCVzRPYWF2ckk3ay84Vml2NWhMVU1la1dPMHg5bz0KPUxrb1MKLS0tLS1FTkQgVEVOREVSTUlOVCBQUklWQVRFIEtFWS0tLS0t
|
||||
gwrelayerKey1: LS0tLS1CRUdJTiBURU5ERVJNSU5UIFBSSVZBVEUgS0VZLS0tLS0Ka2RmOiBiY3J5cHQKc2FsdDogNDc5RDk3RDE2OTE0QkQ4QjlFNUUwQzkzMDA0RDA4RUEKdHlwZTogc2VjcDI1NmsxCgpvTEJ0aUkwT2pudXo5bHlzeVlZOFhQeEVkTnpwYUJOVWFkL0UySlJld2pFWFZNVVNTWll2QVZKbERiN3hEQjlSCmEvdm45SFNPM2hKOFc1QTBKOVFqUVZXRzVoZXBNZVpQUEI4M1FCUT0KPVJuTGEKLS0tLS1FTkQgVEVOREVSTUlOVCBQUklWQVRFIEtFWS0tLS0t
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: node-config
|
||||
data:
|
||||
guardiand.yaml: |
|
||||
ethRPC: "ws://eth-devnet:8545"
|
||||
|
|
|
@ -300,3 +300,48 @@ docker run \
|
|||
ghcr.io/wormhole-foundation/guardiand:latest \
|
||||
spy --nodeKey /node.key --spyRPC "[::]:7073" --network /wormhole/mainnet/2 --bootstrap /dns4/wormhole-v2-mainnet-bootstrap.xlabs.xyz/udp/8999/quic/p2p/12D3KooWNQ9tVrcb64tw6bNs2CaNrUGPM7yRrKvBBheQ5yCyPHKC,/dns4/wormhole.mcf.rocks/udp/8999/quic/p2p/12D3KooWDZVv7BhZ8yFLkarNdaSWaB43D6UbQwExJ8nnGAEmfHcU,/dns4/wormhole-v2-mainnet-bootstrap.staking.fund/udp/8999/quic/p2p/12D3KooWG8obDX9DNi1KUwZNu9xkGwfKqTp2GFwuuHpWZ3nQruS1
|
||||
```
|
||||
|
||||
## Guardian Configurations
|
||||
|
||||
Configuration files, environment variables and flags are all supported.
|
||||
|
||||
### Config File
|
||||
|
||||
**Location/Naming**: By default, the config file is expected to be in the `node/config` directory. The standard name for the config file is `guardiand.yaml`. Currently there's no support for custom directory or filename yet.
|
||||
|
||||
**Format**: We support any format that is supported by [Viper](https://pkg.go.dev/github.com/dvln/viper#section-readme). But YAML format is generally preferred.
|
||||
|
||||
**Example**:
|
||||
```yaml
|
||||
ethRPC: "ws://eth-devnet:8545"
|
||||
ethContract: "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
|
||||
solanaRPC: "http://solana-devnet:8899"
|
||||
solanaContract: "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Prefix**: All environment variables related to the Guardian node should be prefixed with `GUARDIAND_`.
|
||||
|
||||
**Usage**: Environment variables can be used to override settings in the config file. Particularly for sensitive data like API keys that should not be stored in config files.
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
export GUARDIAND_ETHRPC=ws://eth-devnet:8545
|
||||
```
|
||||
|
||||
### Command-Line Flags
|
||||
|
||||
**Usage**: Flags provide the highest precedence and can be used for temporary overrides or for settings that change frequently.
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
./guardiand node --ethRPC=ws://eth-devnet:8545
|
||||
```
|
||||
|
||||
### Precedence Order
|
||||
The configuration settings are applied in the following order of precedence:
|
||||
|
||||
1. **Command-Line Flags**: Highest precedence, overrides any other settings.
|
||||
2. **Environment Variables**: Overrides the config file settings but can be overridden by flags.
|
||||
3. **Config File**: Lowest precedence.
|
||||
|
|
|
@ -417,6 +417,12 @@ var (
|
|||
rootCtxCancel context.CancelFunc
|
||||
)
|
||||
|
||||
var (
|
||||
configFilename = "guardiand"
|
||||
configPath = "node/config"
|
||||
envPrefix = "GUARDIAND"
|
||||
)
|
||||
|
||||
// "Why would anyone do this?" are famous last words.
|
||||
//
|
||||
// We already forcibly override RPC URLs and keys in dev mode to prevent security
|
||||
|
@ -432,9 +438,10 @@ const devwarning = `
|
|||
|
||||
// NodeCmd represents the node command
|
||||
var NodeCmd = &cobra.Command{
|
||||
Use: "node",
|
||||
Short: "Run the guardiand node",
|
||||
Run: runNode,
|
||||
Use: "node",
|
||||
Short: "Run the guardiand node",
|
||||
PersistentPreRunE: initConfig,
|
||||
Run: runNode,
|
||||
}
|
||||
|
||||
// This variable may be overridden by the -X linker flag to "dev" in which case
|
||||
|
@ -443,6 +450,15 @@ var NodeCmd = &cobra.Command{
|
|||
// guardians to reduce risk from a compromised builder.
|
||||
var Build = "prod"
|
||||
|
||||
// initConfig initializes the file configuration.
|
||||
func initConfig(cmd *cobra.Command, args []string) error {
|
||||
return node.InitFileConfig(cmd, node.ConfigOptions{
|
||||
FilePath: configPath,
|
||||
FileName: configFilename,
|
||||
EnvPrefix: envPrefix,
|
||||
})
|
||||
}
|
||||
|
||||
func runNode(cmd *cobra.Command, args []string) {
|
||||
if Build == "dev" && !*unsafeDevMode {
|
||||
fmt.Println("This is a development build. --unsafeDevMode must be enabled.")
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type ConfigOptions struct {
|
||||
FilePath string
|
||||
FileName string
|
||||
EnvPrefix string
|
||||
}
|
||||
|
||||
// InitFileConfig initializes configuration according to the following precedence:
|
||||
// 1. Command line flags
|
||||
// 2. Environment variables
|
||||
// 3. Config file
|
||||
// 4. Cobra default values
|
||||
func InitFileConfig(cmd *cobra.Command, options ConfigOptions) error {
|
||||
v := viper.New()
|
||||
|
||||
v.SetConfigName(options.FileName)
|
||||
v.AddConfigPath(options.FilePath)
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Bind flags to environment variables with a common prefix to avoid conflicts
|
||||
// Example: --ethRPC will be bound to GUARDIAND_ETHRPC
|
||||
v.SetEnvPrefix(options.EnvPrefix)
|
||||
|
||||
// Bind to environment variables
|
||||
v.AutomaticEnv()
|
||||
|
||||
// Bind the current command's flags to viper
|
||||
bindFlags(cmd, v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindFlags(cmd *cobra.Command, v *viper.Viper) {
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
// Determine the naming convention of the flags when represented in the config file
|
||||
configName := f.Name
|
||||
|
||||
// Apply the viper config value to the flag when the flag is not set and viper has a value
|
||||
if !f.Changed && v.IsSet(configName) {
|
||||
val := v.Get(configName)
|
||||
err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to bind flag %s to viper: %v", f.Name, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewTestRootCommand() *cobra.Command {
|
||||
var ethRPC *string
|
||||
var solRPC *string
|
||||
|
||||
// Define test configuration
|
||||
testConfig := ConfigOptions{
|
||||
FilePath: "testdata",
|
||||
FileName: "test",
|
||||
EnvPrefix: "TEST_GUARDIAND",
|
||||
}
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "config_file_reader_test",
|
||||
Short: "Unit test to test config file reader",
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Initialize configuration using Viper
|
||||
return InitFileConfig(cmd, testConfig) // Adjust the filename as needed
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Working with OutOrStdout/OutOrStderr allows us to unit test our command easier
|
||||
out := cmd.OutOrStdout()
|
||||
|
||||
// Print the final resolved value from binding cobra flags and viper config
|
||||
fmt.Fprintln(out, "ethRPC:", *ethRPC)
|
||||
fmt.Fprintln(out, "solRPC:", *solRPC)
|
||||
},
|
||||
}
|
||||
|
||||
ethRPC = rootCmd.Flags().String("ethRPC", "", "Ethereum RPC URL")
|
||||
solRPC = rootCmd.Flags().String("solRPC", "", "Solana RPC URL")
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// Set ethRPC with config file
|
||||
// Tests that the config file is read and the default value is set
|
||||
func TestInitFileConfig(t *testing.T) {
|
||||
|
||||
cmd := NewTestRootCommand()
|
||||
output := &bytes.Buffer{}
|
||||
cmd.SetOut(output)
|
||||
_ = cmd.Execute()
|
||||
|
||||
gotOutput := output.String()
|
||||
wantOutput := `ethRPC: ws://eth-config-file:8545
|
||||
solRPC: ws://sol-config-file:8545
|
||||
`
|
||||
assert.Equal(t, wantOutput, gotOutput, "expected ethRPC to use the config file default")
|
||||
}
|
||||
|
||||
// Set ethRPC with an environment variable
|
||||
// Tests that environment variables take precedence over config file values
|
||||
func TestEnvVarPrecedence(t *testing.T) {
|
||||
os.Setenv("TEST_GUARDIAND_ETHRPC", "ws://eth-env-var:8545")
|
||||
defer os.Unsetenv("TEST_GUARDIAND_ETHRPC")
|
||||
|
||||
cmd := NewTestRootCommand()
|
||||
output := &bytes.Buffer{}
|
||||
cmd.SetOut(output)
|
||||
_ = cmd.Execute()
|
||||
|
||||
gotOutput := output.String()
|
||||
wantOutput := `ethRPC: ws://eth-env-var:8545
|
||||
solRPC: ws://sol-config-file:8545
|
||||
`
|
||||
|
||||
assert.Equal(t, wantOutput, gotOutput, "expected ethRPC to use the environment variable and solRPC to use the config file default")
|
||||
}
|
||||
|
||||
// Set ethRPC with a flag
|
||||
// Tests that flags take precedence over environment variables and config file values
|
||||
func TestFlagPrecedence(t *testing.T) {
|
||||
os.Setenv("TEST_GUARDIAND_ETHRPC", "ws://eth-env-var:8545")
|
||||
defer os.Unsetenv("TEST_GUARDIAND_ETHRPC")
|
||||
|
||||
cmd := NewTestRootCommand()
|
||||
output := &bytes.Buffer{}
|
||||
cmd.SetOut(output)
|
||||
cmd.SetArgs([]string{
|
||||
"--ethRPC",
|
||||
"ws://eth-flag:8545",
|
||||
"--solRPC",
|
||||
"ws://sol-flag:8545",
|
||||
})
|
||||
_ = cmd.Execute()
|
||||
|
||||
gotOutput := output.String()
|
||||
wantOutput := `ethRPC: ws://eth-flag:8545
|
||||
solRPC: ws://sol-flag:8545
|
||||
`
|
||||
|
||||
assert.Equal(t, wantOutput, gotOutput, "expected the ethRPC to use the flag value and solRPC to use the flag value")
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
ethRPC: "ws://eth-config-file:8545"
|
||||
solRPC: "ws://sol-config-file:8545"
|
Loading…
Reference in New Issue