mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
[CORDA-1379]: Allow soft trapping of unknown config keys. (#3011)
This commit is contained in:
parent
efa69ce959
commit
235df69efe
@ -7,6 +7,9 @@ release, see :doc:`upgrade-notes`.
|
|||||||
Unreleased
|
Unreleased
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
* Added program line argument ``on-unknown-config-keys`` to allow specifying behaviour on unknown node configuration property keys.
|
||||||
|
Values are: [FAIL, WARN, IGNORE], default to FAIL if unspecified.
|
||||||
|
|
||||||
* Fix CORDA-1229. Setter-based serialization was broken with generic types when the property was stored
|
* Fix CORDA-1229. Setter-based serialization was broken with generic types when the property was stored
|
||||||
as the raw type, List for example.
|
as the raw type, List for example.
|
||||||
|
|
||||||
|
@ -31,6 +31,9 @@ e.g.:
|
|||||||
The property `"dataSourceProperties.dataSourceClassName" = "val"` in ``reference.conf``
|
The property `"dataSourceProperties.dataSourceClassName" = "val"` in ``reference.conf``
|
||||||
would be not overwritten by the property `dataSourceProperties.dataSourceClassName = "val2"` in ``node.conf``.
|
would be not overwritten by the property `dataSourceProperties.dataSourceClassName = "val2"` in ``node.conf``.
|
||||||
|
|
||||||
|
By default the node will fail to start in presence of unknown property keys. To alter this behaviour, program line argument
|
||||||
|
``on-unknown-config-keys`` can be set to ``WARN`` or ``IGNORE``. Default is ``FAIL`` if unspecified.
|
||||||
|
|
||||||
Defaults
|
Defaults
|
||||||
--------
|
--------
|
||||||
A set of default configuration options are loaded from the built-in resource file ``/node/src/main/resources/reference.conf``.
|
A set of default configuration options are loaded from the built-in resource file ``/node/src/main/resources/reference.conf``.
|
||||||
|
@ -7,6 +7,7 @@ import net.corda.core.identity.CordaX500Name
|
|||||||
import net.corda.core.internal.noneOrSingle
|
import net.corda.core.internal.noneOrSingle
|
||||||
import net.corda.core.internal.uncheckedCast
|
import net.corda.core.internal.uncheckedCast
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
import net.corda.core.utilities.NetworkHostAndPort
|
||||||
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
@ -35,10 +36,10 @@ const val CUSTOM_NODE_PROPERTIES_ROOT = "custom"
|
|||||||
|
|
||||||
// TODO Move other config parsing to use parseAs and remove this
|
// TODO Move other config parsing to use parseAs and remove this
|
||||||
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
|
operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T {
|
||||||
return getValueInternal(metadata.name, metadata.returnType)
|
return getValueInternal(metadata.name, metadata.returnType, UnknownConfigKeysPolicy.IGNORE::handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
|
fun <T : Any> Config.parseAs(clazz: KClass<T>, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle): T {
|
||||||
require(clazz.isData) { "Only Kotlin data classes can be parsed. Offending: ${clazz.qualifiedName}" }
|
require(clazz.isData) { "Only Kotlin data classes can be parsed. Offending: ${clazz.qualifiedName}" }
|
||||||
val constructor = clazz.primaryConstructor!!
|
val constructor = clazz.primaryConstructor!!
|
||||||
val parameters = constructor.parameters
|
val parameters = constructor.parameters
|
||||||
@ -55,14 +56,13 @@ fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
|
|||||||
.filterNot { it == CUSTOM_NODE_PROPERTIES_ROOT }
|
.filterNot { it == CUSTOM_NODE_PROPERTIES_ROOT }
|
||||||
.filterNot(parameterNames::contains)
|
.filterNot(parameterNames::contains)
|
||||||
.toSortedSet()
|
.toSortedSet()
|
||||||
if (unknownConfigurationKeys.isNotEmpty()) {
|
onUnknownKeys.invoke(unknownConfigurationKeys, logger)
|
||||||
throw UnknownConfigurationKeysException.of(unknownConfigurationKeys)
|
|
||||||
}
|
|
||||||
val args = parameters.filterNot { it.isOptional && !hasPath(it.name!!) }.associateBy({ it }) { param ->
|
val args = parameters.filterNot { it.isOptional && !hasPath(it.name!!) }.associateBy({ it }) { param ->
|
||||||
// Get the matching property for this parameter
|
// Get the matching property for this parameter
|
||||||
val property = clazz.memberProperties.first { it.name == param.name }
|
val property = clazz.memberProperties.first { it.name == param.name }
|
||||||
val path = defaultToOldPath(property)
|
val path = defaultToOldPath(property)
|
||||||
getValueInternal<Any>(path, param.type)
|
getValueInternal<Any>(path, param.type, onUnknownKeys)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return constructor.callBy(args)
|
return constructor.callBy(args)
|
||||||
@ -82,7 +82,7 @@ class UnknownConfigurationKeysException private constructor(val unknownKeys: Set
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Any> Config.parseAs(): T = parseAs(T::class)
|
inline fun <reified T : Any> Config.parseAs(noinline onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle): T = parseAs(T::class, onUnknownKeys)
|
||||||
|
|
||||||
fun Config.toProperties(): Properties {
|
fun Config.toProperties(): Properties {
|
||||||
return entrySet().associateByTo(
|
return entrySet().associateByTo(
|
||||||
@ -91,11 +91,11 @@ fun Config.toProperties(): Properties {
|
|||||||
{ it.value.unwrapped().toString() })
|
{ it.value.unwrapped().toString() })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Any> Config.getValueInternal(path: String, type: KType): T {
|
private fun <T : Any> Config.getValueInternal(path: String, type: KType, onUnknownKeys: ((Set<String>, logger: Logger) -> Unit)): T {
|
||||||
return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type) else getCollectionValue(path, type))
|
return uncheckedCast(if (type.arguments.isEmpty()) getSingleValue(path, type, onUnknownKeys) else getCollectionValue(path, type, onUnknownKeys))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Config.getSingleValue(path: String, type: KType): Any? {
|
private fun Config.getSingleValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit): Any? {
|
||||||
if (type.isMarkedNullable && !hasPath(path)) return null
|
if (type.isMarkedNullable && !hasPath(path)) return null
|
||||||
val typeClass = type.jvmErasure
|
val typeClass = type.jvmErasure
|
||||||
return when (typeClass) {
|
return when (typeClass) {
|
||||||
@ -113,7 +113,7 @@ private fun Config.getSingleValue(path: String, type: KType): Any? {
|
|||||||
UUID::class -> UUID.fromString(getString(path))
|
UUID::class -> UUID.fromString(getString(path))
|
||||||
CordaX500Name::class -> {
|
CordaX500Name::class -> {
|
||||||
when (getValue(path).valueType()) {
|
when (getValue(path).valueType()) {
|
||||||
ConfigValueType.OBJECT -> getConfig(path).parseAs()
|
ConfigValueType.OBJECT -> getConfig(path).parseAs(onUnknownKeys)
|
||||||
else -> CordaX500Name.parse(getString(path))
|
else -> CordaX500Name.parse(getString(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -122,12 +122,12 @@ private fun Config.getSingleValue(path: String, type: KType): Any? {
|
|||||||
else -> if (typeClass.java.isEnum) {
|
else -> if (typeClass.java.isEnum) {
|
||||||
parseEnum(typeClass.java, getString(path))
|
parseEnum(typeClass.java, getString(path))
|
||||||
} else {
|
} else {
|
||||||
getConfig(path).parseAs(typeClass)
|
getConfig(path).parseAs(typeClass, onUnknownKeys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Config.getCollectionValue(path: String, type: KType): Collection<Any> {
|
private fun Config.getCollectionValue(path: String, type: KType, onUnknownKeys: (Set<String>, logger: Logger) -> Unit): Collection<Any> {
|
||||||
val typeClass = type.jvmErasure
|
val typeClass = type.jvmErasure
|
||||||
require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" }
|
require(typeClass == List::class || typeClass == Set::class) { "$typeClass is not supported" }
|
||||||
val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type")
|
val elementClass = type.arguments[0].type?.jvmErasure ?: throw IllegalArgumentException("Cannot work with star projection: $type")
|
||||||
@ -151,7 +151,7 @@ private fun Config.getCollectionValue(path: String, type: KType): Collection<Any
|
|||||||
else -> if (elementClass.java.isEnum) {
|
else -> if (elementClass.java.isEnum) {
|
||||||
getStringList(path).map { parseEnum(elementClass.java, it) }
|
getStringList(path).map { parseEnum(elementClass.java, it) }
|
||||||
} else {
|
} else {
|
||||||
getConfigList(path).map { it.parseAs(elementClass) }
|
getConfigList(path).map { it.parseAs(elementClass, onUnknownKeys) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (typeClass == Set::class) values.toSet() else values
|
return if (typeClass == Set::class) values.toSet() else values
|
||||||
@ -242,3 +242,17 @@ private fun Iterable<*>.toConfigIterable(field: Field): Iterable<Any?> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger("net.corda.nodeapi.internal.config")
|
private val logger = LoggerFactory.getLogger("net.corda.nodeapi.internal.config")
|
||||||
|
|
||||||
|
enum class UnknownConfigKeysPolicy(private val handle: (Set<String>, logger: Logger) -> Unit) {
|
||||||
|
|
||||||
|
FAIL({ unknownKeys, _ -> throw UnknownConfigurationKeysException.of(unknownKeys) }),
|
||||||
|
WARN({ unknownKeys, logger -> logger.warn("Unknown configuration keys found: ${unknownKeys.joinToString(", ", "[", "]")}.") }),
|
||||||
|
IGNORE({ _, _ -> });
|
||||||
|
|
||||||
|
fun handle(unknownKeys: Set<String>, logger: Logger) {
|
||||||
|
|
||||||
|
if (unknownKeys.isNotEmpty()) {
|
||||||
|
handle.invoke(unknownKeys, logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import net.corda.core.internal.exists
|
|||||||
import net.corda.node.services.config.ConfigHelper
|
import net.corda.node.services.config.ConfigHelper
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
import net.corda.node.services.config.NodeConfiguration
|
||||||
import net.corda.node.services.config.parseAsNodeConfiguration
|
import net.corda.node.services.config.parseAsNodeConfiguration
|
||||||
|
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||||
import org.slf4j.event.Level
|
import org.slf4j.event.Level
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
@ -42,6 +43,11 @@ class ArgsParser {
|
|||||||
.defaultsTo((Paths.get("certificates") / "network-root-truststore.jks"))
|
.defaultsTo((Paths.get("certificates") / "network-root-truststore.jks"))
|
||||||
private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.")
|
private val networkRootTrustStorePasswordArg = optionParser.accepts("network-root-truststore-password", "Network root trust store password obtained from network operator.")
|
||||||
.withRequiredArg()
|
.withRequiredArg()
|
||||||
|
private val unknownConfigKeysPolicy = optionParser.accepts("on-unknown-config-keys", "How to behave on unknown node configuration property keys: [WARN, FAIL, IGNORE].")
|
||||||
|
.withRequiredArg()
|
||||||
|
.withValuesConvertedBy(object : EnumConverter<UnknownConfigKeysPolicy>(UnknownConfigKeysPolicy::class.java) {})
|
||||||
|
.defaultsTo(UnknownConfigKeysPolicy.FAIL)
|
||||||
|
|
||||||
private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
|
private val isVersionArg = optionParser.accepts("version", "Print the version and exit")
|
||||||
private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info",
|
private val justGenerateNodeInfoArg = optionParser.accepts("just-generate-node-info",
|
||||||
"Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit")
|
"Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then quit")
|
||||||
@ -66,6 +72,7 @@ class ArgsParser {
|
|||||||
val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg)
|
val bootstrapRaftCluster = optionSet.has(bootstrapRaftClusterArg)
|
||||||
val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg)
|
val networkRootTrustStorePath = optionSet.valueOf(networkRootTrustStorePathArg)
|
||||||
val networkRootTrustStorePassword = optionSet.valueOf(networkRootTrustStorePasswordArg)
|
val networkRootTrustStorePassword = optionSet.valueOf(networkRootTrustStorePasswordArg)
|
||||||
|
val unknownConfigKeysPolicy = optionSet.valueOf(unknownConfigKeysPolicy)
|
||||||
|
|
||||||
val registrationConfig = if (isRegistration) {
|
val registrationConfig = if (isRegistration) {
|
||||||
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
||||||
@ -85,7 +92,8 @@ class ArgsParser {
|
|||||||
noLocalShell,
|
noLocalShell,
|
||||||
sshdServer,
|
sshdServer,
|
||||||
justGenerateNodeInfo,
|
justGenerateNodeInfo,
|
||||||
bootstrapRaftCluster)
|
bootstrapRaftCluster,
|
||||||
|
unknownConfigKeysPolicy)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
|
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
|
||||||
@ -103,13 +111,14 @@ data class CmdLineOptions(val baseDirectory: Path,
|
|||||||
val noLocalShell: Boolean,
|
val noLocalShell: Boolean,
|
||||||
val sshdServer: Boolean,
|
val sshdServer: Boolean,
|
||||||
val justGenerateNodeInfo: Boolean,
|
val justGenerateNodeInfo: Boolean,
|
||||||
val bootstrapRaftCluster: Boolean) {
|
val bootstrapRaftCluster: Boolean,
|
||||||
|
val unknownConfigKeysPolicy: UnknownConfigKeysPolicy) {
|
||||||
fun loadConfig(): NodeConfiguration {
|
fun loadConfig(): NodeConfiguration {
|
||||||
val config = ConfigHelper.loadConfig(
|
val config = ConfigHelper.loadConfig(
|
||||||
baseDirectory,
|
baseDirectory,
|
||||||
configFile,
|
configFile,
|
||||||
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell))
|
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell))
|
||||||
).parseAsNodeConfiguration()
|
).parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
|
||||||
if (nodeRegistrationOption != null) {
|
if (nodeRegistrationOption != null) {
|
||||||
require(!config.devMode) { "registration cannot occur in devMode" }
|
require(!config.devMode) { "registration cannot occur in devMode" }
|
||||||
requireNotNull(config.compatibilityZoneURL) {
|
requireNotNull(config.compatibilityZoneURL) {
|
||||||
|
@ -9,12 +9,10 @@ import net.corda.core.utilities.loggerFor
|
|||||||
import net.corda.core.utilities.seconds
|
import net.corda.core.utilities.seconds
|
||||||
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
import net.corda.node.internal.artemis.CertificateChainCheckPolicy
|
||||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||||
import net.corda.nodeapi.internal.config.NodeSSLConfiguration
|
import net.corda.nodeapi.internal.config.*
|
||||||
import net.corda.nodeapi.internal.config.SSLConfiguration
|
|
||||||
import net.corda.nodeapi.internal.config.User
|
|
||||||
import net.corda.nodeapi.internal.config.parseAs
|
|
||||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||||
import net.corda.tools.shell.SSHDConfiguration
|
import net.corda.tools.shell.SSHDConfiguration
|
||||||
|
import org.slf4j.Logger
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@ -120,7 +118,7 @@ data class P2PMessagingRetryConfiguration(
|
|||||||
val backoffBase: Double
|
val backoffBase: Double
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Config.parseAsNodeConfiguration(): NodeConfiguration = parseAs<NodeConfigurationImpl>()
|
fun Config.parseAsNodeConfiguration(onUnknownKeys: ((Set<String>, logger: Logger) -> Unit) = UnknownConfigKeysPolicy.FAIL::handle): NodeConfiguration = parseAs<NodeConfigurationImpl>(onUnknownKeys)
|
||||||
|
|
||||||
data class NodeConfigurationImpl(
|
data class NodeConfigurationImpl(
|
||||||
/** This is not retrieved from the config file but rather from a command line argument. */
|
/** This is not retrieved from the config file but rather from a command line argument. */
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.node
|
|||||||
import joptsimple.OptionException
|
import joptsimple.OptionException
|
||||||
import net.corda.core.internal.delete
|
import net.corda.core.internal.delete
|
||||||
import net.corda.core.internal.div
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||||
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
import net.corda.nodeapi.internal.crypto.X509KeyStore
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||||
@ -42,7 +43,8 @@ class ArgsParserTest {
|
|||||||
noLocalShell = false,
|
noLocalShell = false,
|
||||||
sshdServer = false,
|
sshdServer = false,
|
||||||
justGenerateNodeInfo = false,
|
justGenerateNodeInfo = false,
|
||||||
bootstrapRaftCluster = false))
|
bootstrapRaftCluster = false,
|
||||||
|
unknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -161,4 +163,13 @@ class ArgsParserTest {
|
|||||||
val cmdLineOptions = parser.parse("--bootstrap-raft-cluster")
|
val cmdLineOptions = parser.parse("--bootstrap-raft-cluster")
|
||||||
assertThat(cmdLineOptions.bootstrapRaftCluster).isTrue()
|
assertThat(cmdLineOptions.bootstrapRaftCluster).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on-unknown-config-keys options`() {
|
||||||
|
|
||||||
|
UnknownConfigKeysPolicy.values().forEach { onUnknownConfigKeyPolicy ->
|
||||||
|
val cmdLineOptions = parser.parse("--on-unknown-config-keys", onUnknownConfigKeyPolicy.name)
|
||||||
|
assertThat(cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(onUnknownConfigKeyPolicy)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user