Skip to content

Commit

Permalink
feat(wakunode2): support configuration via environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
Lorenzo Delgado authored Nov 3, 2022
1 parent 85d2842 commit d1df046
Show file tree
Hide file tree
Showing 11 changed files with 494 additions and 3 deletions.
15 changes: 14 additions & 1 deletion apps/wakunode2/config.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ import
libp2p/crypto/crypto,
libp2p/crypto/secp,
nimcrypto/utils
import
../../waku/common/confutils/envvar/defs as confEnvvarDefs,
../../waku/common/confutils/envvar/std/net as confEnvvarNet

export
confTomlDefs,
confTomlNet
confTomlNet,
confEnvvarDefs,
confEnvvarNet


type ConfResult*[T] = Result[T, string]
Expand Down Expand Up @@ -506,6 +511,12 @@ proc readValue*(r: var TomlReader, value: var crypto.PrivateKey) {.raises: [Seri
except CatchableError:
raise newException(SerializationError, getCurrentExceptionMsg())

proc readValue*(r: var EnvvarReader, value: var crypto.PrivateKey) {.raises: [SerializationError].} =
try:
value = parseCmdArg(crypto.PrivateKey, r.readValue(string))
except CatchableError:
raise newException(SerializationError, getCurrentExceptionMsg())


{.push warning[ProveInit]: off.}

Expand All @@ -514,6 +525,8 @@ proc load*(T: type WakuNodeConf, version=""): ConfResult[T] =
let conf = WakuNodeConf.load(
version=version,
secondarySources = proc (conf: WakuNodeConf, sources: auto) =
sources.addConfigFile(Envvar, InputFile("wakunode2"))

if conf.configFile.isSome():
sources.addConfigFile(Toml, conf.configFile.get())
)
Expand Down
2 changes: 1 addition & 1 deletion apps/wakunode2/wakunode2.nim
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ when isMainModule:
if conf.logLevel != LogLevel.NONE:
setLogLevel(conf.logLevel)


##############
# Node setup #
##############
Expand Down
7 changes: 6 additions & 1 deletion tests/all_tests_v2.nim
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import
# Waku common tests
./v2/test_envvar_serialization,
./v2/test_confutils_envvar,
./v2/test_sqlite_migrations

import
# Waku v2 tests
./v2/test_wakunode,
Expand Down Expand Up @@ -36,7 +42,6 @@ import
./v2/test_waku_bridge,
./v2/test_peer_storage,
./v2/test_waku_keepalive,
./v2/test_sqlite_migrations,
./v2/test_namespacing_utils,
./v2/test_waku_dnsdisc,
./v2/test_waku_discv5,
Expand Down
81 changes: 81 additions & 0 deletions tests/v2/test_confutils_envvar.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{.used.}

import
std/[os, options],
stew/results,
stew/shims/net as stewNet,
testutils/unittests,
confutils,
confutils/defs,
confutils/std/net
import
../../waku/common/confutils/envvar/defs as confEnvvarDefs,
../../waku/common/confutils/envvar/std/net as confEnvvarNet


type ConfResult[T] = Result[T, string]

type TestConf = object
configFile* {.
desc: "Configuration file path"
name: "config-file" }: Option[InputFile]

testFile* {.
desc: "Configuration test file path"
name: "test-file" }: Option[InputFile]

listenAddress* {.
defaultValue: ValidIpAddress.init("127.0.0.1"),
desc: "Listening address",
name: "listen-address"}: ValidIpAddress

tcpPort* {.
desc: "TCP listening port",
defaultValue: 60000,
name: "tcp-port" }: Port


{.push warning[ProveInit]: off.}

proc load*(T: type TestConf, prefix: string): ConfResult[T] =
try:
let conf = TestConf.load(
secondarySources = proc (conf: TestConf, sources: auto) =
sources.addConfigFile(Envvar, InputFile(prefix))
)
ok(conf)
except CatchableError:
err(getCurrentExceptionMsg())

{.pop.}


suite "nim-confutils - envvar":
test "load configuration from environment variables":
## Given
let prefix = "test-prefix"

let
listenAddress = "1.1.1.1"
tcpPort = "8080"
configFile = "/tmp/test.conf"

## When
os.putEnv("TEST_PREFIX_CONFIG_FILE", configFile)
os.putEnv("TEST_PREFIX_LISTEN_ADDRESS", listenAddress)
os.putEnv("TEST_PREFIX_TCP_PORT", tcpPort)

let confLoadRes = TestConf.load(prefix)

## Then
check confLoadRes.isOk()

let conf = confLoadRes.get()
check:
conf.listenAddress == ValidIpAddress.init(listenAddress)
conf.tcpPort == Port(8080)

conf.configFile.isSome()
conf.configFile.get().string == configFile

conf.testFile.isNone()
20 changes: 20 additions & 0 deletions tests/v2/test_envvar_serialization.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{.used.}

import
testutils/unittests
import
../../waku/common/envvar_serialization/utils


suite "nim-envvar-serialization - utils":
test "construct env var key":
## Given
let prefix = "some-prefix"
let name = @["db-url"]

## When
let key = constructKey(prefix, name)

## Then
check:
key == "SOME_PREFIX_DB_URL"
24 changes: 24 additions & 0 deletions waku/common/confutils/envvar/defs.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}


import
confutils/defs as confutilsDefs
import
../../envvar_serialization

export
envvar_serialization, confutilsDefs


template readConfutilsType(T: type) =
template readValue*(r: var EnvvarReader, value: var T) =
value = T r.readValue(string)

readConfutilsType InputFile
readConfutilsType InputDir
readConfutilsType OutPath
readConfutilsType OutDir
readConfutilsType OutFile
28 changes: 28 additions & 0 deletions waku/common/confutils/envvar/std/net.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}


import
std/strutils,
stew/shims/net
import
../../../envvar_serialization

export
net,
envvar_serialization


proc readValue*(r: var EnvvarReader, value: var ValidIpAddress) {.raises: [SerializationError].} =
try:
value = ValidIpAddress.init(r.readValue(string))
except ValueError:
raise newException(EnvvarError, "Invalid IP address")

proc readValue*(r: var EnvvarReader, value: var Port) {.raises: [SerializationError, ValueError].} =
try:
value = parseUInt(r.readValue(string)).Port
except ValueError:
raise newException(EnvvarError, "Invalid Port")
63 changes: 63 additions & 0 deletions waku/common/envvar_serialization.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}


import
stew/shims/macros,
serialization
import
./envvar_serialization/reader,
./envvar_serialization/writer

export
serialization,
reader,
writer


serializationFormat Envvar

Envvar.setReader EnvvarReader
Envvar.setWriter EnvvarWriter, PreferredOutput = void


template supports*(_: type Envvar, T: type): bool =
# The Envvar format should support every type
true

template decode*(_: type Envvar,
prefix: string,
RecordType: distinct type,
params: varargs[untyped]): auto =
mixin init, ReaderType

{.noSideEffect.}:
var reader = unpackArgs(init, [EnvvarReader, prefix, params])
reader.readValue(RecordType)

template encode*(_: type Envvar,
prefix: string,
value: auto,
params: varargs[untyped]) =
mixin init, WriterType, writeValue

{.noSideEffect.}:
var writer = unpackArgs(init, [EnvvarWriter, prefix, params])
writeValue writer, value

template loadFile*(_: type Envvar,
prefix: string,
RecordType: distinct type,
params: varargs[untyped]): auto =
mixin init, ReaderType, readValue

var reader = unpackArgs(init, [EnvvarReader, prefix, params])
reader.readValue(RecordType)

template saveFile*(_: type Envvar, prefix: string, value: auto, params: varargs[untyped]) =
mixin init, WriterType, writeValue

var writer = unpackArgs(init, [EnvvarWriter, prefix, params])
writer.writeValue(value)
95 changes: 95 additions & 0 deletions waku/common/envvar_serialization/reader.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}


import
std/[tables, typetraits, options, os],
serialization/object_serialization,
serialization/errors
import
./utils


type
EnvvarReader* = object
prefix: string
key: seq[string]

EnvvarError* = object of SerializationError

EnvvarReaderError* = object of EnvvarError

GenericEnvvarReaderError* = object of EnvvarReaderError
deserializedField*: string
innerException*: ref CatchableError


proc handleReadException*(r: EnvvarReader,
Record: type,
fieldName: string,
field: auto,
err: ref CatchableError) {.raises: [GenericEnvvarReaderError].} =
var ex = new GenericEnvvarReaderError
ex.deserializedField = fieldName
ex.innerException = err
raise ex

proc init*(T: type EnvvarReader, prefix: string): T =
result.prefix = prefix

proc readValue*[T](r: var EnvvarReader, value: var T) {.raises: [ValueError, SerializationError].} =
mixin readValue

when T is string:
let key = constructKey(r.prefix, r.key)
value = os.getEnv(key)

elif T is (SomePrimitives or range):
let key = constructKey(r.prefix, r.key)
getValue(key, value)

elif T is Option:
template getUnderlyingType[T](_: Option[T]): untyped = T
let key = constructKey(r.prefix, r.key)
if os.existsEnv(key):
type uType = getUnderlyingType(value)
when uType is string:
value = some(os.getEnv(key))
else:
value = some(r.readValue(uType))

elif T is (seq or array):
when uTypeIsPrimitives(T):
let key = constructKey(r.prefix, r.key)
getValue(key, value)

else:
let key = r.key[^1]
for i in 0..<value.len:
r.key[^1] = key & $i
r.readValue(value[i])

elif T is (object or tuple):
type T = type(value)
when T.totalSerializedFields > 0:
let fields = T.fieldReadersTable(EnvvarReader)
var expectedFieldPos = 0
r.key.add ""
value.enumInstanceSerializedFields(fieldName, field):
when T is tuple:
r.key[^1] = $expectedFieldPos
var reader = fields[][expectedFieldPos].reader
expectedFieldPos += 1
else:
r.key[^1] = fieldName
var reader = findFieldReader(fields[], fieldName, expectedFieldPos)

if reader != nil:
reader(value, r)
discard r.key.pop()

else:
const typeName = typetraits.name(T)
{.fatal: "Failed to convert from Envvar an unsupported type: " & typeName.}
Loading

0 comments on commit d1df046

Please sign in to comment.