[CORDA-926]: Parsing NodeConfiguration will now fail if unknown properties are present. (#2484)

This commit is contained in:
Michele Sollecito
2018-03-01 14:57:36 +00:00
committed by GitHub
parent 754b87d547
commit b580a2ac30
26 changed files with 278 additions and 86 deletions

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=4.0.2 gradlePluginsVersion=4.0.3
kotlinVersion=1.2.20 kotlinVersion=1.2.20
platformVersion=2 platformVersion=2
guavaVersion=21.0 guavaVersion=21.0

View File

@ -7,6 +7,12 @@ from the previous milestone release.
UNRELEASED UNRELEASED
---------- ----------
* Parsing of ``NodeConfiguration`` will now fail if unknown configuration keys are found.
* The web server now has its own ``web-server.conf`` file, separate from ``node.conf``.
* Introduced a placeholder for custom properties within ``node.conf``; the property key is "custom".
* Added ``NetworkMapCache.getNodesByLegalName`` for querying nodes belonging to a distributed service such as a notary cluster * Added ``NetworkMapCache.getNodesByLegalName`` for querying nodes belonging to a distributed service such as a notary cluster
where they all share a common identity. ``NetworkMapCache.getNodeByLegalName`` has been tightened to throw if more than where they all share a common identity. ``NetworkMapCache.getNodeByLegalName`` has been tightened to throw if more than
one node with the legal name is found. one node with the legal name is found.

View File

@ -94,16 +94,6 @@ absolute path to the node's base directory.
:security: Contains various nested fields controlling user authentication/authorization, in particular for RPC accesses. See :security: Contains various nested fields controlling user authentication/authorization, in particular for RPC accesses. See
:doc:`clientrpc` for details. :doc:`clientrpc` for details.
:webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself.
.. note:: If HTTPS is enabled then the browser security checks will require that the accessing url host name is one
of either the machine name, fully qualified machine name, or server IP address to line up with the Subject Alternative
Names contained within the development certificates. This is addition to requiring the ``/config/dev/corda_dev_ca.cer``
root certificate be installed as a Trusted CA.
.. note:: The driver will not automatically create a webserver instance, but the Cordformation will. If this field
is present the web server will start.
:notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt :notary: Optional configuration object which if present configures the node to run as a notary. If part of a Raft or BFT SMaRt
cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both. cluster then specify ``raft`` or ``bftSMaRt`` respectively as described below. If a single node notary then omit both.
@ -180,6 +170,7 @@ absolute path to the node's base directory.
:attachmentCacheBound: Optionally specify how many attachments should be cached locally. Note that this includes only the key and :attachmentCacheBound: Optionally specify how many attachments should be cached locally. Note that this includes only the key and
metadata, the content is cached separately and can be loaded lazily. Defaults to 1024. metadata, the content is cached separately and can be loaded lazily. Defaults to 1024.
Examples Examples
-------- --------
@ -202,9 +193,72 @@ Simple notary configuration file:
address : "my-corda-node:10003" address : "my-corda-node:10003"
adminAddress : "my-corda-node:10004" adminAddress : "my-corda-node:10004"
} }
webAddress : "localhost:12347"
notary : { notary : {
validating : false validating : false
} }
devMode : true devMode : true
compatibilityZoneURL : "https://cz.corda.net" compatibilityZoneURL : "https://cz.corda.net"
An example ``web-server.conf`` file is as follow:
.. parsed-literal::
myLegalName : "O=Notary Service,OU=corda,L=London,C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
rpcSettings = {
useSsl = false
standAloneBroker = false
address : "my-corda-node:10003"
adminAddress : "my-corda-node:10004"
}
webAddress : "localhost:12347",
rpcUsers : [{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }]
Fields
------
The available config fields are listed below. ``baseDirectory`` is available as a substitution value, containing the absolute
path to the node's base directory.
:myLegalName: The legal identity of the node acts as a human readable alias to the node's public key and several demos use
this to lookup the NodeInfo.
:keyStorePassword: The password to unlock the KeyStore file (``<workspace>/certificates/sslkeystore.jks``) containing the
node certificate and private key.
.. note:: This is the non-secret value for the development certificates automatically generated during the first node run.
Longer term these keys will be managed in secure hardware devices.
:trustStorePassword: The password to unlock the Trust store file (``<workspace>/certificates/truststore.jks``) containing
the Corda network root certificate. This is the non-secret value for the development certificates automatically
generated during the first node run.
.. note:: Longer term these keys will be managed in secure hardware devices.
:rpcSettings: Options for the RPC server.
:useSsl: (optional) boolean, indicates whether the node should require clients to use SSL for RPC connections, defaulted to ``false``.
:standAloneBroker: (optional) boolean, indicates whether the node will connect to a standalone broker for RPC, defaulted to ``false``.
:address: (optional) host and port for the RPC server binding, if any.
:adminAddress: (optional) host and port for the RPC admin binding (only required when ``useSsl`` is ``false``, because the node connects to Artemis using SSL to ensure admin privileges are not accessible outside the node).
:ssl: (optional) SSL settings for the RPC client.
:keyStorePassword: password for the key store.
:trustStorePassword: password for the trust store.
:certificatesDirectory: directory in which the stores will be searched, unless absolute paths are provided.
:sslKeystore: absolute path to the ssl key store, defaulted to ``certificatesDirectory / "sslkeystore.jks"``.
:trustStoreFile: absolute path to the trust store, defaulted to ``certificatesDirectory / "truststore.jks"``.
:trustStoreFile: absolute path to the trust store, defaulted to ``certificatesDirectory / "truststore.jks"``.
:webAddress: The host and port on which the webserver will listen if it is started. This is not used by the node itself.
:rpcUsers: A list of users who are authorised to access the RPC system. Each user in the list is a config object with the
following fields:
:username: Username consisting only of word characters (a-z, A-Z, 0-9 and _)
:password: The password
:permissions: A list of permissions for starting flows via RPC. To give the user the permission to start the flow
``foo.bar.FlowClass``, add the string ``StartFlow.foo.bar.FlowClass`` to the list. If the list
contains the string ``ALL``, the user can start any flow via RPC. This value is intended for administrator
users and for development.

View File

@ -2,5 +2,3 @@ myLegalName : "O=Notary Service,OU=corda,L=London,C=GB"
keyStorePassword : "cordacadevpass" keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass" trustStorePassword : "trustpass"
p2pAddress : "my-network-map:10000" p2pAddress : "my-network-map:10000"
webAddress : "localhost:10001"
sshdAddress : "localhost:10002"

View File

@ -14,7 +14,6 @@ rpcSettings = {
address : "my-corda-node:10003" address : "my-corda-node:10003"
adminAddress : "my-corda-node:10004" adminAddress : "my-corda-node:10004"
} }
webAddress : "localhost:10004"
rpcUsers : [ rpcUsers : [
{ username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] } { username=user1, password=letmein, permissions=[ StartFlow.net.corda.protocols.CashProtocol ] }
] ]

View File

@ -1,4 +1,3 @@
myLegalName : "O=Bank A,L=London,C=GB" myLegalName : "O=Bank A,L=London,C=GB"
p2pAddress : "my-corda-node:10002" p2pAddress : "my-corda-node:10002"
webAddress : "localhost:10003"
verifierType: "OutOfProcess" verifierType: "OutOfProcess"

View File

@ -12,7 +12,7 @@ class CashConfigDataFlowTest {
@Test @Test
fun `issuable currencies are read in from node config`() { fun `issuable currencies are read in from node config`() {
driver { driver {
val node = startNode(customOverrides = mapOf("issuableCurrencies" to listOf("EUR", "USD"))).getOrThrow() val node = startNode(customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("EUR", "USD")))).getOrThrow()
val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow() val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow()
assertThat(config.issuableCurrencies).containsExactly(EUR, USD) assertThat(config.issuableCurrencies).containsExactly(EUR, USD)
} }

View File

@ -16,6 +16,7 @@ import net.corda.finance.EUR
import net.corda.finance.GBP import net.corda.finance.GBP
import net.corda.finance.USD import net.corda.finance.USD
import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies
import java.io.IOException
import java.nio.file.Path import java.nio.file.Path
import java.util.* import java.util.*
@ -35,13 +36,20 @@ class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() {
.let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) } .let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) }
.let { it.javaClass.getMethod("getBaseDirectory").apply { isAccessible = true }.invoke(it)} .let { it.javaClass.getMethod("getBaseDirectory").apply { isAccessible = true }.invoke(it)}
.let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String } .let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String }
var issuableCurrenciesValue: List<Currency>
try {
val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) } val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) }
if (config.hasPath("issuableCurrencies")) { if (config.hasPath("custom.issuableCurrencies")) {
issuableCurrencies = config.getStringList("issuableCurrencies").map { Currency.getInstance(it) } issuableCurrenciesValue = config.getStringList("custom.issuableCurrencies").map { Currency.getInstance(it) }
require(supportedCurrencies.containsAll(issuableCurrencies)) require(supportedCurrencies.containsAll(issuableCurrenciesValue))
} else { } else {
issuableCurrencies = emptyList() issuableCurrenciesValue = emptyList()
} }
} catch (e: IOException) {
issuableCurrenciesValue = emptyList()
}
issuableCurrencies = issuableCurrenciesValue
} }
} }

View File

@ -29,6 +29,8 @@ import kotlin.reflect.jvm.jvmErasure
@Target(AnnotationTarget.PROPERTY) @Target(AnnotationTarget.PROPERTY)
annotation class OldConfig(val value: String) annotation class OldConfig(val value: String)
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)
@ -37,9 +39,24 @@ operator fun <T : Any> Config.getValue(receiver: Any, metadata: KProperty<*>): T
fun <T : Any> Config.parseAs(clazz: KClass<T>): T { fun <T : Any> Config.parseAs(clazz: KClass<T>): 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 args = constructor.parameters val parameters = constructor.parameters
.filterNot { it.isOptional && !hasPath(it.name!!) } val parameterNames = parameters.flatMap { param ->
.associateBy({ it }) { param -> mutableSetOf<String>().apply {
param.name?.let(this::add)
clazz.memberProperties.singleOrNull { it.name == param.name }?.let { matchingProperty ->
matchingProperty.annotations.filterIsInstance<OldConfig>().map { it.value }.forEach { this.add(it) }
}
}
}
val unknownConfigurationKeys = this.entrySet()
.mapNotNull { it.key.split(".").firstOrNull() }
.filterNot { it == CUSTOM_NODE_PROPERTIES_ROOT }
.filterNot(parameterNames::contains)
.toSortedSet()
if (unknownConfigurationKeys.isNotEmpty()) {
throw UnknownConfigurationKeysException.of(unknownConfigurationKeys)
}
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)
@ -48,6 +65,20 @@ fun <T : Any> Config.parseAs(clazz: KClass<T>): T {
return constructor.callBy(args) return constructor.callBy(args)
} }
class UnknownConfigurationKeysException private constructor(val unknownKeys: Set<String>) : IllegalArgumentException(message(unknownKeys)) {
init {
require(unknownKeys.isNotEmpty()) { "Absence of unknown keys should not raise UnknownConfigurationKeysException." }
}
companion object {
fun of(offendingKeys: Set<String>): UnknownConfigurationKeysException = UnknownConfigurationKeysException(offendingKeys)
private fun message(offendingKeys: Set<String>) = "Unknown configuration keys: ${offendingKeys.joinToString(", ", "[", "]")}."
}
}
inline fun <reified T : Any> Config.parseAs(): T = parseAs(T::class) inline fun <reified T : Any> Config.parseAs(): T = parseAs(T::class)
fun Config.toProperties(): Properties { fun Config.toProperties(): Properties {

View File

@ -90,15 +90,17 @@ class NetworkBootstrapper {
} }
private fun generateDirectoriesIfNeeded(directory: Path) { private fun generateDirectoriesIfNeeded(directory: Path) {
val confFiles = directory.list { it.filter { it.toString().endsWith(".conf") }.toList() } val confFiles = directory.list { it.filter { it.toString().endsWith("_node.conf") }.toList() }
val webServerConfFiles = directory.list { it.filter { it.toString().endsWith("_web-server.conf") }.toList() }
if (confFiles.isEmpty()) return if (confFiles.isEmpty()) return
println("Node config files found in the root directory - generating node directories") println("Node config files found in the root directory - generating node directories")
val cordaJar = extractCordaJarTo(directory) val cordaJar = extractCordaJarTo(directory)
for (confFile in confFiles) { for (confFile in confFiles) {
val nodeName = confFile.fileName.toString().removeSuffix(".conf") val nodeName = confFile.fileName.toString().removeSuffix("_node.conf")
println("Generating directory for $nodeName") println("Generating directory for $nodeName")
val nodeDir = (directory / nodeName).createDirectories() val nodeDir = (directory / nodeName).createDirectories()
confFile.moveTo(nodeDir / "node.conf", StandardCopyOption.REPLACE_EXISTING) confFile.moveTo(nodeDir / "node.conf", StandardCopyOption.REPLACE_EXISTING)
webServerConfFiles.firstOrNull { directory.relativize(it).toString().removeSuffix("_web-server.conf") == nodeName }?.moveTo(nodeDir / "web-server.conf", StandardCopyOption.REPLACE_EXISTING)
Files.copy(cordaJar, (nodeDir / "corda.jar"), StandardCopyOption.REPLACE_EXISTING) Files.copy(cordaJar, (nodeDir / "corda.jar"), StandardCopyOption.REPLACE_EXISTING)
} }
Files.delete(cordaJar) Files.delete(cordaJar)

View File

@ -200,6 +200,21 @@ class ConfigParsingTest {
assertThat(DataWithCompanion(3).toConfig()).isEqualTo(config("value" to 3)) assertThat(DataWithCompanion(3).toConfig()).isEqualTo(config("value" to 3))
} }
@Test
fun `unknown configuration keys raise exception`() {
// intentional typo here, parsing should throw rather than sneakily return default value
val knownKey = "mandatory"
val unknownKey = "optioal"
val configuration = config(knownKey to "hello", unknownKey to "world")
assertThatThrownBy { configuration.parseAs<TypedConfiguration>() }.isInstanceOfSatisfying(UnknownConfigurationKeysException::class.java) { exception ->
assertThat(exception.unknownKeys).contains(unknownKey)
assertThat(exception.unknownKeys).doesNotContain(knownKey)
}
}
private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType( private inline fun <reified S : SingleData<V>, reified L : ListData<V>, V : Any> testPropertyType(
value1: V, value1: V,
value2: V, value2: V,
@ -243,6 +258,7 @@ class ConfigParsingTest {
val values: List<T> val values: List<T>
} }
data class TypedConfiguration(private val mandatory: String, private val optional: String = "optional")
data class StringData(override val value: String) : SingleData<String> data class StringData(override val value: String) : SingleData<String>
data class StringListData(override val values: List<String>) : ListData<String> data class StringListData(override val values: List<String>) : ListData<String>
data class StringSetData(val values: Set<String>) data class StringSetData(val values: Set<String>)

View File

@ -82,7 +82,6 @@ class AuthDBTests : NodeBasedTest() {
"password" to "", "password" to "",
"driverClassName" to "org.h2.Driver" "driverClassName" to "org.h2.Driver"
) )
)
), ),
"options" to mapOf( "options" to mapOf(
"cache" to mapOf( "cache" to mapOf(
@ -92,6 +91,7 @@ class AuthDBTests : NodeBasedTest() {
) )
) )
) )
)
node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = securityConfig) node = startNode(ALICE_NAME, rpcUsers = emptyList(), configOverrides = securityConfig)
client = CordaRPCClient(node.internals.configuration.rpcOptions.address!!) client = CordaRPCClient(node.internals.configuration.rpcOptions.address!!)

View File

@ -100,7 +100,7 @@ public class CordaCaplet extends Capsule {
// Read JVM args from the config if specified, else leave alone. // Read JVM args from the config if specified, else leave alone.
List<String> jvmArgs = new ArrayList<>((List<String>) super.attribute(attr)); List<String> jvmArgs = new ArrayList<>((List<String>) super.attribute(attr));
try { try {
List<String> configJvmArgs = nodeConfig.getStringList("jvmArgs"); List<String> configJvmArgs = nodeConfig.getStringList("custom.jvmArgs");
jvmArgs.clear(); jvmArgs.clear();
jvmArgs.addAll(configJvmArgs); jvmArgs.addAll(configJvmArgs);
log(LOG_VERBOSE, "Configured JVM args = " + jvmArgs); log(LOG_VERBOSE, "Configured JVM args = " + jvmArgs);

View File

@ -17,6 +17,7 @@ import net.corda.node.shell.InteractiveShell
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
import net.corda.node.utilities.registration.NetworkRegistrationHelper import net.corda.node.utilities.registration.NetworkRegistrationHelper
import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.addShutdownHook
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
import org.fusesource.jansi.Ansi import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole import org.fusesource.jansi.AnsiConsole
import org.slf4j.bridge.SLF4JBridgeHandler import org.slf4j.bridge.SLF4JBridgeHandler
@ -86,6 +87,9 @@ open class NodeStartup(val args: Array<String>) {
} else { } else {
conf0 conf0
} }
} catch (e: UnknownConfigurationKeysException) {
logger.error(e.message)
return false
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Exception during node configuration", e) logger.error("Exception during node configuration", e)
return false return false

View File

@ -150,7 +150,9 @@ data class NodeConfigurationImpl(
override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode), override val database: DatabaseConfig = DatabaseConfig(initialiseSchema = devMode, exportHibernateJMXStatistics = devMode),
private val transactionCacheSizeMegaBytes: Int? = null, private val transactionCacheSizeMegaBytes: Int? = null,
private val attachmentContentCacheSizeMegaBytes: Int? = null, private val attachmentContentCacheSizeMegaBytes: Int? = null,
override val attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound override val attachmentCacheBound: Long = NodeConfiguration.defaultAttachmentCacheBound,
// do not use or remove (breaks DemoBench together with rejection of unknown configuration keys during parsing)
private val h2port: Int = 0
) : NodeConfiguration { ) : NodeConfiguration {
companion object { companion object {
private val logger = loggerFor<NodeConfigurationImpl>() private val logger = loggerFor<NodeConfigurationImpl>()

View File

@ -38,7 +38,7 @@ class BankOfCordaCordform : CordformDefinition() {
} }
node { node {
name(BOC_NAME) name(BOC_NAME)
extraConfig = mapOf("issuableCurrencies" to listOf("USD")) extraConfig = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD")))
p2pPort(10005) p2pPort(10005)
rpcSettings { rpcSettings {
address("localhost:$BOC_RPC_PORT") address("localhost:$BOC_RPC_PORT")

View File

@ -64,8 +64,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
notary = [validating : true] notary = [validating : true]
p2pPort 10002 p2pPort 10002
rpcSettings { rpcSettings {
port 10003 address("localhost:10003")
adminPort 10023 adminAddress("localhost:10023")
} }
cordapps = ["${project(":finance").group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList rpcUsers = rpcUsersList
@ -75,8 +75,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "O=Bank A,L=London,C=GB" name "O=Bank A,L=London,C=GB"
p2pPort 10005 p2pPort 10005
rpcSettings { rpcSettings {
port 10006 address("localhost:10006")
adminPort 10026 adminAddress("localhost:10026")
} }
cordapps = ["${project(":finance").group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList rpcUsers = rpcUsersList
@ -86,8 +86,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
p2pPort 10008 p2pPort 10008
rpcSettings { rpcSettings {
port 10009 address("localhost:10009")
adminPort 10029 adminAddress("localhost:10029")
} }
cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project.group}:finance:$corda_release_version"]
rpcUsers = rpcUsersList rpcUsers = rpcUsersList
@ -97,8 +97,8 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "O=Regulator,L=Moscow,C=RU" name "O=Regulator,L=Moscow,C=RU"
p2pPort 10011 p2pPort 10011
rpcSettings { rpcSettings {
port 10012 address("localhost:10012")
adminPort 10032 adminAddress("localhost:10032")
} }
cordapps = ["${project.group}:finance:$corda_release_version"] cordapps = ["${project.group}:finance:$corda_release_version"]
cordapps = ["${project(":finance").group}:finance:$corda_release_version"] cordapps = ["${project(":finance").group}:finance:$corda_release_version"]

View File

@ -70,8 +70,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
p2pPort 10002 p2pPort 10002
cordapp project(':finance') cordapp project(':finance')
extraConfig = [ extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ]
]
} }
node { node {
name "O=Bank A,L=London,C=GB" name "O=Bank A,L=London,C=GB"
@ -84,8 +86,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapp project(':finance') cordapp project(':finance')
rpcUsers = ext.rpcUsers rpcUsers = ext.rpcUsers
extraConfig = [ extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ]
]
} }
node { node {
name "O=Bank B,L=New York,C=US" name "O=Bank B,L=New York,C=US"
@ -98,8 +102,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapp project(':finance') cordapp project(':finance')
rpcUsers = ext.rpcUsers rpcUsers = ext.rpcUsers
extraConfig = [ extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ]
]
} }
node { node {
name "O=Bank C,L=Tokyo,C=JP" name "O=Bank C,L=Tokyo,C=JP"
@ -112,8 +118,10 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapp project(':finance') cordapp project(':finance')
rpcUsers = ext.rpcUsers rpcUsers = ext.rpcUsers
extraConfig = [ extraConfig = [
custom: [
jvmArgs: ["-Xmx1g"] jvmArgs: ["-Xmx1g"]
] ]
]
} }
} }

View File

@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import com.typesafe.config.ConfigValueFactory
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.internal.createCordaRPCClientWithSsl import net.corda.client.rpc.internal.createCordaRPCClientWithSsl
import net.corda.cordform.CordformContext import net.corda.cordform.CordformContext
@ -229,7 +230,6 @@ class DriverDSLImpl(
"p2pAddress" to p2pAddress.toString(), "p2pAddress" to p2pAddress.toString(),
"rpcSettings.address" to rpcAddress.toString(), "rpcSettings.address" to rpcAddress.toString(),
"rpcSettings.adminAddress" to rpcAdminAddress.toString(), "rpcSettings.adminAddress" to rpcAdminAddress.toString(),
"webAddress" to webAddress.toString(),
"useTestClock" to useTestClock, "useTestClock" to useTestClock,
"rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() },
"verifierType" to verifierType.name "verifierType" to verifierType.name
@ -355,13 +355,17 @@ class DriverDSLImpl(
val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort() val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort()
val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap() val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap()
val rpcUsers = cordform.rpcUsers val rpcUsers = cordform.rpcUsers
val config = NodeConfig(ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name), val rawConfig = cordform.config + rpcAddress + notary + mapOf(
allowMissingConfig = true,
configOverrides = cordform.config + rpcAddress + notary + mapOf(
"rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers
) )
)) val typesafe = ConfigHelper.loadConfig(
baseDirectory = baseDirectory(name),
allowMissingConfig = true,
configOverrides = rawConfig.toNodeOnly()
)
val cordaConfig = typesafe.parseAsNodeConfiguration()
val config = NodeConfig(rawConfig, cordaConfig)
return startNodeInternal(config, webAddress, null, "200m", localNetworkMap) return startNodeInternal(config, webAddress, null, "200m", localNetworkMap)
} }
@ -384,9 +388,9 @@ class DriverDSLImpl(
override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture<WebserverHandle> { override fun startWebserver(handle: NodeHandle, maximumHeapSize: String): CordaFuture<WebserverHandle> {
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
val process = startWebserver(handle, debugPort, maximumHeapSize) val process = startWebserver(handle as NodeHandleInternal, debugPort, maximumHeapSize)
shutdownManager.registerProcessShutdown(process) shutdownManager.registerProcessShutdown(process)
val webReadyFuture = addressMustBeBoundFuture(executorService, (handle as NodeHandleInternal).webAddress, process) val webReadyFuture = addressMustBeBoundFuture(executorService, handle.webAddress, process)
return webReadyFuture.map { queryWebserver(handle, process) } return webReadyFuture.map { queryWebserver(handle, process) }
} }
@ -726,14 +730,12 @@ class DriverDSLImpl(
* Simple holder class to capture the node configuration both as the raw [Config] object and the parsed [NodeConfiguration]. * Simple holder class to capture the node configuration both as the raw [Config] object and the parsed [NodeConfiguration].
* Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration]. * Keeping [Config] around is needed as the user may specify extra config options not specified in [NodeConfiguration].
*/ */
private class NodeConfig(val typesafe: Config) { private class NodeConfig(val typesafe: Config, val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().also { nodeConfiguration ->
val corda: NodeConfiguration = typesafe.parseAsNodeConfiguration().also { nodeConfiguration ->
val errors = nodeConfiguration.validate() val errors = nodeConfiguration.validate()
if (errors.isNotEmpty()) { if (errors.isNotEmpty()) {
throw IllegalStateException("Invalid node configuration. Errors where:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") throw IllegalStateException("Invalid node configuration. Errors where:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
} }
} })
}
companion object { companion object {
internal val log = contextLogger() internal val log = contextLogger()
@ -771,7 +773,7 @@ class DriverDSLImpl(
throw IllegalStateException("No quasar agent: -javaagent:lib/quasar.jar and working directory project root might fix") throw IllegalStateException("No quasar agent: -javaagent:lib/quasar.jar and working directory project root might fix")
} }
// Write node.conf // Write node.conf
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe) writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly())
// TODO pass the version in? // TODO pass the version in?
val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start() val node = InProcessNode(config.corda, MOCK_VERSION_INFO, cordappPackages).start()
val nodeThread = thread(name = config.corda.myLegalName.organisation) { val nodeThread = thread(name = config.corda.myLegalName.organisation) {
@ -798,7 +800,7 @@ class DriverDSLImpl(
"debug port is " + (debugPort ?: "not enabled") + ", " + "debug port is " + (debugPort ?: "not enabled") + ", " +
"jolokia monitoring port is " + (monitorPort ?: "not enabled")) "jolokia monitoring port is " + (monitorPort ?: "not enabled"))
// Write node.conf // Write node.conf
writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe) writeConfig(config.corda.baseDirectory, "node.conf", config.typesafe.toNodeOnly())
val systemProperties = mutableMapOf( val systemProperties = mutableMapOf(
"name" to config.corda.myLegalName, "name" to config.corda.myLegalName,
@ -844,8 +846,9 @@ class DriverDSLImpl(
) )
} }
private fun startWebserver(handle: NodeHandle, debugPort: Int?, maximumHeapSize: String): Process { private fun startWebserver(handle: NodeHandleInternal, debugPort: Int?, maximumHeapSize: String): Process {
val className = "net.corda.webserver.WebServer" val className = "net.corda.webserver.WebServer"
writeConfig(handle.baseDirectory, "web-server.conf", handle.toWebServerConfig())
return ProcessUtilities.startCordaProcess( return ProcessUtilities.startCordaProcess(
className = className, // cannot directly get class for this, so just use string className = className, // cannot directly get class for this, so just use string
arguments = listOf("--base-directory", handle.baseDirectory.toString()), arguments = listOf("--base-directory", handle.baseDirectory.toString()),
@ -860,6 +863,22 @@ class DriverDSLImpl(
) )
} }
private fun NodeHandleInternal.toWebServerConfig(): Config {
var config = ConfigFactory.empty()
config += "webAddress" to webAddress.toString()
config += "myLegalName" to configuration.myLegalName.toString()
config += "rpcAddress" to configuration.rpcOptions.address!!.toString()
config += "rpcUsers" to configuration.toConfig().getValue("rpcUsers")
config += "useHTTPS" to useHTTPS
config += "baseDirectory" to configuration.baseDirectory.toAbsolutePath().toString()
config += "keyStorePassword" to configuration.keyStorePassword
config += "trustStorePassword" to configuration.trustStorePassword
return config
}
private operator fun Config.plus(property: Pair<String, Any>) = withValue(property.first, ConfigValueFactory.fromAnyRef(property.second))
/** /**
* Get the package of the caller to the driver so that it can be added to the list of packages the nodes will scan. * Get the package of the caller to the driver so that it can be added to the list of packages the nodes will scan.
* This makes the driver automatically pick the CorDapp module that it's run from. * This makes the driver automatically pick the CorDapp module that it's run from.
@ -1053,3 +1072,14 @@ fun writeConfig(path: Path, filename: String, config: Config) {
val configString = config.root().render(ConfigRenderOptions.defaults()) val configString = config.root().render(ConfigRenderOptions.defaults())
configString.byteInputStream().copyTo(path / filename, StandardCopyOption.REPLACE_EXISTING) configString.byteInputStream().copyTo(path / filename, StandardCopyOption.REPLACE_EXISTING)
} }
private fun Config.toNodeOnly(): Config {
return if (hasPath("webAddress")) {
withoutPath("webAddress").withoutPath("useHTTPS")
} else {
this
}
}
private operator fun Config.plus(property: Pair<String, Any>) = withValue(property.first, ConfigValueFactory.fromAnyRef(property.second))

View File

@ -46,17 +46,49 @@ data class NodeConfig(
@Suppress("unused") @Suppress("unused")
private val useTestClock = true private val useTestClock = true
private fun asConfig(): Config { fun nodeConf(): Config {
val config = toConfig() val basic = NodeConfigurationData(myLegalName, p2pAddress, rpcAddress, notary, h2port, rpcUsers, useTestClock, detectPublicIp).toConfig()
val rpcSettings = empty() val rpcSettings = empty()
.withValue("address", ConfigValueFactory.fromAnyRef(rpcAddress.toString())) .withValue("address", ConfigValueFactory.fromAnyRef(rpcAddress.toString()))
.withValue("adminAddress", ConfigValueFactory.fromAnyRef(rpcAdminAddress.toString())) .withValue("adminAddress", ConfigValueFactory.fromAnyRef(rpcAdminAddress.toString()))
.root() .root()
return config.withoutPath("rpcAddress").withoutPath("rpcAdminAddress").withValue("rpcSettings", rpcSettings) return basic.withoutPath("rpcAddress").withoutPath("rpcAdminAddress").withValue("rpcSettings", rpcSettings)
} }
fun toText(): String = asConfig().root().render(renderOptions) fun webServerConf() = WebServerConfigurationData(myLegalName, rpcAddress, webAddress, rpcUsers).asConfig()
fun toNodeConfText() = nodeConf().render()
fun toWebServerConfText() = webServerConf().render()
fun serialiseAsString(): String {
return toConfig().render()
}
private fun Config.render(): String = root().render(renderOptions)
}
private data class NodeConfigurationData(
val myLegalName: CordaX500Name,
val p2pAddress: NetworkHostAndPort,
val rpcAddress: NetworkHostAndPort,
val notary: NotaryService?,
val h2port: Int,
val rpcUsers: List<User> = listOf(NodeConfig.defaultUser),
val useTestClock: Boolean,
val detectPublicIp: Boolean
)
private data class WebServerConfigurationData(
val myLegalName: CordaX500Name,
val rpcAddress: NetworkHostAndPort,
val webAddress: NetworkHostAndPort,
val rpcUsers: List<User>
) {
fun asConfig() = toConfig()
} }
/** /**

View File

@ -121,7 +121,11 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() {
// Write this node's configuration file into its working directory. // Write this node's configuration file into its working directory.
val confFile = config.nodeDir / "node.conf" val confFile = config.nodeDir / "node.conf"
Files.write(confFile, config.nodeConfig.toText().toByteArray()) Files.write(confFile, config.nodeConfig.toNodeConfText().toByteArray())
// Write this node's configuration file into its working directory.
val webConfFile = config.nodeDir / "web-server.conf"
Files.write(webConfFile, config.nodeConfig.toWebServerConfText().toByteArray())
// Execute the Corda node // Execute the Corda node
val cordaEnv = System.getenv().toMutableMap().apply { val cordaEnv = System.getenv().toMutableMap().apply {

View File

@ -58,7 +58,7 @@ class ProfileController : Controller() {
configs.forEach { config -> configs.forEach { config ->
// Write the configuration file. // Write the configuration file.
val nodeDir = fs.getPath(config.key).createDirectories() val nodeDir = fs.getPath(config.key).createDirectories()
val file = Files.write(nodeDir / "node.conf", config.nodeConfig.toText().toByteArray(UTF_8)) val file = Files.write(nodeDir / "node.conf", config.nodeConfig.serialiseAsString().toByteArray(UTF_8))
log.info("Wrote: $file") log.info("Wrote: $file")
// Write all of the non-built-in cordapps. // Write all of the non-built-in cordapps.

View File

@ -11,7 +11,7 @@ class WebServerController : Controller() {
log.info("Web Server JAR: $webserverPath") log.info("Web Server JAR: $webserverPath")
} }
internal fun process() = jvm.processFor(webserverPath) internal fun process() = jvm.processFor(webserverPath, "--config-file", "web-server.conf")
fun webServer() = WebServer(this) fun webServer() = WebServer(this)
} }

View File

@ -6,7 +6,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.node.services.config.parseAsNodeConfiguration import net.corda.node.services.config.parseAsNodeConfiguration
import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.User
import net.corda.nodeapi.internal.config.toConfig
import net.corda.webserver.WebServerConfig import net.corda.webserver.WebServerConfig
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
@ -28,14 +27,14 @@ class NodeConfigTest {
legalName = myLegalName, legalName = myLegalName,
p2pPort = 10001, p2pPort = 10001,
rpcPort = 40002, rpcPort = 40002,
rpcAdminPort = 40003, rpcAdminPort = 40005,
webPort = 20001, webPort = 20001,
h2port = 30001, h2port = 30001,
notary = NotaryService(validating = false), notary = NotaryService(validating = false),
users = listOf(user("jenny")) users = listOf(user("jenny"))
) )
val nodeConfig = config.toConfig() val nodeConfig = config.nodeConf()
.withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString()))
.withFallback(ConfigFactory.parseResources("reference.conf")) .withFallback(ConfigFactory.parseResources("reference.conf"))
.resolve() .resolve()
@ -63,7 +62,7 @@ class NodeConfigTest {
users = listOf(user("jenny")) users = listOf(user("jenny"))
) )
val nodeConfig = config.toConfig() val nodeConfig = config.webServerConf()
.withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString())) .withValue("baseDirectory", ConfigValueFactory.fromAnyRef(baseDir.toString()))
.withFallback(ConfigFactory.parseResources("web-reference.conf")) .withFallback(ConfigFactory.parseResources("web-reference.conf"))
.resolve() .resolve()

View File

@ -70,9 +70,9 @@ class ExplorerSimulation(private val options: OptionSet) {
val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB")
val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US")
val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager), val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager),
customOverrides = mapOf("issuableCurrencies" to listOf("GBP"))) customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("GBP"))))
val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager), val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager),
customOverrides = mapOf("issuableCurrencies" to listOf("USD"))) customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD"))))
notaryNode = defaultNotaryNode.get() notaryNode = defaultNotaryNode.get()
aliceNode = alice.get() aliceNode = alice.get()

View File

@ -25,7 +25,7 @@ class ArgsParser {
private val configFileArg = optionParser private val configFileArg = optionParser
.accepts("config-file", "The path to the config file") .accepts("config-file", "The path to the config file")
.withRequiredArg() .withRequiredArg()
.defaultsTo("node.conf") .defaultsTo("web-server.conf")
private val loggerLevel = optionParser private val loggerLevel = optionParser
.accepts("logging-level", "Enable logging at this level and higher") .accepts("logging-level", "Enable logging at this level and higher")
.withRequiredArg() .withRequiredArg()