[CORDA-1379]: Allow soft trapping of unknown config keys. (#3011)

This commit is contained in:
Michele Sollecito 2018-04-27 15:33:13 +07:00 committed by GitHub
parent efa69ce959
commit 235df69efe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 61 additions and 23 deletions

View File

@ -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.

View File

@ -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``.

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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. */

View File

@ -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)
}
}
} }